
嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤
能力说明:
了解变量作用域、Java类的结构,能够创建带main方法可执行的java应用,从命令行运行java程序;能够使用Java基本数据类型、运算符和控制结构、数组、循环结构书写和运行简单的Java程序。
能力说明:
熟练掌握Docker各类高级特性,包括容器数据卷、DockerFile构建等;熟练使用Docker封装MySQL、Redis、Tomcat、Apache等镜像,并可在公有云或私有云部署并保持稳定运行。
能力说明:
掌握计算机基础知识,初步了解Linux系统特性、安装步骤以及基本命令和操作;具备计算机基础网络知识与数据通信基础知识。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明openshift 是红帽做的一个 kubernetes 发行版,相当于 rancher 的竞品。红帽公司 kubernetes 的基础上,引入了安全机制,身份验证,网络监控,日志可视化等特性,试图在云原生领域分一杯羹。scc(Security Context Constraints)最近在 openshift 上面部署 traefik 出现了点问题。Error creating: pods "traefik-ingress-controller-68cc888857-" is forbidden: unable to validate against any security context constraint: [provider restricted: .spec.securityContext.hostNetwork: Invalid value: true: Host network is not allowed to be used spec.containers[0].securityContext.capabilities.add: Invalid value: "NET_BIND_SERVICE": capability may not be added spec.containers[0].securityContext.hostNetwork: Invalid value: true: Host network is not allowed to be used spec.containers[0].securityContext.containers[0].hostPort: Invalid value: 80: Host ports are not allowed to be used spec.containers[0].securityContext.containers[0].hostPort: Invalid value: 443: Host ports are not allowed to be used spec.containers[0].securityContext.containers[0].hostPort: Invalid value: 8080: Host ports are not allowed to be used]根据错误提示,找到了问题点在于 scc 。官方的介绍如下:OpenShift 的 安全環境限制 (Security Context Constraints)類似於 RBAC 資源控制用戶訪問的方式,管理員可以使用安全環境限制(Security Context Constraints, SCC)來控制Pod 的權限。 您可以使用 SCC 定義 Pod 運行時必須特定條件才能被系統接受。简单地说,scc 是在 rbac 的基础之上,对用户的行为进行了一些限制。包括上文提到的hostnetwork,SecurityContext 等。相当于 openshift 在 PodSecurityPolicy 上面做了一层封装。默认情况下,openshift包含以下8种scc。anyuidhostaccesshostmount-anyuidhostnetworknode-exporternonrootprivilegedrestricted而创建的pod资源默认归属于Restricted策略。管理员用户也可以创建自己的 scc 并赋予自己的 serviceaccount:apiVersion: security.openshift.io/v1 kind: SecurityContextConstraints metadata: annotations: kubernetes.io/description: traefikee-scc provides all features of the restricted SCC but allows users to run with any UID and any GID. name: traefikee-scc priority: 10 allowHostDirVolumePlugin: true allowHostIPC: false allowHostNetwork: false allowHostPID: false allowHostPorts: false allowPrivilegeEscalation: true allowPrivilegedContainer: false allowedCapabilities: - NET_BIND_SERVICE defaultAddCapabilities: null fsGroup: type: RunAsAny groups: - system:authenticated readOnlyRootFilesystem: false requiredDropCapabilities: - MKNOD runAsUser: type: RunAsAny seLinuxContext: type: MustRunAs supplementalGroups: type: RunAsAny users: [] volumes: - configMap - downwardAPI - emptyDir - persistentVolumeClaim - projected - secret oc create -f new-sa.yaml oc create -f new-scc.yaml oadm policy add-scc-to-user new-scc system:serviceaccount:monitor:new-sa所以如果创建的资源未就绪,可以用 kubectl describe pod 看一下是否触犯了 scc 的限制。回到原题,我之所以想部署 traefik 是想做一个接入的控制平面。但是在 openshift 平台上面,其实有自己的一种实现,这种实现叫做 route。route 相关问题同域名默认情况只允许一个命名空间默认情况下禁止同域名跨namespace,需要启用该特性以支持,否则创建 route 会出现 a route in another namespace holds XX 。需要修改 openshift 的内置控制器配置以支持同域名跨namespace route。oc -n openshift-ingress-operator patch ingresscontroller/default --patch '{"spec":{"routeAdmission":{"namespaceOwnership":"InterNamespaceAllowed"}}}' --type=merge泛域名解析建立泛域名解析的 route 时,会提示wildcard routes are not allowed。openshift3可以通过设置ROUTER_ALLOW_WILDCARD_ROUTES 环境变量; openshift4不支持,该问题无解。 参考 https://github.com/openshift/enhancements/blob/master/enhancements/ingress/wildcard-admission-policy.mdingress 转换为了适配大家在其他平台使用的 ingress 。openshift 做了一点兼容性处理,创建 ingress 时会对应创建 route。而如果ingress 中带 TLS ,openshift 也会转换成对应的 route。但 openshift 的route,tls 公私钥是直接存在 route 中的,而不是 secret 。多path解析如果原先的 ingress 存在针对同域名的多path前缀解析。比如ingress a 监听 域名 a 的 /a 路径;ingress b 监听域名 a 的 /b 路径,那么类似 traefik 的 url rewrite 规则,在注解里面也需要加入 rewrite 注解。openshift 会把这个注解加入到转换的 route 中。annotations: haproxy.router.openshift.io/rewrite-target: /网络策略如果应用无法访问跨namespace service/pod,具体体现是请求长时间没有响应。这应该是这个命名空间开启了隔离,需要用oc客户端赋权。oc adm pod-network make-projects-global <project1> <project2>反过来,如果用户要让某个命名空间(在openshift里面也叫做 project)只能namespace 内互访问,则可以这么操作:oc adm pod-network isolate-projects <project1> <project2>CRI问题目前已知的容器运行时有以下三个:containerdCRI-ODockeropenshift 用的是 cri-o 。如果部署的应用强依赖于 containerd/docker ,则部署会导致失败。比如 openkruise 项目就不支持 openshift 。参考链接[1]https://ithelp.ithome.com.tw/articles/10243781[2]https://kubernetes.io/docs/concepts/policy/pod-security-policy/[3]https://cloud.tencent.com/developer/article/1603597[4]https://docs.openshift.com/container-platform/4.8/rest_api/network_apis/route-route-openshift-io-v1.html[5]https://docs.openshift.com/container-platform/3.5/admin_guide/managing_networking.html[6]
斯芬克斯是 地狱双头犬 的女儿,她有一个经典的谜语。叫做斯芬克斯之谜: Which creature has one voice and yet becomes four-footed and two-footed and three-footed? 斯芬克斯的这个谜语代表了自我认知的困难。今天,我就来试着解答这个谜语。 《Docker源码分析》 要想成为一个优秀的云原生开发工程师。这本书是必看的。虽然 docker 容器历经迭代之后,很多设计已经废弃,这本书信息的参考度已经略有下降。但从这本书可以学到一种脚踏实地的治学思想。在国内《21天学会C++》的浮躁技术风盛行的劣币驱逐下,这本书是一股清流,值得收藏。 如果你能拿到这本书的第一版,然后拿到 孙宏亮 的签名,收藏价值 * N 。不过前提是得出卖色相,给 dragonflyoss 贡献代码。 《UNIX环境高级编程(第三版)》 Docker 底层的核心技术包括 Linux 上的命名空间(Namespaces)、控制组(Control groups)、Union 文件系统(Union file systems)和容器格式(Container format)。 本质上,容器技术依附于 Unix 系统,所以初步了解了 docker 容器技术之后,要继续往上发展,得对 Unix 系统有所了解。很多问题溯源分析到最后都会发现,问题出自 Unix 系统本身的设计。 这本书目测是CNCF中国大使张磊的读物。 《活着》 信息技术有着一条非常长的学习链路。但当你学完了基础知识,熬过了无数的夜,加了无数的班之后,基本上可以在以下几本书中汲取新的知识: 《颈椎病康复指南》 《腰椎间盘突出日常护理》 《心脏病的预防与防治》 《高血压降压宝典》 《强迫症的自我恢复》 《精神病症状学》 到了这个时候,就得看余华写的《活着》。脊椎病的治疗其实很简单,练习 下腰 即可。至于其他疾病,只能靠多运动解决,比如慢跑,游泳,打篮球等。 《六祖坛经》 第四重境界叫做万法皆空。如果你经过了第三重境界。头发都快掉光了,离出家也不远了。 《龟虽寿》 神龟虽寿,犹有竟时; 腾蛇乘雾,终为土灰。 老骥伏枥,志在千里; 烈士暮年,壮心不已。 盈缩之期,不但在天; 养怡之福,可得永年。 幸甚至哉,歌以咏志。 第五重境界简单概括叫做“如日东山能在起,大鹏展翅恨天低。” 结论 少年去游荡,中年想掘藏,老年做和尚。 参考链接 [1]程序员的自我修养的四个阶段,你在哪一阶段?https://bbs.csdn.net/topics/391852760 [2]六祖坛经https://www.liaotuo.com/fojing/liuzutanjing/yuanwen.html [3]Sphinxhttps://en.wikipedia.org/wiki/Sphinx [4]the-underlying-technologyhttps://docs.docker.com/get-started/overview/#the-underlying-technology [5] [6]
首先,我们先理清一个概念: 关系型数据库是非关系数据库的真子集,非关系数据库是时间序列数据库的真子集。 关系型数据库 关系型数据库是这样定义的: 采用了关系模型来组织数据的数据库,其以行和列的形式存储数据,以便于用户理解,关系型数据库这一系列的行和列被称为表,一组表组成了数据库。 关系型数据库的集大成者是 MySQL 。关系型数据库的问题在于强调“关系”。但实际上,对象之间的关系是弱关系。强关系只是一种特例。 举个例子。我们定义三个对象:小明,小红和小王。小明和小红是同居的情侣,小王住在他们隔壁。有一天,小明回到家看到小红和小王呆在同一个房间里。这时小明会有什么想法呢? 他到底应该说小王和小红在过家家呢,还是认同他们的解释,认为小王是在给房间里换电灯泡呢? 这个关系的问题,其实就是关系型数据库的最大问题——乱搞关系。在现在很多开发设计规范里面,已经把关系型数据库当做一个数据存储的仓库(堆表),禁止存储过程,也禁止了外键。可以说,这是历史发展的必然结果。 非关系型数据库 那么,非关系型数据库,自然就是 NoSQL 。 NoSQL,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在处理web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,出现了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题。 其中有以 Redis 为代表的 key-value 数据库,也有以 MongoDB 为代表的文档型数据库。 Redis 是基于内存的(虽然也可以持久化到硬盘),内存是相当珍贵的资源。资源的有限限制了资源的广泛使用。 MongoDB 其实有点像 Excel ,在 Excel 文件里面,每一行可以有任意个单元格。 比如,新亘结衣在 MongoDB 中的“爱好”和“配偶”不是空(Null),而是压根就没有这个字段。 姓名 性别 电话 爱好 偶像 特长 王蟑螂 男 +8617051026064 吃耙耙 新亘结衣 新亘结衣 女 +8113766621544 唱歌 如果说 Redis 的问题在于内存过于珍贵,那么我觉得非关系型数据库的问题在于描述的对象“不够自然”。 时间序列数据库 “不够自然”的意思,就是缺少“时间”这个属性。实际上,时间才是数据的第一属性。任何数据如果没有时间这个属性,那么都变得毫无意义。所以很多表在设计的时候,从规范层面就是这样说的:要有创建时间这个属性。 除此以外,时间序列数据库应该满足这样一个特征:不可变性(Immutable) 。不可变性是指数据录入之后不可变更。数据库的业务靠实时数据流引擎去处理。 时间序列数据库,才是最切合现实世界的模型。举个最简单的例子:我要用小拳拳捶你胸口。我们先简化一下这个动作,然后再录入到时序数据库里面吧 ~ “小拳拳捶你胸口”,最简化的模型就是,描述一个点在三维空间里面“线段”(因为移动的距离有限,所以叫线段)运动到另外一个点。 在三维空间里面,求两点之间的距离,其实跟在一维空间求两点的距离,没啥差别,泛化勾股定理就行了。我会这样说,勾股定理,是升高维度的“梯子”。 有这样的认识,在时序数据库里面,定义数据非常之自然而简单。还是以这个问题为例,解决这个问题,只需要隔一段采集数据,并录入即可。 时间 X Y Z 2020-04-01 09:35:00 1 1 1 2020-04-01 09:35:01 2 2 2 2020-04-01 09:35:02 3 3 3 2020-04-01 09:35:03 3 3 3 监控分析 杭州迪火科技监控平台负责人董兵林是这样说的:“(阿里云)TSDB 帮助我们解决了指标数据存储的问题,其表现出的极佳性能,零运维成本,数据永久存储,专门的技术支持,都是我们一直使用的原因。目前我们可以很方便的查看实时指标信息和回溯历史的指标信息,及时发现问题,为进一步的决策提供依据,TSDB 是我们不可缺少的一部分。” 趋势分析 趋势分析,是用来预测未来的一种概率。比如,在上面的数据中,用三维直角坐标系建模,就会得到一条线性趋势线。我们可以预测2020-04-01 09:35:04,我的小拳拳大概率会落于(4,4,4)这个坐标点。 溯源分析 溯源分析跟趋势分析是相反的,是时间倒序的另外一种数据结构。 时间 X Y Z 2020-04-01 09:35:03 3 3 3 2020-04-01 09:35:02 3 3 3 2020-04-01 09:35:01 2 2 2 2020-04-01 09:35:00 1 1 1 在这个问题中,我们建立一条线性趋势线之后,可以用溯源推理分析出2020-04-01 09:34:59我的拳头大概率位于 (0,0,0) 。 也就是说,这是用来预测“过去”的一种概率。溯源分析可以用于刑侦破案,历史遗留问题溯因等场景。 现在的时间序列化数据库,每一行的记录还是比较结构化的。而且,被大量应用于监控领域,这其实是一种误解。广义的时间序列数据库,每一行数据支持任意属性。 结论 我相信在未来,时序数据库将超越关系型数据库和非关系型数据库,成为应用开发的首选方案。 参考链接 关系型数据库 NoSQL 散点图是什么?有什么用? 时间序列数据库 TSDB
web 本质是一种流量,一种数据的流转。当前的 web 只是 Serverless 的一种特例(存活期很长的 Serverless )。如果从这个角度上看,其实广州地铁是一个很优美的 Serverless 系统。 高效率的web,本质是一种希望数据的流转尽可能快(TPS越高越好),这跟广州的地铁的设计理念是不谋而合的。我之前在知乎上说过,广州地铁本质上是一个排水系统 ,他设计的目的不是把你运送到目的地,而是希望你尽快离站。所以现在就算你进站后再出站,也要收费(以前有一段时间我记得不用)。 回到本题。 Serverless 是一种存活期相对较短的设计。比如广州地铁的存活期一般是日间,到了夜间设备要进行维护。我们把整个问题简单化,只取一号线来讲。假设平时只运行 3~50 辆车。那么低峰期应该只运行3辆车(降低成本),高峰期应该在确保安全的前提下,把运营效率尽可能提高(增加班次)。而到了晚上,众鸟归巢。运行实例为0。 Serverless 的优势 减少成本:每一个站台只需要少量的广州地铁人员,就能撑起一个站点。地铁工作人员无需关注底层系统(地铁)的运维。他们只需要在调度室吹空调,按按这个按按那个按钮就行。 快速创新:以前广州地铁需要人工买票什么的,现在支持银联卡,NFC,微信,支付宝计费。 按需使用、灵活弹性:业务可以根据配置的条件灵活调配资源,它会在夜间睡觉(运行实例为0),在低峰期低开(运行实例为3),在高峰期高开(运行实例50),在超高峰期限流,从而实现资源的最大化利用与运营成本的压缩。 广州地铁其实是一种实时数据流处理 从乘客角度,搭地铁本质上是一种数据库事务。持久性(Durability)是妹子要么上车,要么不决定搭地铁;一致性(Consistency)是指妹子上车下车会按照她的期望值,如果她下错站了,说明了她没有实现一致性;隔离性(Isolation)是指妹子懒得理你;持久性(Durability)是指妹子上车这一行为有地铁系统作证(上下车买票的凭证)。 而从地铁角度,整个广州地铁其实是实现了分布式事务的一套实时数据流处理系统。 广州地铁2020 前端常驻,后端动态高效伸缩容 我们会发现,那些固定设施都固定在那里的。基础设施是一种“前端”。而地铁列车是一种“后端”。 因为广州地铁事关人命,所以在伸缩容方面采取的策略是根据过往数据预测未来。而伸缩容目前是用人工方式解决。 这一点也是一种提醒。我们在设计 web 系统的时候通常分前后端。如果按照 Serverless 角度来重新思考这个问题。那么应该让前端资源常驻(前端只是一些简单的静态文件,占用服务器资源很少),而让后端动态伸缩。这样就不会影响用户体验。 流量切分 高峰期广州地铁的运营人员会设置各个栅栏。超高峰期会飞站或者拒绝乘客搭乘。从而实现数据的安全(乘客的安全)。 计量能力 根据乘客的搭乘距离计费。而站台距离本质上是时间。可以说使用时间越长,费用越高。 后端的单一职责 基本上广州地铁是偏向于人运,而不是货运。每次我要搬家的时候,搭广州地铁总是相对比较麻烦。因为一开始我也说了,广州地铁的设计理念就是一个排水系统。如果变成货运的话,运营效率会大幅度降低。 所以,我们在设计 Serverless 后端微服务的时候,也要记住这一点。尽可能让后端微服务的职责尽可能小,小到甚至不需要微服务发现系统。举个例子,xx网盘通过 Serverless 服务处理瞬时的视频转码请求。 结论 广州地铁其实是一个运营很高效的 Serverless 系统。它总结了过往数十年的人流量数据,高效运营的同时保证乘客的安全(数据安全)。
docker company docker company 是大奸商万代公司。虽然他们卖的模型很贵,但是质量还不错。 OCIv1 自由高达1.0的黑白图纸。 OCIv2 自由高达2.0的彩色图纸。它是彩色的,主要是为了解决 OCIv1的历史遗留问题。 关于OCIv1的问题,我在《Dragonfly Nydus——下一代容器格式的一种实现》里面有说。 PouchContainer 自由高达流星装。 docker image docker image 是一堆不会动的数据。 docker container running container 当docker image 动起来之后,就是一只会动的高达模型。 stopped container stopped container 没电了,所以停住了。
最近在学习造核弹,刚好碰到 Dragonfly 群里,来自蚂蚁金服的巴德大佬在直播分享另外一个子项目Nydus。 按照我的理解,Nydus是下一代容器格式的一种实现。其存在主要是为了解决旧的容器格式(container format)存在的问题。 那么问题来了,什么是 Nydus?什么是 container format? Nydus 具体解决了什么问题。这里先来一波名词扫盲。 名词扫盲 实际上,容器技术从1979年发展至今已经超过40年,docker 只能说是目前为止,其中一种比较著名而流行的实现。可以说,docker 解决了应用分发的难题,为日后 kubernetes 的流行奠定了基础。 但是,俗话说得好,勇士战胜恶龙之日,自身亦化作恶龙。不管是 Docker 公司后来各种神操作(把项目改名 Moby ,docker swarm 的弱鸡编排)也好,CoreOS 的崛起也罢,开源世界的战争,是一种技术标准的全球话语权争夺,这种争夺远比你想象的要残酷。 OCI OCI全称 Open Container Initiative ,隶属于Linux基金会,是Docker, CoreOS联合其他容器厂商,于 2015-6-22 建立的一个开源组织。其目的主要是为了制定容器技术的通用技术标准。 OCI旗下主要有2个项目: runtime-spec image-spec OCIv1 OCIv1) 就是目前的容器格式。 OCIv2 OCIv2 就是为了解决 OCIv1 的历史技术债务。 Dragonfly Nydus 2020年4 月 10 日,由云原生计算基金会(CNCF)技术监督委员会投票决议,来自中国的开源项目 Dragonfly 正式晋升为 CNCF 孵化级别的托管项目,成为继 Harbor、TiKV 之后,第三个进入 CNCF 孵化阶段的中国项目。 Dragonfly 的架构主要是为了解决了大规模镜像下载、远距离传输、带宽成本控制、安全传输这四大难题。 Nydus 是OCIv2的一种实现,计划捐给 Dragonfly ,作为其旗下一个子项目运作。 当前容器格式的问题 在直播分享中,巴德大佬提到了OCIv1的几个问题: 分层效率很低 数据没有校验 可重建性问题 分层效率很低 分层效率低主要是指冗余性。如果把 docker image 比喻作汉堡包,镜像A是吉士汉堡包。 FROM centos 镜像B是双层吉士汉堡包。 FROM centos RUN yum update -y # 拉取吉士汉堡包 docker pull h1 # 拉取双层吉士汉堡包 docker pull h2 那么按照目前的设计,镜像之间是独立的,也就是说,拉取h1之后,虽然磁盘里面已经缓存了 centos 的底层镜像,但是拉取h2的时候,还是重新拉取整个镜像,并没有复用 centos 那个底层镜像。最终导致了磁盘的冗余和网络流量的浪费。 数据没有校验(Verifiability) 这里稍加引述巴德大佬的话: 只读层被修改了,容器应用是不知道的。现在的OCI镜像格式下就有可能发生这种事情,镜像在构建和传输过程中是可校验的,但是镜像下载到本地后会被解压,解压后的文件的修改是无法探知的。 镜像需要下载,解压到本地文件系统,然后再交给容器去使用。这个流程中,解压到本地文件系统这一步是丢失可信的关键。 workspace 可重建性问题(repairability) 可重建性可以从某种程度上解决 docker build 慢的问题。 以轻量级 kubernetes event导出组件 kube-eventer为例, FROM golang:1.14 AS build-env ADD . /src/github.com/AliyunContainerService/kube-eventer ENV GOPATH /:/src/github.com/AliyunContainerService/kube-eventer/vendor ENV GO111MODULE on WORKDIR /src/github.com/AliyunContainerService/kube-eventer RUN apt-get update -y && apt-get install gcc ca-certificates RUN make FROM alpine:3.10 COPY --from=build-env /src/github.com/AliyunContainerService/kube-eventer/kube-eventer / COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ ENV TZ "Asia/Shanghai" RUN apk add --no-cache tzdata COPY deploy/entrypoint.sh / ENTRYPOINT ["/kube-eventer"] 我们在机器A上反复执行docker build,每次的构建都是原子的,也就是说,每一次都是从上到下重新跑一遍。但实际上我们知道,很多指令都是重复的,没有必要重复执行。 可重建性还有另外一层意思,从机器A拷贝到机器B之后,继续构建docker image。 我的吐槽 在我看来,目前的 OCIv1 借鉴了 git 的设计,但本质是一个很难吃的汉堡包。只有最上面一层可以吃(读写)。 云原生应用的交付周期,一般是 git Ops --> CI --> docker image --> waiting container/pod(docker pull,sandbox etc) --> running container/pod --> terminated container/pod 云原生应用的安全性由运行时环境和 docker container 组成,一个安全的 docker container ,应当尽量让它在各个环节里面,都没有可乘之机。 比如,从代码到CI的过程中,应当有静态代码分析 + 人工 code review 的机制,确保代码无安全性上的问题;从CI到 docker image 的构建过程中,应当让CI运行在一个可信的环境。这个可信的环境包括了可信的权威DNS,可控的安全防火墙,受限的网络连接以及安全扫描套件(杀毒软件)。 从这个层面上讲,Nydus 计算每一层哈希,不仅不是很专业,而且很慢。这一块内容交给更高效的安全引擎,Nydus 做个异步事件回调/消息发布订阅,也许更好。 综上所述,结合短桶原理,可以得出这样的结论:容器的安全性需要各方协调,云原生应用不存在绝对意义上的安全。 最后,欢迎大家加入Dragonfly项目,项目钉群群主是《Docker源码分析》的作者孙宏亮。在国内《21天学会XX》垃圾技术书风行的大背景下,这本书是一股清流。 同时也欢迎大家参与OCIv2标准的共建。 结论 PPT first,bug secondly. 我想暗中买一批孙宏亮大佬写的第一版《Docker源码分析》,之后再潜入阿里云,要到他的亲笔签名,最后再转卖出去 参考链接 [1]docker、oci、runc以及kubernetes梳理https://xuxinkun.github.io/2017/12/12/docker-oci-runc-and-kubernetes/ [2]About the Open Container Initiativehttps://opencontainers.org/about/overview/ [3]The Road to OCIv2 Images: What's Wrong with Tar?https://www.cyphar.com/blog/post/20190121-ociv2-images-i-tar [4]重磅 | Dragonfly 晋升成为 CNCF 孵化项目https://developer.aliyun.com/article/754452
连接api-server一般分3种情况: Kubernetes Node通过kubectl proxy中转连接 通过授权验证,直接连接(kubectl和各种client就是这种情况) kubectl加载~/.kube/config作为授权信息,请求远端的api-server的resetful API.api-server根据你提交的授权信息判断有没有权限,有权限的话就将对应的结果返回给你。 容器内部通过ServiceAccount连接 容器请求api-server Kubernetes这套RBAC的机制在之前的文章有提过.这里就不解释了 为了方便起见,我直接使用kube-system的admin作为例子. # {% raw %} apiVersion: v1 kind: ServiceAccount metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{},"name":"admin","namespace":"kube-system"}} name: admin namespace: kube-system resourceVersion: "383" secrets: - name: admin-token-wggwk --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: "true" labels: kubernetes.io/bootstrapping: rbac-defaults name: cluster-admin resourceVersion: "51" rules: - apiGroups: - '*' resources: - '*' verbs: - '*' - nonResourceURLs: - '*' verbs: - '*' --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: "true" labels: kubernetes.io/bootstrapping: rbac-defaults name: cluster-admin resourceVersion: "102" roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - apiGroup: rbac.authorization.k8s.io kind: Group name: system:masters # {% endraw %} 简单地说,容器通过ServiceAccount配合RBAC这套机制,让容器拥有访问api-server的权限. 原本我打算在kube-system下面创建一个nginx容器,去访问,但是curl失败了,后来我找了个centos的镜像去测试.大家记得配置好serviceAccount就行 metadata.spec.template.spec.serviceAccount: admin deploy声明sa(ServiceAccount)的本质 在deploy声明sa的本质是把sa的对应的secret挂载到/var/run/secrets/kubernetes.io/serviceaccount目录中. 不声明sa,则把default作为sa挂载进去 # k edit secret admin-token-wggwk # 用edit加载的secret内容都会以base64形式表示 # base64(kube-system):a3ViZS1zeXN0ZW0= apiVersion: v1 data: ca.crt: ******* namespace: a3ViZS1zeXN0ZW0= token: ******* kind: Secret metadata: annotations: kubernetes.io/service-account.name: admin kubernetes.io/service-account.uid: 9911ff2a-8c46-4179-80ac-727f48012229 name: admin-token-wggwk namespace: kube-system resourceVersion: "378" type: kubernetes.io/service-account-token 所以deploy衍生的每一个pod里面的容器,/var/run/secrets/kubernetes.io/serviceaccount目录下面都会有这3个文件 /run/secrets/kubernetes.io/serviceaccount # ls -l total 0 lrwxrwxrwx 1 root root 13 Apr 19 06:46 ca.crt -> ..data/ca.crt lrwxrwxrwx 1 root root 16 Apr 19 06:46 namespace -> ..data/namespace lrwxrwxrwx 1 root root 12 Apr 19 06:46 token -> ..data/token 虽然这3个文件都是软链接而且最终指向了下面那个带日期的文件夹,但是我们不用管它. /run/secrets/kubernetes.io/serviceaccount # ls -a -l total 4 drwxrwxrwt 3 root root 140 Apr 19 06:46 . drwxr-xr-x 3 root root 4096 Apr 19 06:46 .. drwxr-xr-x 2 root root 100 Apr 19 06:46 ..2019_04_19_06_46_10.877180351 lrwxrwxrwx 1 root root 31 Apr 19 06:46 ..data -> ..2019_04_19_06_46_10.877180351 lrwxrwxrwx 1 root root 13 Apr 19 06:46 ca.crt -> ..data/ca.crt lrwxrwxrwx 1 root root 16 Apr 19 06:46 namespace -> ..data/namespace lrwxrwxrwx 1 root root 12 Apr 19 06:46 token -> ..data/token curl请求api-server 集群就绪之后,在default这个命名空间下会有kubernetes这个svc,容器透过ca.crt作为证书去请求即可.跨ns的访问方式为https://kubernetes.default.svc:443 前期准备 kubectl exec -it $po sh -n kube-system cd /var/run/secrets/kubernetes.io/serviceaccount TOKEN=$(cat token) APISERVER=https://kubernetes.default.svc:443 先伪装成一个流氓去访问api-server sh-4.2# curl -voa -s $APISERVER/version * About to connect() to kubernetes.default.svc port 443 (#0) * Trying 172.30.0.1... * Connected to kubernetes.default.svc (172.30.0.1) port 443 (#0) * Initializing NSS with certpath: sql:/etc/pki/nssdb * CAfile: /etc/pki/tls/certs/ca-bundle.crt CApath: none * Server certificate: * subject: CN=kube-apiserver * start date: Jul 24 06:31:00 2018 GMT * expire date: Jul 24 06:44:02 2019 GMT * common name: kube-apiserver * issuer: CN=cc95defe1ffd6401d8ede6d4efb0f0f7c,OU=default,O=cc95defe1ffd6401d8ede6d4efb0f0f7c * NSS error -8179 (SEC_ERROR_UNKNOWN_ISSUER) * Peer's Certificate issuer is not recognized. * Closing connection 0 可以看到,用默认的/etc/pki/tls/certs/ca-bundle.crt公钥去访问,直接就报证书对不上了(Peer's Certificate issuer is not recognized.) 带证书去访问api-server curl -s $APISERVER/version \ --header "Authorization: Bearer $TOKEN" \ --cacert ca.crt { "major": "1", "minor": "11", "gitVersion": "v1.11.5", "gitCommit": "753b2dbc622f5cc417845f0ff8a77f539a4213ea", "gitTreeState": "clean", "buildDate": "2018-11-26T14:31:35Z", "goVersion": "go1.10.3", "compiler": "gc", "platform": "linux/amd64" } 那这样思路就很明确了,curl的时候带上正确的证书(ca.crt)和请求头就行了. 使用curl访问常见API 这里先要介绍一个概念selfLink.在kubernetes里面,所有事物皆资源/对象.selfLink就是每一个资源对应的api-server地址.selfLink跟资源是一一对应的. selfLink是有规律的,由namespace,type,apiVersion,name等组成. get node kubectl get no curl \ -s $APISERVER/api/v1/nodes?watch \ --header "Authorization: Bearer $TOKEN" \ --cacert ca.crt get pod kubectl get po -n kube-system -w curl \ -s $APISERVER/api/v1/namespaces/kube-system/pods?watch \ --header "Authorization: Bearer $TOKEN" \ --cacert ca.crt get pod log kubectl logs -f -c logtail -n kube-system logtail-ds-vvpfr curl \ -s $APISERVER"/api/v1/namespaces/kube-system/pods/logtail-ds-vvpfr/log?container=logtail&follow" \ --header "Authorization: Bearer $TOKEN" \ --cacert ca.crt 完整API见kubernetes API 使用JavaScript客户端访问 api-server 2019-08-23,我在部署 kubeflow 的时候,发现里面有个组件是用 nodejs 去请求 api service 的,观察了一下代码,加载配置的地方大致如此. public loadFromDefault() { if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) { const files = process.env.KUBECONFIG.split(path.delimiter); this.loadFromFile(files[0]); for (let i = 1; i < files.length; i++) { const kc = new KubeConfig(); kc.loadFromFile(files[i]); this.mergeConfig(kc); } return; } const home = findHomeDir(); if (home) { const config = path.join(home, '.kube', 'config'); if (fileExists(config)) { this.loadFromFile(config); return; } } if (process.platform === 'win32' && shelljs.which('wsl.exe')) { // TODO: Handle if someome set $KUBECONFIG in wsl here... try { const result = execa.sync('wsl.exe', ['cat', shelljs.homedir() + '/.kube/config']); if (result.code === 0) { this.loadFromString(result.stdout); return; } } catch (err) { // Falling back to alternative auth } } if (fileExists(Config.SERVICEACCOUNT_TOKEN_PATH)) { this.loadFromCluster(); return; } this.loadFromClusterAndUser( { name: 'cluster', server: 'http://localhost:8080' } as Cluster, { name: 'user' } as User, ); } ...... public loadFromCluster(pathPrefix: string = '') { const host = process.env.KUBERNETES_SERVICE_HOST; const port = process.env.KUBERNETES_SERVICE_PORT; const clusterName = 'inCluster'; const userName = 'inClusterUser'; const contextName = 'inClusterContext'; let scheme = 'https'; if (port === '80' || port === '8080' || port === '8001') { scheme = 'http'; } this.clusters = [ { name: clusterName, caFile: `${pathPrefix}${Config.SERVICEACCOUNT_CA_PATH}`, server: `${scheme}://${host}:${port}`, skipTLSVerify: false, }, ]; this.users = [ { name: userName, authProvider: { name: 'tokenFile', config: { tokenFile: `${pathPrefix}${Config.SERVICEACCOUNT_TOKEN_PATH}`, }, }, }, ]; this.contexts = [ { cluster: clusterName, name: contextName, user: userName, }, ]; this.currentContext = contextName; } 可以看到,加载配置是有先后顺序的. sa 排在比较靠后的优先级. host 和 port 通过读取相应 env 得出(实际上,就算在yaml没有配置ENV, kubernetes 本身也会注入大量ENV,这些ENV大多是svc的ip地址和端口等) 而且默认的客户端 skipTLSVerify: false, 那么使用默认的客户端,要取消SSL验证咋办呢?这里提供一个比较蠢但是万无一失的办法: import * as k8s from '@kubernetes/client-node'; import { Cluster } from '@kubernetes/client-node/dist/config_types'; this.kubeConfig.loadFromDefault(); const context = this.kubeConfig.getContextObject(this.kubeConfig.getCurrentContext()); if (context && context.namespace) { this.namespace = context.namespace; } let oldCluster = this.kubeConfig.getCurrentCluster() let cluster: Cluster = { name: oldCluster.name, caFile: oldCluster.caFile, server: oldCluster.server, skipTLSVerify: true, } kubeConfig.clusters = [cluster] this.coreAPI = this.kubeConfig.makeApiClient(k8s.Core_v1Api); this.customObjectsAPI = this.kubeConfig.makeApiClient(k8s.Custom_objectsApi); 参考链接 Access Clusters Using the Kubernetes API cURLing the Kubernetes API server
缘起 2018年3月,我正式成为运维负责人,接管阿米巴集团内部的云平台账户。 上一任运维负责人是个天才,在他离职交接的最后那三个月,打电话不接,发消息不回。他给我留了一堆完全没有密码的服务器,涵盖了腾讯云和阿里云,此外还有一大堆无效的DNS记录,CDN域名,处理这些垃圾的善后工作,陆陆续续花了我一年多时间。 2018年6月,因缘巧合之下,阿里云华南区P8大佬了哥给我科普了 Kubernetes,我当天下午立即决定,无论遇到多大的困难,必定要将其落地。 当时我们的系统已经有一部分运行在阿里云的 docker swarm 上面,但我看了一下 release note ,预感那玩意应该是弃子。于是,在三个月左右的时间内,通过看英文版的《kubernetes in action》和参与社群,我从 0 docker 基础的渣渣进化为集团内部首席云原生步道师,并升级成为QQ群的管理员。 阿里云Kubernetes早期产品经理 此外,我还成为了阿里云 Kubernetes 的早期产品经理。很多产品建议都是我提出来,由他们内部加以评估改进的。 不过我有一个很皮的习惯:喜欢拆台。我在广州云栖大会的时候问了了哥一个关于存储的问题。 那个问题到现在(2020-08-08)都没有很好解决。 容器镜像服务支持私有仓库海外机器构建 kubernetes web控制台:支持ephemeral-storage的设置 容器镜像服务:支持gcr.io等镜像的代理 kubernetes:尽快废弃 dashboard,并将其功能集成到阿里云控制台 Kubernetes:改进创建svc kubernetes:改进RBAC 阿里云kubernetes:SchedulingDisabled节点会被自动剔除出虚拟服务器组 Kubernetes:扩充"节点不可调度"的功能,改为"维护节点" Kubernetes:改进创建集群选项 k8s:增强云盘数据卷 k8s:变更service的证书标签无法生效 k8s:增加集群节点管理的相关文档 云监控:改进K8S云监控 容器服务:pv显示不友好 K8S:进入POD终端之后的可操作时间过短 k8s:配置deployment页面有问题 k8s:volume的相关局限性以及改进 k8s:namespace信息同步有问题 k8s:取消ingress的TLS不生效 阿里云镜像仓库:优化用户体验 k8s:维护master的时候会多出一些奇怪的负载均衡 k8s:改进HPA 希望阿里云容器服务K8S 能够支持自主绑定 SLB k8s-给路由(Ingress)加上 TLS的时候会有问题 k8s:改进LoadBalancer型服务和负载均衡的绑定 k8s-使用私有镜像创建部署(deployment)的时候会有问题 无意中发现 K8S的部署详情页面有 bug 希望阿里云的容器kubernetes界面不要强行翻译专有名词!!! K8S-创建应用页面的相关教程改进 优化K8S部署应用的用户体验 让用户灵活选择 K8S master 付费方式 容器服务-健康检查形同鸡肋 容器服务-改进日志服务 2018-05-13 至今,围绕容器领域,陆陆续续提了几十个建议。虽然有一部分没被采纳,但我觉得我应该担得起“阿里云Kubernetes早期产品经理”这个称号。 最有印象的 BUG 是这个k8s:取消ingress的TLS不生效 当时我跟进了近三个月,还发了个视频给当时的阿里云对接人“仙游”,也不知道TA人现在在哪。 NoOps 传统应用的瀑布模型,我就不吐槽有多糟糕啦,懂的人自然懂。当初在那个运维负责人坑了我一把之后,我看到 Kubernetes 简直像看到了救星一样。后来我就用 Kubernetes 回收了大部分的服务器,至于那些没密码的服务器,要么用休克疗法半夜重置密码后重启,要么耗个一两年,备份云盘后直接退款。 Kubernetes 时代服务器的忘记密码 可参考我写的 《扩容阿里云kubernetes集群,并升级节点内核》。 略有区别的在于,节点维护 这里要设置为“不可调度”。然后慢慢耗死节点里面的 pod 。 当节点里面剩下的 pod 都不再重要时,便可以直接删除节点并退款相应的ECS。 Alibaba Cloud Kubernetes 社群联合管理员 2020-07-22,我终于成为钉群的联合运营,可以管理钉群啦。 降职成弼马温之后,再也不能乱怼人了,哭哭。阿里云联合CNCF做了一个 云原生技术公开课 ,欢迎大家一起学习进步。 吐槽 阿里云能不能别老是给我发代金券了,我所有的域名已经续满10年了。 参考链接 [1]2017年云趋势——从DevOps到NoOpshttp://dockone.io/article/2126
问题 早期的阿里云 kubernetes 集群,系统镜像一直是`centos_7_04_64_20G_alibase_201701015.vhd`. 这个版本的系统内核和 docker 版本太老了,我不是很喜欢。 添加节点(ECS) 购买机器,之后用 kubernetes控制台 添加节点即可。 节点维护 控制台方式 在 kubernetes 控制台,勾选节点,然后选节点维护即可。目前有三种实现方式,建议用“排空节点”。 命令行方式 # SchedulingDisabled,确保新的容器不会调度到该节点 kubectl cordon $node # 驱逐除了ds以外所有的pod kubectl drain $node --ignore-daemonsets --delete-local-data 节点上线 控制台方式 跟节点维护差不多,都是勾选,然后按按按。 命令行方式 # 维护完成,恢复其正常状态 kubectl uncordon $node 结论 作为早期阿里云 kubernetes 的产品经理, 控制台方式实现上下线是我提出来的需求。 吐槽 docker 我就不升级了 ~ Client: Version: 17.06.2-ce-5 API version: 1.30 Go version: go1.8.3 Git commit: 4b5600f Built: Tue Feb 12 10:00:47 2019 OS/Arch: linux/amd64 Server: Version: 17.06.2-ce-5 API version: 1.30 (minimum version 1.12) Go version: go1.8.3 Git commit: 4b5600f Built: Tue Feb 12 10:02:34 2019 OS/Arch: linux/amd64 Experimental: false
不可变编程是一种编程思想。简单地说,就是对象的属性只能set一次。 ImmutableEphemeralVolume(immutable secret) 以 kubernetes 最近(2019年年底)的一个 ImmutableEphemeralVolume 为例。 我看了一下源代码,大意就是说,configmap 和 secret 在创建后不可更新。 以 secret 为例,目前(2020-04-16)secret 的定义是这样的: // Secret holds secret data of a certain type. The total bytes of the values in // the Data field must be less than MaxSecretSize bytes. type Secret struct { metav1.TypeMeta `json:",inline"` // Standard object's metadata. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata // +optional metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` // Immutable, if set to true, ensures that data stored in the Secret cannot // be updated (only object metadata can be modified). // If not set to true, the field can be modified at any time. // Defaulted to nil. // This is an alpha field enabled by ImmutableEphemeralVolumes feature gate. // +optional Immutable *bool `json:"immutable,omitempty" protobuf:"varint,5,opt,name=immutable"` // Data contains the secret data. Each key must consist of alphanumeric // characters, '-', '_' or '.'. The serialized form of the secret data is a // base64 encoded string, representing the arbitrary (possibly non-string) // data value here. Described in https://tools.ietf.org/html/rfc4648#section-4 // +optional Data map[string][]byte `json:"data,omitempty" protobuf:"bytes,2,rep,name=data"` // stringData allows specifying non-binary secret data in string form. // It is provided as a write-only convenience method. // All keys and values are merged into the data field on write, overwriting any existing values. // It is never output when reading from the API. // +k8s:conversion-gen=false // +optional StringData map[string]string `json:"stringData,omitempty" protobuf:"bytes,4,rep,name=stringData"` // Used to facilitate programmatic handling of secret data. // +optional Type SecretType `json:"type,omitempty" protobuf:"bytes,3,opt,name=type,casttype=SecretType"` } 其实只看 Immutable *bool `json:"immutable,omitempty"` 就可以了。可以看到,这是一个 bool 的指针。因为这个字段目前处于alpha 的阶段,所以用了 omitempty 这个标签忽略掉了。 判断是否已经注入 Secret 有个 String 方法有点意思。 简单地说就是通过反射判断字段是否已经注入。 func (this *Secret) String() string { ...... s := strings.Join([]string{`&Secret{`, `ObjectMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ObjectMeta), "ObjectMeta", "v1.ObjectMeta", 1), `&`, ``, 1) + `,`, `Data:` + mapStringForData + `,`, `Type:` + fmt.Sprintf("%v", this.Type) + `,`, `StringData:` + mapStringForStringData + `,`, `Immutable:` + valueToStringGenerated(this.Immutable) + `,`, `}`, }, "") return s } valueToStringGenerated 方法展开是这样的: func valueToStringGenerated(v interface{}) string { rv := reflect.ValueOf(v) if rv.IsNil() { return "nil" } pv := reflect.Indirect(rv).Interface() return fmt.Sprintf("*%v", pv) } 我简化了一下模型,写了个例子。 例子 package main import ( "fmt" "reflect" ) type Secret struct { Immutable *bool `json:"immutable,omitempty"` } func main() { s := Secret{Immutable: &[]bool{true}[0]} fmt.Println(valueToStringGenerated(s.Immutable)) // *true s = Secret{} fmt.Println(valueToStringGenerated(s.Immutable)) // nil } func valueToStringGenerated(v interface{}) string { rv := reflect.ValueOf(v) if rv.IsNil() { return "nil" } pv := reflect.Indirect(rv).Interface() return fmt.Sprintf("*%v", pv) } 结论 为 struct 增加一个字段,这个字段是一个指针。 通过反射获取 struct 的成员(比如字段),进而判断是否已经注入。 有些情况(比如string),用私有字段,struct 暴露一个单例模式的 Set 方法也行。我猜是 bool 类型比较特殊,所以 kubernetes 官方才用了 *bool 这个数据结构。 参考链接 Kubernetes: What is Immutable Infrastructure? Image volumes and container volume How to set bool pointer to true in struct literal?
查看非内网IP | SELECT client_ip as ip WHERE ip_to_domain(client_ip) != 'intranet' GROUP BY ip | SELECT client_ip as ip,COUNT(*) as count WHERE ip_to_domain(client_ip) != 'intranet' GROUP BY ip ORDER by count desc 分组查看UA | SELECT user_agent as ua,COUNT(*) as count GROUP BY ua ORDER by count desc 查看请求方式 * | SELECT http_method, COUNT(*) as number GROUP BY http_method NOT http_method :'GET' 查看写入oss的客户端IP * | SELECT client_ip WHERE http_method ='PUT' GROUP BY client_ip 客户端IP热力图 * | select ip_to_geo(client_ip) as address, count(1) as count group by address order by count desc
需求 /var/log/containers下面的文件其实是软链接 真正的日志文件在/var/lib/docker/containers这个目录 可选方案: Logstash(过于消耗内存,尽量不要用这个) fluentd filebeat 不使用docker-driver 日志的格式 /var/log/containers { "log": "17:56:04.176 [http-nio-8080-exec-5] INFO c.a.goods.proxy.GoodsGetServiceProxy - ------ request_id=514136795198259200,zdid=42,gid=108908071,从缓存中获取数据:失败 ------\n", "stream": "stdout", "time": "2018-11-19T09:56:04.176713636Z" } { "log": "[][WARN ][2018-11-19 18:13:48,896][http-nio-10080-exec-2][c.h.o.p.p.s.impl.PictureServiceImpl][[msg:图片不符合要求:null];[code:400.imageUrl.invalid];[params:https://img.alicdn.com/bao/uploaded/i2/2680224805/TB2w5C9bY_I8KJjy1XaXXbsxpXa_!!2680224805.jpg];[stack:{\"requestId\":\"514141260156502016\",\"code\":\"400.imageUrl.invalid\",\"msg\":\"\",\"stackTrace\":[],\"suppressedExceptions\":[]}];]\n", "stream": "stdout", "time": "2018-11-19T10:13:48.896892566Z" } Logstash filebeat.yml filebeat: prospectors: - type: log //开启监视,不开不采集 enable: true paths: # 采集日志的路径这里是容器内的path - /var/log/elkTest/error/*.log # 日志多行合并采集 multiline.pattern: '^\[' multiline.negate: true multiline.match: after # 为每个项目标识,或者分组,可区分不同格式的日志 tags: ["java-logs"] # 这个文件记录日志读取的位置,如果容器重启,可以从记录的位置开始取日志 registry_file: /usr/share/filebeat/data/registry output: # 输出到logstash中 logstash: hosts: ["0.0.0.0:5044"] 注:6.0以上该filebeat.yml需要挂载到/usr/share/filebeat/filebeat.yml,另外还需要挂载/usr/share/filebeat/data/registry 文件,避免filebeat容器挂了后,新起的重复收集日志。 logstash.conf input { beats { port => 5044 } } filter { if "java-logs" in [tags]{ grok { match => { "message" => "(?<date>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2},\d{3})\]\[(?<level>[A-Z]{4,5})\]\[(?<thread>[A-Za-z0-9/-]{4,40})\]\[(?<class>[A-Za-z0-9/.]{4,40})\]\[(?<msg>.*)" } remove_field => ["message"] } } #if ([message] =~ "^\[") { # drop {} #} # 不匹配正则,匹配正则用=~ if [level] !~ "(ERROR|WARN|INFO)" { drop {} } } ## Add your filters / logstash plugins configuration here output { elasticsearch { hosts => "0.0.0.0:9200" } } fluentd fluentd-es-image镜像 Kubernetes-基于EFK进行统一的日志管理 Docker Logging via EFK (Elasticsearch + Fluentd + Kibana) Stack with Docker Compose filebeat+ES pipeline 定义pipeline 定义java专用的管道 PUT /_ingest/pipeline/java { "description": "[0]java[1]nginx[last]通用规则", "processors": [{ "grok": { "field": "message", "patterns": [ "\\[%{LOGLEVEL:level}\\s+?\\]\\[(?<date>\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2},\\d{3})\\]\\[(?<thread>[A-Za-z0-9/-]+?)\\]\\[%{JAVACLASS:class}\\]\\[(?<msg>[\\s\\S]*?)\\]\\[(?<stack>.*?)\\]" ] },"remove": { "field": "message" } }] } PUT /_ingest/pipeline/nginx { "description": "[0]java[1]nginx[last]通用规则", "processors": [{ "grok": { "field": "message", "patterns": [ "%{IP:client} - - \\[(?<date>.*?)\\] \"(?<method>[A-Za-z]+?) (?<url>.*?)\" %{NUMBER:statuscode} %{NUMBER:duration} \"(?<refer>.*?)\" \"(?<user-agent>.*?)\"" ] },"remove": { "field": "message" } }] } PUT /_ingest/pipeline/default { "description": "[0]java[1]nginx[last]通用规则", "processors": [] } PUT /_ingest/pipeline/all { "description": "[0]java[1]nginx[last]通用规则", "processors": [{ "grok": { "field": "message", "patterns": [ "\\[%{LOGLEVEL:level}\\s+?\\]\\[(?<date>\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2},\\d{3})\\]\\[(?<thread>[A-Za-z0-9/-]+?)\\]\\[%{JAVACLASS:class}\\]\\[(?<msg>[\\s\\S]*?)\\]\\[(?<stack>.*?)\\]", "%{IP:client} - - \\[(?<date>.*?)\\] \"(?<method>[A-Za-z]+?) (?<url>.*?)\" %{NUMBER:statuscode} %{NUMBER:duration} \"(?<refer>.*?)\" \"(?<user-agent>.*?)\"", ".+" ] } }] } filebeat.yml apiVersion: v1 kind: ConfigMap metadata: name: filebeat-config namespace: kube-system labels: k8s-app: filebeat data: filebeat.yml: |- filebeat.config: inputs: # Mounted `filebeat-inputs` configmap: path: ${path.config}/inputs.d/*.yml # Reload inputs configs as they change: reload.enabled: false modules: path: ${path.config}/modules.d/*.yml # Reload module configs as they change: reload.enabled: false setup.template.settings: index.number_of_replicas: 0 # https://www.elastic.co/guide/en/beats/filebeat/6.5/filebeat-reference-yml.html # https://www.elastic.co/guide/en/beats/filebeat/current/configuration-autodiscover.html filebeat.autodiscover: providers: - type: kubernetes templates: config: - type: docker containers.ids: # - "${data.kubernetes.container.id}" - "*" enable: true processors: - add_kubernetes_metadata: # include_annotations: # - annotation_to_include in_cluster: true - add_cloud_metadata: cloud.id: ${ELASTIC_CLOUD_ID} cloud.auth: ${ELASTIC_CLOUD_AUTH} output: elasticsearch: hosts: ['${ELASTICSEARCH_HOST:elasticsearch}:${ELASTICSEARCH_PORT:9200}'] # username: ${ELASTICSEARCH_USERNAME} # password: ${ELASTICSEARCH_PASSWORD} # pipelines: # - pipeline: "nginx" # when.contains: # kubernetes.container.name: "nginx-" # - pipeline: "java" # when.contains: # kubernetes.container.name: "java-" # - pipeline: "default" # when.contains: # kubernetes.container.name: "" --- apiVersion: extensions/v1beta1 kind: DaemonSet metadata: name: filebeat namespace: kube-system labels: k8s-app: filebeat spec: template: metadata: labels: k8s-app: filebeat spec: tolerations: - key: "elasticsearch-exclusive" operator: "Exists" effect: "NoSchedule" serviceAccountName: filebeat terminationGracePeriodSeconds: 30 containers: - name: filebeat imagePullPolicy: Always image: 'filebeat:6.6.0' args: [ "-c", "/etc/filebeat.yml", "-e", ] env: - name: ELASTICSEARCH_HOST value: 0.0.0.0 - name: ELASTICSEARCH_PORT value: "9200" # - name: ELASTICSEARCH_USERNAME # value: elastic # - name: ELASTICSEARCH_PASSWORD # value: changeme # - name: ELASTIC_CLOUD_ID # value: # - name: ELASTIC_CLOUD_AUTH # value: securityContext: runAsUser: 0 # If using Red Hat OpenShift uncomment this: #privileged: true resources: limits: memory: 200Mi requests: cpu: 100m memory: 100Mi volumeMounts: - name: config mountPath: /etc/filebeat.yml readOnly: true subPath: filebeat.yml - name: data mountPath: /usr/share/filebeat/data - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true volumes: - name: config configMap: defaultMode: 0600 name: filebeat-config - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers # data folder stores a registry of read status for all files, so we don't send everything again on a Filebeat pod restart - name: data hostPath: path: /var/lib/filebeat-data type: DirectoryOrCreate --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: filebeat subjects: - kind: ServiceAccount name: filebeat namespace: kube-system roleRef: kind: ClusterRole name: filebeat apiGroup: rbac.authorization.k8s.io --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: name: filebeat labels: k8s-app: filebeat rules: - apiGroups: [""] # "" indicates the core API group resources: - namespaces - pods verbs: - get - watch - list --- apiVersion: v1 kind: ServiceAccount metadata: name: filebeat namespace: kube-system labels: k8s-app: filebeat 如果output是单节点elasticsearch,可以通过修改模板把导出的filebeat*设置为0个副本 curl -X PUT "10.10.10.10:9200/_template/template_log" -H 'Content-Type: application/json' -d' { "index_patterns" : ["filebeat*"], "order" : 0, "settings" : { "number_of_replicas" : 0 } } ' 参考链接: running-on-kubernetes ELK+Filebeat 集中式日志解决方案详解 filebeat.yml(中文配置详解) Elasticsearch Pipeline 详解 es number_of_shards和number_of_replicas 其他方案 有些是sidecar模式,sidecar模式可以做得比较细致. 使用filebeat收集kubernetes中的应用日志 使用Logstash收集Kubernetes的应用日志 阿里云的方案 Kubernetes日志采集流程 跟随docker启动 docker驱动 kubectl delete po $pod -n kube-system kubectl get po -l k8s-app=fluentd-es -n kube-system pod=`kubectl get po -l k8s-app=fluentd-es -n kube-system | grep -Eoi 'fluentd-es-([a-z]|-|[0-9])+'` && kubectl logs $pod -n kube-system kubectl get events -n kube-system | grep $pod
kubernetes+alpine+php特别容易出现访问外网/解析外网地址的时候出现超时的问题. 原因 docker容器访问外网的时候,整个完整路径是这样的. 容器-->主机-->外网-->主机-->容器 容器到主机之间的流量要经过源地址转换(SNAT)才能顺利流通. SNAT就像是一个搬运工,把砖(流量)从容器搬到主机 如果一个主机上面运行多个容器,并发访问外网(特别是PHP这种没有连接池的)时向系统申请可用端口(nf_nat_l4proto_unique_tuple),不可用时+1,然后再申请,再校验.这个过程一多,最终就会导致寻址超时. 说白了是个系统内核问题. 详细的解释见 记一次Docker/Kubernetes上无法解释的连接超时原因探寻之旅 解决方案 最优解 节点升级到 5.1的Linux内核. iptables升级到1.6.2以上 用基于IPVS模式,尽量少做SNAT/DNAT,支持随机端口SNAT的网络插件启动kubernetes 或者用绕过SNAT的网络插件插件方案,比如阿里云的terway.但这个插件跟阿里云绑定得比较深入,需要每台机器额外购买一个弹性网卡. 次优解 用ds部署name sever,所有节点的DNS解析走节点上的name server,通过最小程度的SNAT+dns cache缓解此类问题. 伪解决方案(不能解决根本问题) 默认的pod的/etc/resolv.conf一般长这样 sh-4.2# cat /etc/resolv.conf nameserver <kube-dns-vip> search <namespace>.svc.cluster.local svc.cluster.local cluster.local localdomain options ndots:5 这个配置的意思是,默认nameserver指向kube-dns/core-dns,所有查询中,如果.的个数少于5个,则会根据search中配置的列表依次搜索,如果没有返回,则最后再直接查询域名本身。ndots就是n个.(dots)的意思 举个例子 sh-4.2# host -v baidu.com Trying "baidu.com.<namespace>.svc.cluster.local" Trying "baidu.com.svc.cluster.local" Trying "baidu.com.cluster.local" Trying "baidu.com.localdomain" Trying "baidu.com" ...... 重开socket lifecycle: postStart: exec: command: - /bin/sh - -c - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf" 设置重开socket是规避容器并发A,AAAA查询 2级域名直接走上层解析 参考kubernetes 使用基于 alpine 镜像无法正常解析外网DNS 做的 直接运行 sed -i 's/options ndots:5/#options ndots:5/g' /etc/resolv.conf 会报错 alpine的echo命令会吞换行符,而resolv.conf格式不对DNS解析会报错 dnsConfig: options: - name: ndots value: "2" - name: single-request-reopen 去掉了options ndots:5,变会默认值1,这样的话,容器内部直接访问还是没问题的,走search列表,<svc>.<namespace>.svc.cluster.local,还是能够访问。 而解析Google.com,实际上是解析Google.com.,.的数量超过1个,这时不走search列表,直接用上层DNS 综上所述,去掉ndots/ndots设为1 降低了频繁DNS查询的可能性。对于外网IP的解析有“奇效”。 但如果该主机运行其他容器(这不废话吗,一个节点不跑多个容器那还用啥kubernetes),其他容器也会并发地请求,SNAT的问题还是会出现,所以说修改/etc/resolv.conf文件并不能解决根本问题 歪门邪道1 lifecycle: postStart: exec: command: - /bin/sh - -c - "head -n 2 /etc/resolv.conf > /etc/temp.conf;cat /etc/temp.conf > /etc/resolv.conf;rm -rf /etc/temp.conf" 歪门邪道2 initContainers: - name: alpine image: alpine command: - /bin/sh - -c - "head -n 2 /etc/resolv.conf > /etc/temp.conf;cat /etc/temp.conf > /etc/resolv.conf;rm -rf /etc/temp.conf" 衍生的问题 DNAT 容器访问clusterIP(因为是虚拟IP所以需要DNAT)也有可能出现这类超时的问题 注意Virtual domain的问题 non-headservice的域名格式是<svc>.<namespace>.svc.cluster.local 如果我们容器直接访问<svc>.<namespace>.svc.cluster.local,因为默认DNS设置的问题,解析的次数反而更多。正确的方式是访问<svc> 例子:假设test下面有个s的svc host -v s # 解析1次 host -v s.test.svc.cluster.local # 解析4次 所以,访问同namespace其他svc,直接用svc名去访问即可,没必要装逼使用<svc>.<namespace>.svc.cluster.local这种格式。 其他知识 dns记录类型 A记录:地址记录,用来指定域名的IPv4地址(如:8.8.8.8),如果需要将域名指向一个IP地址,就需要添加A记录。 CNAME: 如果需要将域名指向另一个域名,再由另一个域名提供ip地址,就需要添加CNAME记录。 TXT:在这里可以填写任何东西,长度限制255。绝大多数的TXT记录是用来做SPF记录(反垃圾邮件)。 NS:域名服务器记录,如果需要把子域名交给其他DNS服务商解析,就需要添加NS记录。 AAAA:用来指定主机名(或域名)对应的IPv6地址(例如:ff06:0:0:0:0:0:0:c3)记录。 MX:如果需要设置邮箱,让邮箱能收到邮件,就需要添加MX记录。 显性URL:从一个地址301重定向到另一个地址的时候,就需要添加显性URL记录(注:DNSPod目前只支持301重定向)。 隐性URL:类似于显性URL,区别在于隐性URL不会改变地址栏中的域名。 SRV:记录了哪台计算机提供了哪个服务。格式为:服务的名字、点、协议的类型,例如:_xmpp-server._tcp。 用到的命令 安装方法: yum install -y bind-utils sudo apt-get install -y dnsutils apk add bind-tools dig dig +trace +ndots=5 +search $host host host -v $host 参考链接: iptables中DNAT、SNAT和MASQUERADE的理解 linux根文件系统 /etc/resolv.conf 文件详解 kube-dns per node #45363 DNS intermittent delays of 5s #56903 Racy conntrack and DNS lookup timeouts /etc/resolv.conf /etc/resolv.conf search和ndots配置 DNS for Services and Pods
实时分析(show full processlist;)结合延后分析(mysql.slow_log),对SQL语句进行优化 设置慢查询参数 slow_query_log 1 log_queries_not_using_indexes OFF long_query_time 5 slow_query_log 1 实时分析 查看有哪些线程正在执行 show processlist; show full processlist; 相比show processlist;我比较喜欢用.因为这个查询可以用where条件 SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST where state !='' order by state,time desc,command ; -- 按照客户端IP对当前连接用户进行分组 SELECT substring_index(Host,':',1) as h,count(Host) as c,user FROM INFORMATION_SCHEMA.PROCESSLIST group by h order by c desc,user; -- 按用户名对当前连接用户进行分组 SELECT substring_index(Host,':',1) as h,count(Host) as c,user FROM INFORMATION_SCHEMA.PROCESSLIST group by user order by c desc,user; 各种耗时SQL对应的特征 改表 Copying to tmp table 内存不够用,转成磁盘 Copying to tmp table on disk 传输数据量大 Reading from net Sending data 没有索引 Copying to tmp table Sorting result Creating sort index Sorting result 重点关注这些状态,参考processlist中哪些状态要引起关注进行优化 延后分析 # 建数据库 CREATE TABLE `slow_log_2019-05-30` ( `start_time` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `user_host` mediumtext NOT NULL, `query_time` time(6) NOT NULL, `lock_time` time(6) NOT NULL, `rows_sent` int(11) NOT NULL, `rows_examined` int(11) NOT NULL, `db` varchar(512) NOT NULL, `last_insert_id` int(11) NOT NULL, `insert_id` int(11) NOT NULL, `server_id` int(10) unsigned NOT NULL, `sql_text` mediumtext NOT NULL, `thread_id` bigint(21) unsigned NOT NULL, KEY `idx_start_time` (`start_time`), KEY `idx_query_time` (`query_time`), KEY `idx_lock_time` (`lock_time`), KEY `idx_rows_examined` (`rows_examined`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- insert into slow_log.slow_log_2019-05-30 select * from mysql.slow_log; -- truncate table mysql.slow_log ; select * FROM slow_log.`slow_log_2019-05-30` where sql_text not like 'xxx`%' order by query_time desc,query_time desc; 按优先级排列,需要关注的列是lock_time,query_time,rows_examined.分析的时候应用二八法则,先找出最坑爹的那部分SQL,率先优化掉,然后不断not like或者删除掉排除掉已经优化好的低效SQL. 低效SQL的优化思路 对于每一个查询,先用explain SQL分析一遍,是比较明智的做法. 一般而言,rows越少越好,提防Extra:Using where这种情况,这种情况一般是扫全表,在数据量大(>10万)的时候考虑增加索引. 慎用子查询 尽力避免嵌套子查询,使用索引来优化它们 EXPLAIN SELECT * FROM ( SELECT * FROM `s`.`t` WHERE status IN (-15, -11) LIMIT 0, 10 ) a ORDER BY a.modified DESC 比如说这种的,根本毫无必要.表面上看,比去掉子查询更快一点,实际上是因为mysql 5.7对子查询进行了优化,生成了Derived table,把结果集做了一层缓存. 按照实际的场景分析发现,status这个字段没有做索引,导致查询变成了全表扫描(using where),加了索引后,问题解决. json类型 json数据类型,如果存入的JSON很长,读取出来自然越慢.在实际场景中,首先要确定是否有使用这一类型的必要,其次,尽量只取所需字段. 见过这样写的 WHERE j_a like '%"sid":514572%' 这种行为明显是对mysql不熟悉,MYSQL是有JSON提取函数的. WHERE JSON_EXTRACT(j_a, "$[0].sid")=514572; 虽然也是全表扫描,但怎么说也比like全模糊查询好吧? 更好的做法,是通过虚拟字段建索引 MySQL · 最佳实践 · 如何索引JSON字段 但是现阶段MYSQL对json的索引做的是不够的,如果json数据列过大,建议还是存MongoDB(见过把12万json存mysql的,那读取速度简直无语). 字符串类型 WHERE a=1 用数字给字符串类型的字段赋值会导致该字段上的索引失效. WHERE a='1' 分组查询 group by,count(x),sum(x),慎用.非常消耗CPU group by select col_1 from table_a where (col_2 > 7 or mtsp_col_2 > 0) and col_3 = 1 group by col_1 这种不涉及聚合查询(count(x),sum(x))的group by明显就是不合理的,去重复查询效果更高点 select distinct(col_1) from table_a where (col_2 > 7 or mtsp_col_2 > 0) and col_3 = 1 limit xxx; count(x),sum(x) x 这个字段最好带索引,不然就算筛选条件有索引也会很慢 order by x x这字段最好带上索引,不然show processlist;里面可能会出现大量Creating sort index的结果 组合索引失效 组合索引有个最左匹配原则 KEY 'idx_a' (a,b,c) WHERE b='' and c ='' 这时组合索引是无效的. 其他 EXPLAIN SQL DESC SQL # INNODB_TRX表主要是包含了正在InnoDB引擎中执行的所有事务的信息,包括waiting for a lock和running的事务 SELECT * FROM information_schema.INNODB_TRX; SELECT * FROM information_schema.innodb_locks; SELECT * FROM information_schema.INNODB_LOCK_WAITS; 参考链接 MySQL慢查询日志总结 MySQL CPU 使用率高的原因和解决方法 mysql优化,导致查询不走索引的原因总结 information_schema中Innodb相关表用于分析sql查询锁的使用情况介绍
介绍下用k8s挂载一些常用的资源 当前版本Kubernetes版本:1.12.2 env env env: - name: GIT_REPO value: 'ssh://git@127.0.0.1:22/a/b.git' 嵌套env env: - name: spring.profiles.active value: 'product' - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: GOMS_API_HTTP_ADDR value: 'http://$(MY_POD_IP):9090' configMap 注意一下,修改configmap不会导致容器里的挂载的configmap文件/环境变量发生改变;删除configmap也不会影响到容器内部的环境变量/文件,但是删除configmap之后,被挂载的pod上面会出现一个warnning的事件 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedMount 64s (x13 over 11m) kubelet, cn-shenzhen.i-wz9498k1n1l7sx8bkc50 MountVolume.SetUp failed for volume "nginx" : configmaps "nginx" not found config map写的很清楚了,这里恬不知耻得copy一下 注意,configmap有1M的限制,一般用来挂载小型配置,大量配置建议上配置中心 挂载单一项 apiVersion: v1 kind: Pod metadata: name: dapi-test-pod spec: containers: - name: test-container image: k8s.gcr.io/busybox command: [ "/bin/sh", "-c", "env" ] env: # Define the environment variable - name: SPECIAL_LEVEL_KEY valueFrom: configMapKeyRef: # The ConfigMap containing the value you want to assign to SPECIAL_LEVEL_KEY name: special-config # Specify the key associated with the value key: special.how restartPolicy: Never 表示挂载special-config这个configmap的special.how项 挂载整个configmap apiVersion: v1 kind: Pod metadata: name: dapi-test-pod spec: containers: - name: test-container image: k8s.gcr.io/busybox command: [ "/bin/sh", "-c", "env" ] envFrom: - configMapRef: name: special-config restartPolicy: Never 参考: Add nginx.conf to Kubernetes cluster Configure a Pod to Use a ConfigMap fieldRef 可以挂载pod的一些属性 env: - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP Selects a field of the pod: supports metadata.name, metadata.namespace, metadata.labels, metadata.annotations, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP. resourceFieldRef Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. 英文介绍得很明白,用来挂载当前yaml里面container的资源(CPU/内存)限制,用得比较少啦其实.此外还可以结合downloadAPI 注意containerName不能配错,不然pod状态会变成CreateContainerConfigError env: - name: a valueFrom: resourceFieldRef: containerName: nginx-test2 resource: limits.cpu secretKeyRef Selects a key of a secret in the pod's namespace env: - name: WORDPRESS_DB_USER valueFrom: secretKeyRef: name: mysecret key: username - name: WORDPRESS_DB_PASSWORD valueFrom: secretKeyRef: name: mysecret key: password 参考: Kubernetes中Secret使用详解 https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#envvarsource-v1-core 目录/文件类挂载 k8s可以挂载的资源实在是太多,这里挑一些比较有代表性的来讲一下 这一类资源一般要先在spec层级定义volumes,然后在containers定义volumeMounts,有种先声明,再使用的意思 hostPath(宿主机目录/文件) 既有目录/文件用Directory/File+nodeSelector 但是用了nodeSelector之后,以后的伸缩都会在匹配的节点上,如果节点只有1个,副本集设置得超出实际节点可承受空间,最终将导致单点问题,这个要注意下 应用启用时读写空文件用DirectoryOrCreate或者FileOrCreate 以下演示第一种方案 #给节点打上标签(这里省略) kubectl get node --show-labels apiVersion: apps/v1beta2 kind: Deployment metadata: labels: app: nginx-test2 name: nginx-test2 namespace: test spec: progressDeadlineSeconds: 600 replicas: 1 revisionHistoryLimit: 2 selector: matchLabels: app: nginx-test2 strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: nginx-test2 spec: containers: - image: 'nginx:1.15.4-alpine' imagePullPolicy: Always name: nginx-test2 resources: {} terminationMessagePolicy: File volumeMounts: - name: host1 mountPath: /etc/nginx/sites-enabled - name: host2 mountPath: /etc/nginx/sites-enabled2/a.com.conf nodeSelector: kubernetes.io/hostname: cn-shenzhen.i-wz9aabuytimkomdmjabq dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 volumes: - name: host1 hostPath: path: /root/site type: Directory - name: host2 hostPath: path: /root/site/a.com.conf type: File configMap 单项挂载(第1种) 这种挂载会热更新,更改后大约10秒后能看到变化 volumeMounts: - name: config-vol mountPath: /etc/config volumes: - name: config-vol configMap: name: log-config items: - key: log_level path: log_level 单项挂载(第2种) 这种挂载方式不会热更新 volumeMounts: - name: nginx mountPath: /etc/nginx/nginx.conf subPath: nginx.conf volumes: - name: nginx configMap: name: amiba-nginx 完全挂载 这种挂载会热更新,更改后大约10秒后能看到变化 volumeMounts: - name: config-vol mountPath: /etc/config volumes: - name: config-vol configMap: name: log-config secret 单项挂载 volumes: - name: secrets secret: secretName: mysecret items: - key: password mode: 511 path: tst/psd - key: username mode: 511 path: tst/usr 完全挂载 这里用了特定权限去挂载文件,默认好像是777 volumeMounts: - name: sshkey mountPath: /root/.ssh volumes: - name: sshkey secret: secretName: pull-gitea defaultMode: 0400 kubectl create secret generic pull-gitea \ --from-file=id_rsa=/Volumes/D/temp/id_rsa \ --from-file=id_rsa.pub=/Volumes/D/temp/id_rsa.pub \ --from-file=known_hosts=/Volumes/D/temp/known_hosts \ 比如这个模式创建出来的secret,容器里面/root/.ssh目录就会有id_rsa,id_rsa.pub,known_hosts3个文件 downwardAPI 参考链接: volumes kubernetes-api/v1.12 原文:Kubernetes挂载常用资源
推荐工具 kubectx kubectx:用来切换集群的访问 kubens:用来切换默认的namespace kubectl-aliases kubectl命令别名 集群管理相关命令 kubectl get cs # 查看节点 kubectl get nodes kubectl get ing pdd --n java # 不调度 kubectl taint nodes node1 key=value:NoSchedule kubectl cluster-info dump kubectl get svc --sort-by=.metadata.creationTimestamp kubectl get no --sort-by=.metadata.creationTimestamp kubectl get po --field-selector spec.nodeName=xxxx kubectl get events --field-selector involvedObject.kind=Service --sort-by='.metadata.creationTimestamp' 参考链接: kubernetes 节点维护 cordon, drain, uncordon 应用管理相关 kubectl top pod kubectl delete deployment,services -l app=nginx kubectl scale deployment/nginx-deployment --replicas=2 kubectl get svc --all-namespaces=true 强制删除 有时 删除pv/pvc时会有问题,这个使用得加2个命令参数--grace-period=0 --force 删除所有失败的pod kubectl get po --all-namespaces --field-selector 'status.phase==Failed' kubectl delete po --field-selector 'status.phase==Failed' 一些技巧 k8s目前没有没有类似docker-compose的depends_on依赖启动机制,建议使用wait-for-it重写镜像的command. 集群管理经(教)验(训) 节点问题 taint别乱用 kubectl taint nodes xx elasticsearch-test-ready=true:NoSchedule kubectl taint nodes xx elasticsearch-test-ready:NoSchedule- master节点本身就自带taint,所以才会导致我们发布的容器不会在master节点上面跑.但是如果自定义taint的话就要注意了!所有DaemonSet和kube-system,都需要带上相应的tolerations.不然该节点会驱逐所有不带这个tolerations的容器,甚至包括网络插件,kube-proxy,后果相当严重,请注意 taint跟tolerations是结对对应存在的,操作符也不能乱用 NoExecute tolerations: - key: "elasticsearch-exclusive" operator: "Equal" value: "true" effect: "NoExecute" kubectl taint node cn-shenzhen.xxxx elasticsearch-exclusive=true:NoExecute NoExecute是立刻驱逐不满足容忍条件的pod,该操作非常凶险,请务必先行确认系统组件有对应配置tolerations. 特别注意用Exists这个操作符是无效的,必须用Equal NoSchedule tolerations: - key: "elasticsearch-exclusive" operator: "Exists" effect: "NoSchedule" - key: "elasticsearch-exclusive" operator: "Equal" value: "true" effect: "NoExecute" kubectl taint node cn-shenzhen.xxxx elasticsearch-exclusive=true:NoSchedule 是尽量不往这上面调度,但实际上还是会有pod在那上面跑 Exists和Exists随意使用,不是很影响 值得一提的是,同一个key可以同时存在多个effect Taints: elasticsearch-exclusive=true:NoExecute elasticsearch-exclusive=true:NoSchedule 其他参考链接: Kubernetes中的Taint和Toleration(污点和容忍) kubernetes的调度机制 隔离节点的正确步骤 # 驱逐除了ds以外所有的pod kubectl drain <node name> --ignore-daemonsets kubectl cordon <node name> 这个时候运行get node命令,状态会变 node.xx Ready,SchedulingDisabled <none> 189d v1.11.5 最后 kubectl delete <node name> 维护节点的正确步骤 kubectl drain <node name> --ignore-daemonsets kubectl uncordon <node name> 节点出现磁盘压力(DiskPressure) --eviction-hard=imagefs.available<15%,memory.available<300Mi,nodefs.available<10%,nodefs.inodesFree<5% kubelet在启动时指定了磁盘压力,以阿里云为例,imagefs.available<15%意思是说容器的读写层少于15%的时候,节点会被驱逐.节点被驱逐的后果就是产生DiskPressure这种状况,并且节点上再也不能运行任何镜像,直至磁盘问题得到解决.如果节点上容器使用了宿主目录,这个问题将会是致命的.因为你不能把目录删除掉,但是真是这些宿主机的目录堆积,导致了节点被驱逐. 所以,平时要养好良好习惯,容器里面别瞎写东西(容器里面写文件会占用ephemeral-storage,ephemeral-storage过多pod会被驱逐),多使用无状态型容器,谨慎选择存储方式,尽量别用hostpath这种存储 出现状况时,真的有种欲哭无泪的感觉. Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FreeDiskSpaceFailed 23m kubelet, node.xxxx1 failed to garbage collect required amount of images. Wanted to free 5182058496 bytes, but freed 0 bytes Warning FreeDiskSpaceFailed 18m kubelet, node.xxxx1 failed to garbage collect required amount of images. Wanted to free 6089891840 bytes, but freed 0 bytes Warning ImageGCFailed 18m kubelet, node.xxxx1 failed to garbage collect required amount of images. Wanted to free 6089891840 bytes, but freed 0 bytes Warning FreeDiskSpaceFailed 13m kubelet, node.xxxx1 failed to garbage collect required amount of images. Wanted to free 4953321472 bytes, but freed 0 bytes Warning ImageGCFailed 13m kubelet, node.xxxx1 failed to garbage collect required amount of images. Wanted to free 4953321472 bytes, but freed 0 bytes Normal NodeHasNoDiskPressure 10m (x5 over 47d) kubelet, node.xxxx1 Node node.xxxx1 status is now: NodeHasNoDiskPressure Normal Starting 10m kube-proxy, node.xxxx1 Starting kube-proxy. Normal NodeHasDiskPressure 10m (x4 over 42m) kubelet, node.xxxx1 Node node.xxxx1 status is now: NodeHasDiskPressure Warning EvictionThresholdMet 8m29s (x19 over 42m) kubelet, node.xxxx1 Attempting to reclaim ephemeral-storage Warning ImageGCFailed 3m4s kubelet, node.xxxx1 failed to garbage collect required amount of images. Wanted to free 4920913920 bytes, but freed 0 bytes 参考链接: Eviction Signals 10张图带你深入理解Docker容器和镜像 节点CPU彪高 有可能是节点在进行GC(container GC/image GC),用describe node查查.我有次遇到这种状况,最后节点上的容器少了很多,也是有点郁闷 Events: Type Reason Age From Message ---- ------ ---- ---- Warning ImageGCFailed 45m kubelet, cn-shenzhen.xxxx failed to get image stats: rpc error: code = DeadlineExceeded desc = context deadline exceeded 参考: kubelet 源码分析:Garbage Collect 对象问题 pod pod频繁重启 原因有多种,不可一概而论 资源达到limit设置值 调高limit或者检查应用 Readiness/Liveness connection refused Readiness检查失败的也会重启,但是Readiness检查失败不一定是应用的问题,如果节点本身负载过重,也是会出现connection refused或者timeout 这个问题要上节点排查 pod被驱逐(Evicted) 节点加了污点导致pod被驱逐 ephemeral-storage超过限制被驱逐 EmptyDir 的使用量超过了他的 SizeLimit,那么这个 pod 将会被驱逐 Container 的使用量(log,如果没有 overlay 分区,则包括 imagefs)超过了他的 limit,则这个 pod 会被驱逐 Pod 对本地临时存储总的使用量(所有 emptydir 和 container)超过了 pod 中所有container 的 limit 之和,则 pod 被驱逐 ephemeral-storage是一个pod用的临时存储. resources: requests: ephemeral-storage: "2Gi" limits: ephemeral-storage: "3Gi" 节点被驱逐后通过get po还是能看到,用describe命令,可以看到被驱逐的历史原因 Message: The node was low on resource: ephemeral-storage. Container codis-proxy was using 10619440Ki, which exceeds its request of 0. 参考: Kubernetes pod ephemeral-storage配置 Managing Compute Resources for Containers kubectl exec 进入容器失败 这种问题我在搭建codis-server的时候遇到过,当时没有配置就绪以及健康检查.但获取pod描述的时候,显示running.其实这个时候容器以及不正常了. ~ kex codis-server-3 sh rpc error: code = 2 desc = containerd: container not found command terminated with exit code 126 解决办法:删了这个pod,配置livenessProbe pod的virtual host name Deployment衍生的pod,virtual host name就是pod name. StatefulSet衍生的pod,virtual host name是<pod name>.<svc name>.<namespace>.svc.cluster.local.相比Deployment显得更有规律一些.而且支持其他pod访问 pod接连Crashbackoff Crashbackoff有多种原因. 沙箱创建(FailedCreateSandBox)失败,多半是cni网络插件的问题 镜像拉取,有中国特色社会主义的问题,可能太大了,拉取较慢 也有一种可能是容器并发过高,流量雪崩导致. 比如,现在有3个容器abc,a突然遇到流量洪峰导致内部奔溃,继而Crashbackoff,那么a就会被service剔除出去,剩下的bc也承载不了那么多流量,接连崩溃,最终网站不可访问.这种情况,多见于高并发网站+低效率web容器. 在不改变代码的情况下,最优解是增加副本数,并且加上hpa,实现动态伸缩容. deploy MinimumReplicationUnavailable 如果deploy配置了SecurityContext,但是api-server拒绝了,就会出现这个情况,在api-server的容器里面,去掉SecurityContextDeny这个启动参数. 具体见Using Admission Controllers service 建了一个服务,但是没有对应的po,会出现什么情况? 请求时一直不会有响应,直到request timeout 参考 Configure Out Of Resource Handling service connection refuse 原因可能有 pod没有设置readinessProbe,请求到未就绪的pod kube-proxy宕机了(kube-proxy负责转发请求) 网络过载 service没有负载均衡 检查一下是否用了headless service.headless service是不会自动负载均衡的... kind: Service spec: # clusterIP: None的即为`headless service` type: ClusterIP clusterIP: None 具体表现service没有自己的虚拟IP,nslookup会出现所有pod的ip.但是ping的时候只会出现第一个pod的ip / # nslookup consul nslookup: can't resolve '(null)': Name does not resolve Name: consul Address 1: 172.31.10.94 172-31-10-94.consul.default.svc.cluster.local Address 2: 172.31.10.95 172-31-10-95.consul.default.svc.cluster.local Address 3: 172.31.11.176 172-31-11-176.consul.default.svc.cluster.local / # ping consul PING consul (172.31.10.94): 56 data bytes 64 bytes from 172.31.10.94: seq=0 ttl=62 time=0.973 ms 64 bytes from 172.31.10.94: seq=1 ttl=62 time=0.170 ms ^C --- consul ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 0.170/0.571/0.973 ms / # ping consul PING consul (172.31.10.94): 56 data bytes 64 bytes from 172.31.10.94: seq=0 ttl=62 time=0.206 ms 64 bytes from 172.31.10.94: seq=1 ttl=62 time=0.178 ms ^C --- consul ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 0.178/0.192/0.206 ms 普通的type: ClusterIP service,nslookup会出现该服务自己的IP / # nslookup consul nslookup: can't resolve '(null)': Name does not resolve Name: consul Address 1: 172.30.15.52 consul.default.svc.cluster.local ReplicationController不更新 ReplicationController不是用apply去更新的,而是kubectl rolling-update,但是这个指令也废除了,取而代之的是kubectl rollout.所以应该使用kubectl rollout作为更新手段,或者懒一点,apply file之后,delete po. 尽量使用deploy吧. StatefulSet更新失败 StatefulSet是逐一更新的,观察一下是否有Crashbackoff的容器,有可能是这个容器导致更新卡住了,删掉即可. 进阶调度 使用亲和度确保节点在目标节点上运行 nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: elasticsearch-test-ready operator: Exists 参考链接: advanced-scheduling-in-kubernetes kubernetes-scheulder-affinity 使用反亲和度确保每个节点只跑同一个应用 affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: 'app' operator: In values: - nginx-test2 topologyKey: "kubernetes.io/hostname" namespaces: - test 容忍运行 master节点之所以不允许普通镜像,是因为master节点带了污点,如果需要强制在master上面运行镜像,则需要容忍相应的污点. tolerations: - effect: NoSchedule key: node-role.kubernetes.io/master operator: Exists - effect: NoSchedule key: node.cloudprovider.kubernetes.io/uninitialized operator: Exists 阿里云Kubernetes问题 修改默认ingress 新建一个指向ingress的负载均衡型svc,然后修改一下kube-system下nginx-ingress-controller启动参数. - args: - /nginx-ingress-controller - '--configmap=$(POD_NAMESPACE)/nginx-configuration' - '--tcp-services-configmap=$(POD_NAMESPACE)/tcp-services' - '--udp-services-configmap=$(POD_NAMESPACE)/udp-services' - '--annotations-prefix=nginx.ingress.kubernetes.io' - '--publish-service=$(POD_NAMESPACE)/<自定义svc>' - '--v=2' LoadBalancer服务一直没有IP 具体表现是EXTERNAL-IP一直显示pending. ~ kg svc consul-web NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE consul-web LoadBalancer 172.30.13.122 <pending> 443:32082/TCP 5m 这问题跟Alibaba Cloud Provider这个组件有关,cloud-controller-manager有3个组件,他们需要内部选主,可能哪里出错了,当时我把其中一个出问题的pod删了,就好了. 清理Statefulset动态PVC 目前阿里云Statefulset动态PVC用的是nas。 对于这种存储,需要先把容器副本将为0,或者整个Statefulset删除。 删除PVC 把nas挂载到任意一台服务器上面,然后删除pvc对应nas的目录。 升级到v1.12.6-aliyun.1之后节点可分配内存变少 该版本每个节点保留了1Gi,相当于整个集群少了N GB(N为节点数)供Pod分配. 如果节点是4G的,Pod请求3G,极其容易被驱逐. 建议提高节点规格. Server Version: version.Info{Major:"1", Minor:"12+", GitVersion:"v1.12.6-aliyun.1", GitCommit:"8cb561c", GitTreeState:"", BuildDate:"2019-04-22T11:34:20Z", GoVersion:"go1.10.8", Compiler:"gc", Platform:"linux/amd64"} 新加节点出现NetworkUnavailable RouteController failed to create a route 看一下kubernetes events,是否出现了 timed out waiting for the condition -> WaitCreate: ceate route for table vtb-wz9cpnsbt11hlelpoq2zh error, Aliyun API Error: RequestId: 7006BF4E-000B-4E12-89F2-F0149D6688E4 Status Code: 400 Code: QuotaExceeded Message: Route entry quota exceeded in this route table 出现这个问题是因为达到了VPC的自定义路由条目限制,默认是48,需要提高vpc_quota_route_entrys_num的配额 参考(应用调度相关): Kubernetes之健康检查与服务依赖处理 kubernetes如何解决服务依赖呢? Kubernetes之路 1 - Java应用资源限制的迷思 Control CPU Management Policies on the Node Reserve Compute Resources for System Daemons Configure Out Of Resource Handling
使用docker-compose演示redis的各种使用情景,最后介绍了codis和kubernetes方案 总结 模式 特点 单机版-RDB 备份快,但数据可能不完整 单机版-AOF 备份慢,但是数据比较完整 1主N从 读写分离的典范 树状主从(N级缓存) 为了规避主重启导致的大规模全量复制,但是需要维持每一个中间master的健康 主从自动切换(Sentinel) 在主从的基础上加了Sentinel角色,通过Sentinel实现主从的自动切换 集群(N主N从) 基于slot的key分片,客户端支持得不是很多,所以用的人不多 单机版 RDB模式(默认模式) 定期快照模式 version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" networks: - default entrypoint: - redis-server volumes: - ./data:/data AOF模式 逐一写入,数据比较完整,文件较大,但恢复较慢 version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --appendonly yes volumes: - ./data:/data 主从复制版 1主N从 这种模式简单粗暴,但是master一旦重启,多从节点全量复制,IO将会比较繁重 version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --save 1 1 volumes: - ./data:/data redis-slave1: image: redis ports: - "12661:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 redis-slave2: image: redis ports: - "12662:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 树状主从(N级缓存) 从节点作为主节点. 这种模式规避了单master version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --save 1 1 volumes: - ./data:/data redis-slave1: image: redis ports: - "12661:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-slave2: image: redis ports: - "12662:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-slave1 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx 主从自动切换(Sentinel模式) 这时需要引入 Sentinel 的概念 Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务: 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。 默认的Sentinel配置去掉注释后长这样 port 26379 daemonize no pidfile /var/run/redis-sentinel.pid logfile "" dir /tmp # 监视主服务器,下线master需要2个Sentinel同意 sentinel monitor mymaster redis-master 6379 2 # 30秒内有效回复视为master健康,否则下线 sentinel down-after-milliseconds mymaster 30000 # 故障转移时,从服务器同部数最大值 sentinel parallel-syncs mymaster 1 # 1. 同一个sentinel对同一个master两次failover之间的间隔时间。 # 2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 # 3.当想要取消一个正在进行的failover所需要的时间。 # 4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了。 sentinel failover-timeout mymaster 90000 基本格式是sentinel <选项的名字> <主服务器的名字> <选项的值> 原本我想通过docker-compose up --scale redis-sentinel=3直接启动3个容器,结果发现这容器竟然会修改配置文件,所以只能分开写了 初版 # docker-compose up --scale redis-sentinel=3 虽然可以启动,但是3个容器同时写同一个文件,感觉不是很好 version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" entrypoint: - redis-server - --save 1 1 volumes: - ./data:/data # 重启策略改为no手动让他宕机模拟主从切换 restart: "no" redis-slave1: image: redis ports: - "12661:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-slave2: image: redis ports: - "12662:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-slave1 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-sentinel: image: redis expose: - "26379" entrypoint: - redis-server - /usr/local/etc/redis/redis.conf - --sentinel volumes: - ./redis-sentinel.conf:/usr/local/etc/redis/redis.conf 最终版: version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" entrypoint: - redis-server - --save 1 1 volumes: - ./data:/data # 重启策略改为no手动让他宕机模拟主从切换 restart: "no" redis-slave1: image: redis ports: - "12661:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-slave2: image: redis ports: - "12662:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-slave1 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-sentinel1: image: redis expose: - "26379" entrypoint: - redis-server - /usr/local/etc/redis/redis.conf - --sentinel volumes: - ./redis-sentinel1.conf:/usr/local/etc/redis/redis.conf redis-sentinel2: image: redis expose: - "26379" entrypoint: - redis-server - /usr/local/etc/redis/redis.conf - --sentinel volumes: - ./redis-sentinel2.conf:/usr/local/etc/redis/redis.conf redis-sentinel3: image: redis expose: - "26379" entrypoint: - redis-server - /usr/local/etc/redis/redis.conf - --sentinel volumes: - ./redis-sentinel3.conf:/usr/local/etc/redis/redis.conf sentinel启动之后,配置发生了变化 这些内容没了 sentinel monitor mymaster redis-master 6379 2 sentinel down-after-milliseconds mymaster 30000 sentinel parallel-syncs mymaster 1 变成 sentinel myid 3ae98d6815c1a9b941f8283b7e48bfeef7435905 sentinel deny-scripts-reconfig yes # Generated by CONFIG REWRITE sentinel config-epoch mymaster 0 sentinel leader-epoch mymaster 0 sentinel known-replica mymaster 172.24.0.4 6379 sentinel known-sentinel mymaster 172.24.0.7 26379 19ac7e0519e3c30a75e23bac34a7033594256c54 sentinel known-sentinel mymaster 172.24.0.5 26379 c596734a7f55ba2b6c7e3e81562aa6687e45fdeb sentinel current-epoch 0 之后通过docker ps和docker stop手动停掉了master那个容器,sentinel发觉了,并重新选主. 此时通过redis-cli输入info,发现它已经成功变成了可以写入数据的master # Generated by CONFIG REWRITE sentinel config-epoch mymaster 1 sentinel leader-epoch mymaster 1 sentinel known-replica mymaster 172.24.0.3 6379 sentinel known-replica mymaster 172.24.0.2 6379 sentinel known-sentinel mymaster 172.24.0.7 26379 19ac7e0519e3c30a75e23bac34a7033594256c54 sentinel known-sentinel mymaster 172.24.0.5 26379 c596734a7f55ba2b6c7e3e81562aa6687e45fdeb sentinel current-epoch 1 此时重启master,虽然他以server形式启动,但是角色已经自动被贬为slave. 此时master没有变化,所以sentinel的配置内容没有变 集群版(N主N从) 集群基于16384个slot做分片.目前各语言客户端实现比较少.所以用的人不是很多. 在docker中运行时,需要使用host网络模式(--net=host) redis-trib.rb redis版本<5时,可以用redis-trib.rb建集群 ./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \ 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 # 引入新节点 ./redis-trib.rb add-node 127.0.0.1:7006 <任意节点IP>:<节点端口> # 重新分片 ./redis-trib.rb reshard <任意节点IP>:<节点端口> redis-cli =5直接用redis-cli即可. redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \ 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ --cluster-replicas 1 redis-cli --cluster reshard 127.0.0.1:7000 redis-cli reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 # Adding a new node as a replica redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>` 集群指令 CLUSTER REPLICATE <master-node-id> cluster nodes Kubernetes 个人觉得吧,redis跟Kubernetes不是特别契合.kubernetes本身有网络瓶颈的问题,通过svc去访问,频繁DNS解析也不好对吧.这对于高频访问redis的场景来说是致命的. 而且pod这种易失架构注定了在Kubernetes上面用redis要么用数据卷挂载,要么用主从自动切换模式(纯内存的话至少要1主2从,并且用反亲和度错开彼此的运行节点). 主从和单机版倒好解决,单机的话挂载好数据卷,主从的话,主和从分开2个deploy/statefulset部署即可. 但是集群版就比较麻烦.官方的设计还是偏向于传统二进制人工运维,没有做到云原生 看了一下官方的helm chart,也是用的主从模式. codis codis是redis集群没出来之前,豌豆荚团队做的一个方案,通过proxy,隔离了后端的redis集群 优点 支持Kubernetes 有web图形界面,方便运维 Redis获得动态扩容/缩容的能力,增减redis实例对client完全透明、不需要重启服务 缺点 稳定性堪忧 依赖于国内的豌豆荚团队开发,迭代速度较慢 原版的docker镜像较大,没有根据组件进行分开 基于redis 3.x,而且很多原生的redis指令被阉割了 强依赖注册中心(Zookeeper、Etcd、Fs) 我们用了几个月吧,到后期频繁出现 ERR handle response, backend conn reset 此外,日常观察发现pod退出/重启困难.如果某个group节点全部挂掉的话,整个集群将不可读写. 综上,codis已经影响到了严重影响到了我们程序的正确性,决定弃用codis.改为普通的1主N从的模式. 主挂了之后导致服务不可用 #1356 参考链接 【Redis笔记】 第4篇: redis.conf中Replication配置项说明 Redis 配置文件详解 redis命令参考 redis主从复制常见的一些坑 Redis 的各项功能解决了哪些问题? 集群教程
为方便初学 Java8/C# 集合操作的人,特意写下这篇文章. 前期准备 C#版 java版 单集合 分类筛选 计数(Count) Date time1 = convertLocalDateToTimeZone(LocalDate.of(1990, 1, 1)); //0 Long count1 = list1.stream().filter(o -> o.getBirthday().equals(time1)).count(); int count1 = list1.Where(o => o.Birthday.Equals(new DateTime(1990, 1, 1)) && o.Sex == Sex.Male).Count(); long count2 = list1.Where(o => o.Birthday.Equals(new DateTime(1990, 1, 1)) && o.Sex == Sex.Male).LongCount(); /* 0 0 */ 分组(GroupBy) Map<Sex, List<Person>> group1 = list1.stream().collect(Collectors.groupingBy(Person::getSex)); Iterator it = group1.entrySet().iterator(); while (it.hasNext()) { Map.Entry<Sex, List<Person>> groupByItem = (Map.Entry) it.next(); Sex sex = groupByItem.getKey(); out.println(sex); groupByItem.getValue().forEach(person -> { out.println(new Gson().toJson(person)); }); } /* 输出结果: Male {"height":170,"weight":50,"identifier":"2","address":"北京","birthday":"Feb 1, 1982 12:00:00 AM","hobbies":["吃飯","看電影"],"sex":"Male"} Female {"height":165,"weight":50,"identifier":"1","address":"北京","birthday":"Jan 1, 1981 12:00:00 AM","hobbies":["吃飯","逛街"],"sex":"Female"} X {"height":170,"weight":50,"identifier":"3","address":"北京","birthday":"Mar 1, 1983 12:00:00 AM","hobbies":["吃飯","上網"],"sex":"X"} */ var group1 = list1.GroupBy(o => o.Sex); //当我们使用 GroupBy() 扩展方法时,使用了延迟执行。 这意味着,当你遍历集合的时候,下一个要出现的项目可能会或者可能不会被加载。 这是一个很大的性能改进,但它会引起有趣的副作用。 list1.RemoveAll(o => o.Sex == Sex.X);//定义 groupby 集合后对原集合进行修改,会发现group1里面已经没了 Sex=X的分组 foreach (var groupByItem in group1) { Sex sex = groupByItem.Key; System.Console.WriteLine(sex); foreach (Person person in groupByItem) { System.Console.WriteLine(JsonConvert.SerializeObject(person)); } } /* 输出结果: {"Height":165,"Weight":50,"Birthday":"1981-01-01T00:00:00","Hobbies":["吃飯","逛街"],"Identifier":"1","Address":"北京","Sex":2} Male {"Height":170,"Weight":50,"Birthday":"1982-02-01T00:00:00","Hobbies":["吃飯","看電影"],"Identifier":"2","Address":"北京","Sex":1} Female {"Height":165,"Weight":50,"Birthday":"1981-01-01T00:00:00","Hobbies":["吃飯","逛街"],"Identifier":"1","Address":"北京","Sex":2} Male {"Height":170,"Weight":50,"Birthday":"1982-02-01T00:00:00","Hobbies":["吃飯","看電影"],"Identifier":"2","Address":"北京","Sex":1} */ //该 ToLookup() 方法创建一个类似 字典(Dictionary ) 的列表List, 但是它是一个新的 .NET Collection 叫做 lookup。 Lookup,不像Dictionary, 是不可改变的。 这意味着一旦你创建一个lookup, 你不能添加或删除元素。 var group2 = list1.ToLookup(o => o.Sex); foreach (var groupByItem in group2) { Sex sex = groupByItem.Key; foreach (Person person in groupByItem) { System.Console.WriteLine(sex); System.Console.WriteLine(JsonConvert.SerializeObject(person)); } } /* 输出结果: {"Height":165,"Weight":50,"Birthday":"1981-01-01T00:00:00","Hobbies":["吃飯","逛街"],"Identifier":"1","Address":"北京","Sex":3} {"Height":170,"Weight":50,"Birthday":"1982-02-01T00:00:00","Hobbies":["吃飯","看電影"],"Identifier":"2","Address":"北京","Sex":3} */ 与此对比,stream没有RemoveAll的操作 匹配的第一项(findFirst/First,FirstOrDefault) Person after90 = list1.stream() .filter(o -> o.getBirthday().after(convertLocalDateToTimeZone(LocalDate.of(1990, 1, 1)))) .findFirst() .orElse(null); // null var after90 = list1.Where(o => o.Birthday >= new DateTime(1990, 1, 1)).First();//如果结果为空,将会导致异常,所以一般极少使用该方法 //An unhandled exception of type 'System.InvalidOperationException' occurred in System.Linq.dll: 'Sequence contains no elements' after90 = list1.Where(o => o.Birthday >= new DateTime(1990, 1, 1)).FirstOrDefault(); var after00 = list1.Where(o => o.Birthday >= new DateTime(2000, 1, 1)).FirstOrDefault(); 遍历(ForEach) list1.stream().forEach(o -> { //在ForEach當中可對集合進行操作 o.setSex(Sex.X); }); list1.forEach(o -> { out.println(new Gson().toJson(o)); }); /* {"height":165,"weight":50,"identifier":"1","address":"北京","birthday":"Jan 1, 1981 12:00:00 AM","hobbies":["吃飯","逛街"],"sex":"X"} {"height":170,"weight":50,"identifier":"2","address":"北京","birthday":"Feb 1, 1982 12:00:00 AM","hobbies":["吃飯","看電影"],"sex":"X"} {"height":170,"weight":50,"identifier":"3","address":"北京","birthday":"Mar 1, 1983 12:00:00 AM","hobbies":["吃飯","上網"],"sex":"X"} */ list1.ForEach(item => { //在ForEach當中可對集合進行操作 item.Sex = Sex.X; }); list1.ForEach(item => { System.Console.WriteLine(JsonConvert.SerializeObject(item)); }); 极值Max/Min //IntStream的max方法返回的是OptionalInt,要先判断有没有值再读取值.isPresent=false 时直接getAsInt会报错.mapToLong,mapToDouble同理 OptionalInt maxHeightOption = list1.stream().mapToInt(Person::getHeight).max(); //字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。 //当集合为长度0的集合时会返回起始值Integer.MIN_VALUE,起始值也不能乱传,个中缘由我暂不清楚 int maxHeight = list1.stream().mapToInt(Person::getHeight).reduce(Integer.MIN_VALUE, Integer::max); out.println(maxHeight); if (maxHeightOption.isPresent()) { maxHeight = maxHeightOption.getAsInt(); out.println(maxHeight); } //mapToInt参数的2种写法都一样,我比较喜欢以下写法,但是 idea 会报 warning OptionalInt minWeightOption = list1.stream().mapToInt(o -> o.getHeight()).min(); int minWeight = list1.stream().mapToInt(o -> o.getHeight()).reduce(Integer.MAX_VALUE, Integer::min); int maxHeight = list1.Select(o => o.Height).Max(); //同 list1.Max(o => o.Height); int minWeight = list1.Min(o => o.Weight); 跳过(skip/Skip),截取(limit/Take) //skip和 limit参数都是long, 这个要注意 list1.stream().skip(1L).limit(2L); 排序 去重复(Distinct) list1.stream().map(Person::getIdentifier).distinct(); list1.Select(o=>o.Identifier).Distinct(); list1.Skip(1).Take(2); 升序(sort/OrderBy) out.println("------------------------------------|升序|------------------------------------"); list1 = list1.stream().sorted(Comparator.comparing(Person::getBirthday)).collect(Collectors.toList()); out.println(new Gson().toJson(list1)); list1 = list1.stream().sorted((left, right) -> left.getBirthday().compareTo(right.getBirthday())).collect(Collectors.toList()); out.println(new Gson().toJson(list1)); //升序 list1 = list1.OrderBy(o => o.Birthday).ToList(); 降序(sort/OrderByDescending) out.println("------------------------------------|降序|------------------------------------"); list1 = list1.stream().sorted(Comparator.comparing(Person::getBirthday).reversed()).collect(Collectors.toList()); out.println(new Gson().toJson(list1)); list1 = list1.stream().sorted((left, right) -> right.getBirthday().compareTo(left.getBirthday())).collect(Collectors.toList()); out.println(new Gson().toJson(list1)); //降序 list1 = list1.OrderByDescending(o => o.Birthday).ToList(); 多集合 交集 list1 ∩ list2 out.println("------------------------------------|交集 list1 ∩ list2|------------------------------------"); list1.stream().filter(o -> list2.contains(o)).collect(Collectors.toList()); //连接,下面表示把 list1和 list2当中相同身份证号的取出来,生成一个新的集合 //实际上, join 有另外的用法,类似 sqlserver 里面的多表连接,将不同数据源结合到一起,生成新的数据结构 var intersect = list1.Join(list2, o => o.Identifier, o => o.Identifier, (a, b) => a).ToList(); //交集 list1 ∩ list2 intersect = list1.Intersect(list2).ToList(); 并集list1 ∪ list2 out.println("------------------------------------|并集list1 ∪ list2 |------------------------------------"); list1.addAll(list2); //并集list1 ∪ list2 var union = list1.Union(list2).ToList(); 差集list1 - list2 out.println("------------------------------------|差集list1 - list2|------------------------------------"); list1.stream().filter(item1 -> !list2.contains(item1)).collect(Collectors.toList()); //差集list1 - list2 var except = list1.Except(list2).ToList(); 数据结构转换 out.println("------------------------------------|数据结构转换|------------------------------------"); List<Person> list3 = list1.stream().filter(o -> true).collect(Collectors.toList()); ArrayList<Person> list4 = list1.stream().filter(o -> true).collect(Collectors.toCollection(ArrayList::new)); Set<Person> list5 = list1.stream().filter(o -> true).collect(Collectors.toSet()); Object[] list6 = list1.stream().toArray(); Person[] list7 = list1.stream().toArray(Person[]::new); //数据结构转换 list1.ToArray(); //注意如果 key 重复,ToDictionary会导致出错 list1.ToDictionary(o => o.Identifier, o => o); list1.ToHashSet();
设置git git config --global user.name "Rose" git config --global user.email 增加远程origin git remote add origin <> 第一次提交 cd <project> git remote add origin “” git add . git commit -am "处女提交" git push -u origin master 日常的提交 git add . d: cd <project> git commit -am "test" git push 分支管理 推送特定分支 git push origin NewUI 查看当前分支 git branch 列出远程分支 git branch -r 列出所有分支 git branch -a 创建分支 git branch branchName 切换分支 git checkout AndroidStudio git checkout master 创建并切换到分支 git checkout -b branchName 灾难处理 修复未提交文件中的错误(重置) 如果你现在的工作目录(work tree)里搞的一团乱麻, 但是你现在还没有把它们提交; 你可以通过下面的命令, 让工作目录回到上次提交时的状态(last committed state): git reset --hard HEAD 回退所有内容到上一个版本 git reset HEAD^ 回溯到特定的commitid 在.git/logs/head 里面找到我那个提交了用 git reset <commit id> --hard 紧急热修复 思路是找到上次的稳定版本,修改后推送到目标分支.这种修复方式是最快的 # git checkout HEAD^1 回滚到上一次release的commitID git checkout <commit ID> git commit -am "fix" git push origin HEAD:master 撤销上一次提交 (该操作非常凶险,所有这次提交的内容都会不见) git revert HEAD git log git log --stat -n 5 // 简单的列出了修改过的文件 git log -p -n 5 // 详细的列出修改过的文件,及提交文件的对比 git log --graph // ASCII 字符串表示的简单图形,形象地展示了每个提交所在的分支及其分化衍合情况 git log --all --decorate --graph git log --pretty=oneline // 只显示哈希值和提交说明 git log --pretty=oneline/short/full/fuller/format:""(格式等) git log --name-only // 仅在提交信息后显示已修改的文件清单 git log --no-merges // 不显示merge的log :格式化日志 git log --pretty=format:" %cd %h - %s" --since=3.days 参考链接:Git 基础 - 查看提交历史 其他命令 git checkout //后面不跟任何参数,则就是对工作区进行检查 git checkout --filename //从暂存区中恢复文件(确保filename与branch名称不同) VS插件异常处理 无法将分支 master 发布到远程 origin,因为远程存储库中已存在具有同一名称的分支。发布此分支将导致远程存储库中的分支发生非快进更新。 git push --set-upstream origin master 无法将本地分支master发布到远程存储库origin git config branch.master.remote origin git config branch.master.merge refs/heads/master git的错误处理 2边的文件都有修改 You have unmerged paths.(fix conflicts and run "git commit")Move/rename/delete/copy/edit/whatever those files to get your code to the desired state, then git add the changes and git commit the conflict resolution. git reset 9b25cd5635b4986aa1f1d614381226f79d123f87 --hard 中途初始化git项目之后从远程仓库中拉取发生错误 The following untracked working tree files would be overwritten by merge git fetch origin git reset --hard origin/master git pull需要密码 把C:\Users\<用户>\.ssh里的id_rsa和id_rsa.pub文件复制到git安装目录下的.ssh目录里面即可 fatal: The current branch master has no upstream branch [branch "master"] remote = origin merge = refs/heads/master mac ssh 出现 permission deny 的解决方案 chmod 600 ~/.ssh/id_rsa ssh-add ~/.ssh/id_rsa Your branch and 'origin/master' have diverged 注意,此操作会废了本地的提交 git fetch origin git reset --hard origin/master 特别的技巧 修改.gitignore文件后,清理项目文件夹 git rm -r --cached . git add . git commit -am "clean" git push 清除垃圾前提示 git clean -i -d 强制清除垃圾 git clean -f -d 多个pushurl,一个pull地址 git remote set-url --push --add origin <url> git remote set-url --push --add origin <url> 效果如下: [remote "origin"] url = <url1> pushurl = <url2> pushurl = <url1> fetch = +refs/heads/*:refs/remotes/origin/* 修改远程url git remote set-url origin <URL> 从历史中永远删除某个文件 从 https://rtyley.github.io/bfg-repo-cleaner/ 中下载 bfg-1.13.0.jar java -jar bfg.jar --strip-blobs-bigger-than 100M <git项目完整路径> git reflog expire --expire=now --all && git gc --prune=now --aggressive git push push所有本地的分支到远程仓库中对应匹配的分支。 git config --global push.default matching 只有当前分支会被push到你使用git pull获取的代码。 git config --global push.default simple 更改origin url git remote set-url origin <URL> 为git设置代理和取消代理 git config --global http.proxy 'socks5://127.0.0.1:1080' git config --global https.proxy 'socks5://127.0.0.1:1080' git config --local http.proxy 'http://127.0.0.1:1080' git config --local https.proxy 'http://127.0.0.1:1080' # 取消代理 git config --global --unset http.proxy git config --global --unset https.proxy 抹除所有提交历史,重新提交 git checkout --orphan latest_branch git add -A git commit -am "重新提交" git branch -D master git branch -m master git push -f origin master **gitlab的注意事项** gitlab的话,要先取消protected_branches中 master 的保护,然后再将本地的这个 master 提交到另外一个远程分支git push origin master:temp; 然后设置这个temp 分支为默认分支,然后再提交到master远程分支git push origin master:master; 删除 temp 远程分支,设置 master 为默认分支 : 删除远程master分支git push origin :temp 设置发布用公钥 把本地用户的 ~/.ssh/id_rsa.pub 的内容追加到 Git 服务器仓库所属用户的 ~/.ssh/authorized_keys 文件的末尾即可。文件不存在的话用`ssh-keygen`生成 管理大文件 Git Large File Storage,原理是历史里面存放文件的指针,文件放在远程仓库,提高整个拉取,推送效率。 git lfs install brew install git-lfs git lfs track "*.psd" git add .gitattributes 如果大文件以及存在于历史中,则需要输入另外的命令 git lfs migrate import --include="*.psd" 从别的地方重新拉取仓库之后,被track的文件已指针的形式存在,如果需要还原文件,得输入2个命令 git lfs install git lfs pull 获得命令帮助 git lfs help 参考链接: ssh 连接提示 Permission denied (publickey) 怎么破 Linux权限详解(chmod、600、644、666、700、711、755、777、4755、6755、7755) SSH Permission denied (publickey)解决办法 关于Gitlab若干权限问题 git删除master分支后重建 git: Your branch and 'origin/master' have diverged - how to throw away local commits?
CIDR是一种表示网段的工具,格式比较简洁. CIDR表示方法:IP地址/网络ID的位数 一开始我总是不懂最后那位数字的关系,后来又被CIDR的IP地址给迷惑了, 后来我明白了,斜杆后面的数字表示保持不变的IP位数 IPV4是8*4=32位的.举个例子,192.168.15.0/19(等价于192.168.0.0/19)表示192.168.0.0-192.168.31.255这个网段 192.168.15.0取第三段IP15,二进制为1111 192.168.31.255取第三段31,二进制为11111 补齐到8位,就是 00001111 00011111 19的意思就是保留前19位IP不变,前2段那么19-8*2=3,起始的IP15换成二进制都不达到5位,所以等价于0.而五位数的二进制最大值是11111,所以最大网段就是31. 最小的6个长度的二进制数是32,所以192.168.32.0/19等同于192.168.32.0-192.168.63.255 192.168.1.0/19,192.168.2.0/19......192.168.31.0/19都等同于192.168.0.0/19
2021年08月
2020年09月
2020年08月
2020年06月
2019年08月
以前阿难尊者在公司整天屁事不干,就知道谈感情。有一次我无意间窥到他的屏幕,他竟然脸色一变,说了句“不要看领导的屏幕。”
这种人,拿到一根鸡毛就当令箭。于是我愤而离职,而他后来装逼被雷劈,用人不慎,导致公司亏空不少。也不知道为啥,当时笑面虎还留了他三个月。
而在他离职交接的最后那三个月,打电话不接,发消息不回。他给我留了一堆“无字真经”(完全没有密码的服务器),涵盖了腾讯云和阿里云,此外还有一大堆无效的DNS记录,CDN域名。处理这些垃圾的善后工作,陆陆续续花了我一年多时间。
但是天无绝人之路,我获得了阿里云和公司CTO的支持,看了英文版的《kubernetes in action》,在三个月的时间里面从 0 docker 基础,到落地 kubernetes,之后又在公司内部培训员工,我们一起把容器化工作从 Java node port 扩展到 PHP,golang 微服务。
总结这段经历,我写下了《某运维负责人之死-阿里云开发者社区》。
最后,我想说一句,搭理心胸狭隘的人,纯粹就是浪费自己时间。这个世界是有上帝之手的,那些内心狭隘,表里不一的人,最后一概会被神罚。
你觉得现在的互联网有什么问题,下一代互联网的架构是什么样子的?
支持.deploy 有个replicas 字段,表示容器的副本数.
kubeflow
controller 得建立跟你阿里云账户的关联.