@version 180521:1
@author zhangxuhong <zhangxuhong@xitu.io>
container-guide - 容器化参考文档.
首先, 请阅读这个gitbook来补充有关kubernetes的相关知识. https://jimmysong.io/kubernetes-handbook/
- 简单了解 kubernetes 架构
我们假设接到了个计数器的需求 access-counter, 该需求要求用户传入自己的suid, 然后在redis中对该suid进行加一操作.
输出 json 结构为: {"suid":"ZFnUF6YraFRqRbY7izMm", "count":12}.
如果用户传入的suid为空, 则调用 kubernetes 集群中的 suid-generator 接口生成一个suid, 然后按照上面的格式返回.
注意: 本示例只是为了展示 kubernetes 使用, 这个例子存在很明显的问题, 比如没有鉴权, 以及生成 suid 不应该由计数器负责.
我们的容器化方案是 docker + kubernetes, 因此我们的第一个步骤就是将我们现有的业务装到docker中.
在装入 docker 之前, 我们需要简单修改一下程序来适应容器环境的一些需求.
log 问题
由于我们默认容器是不映射实体存储设备的, 也就意味着我们的容器销毁后里面的内容就全部丢失了, 所以在容器内部写日志本身就毫无意义.
因此, 我们需要将 info 级别的日志直接打印到 stdout, error 级别的日志直接打印到 stderr, 然后通过特定的日志收集程序进行统一处理.
// 打印到 stdout
error_log($log, 3, "php://stdout");
// 打印到 stderr
error_log($log, 3, "php://stderr");
// 注意 error_log 函数不是二进制安全的, 意味着如果 $log 变量中的字符含有 "\0" 的话, log 会被截断, 后半部分会丢失.
// 要么过滤文本中的 \0, 要么对文本进行转义(不推荐, 会导致日志人肉不可读), 要么保证文本没有 \0 (比如日志是你自己写的字面量).
-- 打印到 stdout
io.stdout.write(log)
-- 打印到 stderr
io.stderr.write(log)
// 打印到 stdout
fmt.Fprintln(os.Stdout, log)
// 打印到 stderr
fmt.Fprintln(os.Stderr, log)
os.Stderr.WriteString(log)
logInstance := log.New(os.Stderr, "", 0)
logInstance.Println(log)
// 总之 go 想打印的话方式还是很多的.
// @todo: 待好心的同学有时间补完这里, 我不会写js ...
注意以上只是你自己写的日志, 你的 runtime (例如: php-fpm, luajit, node.js) 本身也会报错, 你使用的框架也会报错. 因此还需要根据场景将 runtime 和框架的错误日志也写到 stderr. 否则线上出了故障要看日志只能 attach 到容器上去翻看了. 然后容器如果是触发故障就崩溃, kubernetes 会自动重启故障, 你的故障日志就消失不见了. 至于系统日志, 则会由 systemctl 统一接管, 可以用 journalctl -u {systemctlUnitName} -f 查看, 不用担心.
容器互相调用问题
既然容器隔离开了, 那么容器间怎么通信呢? 其实很简单, 直接调用容器的 service name, kubernetes 的内置 dns 就可以解析了.
location ~ \.php$ {
if ( $fastcgi_script_name ~ \..*\/.*php ) {
return 403;
}
include fastcgi.conf;
fastcgi_pass ac-counter:9000;
fastcgi_index index.php;
}
php 代码调用集群内其他服务的问题
同上, 直接写 service name 即可.
/**
* config here
*/
$conf = array(
'global' => array(
'global_id' => 'access-counter',
'folder' => '/data/repo/access-counter/',
),
'log' => array(
'open' => true,
'address' => '/data/repo/access-counter/logs/', // the '/' at end of line is necessary
'split' => 'day', // options: month, day, hour, minute
),
'cache' => array(
'access_counter_cache' => array(
'host' => 'ac-counter-rds',
// 'host' => '127.0.0.1',
'port' => 6379,
'pass' => null,
'database' => 0,
'timeout' => 0,
),
),
'suid_generator_api' => 'http://suid-generator/v1/gen_suid?src=%',
// 'suid_generator_api' => 'http://suid-generator-api-ms.juejin.im/v1/gen_suid?src=%',
);
php 代码调用集群外服务问题.
同样, 写 service name, 不过我们要建立一个代理用的 service, 我们在下面的小节讲述这个问题.
生产环境问题
我们尽量遵循原则 "不在代码中内嵌环境信息" 的原则. 所以我们需要通过外部配置文件来根据环境来进行配置. 我们来列一下我们需要根据生产环境来切换的资源
接口
数据库 同接口
逻辑 这个是最难的, 比如我们想在接口中输出当前环境是 test, beta 还是 prod. 这时候就必须让代码(逻辑)感知到环境. 注意, 这种能没有就不要有. 他破坏了我们代码的可部署性, 试想一下哪天我们多了个环境叫beta2, 你没准就要痛苦的修改1000多个repo的代码. 因此, 良好的设计是, 获取当前环境的名称, 然后打印出来, 这样逻辑只是"获取环境参数, 并打印"跟环境无关. 针对 php-fpm 的场景, 我们有几种方式获取当前环境.
env["SERVER_ENV"] = $SERVER_ENV
然后 我们在 deployments 文件中设置环境变量
# ac-counter-deployment.yaml
#
# @version 180806:2
# @author zhangxuhong <zhangxuhong@xitu.io>
kind: Deployment
apiVersion: apps/v1
metadata:
name: ac-counter
labels:
name: ac-counter
role: backend
pl: php
application: php
version: 7.2.9
division: infrastructure
spec:
replicas: 3
selector:
matchLabels:
name: ac-counter
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
template:
metadata:
labels:
name: ac-counter
spec:
containers:
- name: ac-counter
image: __IMAGE__
imagePullPolicy: Always
ports:
- name: ac-counter
containerPort: 9000
protocol: TCP
env:
- name: SERVER_ENV
value: "test"
首先我们改造好repo后, 接下来就可以开始装入容器了. 下面开始编写Dockerfile.
构建镜像的注意事项详见 build-a-docker-image.md
# ac-counter-ngx.dockerfile
# Dockerfile for demo ac-counter
# This docker file base on harbor02.juejin.id/lib/php:7.2.9-fpm-alpine3.8
# @version 180719:2
# @author zhangxuhong <zhangxuhong@xitu.io>
#
# base info
FROM harbor02.juejin.id/infrastructure/nginx-1.14.0-centos:latest
MAINTAINER zhangxuhong <zhangxuhong@xitu.io>
USER root
# copy config to /data/apps/nginx/conf/vhost/
COPY ./config/nginx/ /data/apps/nginx/conf/vhost/
# define health check
HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs http://127.0.0.1:80/status?src=docker_health_check -H"Host:access-counter-api.juejin.im" || exit 1
# run php-fpm
EXPOSE 80
ENTRYPOINT ["/data/apps/nginx/sbin/nginx", "-g", "daemon off;"]
# ac-counter.dockerfile
# Dockerfile for demo access-counter
# This docker file base on harbor02.juejin.id/lib/php:7.2.9-fpm-alpine3.8
# @version 180719:2
# @author zhangxuhong <zhangxuhong@xitu.io>
#
# base info
FROM harbor02.juejin.id/lib/php:7.2.9-fpm-alpine3.8
MAINTAINER zhangxuhong <zhangxuhong@xitu.io>
USER root
# init extension
RUN apk add --update --no-cache --virtual .build-deps \
curl \
g++ \
gcc \
gnupg \
libgcc \
make \
alpine-sdk \
autoconf
RUN pecl install redis-4.1.1 && docker-php-ext-enable redis
# copy repo to /data/repo
COPY . /data/repo/access-counter/
# define health check
HEALTHCHECK --interval=5s --timeout=3s CMD netstat -an | grep 9000 > /dev/null; if [ 0 != $? ]; then exit 1; fi;
# run php-fpm
EXPOSE 9000
ENTRYPOINT ["php-fpm"]
装入完毕后开始构建镜像准备本地测试.
docker build ./ -t suid-generator
然后运行镜像进行测试.
docker run suid-generator
docker exec -i -t {docker id} /bin/sh
curl http://{docker-port-ip}/status?src=tester -H"Host: access-counter-api.juejin.im"
如果curl正常返回结果就代表测试成功了.
准备 kubernetes 配置文件
Deployment
Deployment文件负责描述整个部署的 Pods 和 ReplicaSets.
Service
Service 负责映射配置, Service将服务名称与具体的Pod及暴露的端口映射到一起这个映射关系就叫endpoints. Service 映射外部IP或者域名见 mapping-external-services.md
Ingress
Ingress 负责配置负载均衡, 根据提供的域名和path将业务路由到指定的service.
Endpoints
endpoint 用来描述 service 对应的流量关系.
具体的配置文件请结合 access-counter 项目学习.
Jenkinsfile
Jenkinsfile 其实是Groovy脚本, 通过配置来描述部署过程和配置.
具体编写和注意事项见 jenkins-pipline-usage.md
创建 pipline jenkins 任务
注意最好按照我们的命名规则 {repoName}.{clusterNamespace}.{clusterName} 来给jenkins 任务命名.
配置 gitlab trigger
多生产环境问题
如何debug?
如何扩容?
直接修改Deployment文件中的replica数量, 然后CI流程重新部署.
用kubectl命令
kubectl autoscale deployment {deploymentName} --min=2 --max=10
kubectl scale --replicas=3 -f {deploymentFile}
建议除了测试以外用第一种进行扩容, 否则线上与git中的 deployment 文件不一致, 再次发办可能会面临风险.
如何删除?
我们部署了总计三个 resource: deployment, service, ingress.
那么直接执行kubectl delete {resourceName} {repoName} --namespace={yourDeploymentNamespace} 即可.
例如:
kubectl delete deployment {repoName} --namesapce=test
kubectl delete service {repoName} --namesapce=test
kubectl delete ingress {repoName} --namesapce=test
如果感兴趣可以阅读其他参考资料书籍(按推荐程度排序):
name | ip address | location | description |
---|---|---|---|
docker-client01v.lobj.juejin.id | 192.168.0.233 | lobj | 本地测试集群01 |
name | location | description |
---|---|---|
test.kube01.lobj.juejin.id | lobj | 本地测试集群01 |
beta.kube01.lobj.juejin.id | lobj | 本地beta测试集群01 |
prod.kube01.qcbj3b.juejin.id | qcbj3b | 青云北京3B区线上集群01 |
注意要严格按照列表中的IP和PORT的对应关系来调用PORT, 否则可能会发生ingress流量调度会不起作用或大量流量打到同一个IP上的问题, 业务就无法访问了.
type | ip | port | cluster | instance |
---|---|---|---|---|
test | 192.168.0.159 | 80 | test.kube01.lobj.juejin.id | traefik |
test | 192.168.0.158 | 8000 | test.kube01.lobj.juejin.id | traefik |
test | 192.168.0.157 | 8080 | test.kube01.lobj.juejin.id | traefik |
type | ip | port | cluster | instance |
---|---|---|---|---|
beta | 192.168.0.99 | 80 | beta.kube01.lobj.juejin.id | traefik |
beta | 192.168.0.98 | 8000 | beta.kube01.lobj.juejin.id | traefik |
beta | 192.168.0.97 | 8080 | beta.kube01.lobj.juejin.id | traefik |
type | ip | port | cluster | instance | comment |
---|---|---|---|---|---|
prod | 172.16.0.199 | 80 | prod.kube01.qcbj3b.juejin.id | traefik | |
prod | 172.16.0.198 | 8000 | prod.kube01.qcbj3b.juejin.id | traefik | |
prod | 172.16.0.197 | 8080 | prod.kube01.qcbj3b.juejin.id | traefik | |
prod | 139.198.15.232 | 80/443 | prod.kube01.qcbj3b.juejin.id | traefik | 线上外网出口 |
prod | 139.198.14.107 | 80/443 | prod.kube01.qcbj3b.juejin.id | traefik | 线上外网出口 |
ip | hostname | role | disk |
---|---|---|---|
192.168.0.157 | ingress-8080.test.kube01.lobj.juejin.id | ingress-vip | |
192.168.0.158 | ingress-8000.test.kube01.lobj.juejin.id | ingress-vip | |
192.168.0.159 | ingress-80.test.kube01.lobj.juejin.id | ingress-vip | |
192.168.0.160 | test.kube01.lobj.juejin.id | master-vip | |
192.168.0.171 | etcd01v.lobj.juejin.id | etcd | 40GB iSCSI |
192.168.0.172 | etcd02v.lobj.juejin.id | etcd | 40GB iSCSI |
192.168.0.173 | etcd03v.lobj.juejin.id | etcd | 40GB iSCSI |
192.168.0.161 | kubernetes-master01v.lobj.juejin.id | kubernetes-master | 100GB iSCSI |
192.168.0.162 | kubernetes-master02v.lobj.juejin.id | kubernetes-master | 100GB iSCSI |
192.168.0.163 | kubernetes-master03v.lobj.juejin.id | kubernetes-master | 100GB iSCSI |
192.168.0.164 | kubernetes-node01v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
192.168.0.165 | kubernetes-node02v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
192.168.0.166 | kubernetes-node03v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
192.168.0.167 | kubernetes-node04v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
192.168.0.168 | kubernetes-node05v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
ip | hostname | role | disk |
---|---|---|---|
192.168.0.97 | ingress-8080.beta.kube01.lobj.juejin.id | ingress-vip | |
192.168.0.98 | ingress-8000.beta.kube01.lobj.juejin.id | ingress-vip | |
192.168.0.99 | ingress-80.beta.kube01.lobj.juejin.id | ingress-vip | |
192.168.0.100 | beta.kube01.lobj.juejin.id | master-vip | |
192.168.0.121 | etcd04v.lobj.juejin.id | etcd | 40GB iSCSI |
192.168.0.122 | etcd05v.lobj.juejin.id | etcd | 40GB iSCSI |
192.168.0.123 | etcd06v.lobj.juejin.id | etcd | 40GB iSCSI |
192.168.0.101 | kubernetes-master04v.lobj.juejin.id | kubernetes-master | 100GB iSCSI |
192.168.0.102 | kubernetes-master05v.lobj.juejin.id | kubernetes-master | 100GB iSCSI |
192.168.0.103 | kubernetes-master06v.lobj.juejin.id | kubernetes-master | 100GB iSCSI |
192.168.0.104 | kubernetes-node06v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
192.168.0.105 | kubernetes-node07v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
192.168.0.106 | kubernetes-node08v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
192.168.0.107 | kubernetes-node09v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
192.168.0.108 | kubernetes-node10v.lobj.juejin.id | kubernetes-node | 100GB iSCSI |
ip | hostname | role | disk |
---|---|---|---|
172.16.0.197 | ingress-8080.prod.kube01.qcbj3b.juejin.id | ingress-vip | |
172.16.0.198 | ingress-8000.prod.kube01.qcbj3b.juejin.id | ingress-vip | |
172.16.0.199 | ingress-80.prod.kube01.qcbj3b.juejin.id | ingress-vip | |
172.16.0.200 | prod.kube01.qcbj3b.juejin.id | master-vip | |
172.16.0.11 | etcd01v.qcbj3b.juejin.id | etcd | 40GB SSD |
172.16.0.12 | etcd02v.qcbj3b.juejin.id | etcd | 40GB SSD |
172.16.0.13 | etcd03v.qcbj3b.juejin.id | etcd | 40GB SSD |
172.16.0.14 | kubernetes-master01v.qcbj3b.juejin.id | kubernetes-master | 100GB SSD |
172.16.0.15 | kubernetes-master02v.qcbj3b.juejin.id | kubernetes-master | 100GB SSD |
172.16.0.16 | kubernetes-master03v.qcbj3b.juejin.id | kubernetes-master | 100GB SSD |
172.16.0.17 | kubernetes-node01v.qcbj3b.juejin.id | kubernetes-node | 100GB SSD |
172.16.0.18 | kubernetes-node02v.qcbj3b.juejin.id | kubernetes-node | 100GB SSD |
172.16.0.19 | kubernetes-node03v.qcbj3b.juejin.id | kubernetes-node | 100GB SSD |
172.16.0.20 | kubernetes-node04v.qcbj3b.juejin.id | kubernetes-node | 100GB SSD |
172.16.0.21 | kubernetes-node05v.qcbj3b.juejin.id | kubernetes-node | 100GB SSD |