花了大概30分钟时间,做了一个PHP8的自用镜像,为什么那么快呢?因为是基于以前的PHP7修改而来的呀,这下就有JIT的支持了!
docker hub官方地址:https://hub.docker.com/r/nickzhuo/phpallinone
源代码地址:https://github.com/nickzhuo/phpallinone
花了大概30分钟时间,做了一个PHP8的自用镜像,为什么那么快呢?因为是基于以前的PHP7修改而来的呀,这下就有JIT的支持了!
docker hub官方地址:https://hub.docker.com/r/nickzhuo/phpallinone
源代码地址:https://github.com/nickzhuo/phpallinone
用docker很久很久,但build都是放在服务器执行,一方面是拉代码有缓存,另外一方面服务器是linux的,会快点点。在本地run的时候目前用的是WSL内核的Windows版本docker,但是我发现一个奇怪的问题。在linux上build一切正常,但是windows上build出来的镜像不能用,即使是在WSL环境下build的,也是启动入口报错。
/usr/local/bin/docker-php-entrypoint: exec: line 9: /start.sh: not found
乍一推测还以为是win下面斜杠的问题,但其实不是,这个问题需要把镜像中启动文件start.sh
,从文件CRLF改成LF格式,这样在win下build就顺利成功了。说来也挺奇怪的,WSL应该是纯linux了,但是也是有这一问题。
接上回说到,从K8S回归到Docker Compose,有一块调整的比较大,就是日志。原来有点懒惰,腾讯云的TKE集群可以默认接入他的LogListener,这个日志组件可以很方便从Container里取日志,方便到你根本不用改造原来的应用。但现在由于离开了K8S,还是要提升下收集日志的效率,所以所有容器内的应用都会由init进程管理输出到stdout或stderr,再由容器接住,新版本docker已经支持--log-driver=fluentd
来指定一个fluentd的后端来接受所有日志。
Monolog写php://stderr → PHP-FPM接到error_log → init进程Supervisor接到/dev/stderr → Container接到日志 → Docker daemon设置了log-driver → 写到Fluentd → Fluentd处理后通过Kafka协议发到腾讯云CLS日志处理
因为Traefik不支持FastCGI协议,所以FPM前面是挡了一层Nginx,访问日志都从Nginx取,类似应用日志,不同的是访问日志都写了stdout,这其实并没有关系,因为最后在Fluentd处理的时候还是会用正则匹配的。
在nginx.conf的http段中配置JSON格式的字段,根据你的需要来,我建议加入request_id,方便关联应用日志。
http {
log_format mylog escape=json '{"time_local":"$time_iso8601",'
'"request_id":"$request_id",'
'"host":"$server_addr",'
'"clientip":"$remote_addr",'
'"size":$body_bytes_sent,'
'"request":"$request",'
'"request_body":"$request_body",'
'"responsetime":$request_time,'
'"upstreamtime":"$upstream_response_time",'
'"http_host":"$host",'
'"url":"$uri",'
'"domain":"$host",'
'"xff":"$http_x_forwarded_for",'
'"referer":"$http_referer",'
'"http_user_agent":"$http_user_agent",'
'"status":"$status"}';
include mime.types;
server_tokens off;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 120;
types_hash_max_size 2048;
client_max_body_size 100m;
default_type application/octet-stream;
access_log off;
error_log off;
gzip on;
gzip_disable "msie6";
include vhost/*.conf;
}
在具体的虚拟机中就可以配置你设置的json格式access log
server {
server_name symfony.tld;
root /data/www/public;
location / {
# try to serve file directly, fallback to index.php
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/dev/shm/php-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_param HTTPS off;
fastcgi_param REQUEST_ID $request_id;
internal;
}
# return 404 for all other php files not matching the front controller
# this prevents access to other php files you don't want to be accessible.
location ~ \.php$ {
return 404;
}
error_log stderr;
access_log /dev/stdout mylog;
}
App的日志一般都会用主流的包可以选择,在这个例子中,我用的是Symfony原配的Monolog,这里可以根据自己的需要进行拦截。例如访问量小的时候,你可以把所有用户侧的异常抓下来看看,所以app这个pool里,level是info的我都抓了。
在你的异常拦截里面加上日志,可以从$_SERVER['REQUEST_ID']
取得你需要的nginx侧请求ID。
$this->logger->info($exception->getMessage(),[你需要的其他数据]);
mololog的配置
monolog:
handlers:
app:
level: info
type: stream
path: "php://stderr"
channels: [ app ]
formatter: 'monolog.formatter.json'
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_404s:
# regex: exclude all 404 errors from the logs
- ^/
nested:
type: stream
path: "php://stderr"
formatter: 'monolog.formatter.json'
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
记得给启动命令加上参数--force-stderr --nodaemonize
,否则PHP-FPM的/proc/pid/fd/2不是pipe,参考之前文章PHP-FPM在Docker没有日志输出到/dev/stder。
[program:php-fpm]
command=/usr/local/sbin/php-fpm --force-stderr --nodaemonize --fpm-config /usr/local/etc/php-fpm.d/www.conf
autostart=true
autorestart=true
priority=5
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopsignal=QUIT
普通容器可以在run的时候加上--log-driver=fluentd
,如果是compose可以在配置里加上logging字段。
version: '3'
services:
delta:
image: ccr.ccs.tencentyun.com/zhouzhou/phpallinone:allinone-master-20220331
ports:
- "80:80"
logging:
driver: "fluentd"
options:
fluentd-address: localhost:24224
tag: delta
networks:
default:
driver: bridge
Fluentd可以做很多事情,当然其他日志收集器也可以,但Docker内置了Fluentd驱动,就用了它。我使用的场景很简单,单纯的在宿主机进行收集,简单的格式化处理,由于日质体量很小,所以发送到腾讯云CLS来处理。
发送到腾讯云CLS需要Kafka协议,所以我们要做一个自己的Fluentd镜像,加上了kafka协议和兼容腾讯侧的rdkafka库,请注意,github官网的debian镜像是有问题的,例子用的是Alpine版本。
FROM fluentd:v1.14.0-1.0
# Use root account to use apk
USER root
# below RUN includes plugin as examples elasticsearch is not required
# you may customize including plugins as you wish
# 安装rdkafka和kafka插件
RUN apk add --no-cache --update --virtual .build-deps \
sudo build-base ruby-dev bash\
&& sudo gem install rdkafka \
&& sudo gem install fluent-plugin-kafka \
&& sudo gem sources --clear-all \
&& apk del .build-deps \
&& rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem
USER fluent
# 覆盖一下
ENTRYPOINT ["tini", "--", "/bin/entrypoint.sh"]
CMD ["fluentd"]
配置这里说下思路,Fluentd的配置顺序简单来说是由上自下,并没有很强大的语法,利用本身的插件来完成一些其实本身很简单的操作,所以有点反直觉。我的容器中目前有访问日志和应用日志两类,我需要根据各自的特征发送到各自在腾讯云的主题进行分析。
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
<match *>
@type copy
<store>
@type relabel
@label @PHPERROR
</store>
<store>
@type relabel
@label @NGINXACCESS
</store>
</match>
<label @PHPERROR>
<filter delta>
@type grep
<regexp>
key log
pattern \[pool ([^\]]*)\]
</regexp>
</filter>
# 解析原始的PHP-FPM日志
<filter delta>
@type parser
key_name log
reserve_data true
# reserve_data true
# hash_value_field parsed
<parse>
@type regexp
expression /^\[(?<logtime>[^\]]*)\] (?<level>(DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY)): \[pool (?<pool>[^\]]*)\] child (?<child>\d+) said into stderr: \"(?<message>.+)\"$/
</parse>
</filter>
# 转json
<filter delta>
@type parser
reserve_data true
key_name message
<parse>
@type json
time_key logtime
</parse>
</filter>
<match delta>
@type rdkafka2
brokers sh-producer.cls.tencentcs.com:9096
use_event_time true
<format>
@type json
</format>
<buffer topic>
@type memory
flush_interval 3s
</buffer>
# 腾讯云日志主题
default_topic 9d7fc234-e090-4a48-8888-642f5b56e82c
# producer settings
required_acks -1
rdkafka_delivery_handle_poll_timeout 5
rdkafka_options {
"security.protocol" : "SASL_PLAINTEXT",
"sasl.mechanism" : "PLAIN",
# 日志集主题
"sasl.username" : "e63de52e-a012-45f6-8888-ca2dab7aeda1",
"sasl.password" : "SecurityId#SecurityKey"
}
</match>
</label>
<label @NGINXACCESS>
<filter delta>
@type grep
<regexp>
key source
pattern stdout
</regexp>
</filter>
<filter delta>
@type parser
key_name log
<parse>
@type json
# 指定nginx原始日志的事件作为事件事件
time_key time_local
</parse>
</filter>
<match delta>
@type rdkafka2
brokers sh-producer.cls.tencentcs.com:9096
use_event_time true
<format>
@type json
</format>
<buffer topic>
@type memory
flush_interval 3s
</buffer>
# 腾讯云日志主题
default_topic 0c0872c5-00b5-8888-b658-e88769118c30
# producer settings
required_acks -1
rdkafka_delivery_handle_poll_timeout 5
rdkafka_options {
"security.protocol" : "SASL_PLAINTEXT",
"sasl.mechanism" : "PLAIN",
# 日志集主题
"sasl.username" : "e63de52e-a012-8888-8932-ca2dab7aeda1",
"sasl.password" : "SecurityId#SecurityKey"
}
</match>
</label>
以上配置简单的把单容器所有日志输出接住,先用relabel这个插件把日志复制出两股Label进行分别处理,这里有点反直觉。然后在不同的Label下进行json处理和投递工作,注意的是,PHP-FPM的Error LOG是不能定义格式的,所以需要正则匹配,然后把message部分进行json处理,注意使用
reserve_data = true
防止之前的key被覆写。最后记得腾讯云目前的Kafka环境必须用rdkafka库,SASL_PLAINTEXT密码的配置请注意!
请注意的时候,腾讯云的kafka可以走内网,域名和端口和外网的不同。你也可以自己做Elasticsearch和Kibana,组你自己的EFK。
今天把腾讯kubernetes TKE上的一些服务弄到单机docker-compose里去了。为什么迁TKE的问题先不谈,因为原来的服务日志都是依赖腾讯Loglistener,这个日志端和腾讯TKE是完美配合的,去node上拿文件日志非常方便,也没有去优化。但这次由于结构换了,期望从docker-compose的Fluentd驱动来获得日志然后用kafka协议发到接收端,第一步就是要把容器都输出到/dev/stdout和/dev/stderr。
代码用的是php里最有名的monolog,先改配置。把日志流输出到path: "php://stderr"
,这里有第一个发现,不能写path: "/dev/stderr"
或者path: "/proc/1/fd/2"
,这两种写法都会报File Append的错误,显然要使用最前面的封装版本,当然这和手册上的有点出入。
推荐你简单使用常量 STDIN、 STDOUT 和 STDERR 来代替手工打开这些封装器。
https://www.php.net/manual/zh/wrappers.php.php 手册上原话。
容器服务是用Supervisor管理的,一个容器上跑php-fpm和nginx,Supervisor里FPM的配置是写到/dev/stderr里去的,但结果就是无论如何配置都无法写到/dev/stderr。
bash-5.1# ps aux | grep master
7 root 0:00 php-fpm: master process (/usr/local/etc/php-fpm.d/www.conf)
8 root 0:00 nginx: master process /usr/local/nginx/sbin/nginx -g daemon off;
查看下进程err输出情况。
bash-5.1# ls -l /proc/7/fd/2
l-wx------ 1 root root 64 Apr 7 14:44 /proc/7/fd/2 -> /usr/local/var/log/php-fpm.log
日志被牢牢的写进了/usr/local/var/log/php-fpm.log,没有进管道,找了半天发现,是一个神奇的配置组合问题。
[program:php-fpm]
command=/usr/local/sbin/php-fpm --force-stderr --nodaemonize --fpm-config /usr/local/etc/php-fpm.d/www.conf
autostart=true
autorestart=true
priority=5
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopsignal=QUIT
关键在于,当你用了nodaemonized,就要用–force-stderr来保证进程写到容器的stderr输出。参考,https://stackoverflow.com/questions/50995042/docker-does-not-catch-php-fpm-outputs-with-symfony-and-monolog
As php-fpm is run in nodaemonized mode, you need to set the –force-stderr flag which "Force output to stderr in nodaemonize even if stderr is not a TTY."
检查一下,连接对了!用docker logs -f container
检查也没问题了!
bash-5.1# ls -l /proc/7/fd/2
l-wx------ 1 root root 64 Apr 7 14:44 /proc/7/fd/2 -> pipe:[13099270]
最近把一套服务从K8S降级为普通的docker-compose,使用了Traefik来做反向代理,也称为边缘路由,相当于守护整套服务的边界,他和Docker配合完美,可以轻松的发现服务并且发布服务,当然也可以配置路由,以及很简单就能实现的中间件。最早nginx-proxy最先实现了其中docker服务发现的功能,但是Traefik的使用更方便,更强大,还带有一个UI控制面板。
基于2.6的Traefik,来看下我的部署笔记,一个反向代理,后面挂载一个api服务,还有api要用的缓存,分别拥有两个网络。
traefik.docker.network=traefik
配置发现用的网络,否则很有可能出504 Gateway timeout。version: '3'
services:
proxy:
image: traefik:2.6
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "9000:9000"
depends_on:
- delta
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.toml:/traefik.toml # traefik静态配置
- ./logs:/var/log # traefik日志
- ./dynamic_conf.toml:/dynamic_conf.toml # traefik动态配置
- ./ssl:/data/ssl/ # 证书
networks:
- traefik
delta:
image: ccr.ccs.tencentyun.com/zhouzhou/phpallinone:allinone-master-20220331 # 一个简单的nginx
expose:
- 80 # 一定要有 不会自动发现 超过一个端口就配置loadbalancer.server.port
deploy:
replicas: 2 # 副本数量
labels:
- "traefik.enable=true" # 声明公开此容器访问
- "traefik.http.routers.delta.rule=Host(`api.zhouzhou.net`)" # 解析规则用域名
- "traefik.http.routers.delta.entrypoints=web" # 只允许来自预定义的入口点 web 80的请求
- "traefik.http.routers.delta-ssl.rule=Host(`api.zhouzhou.net`)" # ssl单独写一条 routers
- "traefik.http.routers.delta-ssl.entrypoints=websecure" # 只允许来自预定义的入口点 443的请求
- "traefik.http.routers.delta-ssl.tls=true" # 443的请求要ssl
# - "traefik.http.services.delta.loadbalancer.server.port=80"
- traefik.docker.network=traefik # 发现网络
- "traefik.http.routers.delta.middlewares=https-redirect" # 跳转中间件
- "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" # http跳https
networks:
- traefik
- delta
depends_on:
- cache
environment:
- APP_ENV=prod
cache:
image: bitnami/redis:latest
environment:
- ALLOW_EMPTY_PASSWORD=yes
networks:
- delta
networks:
traefik:
external: true # 必须有外部网络 方便其他访问 以及在两个网络情况下traefik.docker.network起作用
delta:
################################################################
#
# Configuration sample for Traefik v2.
#
# For Traefik v1: https://github.com/traefik/traefik/blob/v1.7/traefik.sample.toml
#
################################################################
################################################################
# Global configuration
################################################################
[global]
checkNewVersion = true
sendAnonymousUsage = true
################################################################
# Entrypoints configuration
################################################################
# Entrypoints definition
#
# Optional
# Default:
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.websecure]
address = ":443"
[entryPoints.traefik]
address = ":9000"
################################################################
# Traefik logs configuration
################################################################
# Traefik logs
# Enabled by default and log to stdout
#
# Optional
#
[log]
# Log level
#
# Optional
# Default: "ERROR"
#
# level = "DEBUG"
# Sets the filepath for the traefik log. If not specified, stdout will be used.
# Intermediate directories are created if necessary.
#
# Optional
# Default: os.Stdout
#
filePath = "/var/log/traefik.log"
# Format is either "json" or "common".
#
# Optional
# Default: "common"
#
format = "json"
################################################################
# Access logs configuration
################################################################
# Enable access logs
# By default it will write to stdout and produce logs in the textual
# Common Log Format (CLF), extended with additional fields.
#
# Optional
#
# [accessLog]
# Sets the file path for the access log. If not specified, stdout will be used.
# Intermediate directories are created if necessary.
#
# Optional
# Default: os.Stdout
#
# filePath = "/path/to/log/log.txt"
# Format is either "json" or "common".
#
# Optional
# Default: "common"
#
# format = "json"
################################################################
# API and dashboard configuration
################################################################
# Enable API and dashboard
[api]
# Enable the API in insecure mode
#
# Optional
# Default: false
#
insecure = false
# Enabled Dashboard
#
# Optional
# Default: true
#
dashboard = true
################################################################
# Ping configuration
################################################################
# Enable ping
[ping]
# Name of the related entry point
#
# Optional
# Default: "traefik"
#
entryPoint = "traefik"
################################################################
# Docker configuration backend
################################################################
# Enable Docker configuration backend
[providers.docker]
# Docker server endpoint. Can be a tcp or a unix socket endpoint.
#
# Required
# Default: "unix:///var/run/docker.sock"
#
endpoint = "unix:///var/run/docker.sock"
# Default host rule.
#
# Optional
# Default: "Host(`{{ normalize .Name }}`)"
#
# defaultRule = "Host(`{{ normalize .Name }}.docker.localhost`)"
# Expose containers by default in traefik
#
# Optional
# Default: true
#
# 限制服务发现范围
# 如果设置为 false, 则没有 traefik.enable=true 标签的容器将从生成的路由配置中忽略
exposedByDefault = false
# network = "traefik"
# 动态配置
[providers.file]
filename = "dynamic_conf.toml"
watch = true
providers.file
,根据官方手册,可以修改立即生效。# dashboard界面
[http.routers.api]
rule = "Host(`traefik.zhouzhou.net`)" # 匹配规则
entrypoints = ["websecure"] #界面走443
service = "api@internal"
middlewares = ["myAuth"]
[http.routers.api.tls]
# 给dashboard准备的密码 nickzhuo 密码 test
[http.middlewares.myAuth.basicAuth]
users = [
"nickzhuo:$apr1$mattJycX$3UG/8ObEI4kowL9yBBfE01"
]
# 证书配置
[tls]
[[tls.certificates]]
certFile = "/data/ssl/api.zhouzhou.net_bundle.crt"
keyFile = "/data/ssl/api.zhouzhou.net.key"
[[tls.certificates]]
certFile = "/data/ssl/traefik.zhouzhou.net_bundle.crt"
keyFile = "/data/ssl/traefik.zhouzhou.net.key"
TKE和腾讯云的资源结合很好,和自己的云服务器CVM配合完美,如果有钱也可以用黑石服务器。
使用了TKE就不用关心Master节点了,因为完全被腾讯云托管了,换言之,不管你有几个node,你不用支付Master节点的费用。
随着TencentHub这个DevOps半成品被腾讯自己收购的coding取代,你会发现TKE的镜像特别好用,因为大部分走了容器这条路之后,大部分公司尤其初创企业他没有特别多的构建步骤,TKE的镜像纯BUILD是完全可以满足你的,如果你要走完成的持续集成也不难。
TKE的日志服务也很方便,可以走kafka,ES,或者你自己的CLS服务,如果是小项目直接cls的控制台操作操作就很方便,开箱即用。
TKE的K8S版本更新还是比较快的,尤其是Master节点被托管了,升级没有什么压力,如果node升级失败,移出集群重新加回来即可。
我自己用了2年TKE,看着他一点点增强,还是非常满意的,即使遇到问题,腾云客服的响应速度也是杠杠滴!