恭喜老詹,35岁的詹姆斯,依旧狂奔在追逐乔丹的路上!


在去年7月,这支湖人刚刚组建完成时,有几个人愿意相信他们能够成功地走到最后呢?在那些质疑者的眼里,这是一支危机四伏,随时都有可能被推翻重来的球队。
他们认为湖人的管理团队太过混乱,缺少成为一支伟大球队所需要的稳定性。
他们虽然认可球队的教练组豪华万分,但却认为由三名前任NBA主帅搭建起来的核心构架,更有可能发生的是暗流涌动,各自心怀鬼胎。
他们认为湖人的阵容缺乏深度,虽有詹眉打底,但却没有稳定可靠的第三点,远不及其他的竞争对手。
他们认为詹姆斯老了,即将年满35周岁的他,很有可能再次被伤病给盯上,一个无法全力输出的詹姆斯,必定无法统帅一支成功的球队。
但,他们错了。
在走完了这段长达一年多的漫长旅途后,洛杉矶湖人,时隔十年再度踏上了世界之巅。至少在这个夜晚,那些质疑者们可以闭嘴了。
湖人这一次的成功,属于队里的每一个人。

他们低估了湖人的教练组。
事后来看,这是一个出色的团队,初期的试探,赛间的调整,甚至有魄力在赛中对阵容的搭配进行临时的修整。他们的准备之充分,应对手法之深厚,显然超过了外界最初的预期。
他们低估了湖人的角色球员与年轻人,能在一年的时间里所收获的成长。
这些失意的,不被人看好的,无人问津的球员,在洛杉矶重新收获了信任,他们打出了强硬的防守表现,留下了无数闪耀的瞬间。

他们低估了安东尼-戴维斯的价值。
在来到这之前,他是众人嘴中调侃的“美国之子”,现役最著名的“荣誉摇摆人”,名大于实的代表人物。
但在来到这之后,他用行动证明了自己的价值,证明了为什么他是这个时代最好最全能的内线球员。他不仅构建起了球队的防守体系,更是他们在进攻端最锋利的刺刃,因为浓眉的存在,让洛杉矶人拥有了更多破解防守与应对进攻的办法,他是球队能够登顶的重要功臣与关键之一。

当然,被他们所低估的,还有勒布朗-詹姆斯的领导力与决心。
的确,他35岁了,跑的没有以前那么快了,跳的没有年轻时那么高了,身体也会受到一些伤病的困扰了,可他追逐伟大的心,依旧炽热。
“我努力奋斗的所有动力,都是为了追逐那个曾经在芝加哥打球的幽灵。”
4年前,刚刚率领着克利夫兰骑士打破52年魔咒,逆天登顶的勒布朗,在接受时任《体育画报》记者的詹金斯采访时,说出了这句话。而在时隔4年之后今天,他依旧在用行动践行着自己曾经许下的诺言。
每当詹姆斯赢得一次总冠军,关于他能否超越乔丹这个议题,就会重新开始被人热议。对此,我也听过不少的“狠话”,总结着来讲,大概意思就是:“从从前,到现在,乃至将来,‘乔丹是至高无上的篮球之神’这件事,永远都不会发生改变。”
我当然也跟大多数人一样,非常认同乔丹在篮球世界里如象征图腾一般的地位。他所取得的荣誉是无上的,他所做到过的那些事情是极富传奇色彩的,他配得上任何人为他送上的任何赞美,哪怕距离他完全退役已过去近20年之久,迈克尔-乔丹这个名字也依旧还在影响着这项运动,正如我们每个人在最初遇见篮球时所知道的第一个名字,永远属于他一样。
但是,仅就我个人而言,我并不相信所谓“真理的永恒”。
乔丹之所以被称为篮球之神,绝非靠的一手虚无的赞美,而是因为那一连串的数字实在太过耀眼——10届得分王,3届抢断王,1个DPOY,5个MVP,6次夺得总冠军,6次拿下FMVP,10个年度一阵,9个年度一防,外加一票多到数不清,哪哪都有他的数据纪录——他的伟大,无需吹捧,也经得起任何比较。
但这并不意味着,当这些数字开始受到别人挑战时,他依旧能靠着所谓“神性”,永远高枕无忧地坐在那个位子上。
虽然詹姆斯目前还不足以威胁到乔丹,但显然,他已经拉近了自己与少年偶像的距离。只是偏见与固有看法的存在,让人或多或少地低估了眼前人所创造的那些传奇成就。

比如有的人会用总决赛胜率来贬低詹姆斯。
的确,40%的总决赛胜率在“乔丹不打第七场”和“乔丹从未输过总决赛”面前,显得有些脆弱不堪。
但又有多少人忽略了,那可是长达10年的总决赛征战啊,更何况我们在评判一件事物的好与坏时,真的该用如此一刀切的方式么?
2007年,四年级的新星勒布朗-詹姆斯独自率领着克利夫兰骑士,史无前例地杀进了总决赛,然后败给了无论阵容,或是核心都要更加强大且老辣的圣安东尼奥马刺,这样的失败,难道不该被加上引号么;
2015年,乐福与欧文相继倒下,詹姆斯带着82万年薪的先发控卫,总价不过2120万美元的核心6人组,与勇士厮杀了6场最终抱憾收场的失败,也应该受人唾弃么;
2018年,欧文出走,球队中期地震式动荡,在经历了两轮大清洗之后,不仅丢掉了明星实力,也彻底失去了自己的阵容深度。整个季后赛,除詹姆斯能每场稳定贡献34分之外,仅乐福一人场均得分上双(14.9分),且命中率只有惨淡的39.2%。但就是在这种极端的情况下,他依旧扛着骑士踏上了总决赛的舞台。的确,在强悍的勇士面前,他一场都没赢,可单场51分拼到加时的神勇,你真的会觉得这是一种耻辱么?
我永远不会这么认为,我只会觉得这是詹姆斯将一支本不属于总决赛的球队,带到了那个位置上,我更愿意将其视作是个人的一次巨大成功。

再比如,有人认为詹姆斯的数据积累之所以如此骇人,纯粹只是因为他打的比较多而已。
但近在眼前的例子——金州勇士核心成员的遭遇——告诉我们:长时间高强度的征战,所能带来的东西绝非只有数据与荣誉,更有身体上的超负荷运转。
17年的职业生涯,有超过一半的时间,詹姆斯都打进了总决赛,他在季后赛的出场时间超过了1万分钟,排名NBA历史第一,几乎是现役排行老二的凯文-杜兰特的一倍之多。
他能够扛住如此长时间的高强度对抗,除了得益于他惊人的身体天赋之外,更重要的,是数年如一日的努力,是对饮食与生活的严苛管理,以及对自身身体的精心照料。
我们早已习惯了他展现在世人面前的优秀,却总是会忽略掉掩藏在这一切背后的血与泪。
打的比赛多,是努力付出之后的回报,更是能力的体现。这本该受到褒奖,但在詹姆斯身上,却无奈地成了另一种被人看低的方式。
这一切,真的就本该如此么?

我不想说詹姆斯未来必定还能取得怎样的荣誉,或是数据积累。毕竟谁都无法确定没有发生过的事。但在今天,在詹姆斯拿下自己职业生涯第4个总冠军和第4座总决赛MVP的奖杯后,他已经成为了NBA历史上仅次于迈克尔-乔丹的球员——在这之前,除了乔丹,没人拿到过超过3个FMVP——他理应获得足够的尊重。
就像他在捧起奖杯时说的那番话一样:“我想得到的,是尊重。”
在他出现在这之前,我们甚至没有见过一个哪怕能够接近乔丹的人,而在这之后,我们也无从确认,下一个拥有这等能力的天才,会在何年何月再次降临。
第17个赛季落幕,年近36岁的詹姆斯,依旧狂奔在这条追赶伟大幽灵的路上。他或许可以成功,又或许不能。但我想,当你有机会去见证这样一段跨越历史的较量时,期待与祝福,终归是要强过诋毁与谩骂的。
在这样一个时刻,在所有人在高呼着“湖人总冠军”的时候,我更想把我的心里话,送给这个时代最伟大的篮球运动员:
“恭喜你,勒布朗-詹姆斯,在经历了17年的漫长旅途之后,你终于获得了挑战神的资格。这个画面,是自你踏入联盟的第一天起,就被世人所寄予的期待。就现在,抬起头看看,上帝的神像就矗立在那座山峰之上,你能看见他,他不远了,去挑战他吧!”

kubectl 技巧总结

Kubectl 是 Kubernetes 最重要的命令行工具。在 Flant,我们会在 Wiki 和 Slack 上相互分享 Kubectl 的妙用(其实我们还有个搜索引擎,不过那就是另外一回事了)。多年以来,我们在 kubectl 方面积累了很多技巧,现在想要将其中的部分分享给社区。

我相信很多读者对这些命令都非常熟悉;然而我还是希望读者能够从本文中有所获益,进而提高生产力。

下列内容有的是来自我们的工程师,还有的是来自互联网。我们对后者也进行了测试,并且确认其有效性。

现在开始吧。

获取 Pod 和节点

  1. 我猜你知道如何获取 Kubernetes 集群中所有 Namespace 的 Pod——使用 --all-namepsaces 就可以。然而不少朋友还不知道,现在这一开关还有了 -A 的缩写。
  2. 如何查找非 running 状态的 Pod 呢? kubectl get pods -A --field-selector=status.phase!=Running | grep -v Complete顺便一说,--field-selector 是个值得深入一点的参数。
  3. 如何获取节点列表及其内存容量: kubectl get no -o json | \
    jq -r '.items | sort_by(.status.capacity.memory)[]|[.metadata.name,.status.capacity.memory]| @tsv'
  4. 获取节点列表,其中包含运行在每个节点上的 Pod 数量: kubectl get po -o json --all-namespaces | \
    jq '.items | group_by(.spec.nodeName) | map({"nodeName": .[0].spec.nodeName, "count": length}) | sort_by(.count)'
  5. 有时候 DaemonSet 因为某种原因没能在某个节点上启动。手动搜索会有点麻烦: $ ns=my-namespace
    $ pod_template=my-pod
    $ kubectl get node | grep -v \"$(kubectl -n ${ns} get pod --all-namespaces -o wide | fgrep ${pod_template} | awk '{print $8}' | xargs -n 1 echo -n "\|" | sed 's/[[:space:]]*//g')\"
  6. 使用 kubectl top 获取 Pod 列表并根据其消耗的 CPU 或 内存进行排序: # cpu
    $ kubectl top pods -A | sort --reverse --key 3 --numeric
    # memory
    $ kubectl top pods -A | sort --reverse --key 4 --numeric
  7. 获取 Pod 列表,并根据重启次数进行排序:kubectl get pods —sort-by=.status.containerStatuses[0].restartCount当然也可以使用 PodStatus 以及 ContainerStatus 的其它字段进行排序。

获取其它数据

  1. 运行 Ingress 时,经常要获取 Service 对象的 selector 字段,用来查找 Pod。过去要打开 Service 的清单才能完成这个任务,现在使用 -o wide 参数也可以: $ kubectl -n jaeger get svc -o wide
    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
    jaeger-cassandra ClusterIP None <none> 9042/TCP 77d app=cassandracluster,cassandracluster=jaeger-cassandra,cluster=jaeger-cassandra
  2. 如何输出 Pod 的 requests 和 limits $ kubectl get pods -A -o=custom-columns='NAME:spec.containers[*].name,MEMREQ:spec.containers[*].resources.requests.memory,MEMLIM:spec.containers[*].resources.limits.memory,CPUREQ:spec.containers[*].resources.requests.cpu,CPULIM:spec.containers[*].resources.limits.cpu'
    NAME MEMREQ MEMLIM CPUREQ CPULIM
    coredns 70Mi 170Mi 100m <none>
    coredns 70Mi 170Mi 100m <none>
    ...
  3. kubectl run(以及 createapplypatch)命令有个厉害的参数 --dry-run,该参数让用户无需真正操作集群就能观察集群的行为,如果配合 -o yaml,就能输出命令对应的 YAML: $ kubectl run test --image=grafana/grafana --dry-run -o yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    creationTimestamp: null
    labels:
    run: test
    name: test
    spec:
    replicas: 1
    selector:
    matchLabels:
    run: test
    简单的把输出内容保存到文件,删除无用字段就可以使用了。1.18 开始 kubectl run 生成的是 Pod 而非 Deployment。
  4. 获取指定资源的描述清单: kubectl explain hpa
    KIND: HorizontalPodAutoscaler
    VERSION: autoscaling/v1
    DESCRIPTION:
    configuration of a horizontal pod autoscaler.
    FIELDS:
    apiVersion <string>
    ...

网络

  1. 获取集群节点的内部 IP: $ kubectl get nodes -o json | jq -r '.items[].status.addresses[]? | select (.type == "InternalIP") | .address' | \
    paste -sd "\n" -
    9.134.14.252
  2. 获取所有的 Service 对象以及其 nodePort $ kubectl get -A svc -o json | jq -r '.items[] | [.metadata.name,([.spec.ports[].nodePort | tostring ] | join("|"))]| @tsv'

    kubernetes null
    ...
  3. 在排除 CNI(例如 Flannel)故障的时候,经常会需要检查路由来识别故障 Pod。Pod 子网在这里非常有用: $ kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' | tr " " "\n" fix-doc-azure-container-registry-config ✭
    10.120.0.0/24
    10.120.1.0/24
    10.120.2.0/24

日志

  1. 使用可读的时间格式输出日志: $ kubectl logs -f fluentbit-gke-qq9w9 -c fluentbit --timestamps
    2020-09-10T13:10:49.822321364Z Fluent Bit v1.3.11
    2020-09-10T13:10:49.822373900Z Copyright (C) Treasure Data
    2020-09-10T13:10:49.822379743Z
    2020-09-10T13:10:49.822383264Z [2020/09/10 13:10:49] [ info] Configuration:
  2. 只输出尾部日志: kubectl logs -f fluentbit-gke-qq9w9 -c fluentbit --tail=10
    [2020/09/10 13:10:49] [ info] ___________
    [2020/09/10 13:10:49] [ info] filters:
    [2020/09/10 13:10:49] [ info] parser.0
    ...
  3. 输出一个 Pod 中所有容器的日志:kubectl -n my-namespace logs -f my-pod —all-containers
  4. 使用标签选择器输出多个 Pod 的日志:kubectl -n my-namespace logs -f -l app=nginx
  5. 获取“前一个”容器的日志(例如崩溃的情况):kubectl -n my-namespace logs my-pod —previous

其它

  1. 把 Secret 复制到其它命名空间: kubectl get secrets -o json --namespace namespace-old | \
    jq '.items[].metadata.namespace = "namespace-new"' | \
    kubectl create-f -
  2. 下面两个命令可以生成一个用于测试的自签发证书: openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=grafana.mysite.ru/O=MyOrganization"
    kubectl -n myapp create secret tls selfsecret --key tls.key --cert tls.crt

相关链接

本文没什么结论,但是可以提供一个小列表,其中包含本文相关的有用链接。

  1. Kubernetes 官方文档:https://kubernetes.io/docs/reference/kubectl/cheatsheet/
  2. Linux Academy 的入门参考:https://linuxacademy.com/blog/containers/kubernetes-cheat-sheet/
  3. Blue Matador 分类整理的命令列表:https://www.bluematador.com/learn/kubectl-cheatsheet
  4. 另一个命令指南,部分内容和本文重复:https://gist.github.com/pydevops/0efd399befd960b5eb18d40adb68ef83
  5. kubectl 别名搜集:https://github.com/ahmetb/kubectl-aliases

高并发架构实践

高并发经常会发生在有大活跃用户量,用户高聚集的业务场景中,如:秒杀活动,定时领取红包等。
为了让业务可以流畅的运行并且给用户一个好的交互体验,我们需要根据业务场景预估达到的并发量等因素,来设计适合自己业务场景的高并发处理方案。

在电商相关产品开发的这些年,我有幸的遇到了并发下的各种坑,这一路摸爬滚打过来有着不少的血泪史,这里进行的总结,作为自己的归档记录,同时分享给大家。

服务器架构

业务从发展的初期到逐渐成熟,服务器架构也是从相对单一到集群,再到分布式服务。 
一个可以支持高并发的服务少不了好的服务器架构,需要有均衡负载,数据库需要主从集群,nosql缓存需要主从集群,静态文件需要上传cdn,这些都是能让业务程序流畅运行的强大后盾。

服务器这块多是需要运维人员来配合搭建,具体我就不多说了,点到为止。
大致需要用到的服务器架构如下:

  • 服务器
    • 均衡负载(如:nginx,阿里云SLB)
    • 资源监控
    • 分布式
  • 数据库
    • 主从分离,集群
    • DBA 表优化,索引优化,等
    • 分布式
  • nosql
    • redis
      • 主从分离,集群
    • mongodb
      • 主从分离,集群
    • memcache
      • 主从分离,集群
  • cdn
    • html
    • css
    • js
    • image

并发测试

高并发相关的业务,需要进行并发的测试,通过大量的数据分析评估出整个架构可以支撑的并发量。

测试高并发可以使用第三方服务器或者自己测试服务器,利用测试工具进行并发请求测试,分析测试数据得到可以支撑并发数量的评估,这个可以作为一个预警参考,俗话说知己自彼百战不殆。

第三方服务:

  • 阿里云性能测试

并发测试工具:

  • Apache JMeter
  • Visual Studio性能负载测试
  • Microsoft Web Application Stress Tool

实战方案

通用方案

日用户流量大,但是比较分散,偶尔会有用户高聚的情况;

通用

场景: 用户签到,用户中心,用户订单,等
服务器架构图: 

说明:

场景中的这些业务基本是用户进入APP后会操作到的,除了活动日(618,双11,等),这些业务的用户量都不会高聚集,同时这些业务相关的表都是大数据表,业务多是查询操作,所以我们需要减少用户直接命中DB的查询;优先查询缓存,如果缓存不存在,再进行DB查询,将查询结果缓存起来。

更新用户相关缓存需要分布式存储,比如使用用户ID进行hash分组,把用户分布到不同的缓存中,这样一个缓存集合的总量不会很大,不会影响查询效率。

方案如:

  • 用户签到获取积分
    • 计算出用户分布的key,redis hash中查找用户今日签到信息
    • 如果查询到签到信息,返回签到信息
    • 如果没有查询到,DB查询今日是否签到过,如果有签到过,就把签到信息同步redis缓存。
    • 如果DB中也没有查询到今日的签到记录,就进行签到逻辑,操作DB添加今日签到记录,添加签到积分(这整个DB操作是一个事务)
    • 缓存签到信息到redis,返回签到信息
    • 注意这里会有并发情况下的逻辑问题,如:一天签到多次,发放多次积分给用户。
  • 用户订单
    • 这里我们只缓存用户第一页的订单信息,一页40条数据,用户一般也只会看第一页的订单数据
    • 用户访问订单列表,如果是第一页读缓存,如果不是读DB
    • 计算出用户分布的key,redis hash中查找用户订单信息
    • 如果查询到用户订单信息,返回订单信息
    • 如果不存在就进行DB查询第一页的订单数据,然后缓存redis,返回订单信息
  • 用户中心
    • 计算出用户分布的key,redis hash中查找用户订单信息
    • 如果查询到用户信息,返回用户信息
    • 如果不存在进行用户DB查询,然后缓存redis,返回用户信息
  • 其他业务
    • 上面例子多是针对用户存储缓存,如果是公用的缓存数据需要注意一些问题,如下
    • 注意公用的缓存数据需要考虑并发下的可能会导致大量命中DB查询,可以使用管理后台更新缓存,或者DB查询的锁住操作。
    • 我的博文[大话Redis进阶]对更新缓存问题和推荐方案的分享。

以上例子是一个相对简单的高并发架构,并发量不是很高的情况可以很好的支撑,但是随着业务的壮大,用户并发量增加,我们的架构也会进行不断的优化和演变,比如对业务进行服务化,每个服务有自己的并发架构,自己的均衡服务器,分布式数据库,nosql主从集群,如:用户服务、订单服务;

消息队列

秒杀、秒抢等活动业务,用户在瞬间涌入产生高并发请求

消息队列

场景:定时领取红包,等
服务器架构图:

说明:

场景中的定时领取是一个高并发的业务,像秒杀活动用户会在到点的时间涌入,DB瞬间就接受到一记暴击,hold不住就会宕机,然后影响整个业务;

像这种不是只有查询的操作并且会有高并发的插入或者更新数据的业务,前面提到的通用方案就无法支撑,并发的时候都是直接命中DB;

设计这块业务的时候就会使用消息队列的,可以将参与用户的信息添加到消息队列中,然后再写个多线程程序去消耗队列,给队列中的用户发放红包;

方案如:

  • 定时领取红包
    • 一般习惯使用 redis的 list
    • 当用户参与活动,将用户参与信息push到队列中
    • 然后写个多线程程序去pop数据,进行发放红包的业务
    • 这样可以支持高并发下的用户可以正常的参与活动,并且避免数据库服务器宕机的危险

附加: 
通过消息队列可以做很多的服务。 
如:定时短信发送服务,使用sset(sorted set),发送时间戳作为排序依据,短信数据队列根据时间升序,然后写个程序定时循环去读取sset队列中的第一条,当前时间是否超过发送时间,如果超过就进行短信发送。

一级缓存

高并发请求连接缓存服务器超出服务器能够接收的请求连接量,部分用户出现建立连接超时无法读取到数据的问题;

因此需要有个方案当高并发时候时候可以减少命中缓存服务器;

这时候就出现了一级缓存的方案,一级缓存就是使用站点服务器缓存去存储数据,注意只存储部分请求量大的数据,并且缓存的数据量要控制,不能过分的使用站点服务器的内存而影响了站点应用程序的正常运行,一级缓存需要设置秒单位的过期时间,具体时间根据业务场景设定,目的是当有高并发请求的时候可以让数据的获取命中到一级缓存,而不用连接缓存nosql数据服务器,减少nosql数据服务器的压力

比如APP首屏商品数据接口,这些数据是公共的不会针对用户自定义,而且这些数据不会频繁的更新,像这种接口的请求量比较大就可以加入一级缓存;

通用

服务器架构图:

合理的规范和使用nosql缓存数据库,根据业务拆分缓存数据库的集群,这样基本可以很好支持业务,一级缓存毕竟是使用站点服务器缓存所以还是要善用。

静态化数据

高并发请求数据不变化的情况下如果可以不请求自己的服务器获取数据那就可以减少服务器的资源压力。

对于更新频繁度不高,并且数据允许短时间内的延迟,可以通过数据静态化成JSON,XML,HTML等数据文件上传CDN,在拉取数据的时候优先到CDN拉取,如果没有获取到数据再从缓存,数据库中获取,当管理人员操作后台编辑数据再重新生成静态文件上传同步到CDN,这样在高并发的时候可以使数据的获取命中在CDN服务器上。

CDN节点同步有一定的延迟性,所以找一个靠谱的CDN服务器商也很重要

其他方案

  • 对于更新频繁度不高的数据,APP,PC浏览器,可以缓存数据到本地,然后每次请求接口的时候上传当前缓存数据的版本号,服务端接收到版本号判断版本号与最新数据版本号是否一致,如果不一样就进行最新数据的查询并返回最新数据和最新版本号,如果一样就返回状态码告知数据已经是最新。减少服务器压力:资源、带宽

分层,分割,分布式

大型网站要很好支撑高并发,这是需要长期的规划设计 
在初期就需要把系统进行分层,在发展过程中把核心业务进行拆分成模块单元,根据需求进行分布式部署,可以进行独立团队维护开发。

  • 分层
    • 将系统在横向维度上切分成几个部分,每个部门负责一部分相对简单并比较单一的职责,然后通过上层对下层的依赖和调度组成一个完整的系统
    • 比如把电商系统分成:应用层,服务层,数据层。(具体分多少个层次根据自己的业务场景)
    • 应用层:网站首页,用户中心,商品中心,购物车,红包业务,活动中心等,负责具体业务和视图展示
    • 服务层:订单服务,用户管理服务,红包服务,商品服务等,为应用层提供服务支持
    • 数据层:关系数据库,nosql数据库 等,提供数据存储查询服务
    • 分层架构是逻辑上的,在物理部署上可以部署在同一台物理机器上,但是随着网站业务的发展,必然需要对已经分层的模块分离部署,分别部署在不同的服务器上,使网站可以支撑更多用户访问
  • 分割
    • 在纵向方面对业务进行切分,将一块相对复杂的业务分割成不同的模块单元
    • 包装成高内聚低耦合的模块不仅有助于软件的开发维护,也便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展
    • 比如用户中心可以分割成:账户信息模块,订单模块,充值模块,提现模块,优惠券模块等
  • 分布式
    • 分布式应用和服务,将分层或者分割后的业务分布式部署,独立的应用服务器,数据库,缓存服务器
    • 当业务达到一定用户量的时候,再进行服务器均衡负载,数据库,缓存主从集群
    • 分布式静态资源,比如:静态资源上传cdn
    • 分布式计算,比如:使用hadoop进行大数据的分布式计算
    • 分布式数据和存储,比如:各分布节点根据哈希算法或其他算法分散存储数据
image

网站分层-图1来自网络

集群

对于用户访问集中的业务独立部署服务器,应用服务器,数据库,nosql数据库。 核心业务基本上需要搭建集群,即多台服务器部署相同的应用构成一个集群,通过负载均衡设备共同对外提供服务, 服务器集群能够为相同的服务提供更多的并发支持,因此当有更多的用户访问时,只需要向集群中加入新的机器即可, 另外可以实现当其中的某台服务器发生故障时,可以通过负载均衡的失效转移机制将请求转移至集群中其他的服务器上,因此可以提高系统的可用性

  • 应用服务器集群
    • nginx 反向代理
    • slb
    • … …
  • (关系/nosql)数据库集群
    • 主从分离,从库集群
image

通过反向代理均衡负载-图2来自网络

异步

在高并发业务中如果涉及到数据库操作,主要压力都是在数据库服务器上面,虽然使用主从分离,但是数据库操作都是在主库上操作,单台数据库服务器连接池允许的最大连接数量是有限的 
当连接数量达到最大值的时候,其他需要连接数据操作的请求就需要等待有空闲的连接,这样高并发的时候很多请求就会出现connection time out 的情况 
那么像这种高并发业务我们要如何设计开发方案可以降低数据库服务器的压力呢?

  • 如:
    • 自动弹窗签到,双11跨0点的时候并发请求签到接口
    • 双11抢红包活动
    • 双11订单入库
  • 设计考虑:
    • 逆向思维,压力在数据库,那业务接口就不进行数据库操作不就没压力了
    • 数据持久化是否允许延迟?
    • 如何让业务接口不直接操作DB,又可以让数据持久化?
  • 方案设计:
    • 像这种涉及数据库操作的高并发的业务,就要考虑使用异步了
    • 客户端发起接口请求,服务端快速响应,客户端展示结果给用户,数据库操作通过异步同步
    • 如何实现异步同步?
    • 使用消息队列,将入库的内容enqueue到消息队列中,业务接口快速响应给用户结果(可以温馨提示高峰期延迟到账)
    • 然后再写个独立程序从消息队列dequeue数据出来进行入库操作,入库成功后刷新用户相关缓存,如果入库失败记录日志,方便反馈查询和重新持久化
    • 这样一来数据库操作就只有一个程序(多线程)来完成,不会给数据带来压力
  • 补充:
    • 消息队列除了可以用在高并发业务,其他只要有相同需求的业务也是可以使用,如:短信发送中间件等
    • 高并发下异步持久化数据可能会影响用户的体验,可以通过可配置的方式,或者自动化监控资源消耗来切换时时或者使用异步,这样在正常流量的情况下可以使用时时操作数据库来提高用户体验
    • 异步同时也可以指编程上的异步函数,异步线程,在有的时候可以使用异步操作,把不需要等待结果的操作放到异步中,然后继续后面的操作,节省了等待的这部分操作的时间

缓存

高并发业务接口多数都是进行业务数据的查询,如:商品列表,商品信息,用户信息,红包信息等,这些数据都是不会经常变化,并且持久化在数据库中
高并发的情况下直接连接从库做查询操作,多台从库服务器也抗不住这么大量的连接请求数(前面说过,单台数据库服务器允许的最大连接数量是有限的)
那么我们在这种高并发的业务接口要如何设计呢?

  • 设计考虑:
    • 还是逆向思维,压力在数据库,那么我们就不进行数据库查询
    • 数据不经常变化,我们为啥要一直查询DB?
    • 数据不变化客户端为啥要向服务器请求返回一样的数据?
  • 方案设计:
    • 数据不经常变化,我们可以把数据进行缓存,缓存的方式有很多种,一般的:应用服务器直接Cache内存,主流的:存储在memcache、redis内存数据库
    • Cache是直接存储在应用服务器中,读取速度快,内存数据库服务器允许连接数可以支撑到很大,而且数据存储在内存,读取速度快,再加上主从集群,可以支撑很大的并发查询
    • 根据业务情景,使用配合客户端本地存,如果我们数据内容不经常变化,为啥要一直请求服务器获取相同数据,可以通过匹配数据版本号,如果版本号不一样接口重新查询缓存返回数据和版本号,如果一样则不查询数据直接响应
    • 这样不仅可以提高接口响应速度,也可以节约服务器带宽,虽然有些服务器带宽是按流量计费,但是也不是绝对无限的,在高并发的时候服务器带宽也可能导致请求响应慢的问题
  • 补充:
    • 缓存同时也指静态资源客户端缓存
    • cdn缓存,静态资源通过上传cdn,cdn节点缓存我们的静态资源,减少服务器压力
image

面向服务

  • SOA面向服务架构设计
  • 微服务更细粒度服务化,一系列的独立的服务共同组成系统

使用服务化思维,将核心业务或者通用的业务功能抽离成服务独立部署,对外提供接口的方式提供功能。
最理想化的设计是可以把一个复杂的系统抽离成多个服务,共同组成系统的业务,优点:松耦合,高可用性,高伸缩性,易维护。
通过面向服务化设计,独立服务器部署,均衡负载,数据库集群,可以让服务支撑更高的并发

  • 服务例子:
    • 用户行为跟踪记录统计
  • 说明:
    • 通过上报应用模块,操作事件,事件对象,等数据,记录用户的操作行为
    • 比如:记录用户在某个商品模块,点击了某一件商品,或者浏览了某一件商品
  • 背景:
    • 由于服务需要记录用户的各种操作行为,并且可以重复上报,准备接入服务的业务又是核心业务的用户行为跟踪,所以请求量很大,高峰期会产生大量并发请求。
  • 架构:
    • nodejs WEB应用服务器均衡负载
    • redis主从集群
    • mysql主
    • nodejs+express+ejs+redis+mysql
    • 服务端采用nodejs,nodejs是单进程(PM2根据cpu核数开启多个工作进程),采用事件驱动机制,适合I/O密集型业务,处理高并发能力强
  • 业务设计:
    • 并发量大,所以不能直接入库,采用:异步同步数据,消息队列
    • 请求接口上报数据,接口将上报数据push到redis的list队列中
    • nodejs写入库脚本,循环pop redis list数据,将数据存储入库,并进行相关统计Update,无数据时sleep几秒
    • 因为数据量会比较大,上报的数据表按天命名存储
  • 接口:
    • 上报数据接口
    • 统计查询接口
  • 上线跟进:
    • 服务业务基本正常
    • 每天的上报表有上千万的数据

冗余,自动化

当高并发业务所在的服务器出现宕机的时候,需要有备用服务器进行快速的替代,在应用服务器压力大的时候可以快速添加机器到集群中,所以我们就需要有备用机器可以随时待命。 最理想的方式是可以通过自动化监控服务器资源消耗来进行报警,自动切换降级方案,自动的进行服务器替换和添加操作等,通过自动化可以减少人工的操作的成本,而且可以快速操作,避免人为操作上面的失误。

  • 冗余
    • 数据库备份
    • 备用服务器
  • 自动化
    • 自动化监控
    • 自动化报警
    • 自动化降级

通过GitLab事件,我们应该反思,做了备份数据并不代表就万无一失了,我们需要保证高可用性,首先备份是否正常进行,备份数据是否可用,需要我们进行定期的检查,或者自动化监控, 还有包括如何避免人为上的操作失误问题。(不过事件中gitlab的开放性姿态,积极的处理方式还是值得学习的)

总结

高并发架构是一个不断衍变的过程,冰洞三尺非一日之寒,长城筑成非一日之功 
打好基础架构方便以后的拓展,这点很重要

image

这里重新整理了下高并发下的架构思路,举例了几个实践的例子,如果对表述内容有啥意见或者建议欢迎留言。

Mac下安装Rust

Mac 上有两种安装方式,一种是官方的命令行运行:

 $ curl https://sh.rustup.rs -sSf | sh

可以参考: Wiki:Rust 语言环境安装:Linux 开发环境(Ubuntu 18)

另一种是使用 Homebrew,这里我们使用此方法来安装,还没有安装 Homebrew 的用户需先自行安装。

以下操作使用 macOS Mojave (10.14.2) 系统,其他比较新的 Mac 系统应该也类似。

# 编辑文件
$ vim ~/.bashrc
# 在文件中加入以下两句
export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
# 接下来运行命令使文件生效
$ source ~/.bashrc
# 然后就可以进行安装
curl -sSf https://mirrors.ustc.edu.cn/rust-static/rustup.sh | sh -s
# 进行最后的配置,config可能不存在,创建就完了
$ cd /root/
$ mkdir .cargo
$ cd .cargo 
$ vim config
# 在文件中填入以下内容
[registry]
index = "https://mirrors.ustc.edu.cn/crates.io-index/"
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "https://mirrors.ustc.edu.cn/crates.io-index/"

brew install rust

命令行:

$ brew install rust

会安装下载已经编译好的二进制文件,总共大几百兆,安装成功后测试下:

$ rustc --version

$ cargo --version
Mac 开发环境

Hello World

创建 hello.rs 文件,内容如下:

hello.rs

// This is a comment
// hello.rs

// main function
fn main() {

    // Print text to the console
    println!("Hello World!");
}

命令行 rustchello.rs 编译为可自行文件:

$ rustc hello.rs

接下来运行可执行文件:

$ ./hello
Hello World!
Mac 开发环境

Linux背后的思想

01Linus Torvalds
Linus Torvalds两次改变了技术,第一次是Linux内核,它帮助互联网的发展;第二次是Git,全球开发者使用的源代码管理系统。在一次TED的采访中,Torvalds以极其开放的态度讨论了他独特的工作方式和性格特点。Torvalds说:“我不是一个空想家,我是一名工程师,我非常乐意跟梦想家在一起,他们行走四方,仰望苍穹,看着满天星辰说,“我想到那儿去。”但我是低头看路的那种人,我只想填好眼前这个坑,不让自己掉进去,这就是我。”

02关于开源
Linus TorvaldsLinux并不是一个合作的产物,它是我一系列项目中的一个,纯粹出于自己当时的需要,部分原因是我需要得到结果,但更重要的原因是我享受编程。这段旅程的终点,在25年后的今天(2016),我们仍未达到。当年我只是想做一个完全属于自己的项目,我压根就没想过开源这件事。但在那之后,随着项目越来越大, 你会开始想让别人知道。感觉就像“哇,快来看看我的成果!”
成千上万的人想参与进来(Linux内核项目),但很多时候,我成为了那个断点,我无法让自己跨出那一步,同上千人合作。
因此Git是我的第二个大项目,它存在的意义就是维护我的第一个大项目。事实上这就是我的工作方式。我编程并不是为了… ,我编程是因为好玩,但我也想做一些有意义的事情,因此我设计每一个程序仅仅是因为我自己需要。
而我喜欢开源软件的一点就是,它能让形形色色的人在一起合作。我们不必相互喜欢,有时候我们甚至互相讨厌。是真的,我们经常吵得不可开交。
科学界的开源显然是一种回归,科学最初是开源的。但之后变得越来越封闭,只存在那些昂贵的科学期刊上。开源让科学回归了,我们有了arXiv和开放期刊。小编有话说

Linux并不是选择了开源,只是因为开源恰好是Linux需要的。就如Linus Torvalds所说:“纯粹出于自己当时的需要。”

Linus Torvalds是睿智的,做好自己能控制的。

开源不仅仅代表源代码的开放,开源更是一种工作方式,一种教育方式。因为有了开源,我们多了一种更好的合作共赢的工作方式;因为有了开源,让更多从业者和学生能够学习到更好的技术。
03代码的品味Linus Torvalds有时候你可以换个角度看问题,重写代码,排除特例,完美覆盖所有情况,这就是好的代码。同时也很简单,这是最基本的原则。细节非常重要。对我来说,我愿意与之共事的人,必须有好的品位。

大牛们总是对自己严格要求,不仅仅是要实现功能,并且要优雅的实现。下面我们来看看采访中Linus Torvalds对比的两段代码:

1. 不怎么漂亮的代码

remove_list_entry(entry)
{ prev = NULL;
walk = head;
// Walk the list
while (walk != entry) {

prev = walk;
walk = walk->next;   
}    
// Remove the entry by updating the
// head or the previous entry
if(!prev) {
head = entry->next;
} else {
prev->next = entry->next;   
}
}

上面的代码,需要区分要移除的成员是否为链表的头一个成员。需要单独处理特例情况(要移除的成员为链表的头一个成员)。这个函数比较好理解,这里小编就不做更多的解释了,如有疑问,请添加小编微信交流。

2. 好的代码

remove_list_entry(entry){    
// The "indirect" pointer points to the
// *address* of the thing we'll update
indirect = &head;
// Walk the list, looking for the thing that

// points to the entry we want to remove
while ((*indirect) != entry))
{
indirect = &(*indirect)->next; }
// .. and just remove it

*indirect = entry->next;
}

这个代码完全不需要单独处理特例情况,程序整体更加整洁、优雅。其实现原理为:指针变量indirect保存的是链表成员结构体中的next成员的地址(head指针也可这样看),如下图所示:

所以变量*indirect就相当于是前一个链表成员的next成员(相对于要移除的成员来说)。当找到要移除的成员后,进行如下操作即可:

*indirect = entry->next;

行业内关于智能客服、聊天机器人的应用和架构、算法分享和介绍

阿里巴巴

小蜜

通用领域对话问答

非技术推广

其他

天猫

蚂蚁

闲鱼

云问(拼多多、当当)

携程

去哪儿

京东

Uber

58同城

饿了么

美团

滴滴

瓜子

小冰

苏宁

贝壳

第四范式

腾讯

其他

聊天机器人介绍

其他

汇总

对话管理

知识图谱

智能客服

智齿

智齿 | 机器人来当客服 会是什么样?

知识库

APP架构设计经验谈:接口的设计

App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉。

安全机制的设计

现在,大部分App的接口都采用RESTful架构,RESTFul最重要的一个设计原则就是,客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息。实现上,大部分都采用token的认证方式,一般流程是:

  1. 用户用密码登录成功后,服务器返回token给客户端;
  2. 客户端将token保存在本地,发起后续的相关请求时,将token发回给服务器;
  3. 服务器检查token的有效性,有效则返回数据,若无效,分两种情况:
    • token错误,这时需要用户重新登录,获取正确的token
    • token过期,这时客户端需要再发起一次认证请求,获取新的token

然而,此种验证方式存在一个安全性问题:当登录接口被劫持时,黑客就获取到了用户密码和token,后续则可以对该用户做任何事情了。用户只有修改密码才能夺回控制权。

如何优化呢?第一种解决方案是采用HTTPS。HTTPS在HTTP的基础上添加了SSL安全协议,自动对数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,安全性可以提高很多。不过,SSL也不是绝对安全的,也存在被劫持的可能。另外,服务器对HTTPS的配置相对有点复杂,还需要到CA申请证书,而且一般还是收费的。而且,HTTPS效率也比较低。一般,只有安全要求比较高的系统才会采用HTTPS,比如银行。而大部分对安全要求没那么高的App还是采用HTTP的方式。

我们目前的做法是给每个接口都添加签名。给客户端分配一个密钥,每次请求接口时,将密钥和所有参数组合成源串,根据签名算法生成签名值,发送请求时将签名一起发送给服务器验证。类似的实现可参考OAuth1.0的签名算法。这样,黑客不知道密钥,不知道签名算法,就算拦截到登录接口,后续请求也无法成功操作。不过,因为签名算法比较麻烦,而且容易出错,只适合对内的接口。如果你们的接口属于开放的API,则不太适合这种签名认证的方式了,建议还是使用OAuth2.0的认证机制。

我们也给每个端分配一个appKey,比如Android、iOS、微信三端,每个端分别分配一个appKey和一个密钥。没有传appKey的请求将报错,传错了appKey的请求也将报错。这样,安全性方面又加多了一层防御,同时也方便对不同端做一些不同的处理策略。

另外,现在越来越多App取消了密码登录,而采用手机号+短信验证码的登录方式,我在当前的项目中也采用了这种登录方式。这种登录方式有几种好处:

  1. 不需要注册,不需要修改密码,也不需要因为忘记密码而重置密码的操作了;
  2. 用户不再需要记住密码了,也不怕密码泄露的问题了;
  3. 相对于密码登录其安全性明显提高了。

接口数据的设计

接口的数据一般都采用JSON格式进行传输,不过,需要注意的是,JSON的值只有六种数据类型:

  • Number:整数或浮点数
  • String:字符串
  • Boolean:true 或 false
  • Array:数组包含在方括号[]中
  • Object:对象包含在大括号{}中
  • Null:空类型

所以,传输的数据类型不能超过这六种数据类型。以前,我们曾经试过传输Date类型,它会转为类似于”2016年1月7日 09时17分42秒 GMT+08:00″这样的字符串,这在转换时会产生问题,不同的解析库解析方式可能不同,有的可能会转乱,有的可能直接异常了。要避免出错,必须做特殊处理,自己手动去做解析。为了根除这种问题,最好的解决方案是用毫秒数表示日期。

另外,以前的项目中还出现过字符串的”true”和”false”,或者字符串的数字,甚至还出现过字符串的”null”,导致解析错误,尤其是”null”,导致App奔溃,后来查了好久才查出来是该问题导致的。这都是因为服务端对数据没处理好,导致有些数据转为了字符串。所以,在客户端,也不能完全信任服务端传回的数据都是对的,需要对所有异常情况都做相应处理。

服务器返回的数据结构,一般为:

{
    code:0,
    message: "success",
    data: { key1: value1, key2: value2, ... }
}
  • code: 返回码,0表示成功,非0表示各种不同的错误
  • message: 描述信息,成功时为”success”,错误时则是错误信息
  • data: 成功时返回的数据,类型为对象或数组

不同错误需要定义不同的返回码,属于客户端的错误和服务端的错误也要区分,比如1XX表示客户端的错误,2XX表示服务端的错误。这里举几个例子:

  • 0:成功
  • 100:请求错误
  • 101:缺少appKey
  • 102:缺少签名
  • 103:缺少参数
  • 200:服务器出错
  • 201:服务不可用
  • 202:服务器正在重启

错误信息一般有两种用途:一是客户端开发人员调试时看具体是什么错误;二是作为App错误提示直接展示给用户看。主要还是作为App错误提示,直接展示给用户看的。所以,大部分都是简短的提示信息。

data字段只在请求成功时才会有数据返回的。数据类型限定为对象或数组,当请求需要的数据为单个对象时则传回对象,当请求需要的数据是列表时,则为某个对象的数组。这里需要注意的就是,不要将data传入字符串或数字,即使请求需要的数据只有一个,比如token,那返回的data应该为:

// 正确
data: { token: 123456 }

// 错误
data: 123456

接口版本的设计

接口不可能一成不变,在不停迭代中,总会发生变化。接口的变化一般会有几种:

  • 数据的变化,比如增加了旧版本不支持的数据类型
  • 参数的变化,比如新增了参数
  • 接口的废弃,不再使用该接口了

为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:

  1. 每个接口有各自的版本,一般为接口添加个version的参数。
  2. 整个接口系统有统一的版本,一般在URL中添加版本号,比如http://api.domain.com/v2。

大部分情况下会采用第一种方式,当某一个接口有变动时,在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version。

如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级。

有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。因此,最好还要有一套完善的测试机制保证每次接口变更都能测试到所有相关层面。

写在最后

关于接口设计,暂时想到的就这么多了。各位看官看完觉得有遗漏或有哪些需要优化的欢迎提出一起讨论。

持续交付2.0:业务引领的DevOps精要

经典图书《持续交付》已出版8年,一直受到软件行业从业者的关注。书中的软件开发原则和实践也随着商业环境VUCA特性的明显增强而逐渐受到软件技术人员的认可。

VUCA是volatility(易变性)、uncertainty(不确定性)、complexity(复杂性)和ambiguity(模糊性)的首字母缩写。VUCA这个术语源于军事用语,在20世纪90年代开始被普遍使用,用来描述冷战结束后的越发不稳定的、不确定的、复杂、模棱两可和多边的世界。在2001年9月11日恐怖袭击发生之后,这一概念和首字母缩写才真正被确定。随后,VUCA被战略性商业领袖们用来描述已成为“新常态”的、混乱的和快速变化的商业环境。

然而,在应用这些为达成持续交付目标所需的软件技术相关原则与实践时,我们会遇到很多难题。例如,业务压力太大,没有时间改进;开发和测试的时间被压缩得太少了,没有时间这么干;这么做的风险太高了,质量很难保障。而这些难题正是由于软件工程的发展惯性带来的,是到了改变的时候了。

1.1 软件工程发展概述

“软件工程”这一学科出现于1968年,当时正值第一次软件危机。第一次软件危机是落后的软件生产方式无法满足迅速增长的计算机软件需求,从而导致软件开发与维护过程中出现一系列严重问题的现象。人们试图借鉴建筑工程领域的工程方法来解决这一问题,以实现“按预算准时交付所需功能的软件项目”的愿望。

1.1.1 瀑布软件开发方法

瀑布软件开发模型由Dr. Winston W. Rovce在1970年发表的“Managing the development of large software systems”一文中首次提出,如图1-1所示。它将软件开发过程定义为多个阶段,每个阶段均有严格的输入和输出标准,项目管理者希望通过这种重计划、重流程、重文档的方式来解决软件危机。很多人将具有以上3个特征的软件开发方法统称为“重型软件开发方法”。

1-1{65%}

图1-1 瀑布软件开发模型

在20世纪,瀑布软件开发模型的每个阶段都需要花费数月的时间。在写出第一行产品代码之前,甲乙双方需要花费大量精力确定需求范围,审核比《新华字典》厚得多的软件需求规格说明书。即便如此,双方还是要为“是否发生了需求范围的变更”“是否准时交付了软件”“交付的软件是否满足了预先设定的业务需求”而纠缠不清。

1.1.2 敏捷软件开发方法

从20世纪80年代开始,微型计算机开始快速普及。20世纪90年代,人们对软件的需求迅速扩大。然而,Standish Group的Chaos Report 1994显示,软件交付项目的失败或交付困难的比率仍旧很高(成功率只有16.2%,受到挑战的项目占比52.7%,而失败率为31.1%)。此时,很多优秀的软件工作者不满意瀑布软件开发方法的交付成果,并在各自工作实践中总结了各种新的软件开发方法,例如我们现在经常听说的Scrum和极限编程,都是在那个时代涌现出来的软件开发方法。

2001年,17位软件大师齐聚加拿大的一个小镇——雪鸟(Snowbird),总结了当时涌现的这些轻量级软件开发方法所具备的特点,共同发表了“敏捷宣言”,提出敏捷软件开发方法应该遵循的十二原则,见附录A。与会人员一致同意,凡符合这一宣言所倡导的价值观且遵循十二开发原则的方法均可被认为是“敏捷软件开发方法”。因此,“敏捷软件开发方法”这一说法从其诞生开始就是一簇软件开发方法的代名词,而不是特指某一种软件开发方法。

敏捷软件开发方法强调发挥人的主观能动性,提倡面对面沟通、拥抱变化、通过迭代和增量开发尽早交付有价值的软件。此时,很多团队已认识到,软件开发实际上是一个不断迭代学习的过程,即软件工程师需要快速学习并理解领域知识,并将其转化成数字世界的表达形式,通过与业务专家的交流讨论来学习并持续迭代这个过程。一个软件交付计划被划分成多个迭代,强调在每个迭代结束时应该得到可运行的软件,如图1-2所示。

1-2{80%}

图1-2 迭代开发,多批次部署发布

与瀑布软件开发方法只在项目交付后期才能看到可运行的软件相比,敏捷软件开发方法在这方面有很大的进步。“持续集成”作为敏捷开发方法中的一个工程实践,率先被更广泛的IT组织所接受,即便那些没有采纳敏捷开发方法的团队也会使用它,因为其强调的频繁自动化构建和自动化测试减少了质量保障团队的重复工作量,也排除了开发团队与质量保障团队之间的沟通障碍。

当时的主流软件需求仍旧是来自企业级定制软件开发。虽然敏捷开发方法使用的是迭代模型,但两次软件发布之间的间隔时间仍旧较长(通常是数月,甚至一年以上)。因此,业务人员与研发团队之间关于需求变更和研发效率的矛盾仍旧是主要矛盾。系统的部署发布工作在整个发布周期中所占用的时间和成本相对较小,部署和运维工作还不是突出矛盾。另外,部署活动通常由专门的技术运维团队执行,产品研发团队对其无感。

在这一时期,无论瀑布开发还是敏捷开发,在软件行业中最重要的关注点都是可交付的软件包本身,即如何快速地将软件需求变为可交付的软件包。

1.1.3 DevOps运动

DevOps的萌芽源于2008年8月敏捷大会多伦多站的一个临时话题“敏捷基础设施(Agile Infrastructure)”。当时比利时独立IT咨询师Patrick Debois非常感兴趣,并且分享了关于“将敏捷实践应用于运维领域”。2009年10月,Patrick在比利时的根特组织了一个“DevOpsDays”社区会议,并正式启用了“DevOps”这个术语。

2010年“The Agile Admin”网站上发表的一篇题为“What is DevOps”(什么是DevOps)的文章中指出:“DevOps是一组概念集合的代称,虽然并非全部都是新概念,但它们已经催化为一种运动,并迅速在整个技术社区中传播。”同时,该文章也给出了其原始定义,即“DevOps是运维工程师和开发工程师参与整个服务生命周期(从设计到开发再到生产支持)的一组实践”,并提出,DevOps应该倡导运维人员更多地使用和开发人员使用的相同技术来进行系统运维工作。

DevOps在维基百科上的定义也在随着时间的推进而不断变化着。截止到2017年,其定义为:

DevOps是一种软件工程文化和实践,旨在统一整合软件开发和软件运维。DevOps运动的主要特点是强烈倡导对构建软件的所有环节(从集成、测试、发布到部署和基础架构管理)进行全面的自动化和监控。DevOps的目标是缩短开发周期,提高部署频率和更可靠地发布,与业务目标保持一致。

事实上,到写作本书之时,业界对DevOps并没有统一的标准定义。正如“敏捷”一样,每一位从业者、每一个企业都有自己所理解的DevOps。有些人认为DevOps是敏捷的一个子集,有些人认为“敏捷做对了,就是DevOps”,还有人则将DevOps看作围绕自动化实施的一套实践,或多或少与敏捷有些关联性。

从历史时间点上来看,DevOps源于敏捷思想和实践在运维领域的应用,但当时的实操指导性不足,而近乎同期出版的《持续交付》一书使其更加具象化,在企业实践方面更具有可操作性。书中给出了一系列的原则、方法与实践,使DevOps运动的参与者有线索可循,例如持续交付中强烈倡导的“一切皆代码,自动化一切,部署流水线尽早反馈”等。

DevOps并非一个标准、一种模式或者一套固定方法,而是一种IT组织管理的发展趋势,也就是说,通过多种方式打破IT职能部门之间的隔阂,改变IT组织内部的原有合作模式,使之更紧密结合,从而促进业务迭代速度更快。这种发展趋势将会引起IT组织内部原有角色与分工的变化,甚至范围更大,会影响到相关的业务组织。对互联网公司来说,其软件产品对业务发展起到极其关键的作用,业务结果与IT效能强关联,因此顺应这一发展趋势的动力更加明显和迫切。

既然DevOps是一种组织管理的发展趋势,那么它就是IT领域普适的。对于不同行业、不同企业中的IT组织,需要根据其所在行业的行业特点以及企业实际状况进行一系列的管理定制。

1.1.4 持续交付1.0

2006年,Jez Humble、Chris Read和Dan North共同发表了一篇题为“The Deployment Production Line”(部署生产线)的文章。文中讨论了软件部署带来的生产效率问题,并首次提出“部署生产线”模式:

……测试和部署是软件开发过程中最困难且耗时的阶段。即使团队已经使用自动构建完成了代码的测试工作,也需要几天时间做生产部署的情况仍旧很常见……我们描述的原则和实践使你可以一键创建、配置和部署新的环境。

……通过多阶段自动化工作流程,测试和部署过程可以完全自动化。利用这种“部署生产线”,可以将已经过验证的代码快速部署到生产环境中,并且一旦发生问题,就可以轻松地回退到以前的版本。

该文章的3位作者当年均就职于ThoughtWorks公司,而该公司是敏捷软件开发方法的践行者、倡导者和推广者。该公司使用的软件开发方法也源自极限编程方法。作者通过各自的实践总结出了“部署流水线”模式的雏形,并且对如何使自动化部署活动更轻松给出了以下4条指导原则。

(1)每个构建阶段都应该交付可工作的软件,即对于中间产物的生成(例如搭建软件框架)不应该是一个单独的阶段。

(2)用同一个制品(artifacts)向不同类型的环境部署,即将其与运行时配置分开管理。

(3)自动化测试和部署,即根据测试目的,分成几个独立的质量关卡。

(4)这个部署生产线设计也应该随着你的应用程序的发展而不断演进。

2007年,ThoughtWorks公司的Dave Farley也发表了一篇题为“The Deployment Pipeline – Extending the range of Continuous Integration”(部署流水线——持续集成的延伸)的文章。文中指出,部署流水线就是通过自动化方式将多个质量验证关卡及其中的验证内容联系在一起,如图1-3所示。

1-3{80%}

图1-3 Dave Farley在2007年定义的部署流水线

同年12月,ThoughtWorks在北京正式组建了产品研发团队,启动了以这种部署流水线为指导思想的软件持续发布管理工具的研发。当时Jez Humble是产品经理,我负责该产品的交付与业务分析。该产品的第一个版本发布于2008年7月,取名为Cruise(现名为GoCD)。其最重要的一个功能特性就是“部署流水线”(deployment pipeline)。而且很多特性设计(包括内置的制品仓库、多阶段之间的制品引用)也体现了“一次构建,多次使用”的原则。

在开发GoCD期间,Jez Humble和Dave Farley合著了《持续交付》(Continuous Delivery

)一书,英文版于2010年正式出版,该书于2011年获得Jolt杰出大奖,中文版也于2011年在国内上市,这标志着“持续交付”这一术语的正式诞生。Jez Humble说:“持续交付是一种能力,也就是说,能够以可持续方式,安全快速地把代码变更(包括特性、配置、缺陷和试验)部署到生产环境上,让用户使用。”本书将这一定义称为“持续交付1.0”。

在《持续交付》一书中,讲述了持续交付1.0所遵循的理念、原则和众多方法与实践,并在该书最后一章指出:“它不仅仅是一种新的软件交付方法论,而且对依赖软件的业务来说,是一个全新的范式[1]
。”

持续交付1.0提供的很多原则及方法是DevOps运动的具体实操指引,它们可以为企业的IT团队将DevOps运动落地实施提供非常具体的指导,如图1-4所示。

1-4{80%}

图1-4 持续交付将发布权交还给业务方

从所涉及的协作角色来看,敏捷开发更多地涉及产品需求方、软件开发工程师和软件测试工程师。在历史上,DevOps更多地涉及软件研发团队(包括开发工程师和测试工程师)与运维工程师,而持续交付1.0涉及产品需求方、软件研发团队和运维工程师,如图1-5所示。

1-5{60%}

图1-5 相关概念在组织角色的主要触达点

持续部署与持续交付

 

很多人认为,持续部署(continuous deployment)是持续交付(continuous delivery)的进阶状态,是指代码提交后一旦成功通过所有质量验证,就立即自动部署到生产环境中,不需要任何人的审批。事实上,“部署”与“交付”这两个主干词的意义并不相同。

“部署”是一种技术领域的操作,也就是说,从某处获取软件包,并按照预先设计的方案将其安装到计算节点上,并确保系统可以正常启动,但它并不一定意味着“必须包含业务功能的发布或交付”。“交付”则是一个业务决策活动,通常也被称为“发布”,也就是说,如果将新构建的特性交付到客户(用户)手中,用户就可以看到并使用它们。

之所以“部署”与“发布”几乎成为等价词,是历史原因造成的。很久以前,软件的发布周期较长,每次新功能部署之后就会立即发布。久而久之,“部署”就成了“发布”的代名词。为了保证软件质量,IT部门通常不允许无关代码(即与本次发布新特性集合无关,例如未开发完成的功能、不完善的功能集)被带到生产环境中。因此,每次部署就一定是重大功能的发布。

随着互联网软件的出现,“部署”和“发布”内容与频率不同的情况也是很常见的。我们可以向环境多次部署,但只有当业务需要时才向用户发布,如图1-4中的箭头所示。例如,Facebook公司的发布工程师Chuck Rossi在2011年该公司举办的技术分享会上指出:“我们现在每天会对Facebook网站进行一次部署操作……Facebook公司在半年后将要主推的功能特性,现在已经上线了,只是用户看不到而已。”

1.2 持续交付2.0

持续交付1.0关注于“从提交代码到产品发布”的过程,如图1-6所示,并且提供了一系列工作原则和优秀的实践方法,可以提升软件开发活动的效率。

1-6{55%}

图1-6 持续交付1.0的关注点

但是,我在实际咨询过程中发现,一些软件功能在开发完成之后,对用户或者业务来说,并没有产生什么影响,有些功能根本没有用户来使用。可是,这些功能的确花费了团队的很多精力才设计实现。这是一种巨大的浪费。这种“无用”的功能生产得越多,浪费就越大。我们是否可以找到一些方法,让我们付出的努力对业务改善更加有效,或者只用很少的成本就可以验证对业务无效呢?

1.2.1 精益思想

2011年出版的《精益创业》一书给了我一些启示。其核心思想是,开发新产品时,先做出一个简单的原型——最小化可行产品(Minimum Viable Product,MVP),这个原型的目标并不是马上生产出一个完美的产品,而是为了验证自己心中的商业假设。得到用户的真实反馈后,从每次试验的结果中学习,再快速迭代,持续修正,在资源耗尽前从迷雾中找到通往成功的道路,最终适应市场的需求。

Eric Rise在书中强调,精益创业就是一个“开发—测量—认知”的验证学习过程,如图1-7所示。也就是说,把创意快速转化为产品,衡量顾客的反馈,然后再决定是改弦更张,还是坚守不移。

该书主要关注于创业初始阶段,将精益思想贯穿于产品“从0到1”的过程。事实上,它也可以用于产品“从1到n
”的过程中。

1996年,Womack、Jones和Roos在《精益思想》一书中指出,精益思想是指导企业根据用户需求,定义企业生产价值,按照价值流来组织全部生产活动,使价值在生产活动之间流动起来,由需求拉动产品的生产,从而识别整个生产过程中不经意间产生的浪费,并消除之。

1-7{45%}

图1-7 “开发—测量—认知”环

在精益管理理论中,“浪费”是指从客户角度出发,对优质产品与良好服务不增加价值的生产活动或管理流程。并指出,业务生产中所有活动都可以归结为以下两种活动,也就是增加价值的活动和不增加价值的活动,而不增加价值的活动就是浪费。在被归类为“浪费”的活动中,又可以分为必要的非增值活动和纯粹的浪费。必要的非增值活动是指从客户的角度看虽没有价值,却可以避免(潜在的)更大的浪费或降低系统性风险的活动,如图1-8所示。

1-8{65%}

图1-8 软件产品开发中的活动浪费

例如,生产流水线上的装配工作是增值活动,质量检查是必要的非增值活动,而因材料供应不足产生的生产等待以及因质量缺陷导致的返工都被认为是不必要的浪费。在软件产品服务的全生命周期中,也同样包含多种“浪费”,例如,无效果的功能特性、各生产环节中的等待(如图1-9所示)、没人看的文档、软件缺陷、机械性的重复工作等。

1-9{75%}

图1-9 用户视角的增值活动与浪费

尽管“消除所有浪费”几乎是不可能的,但是,我们仍旧要全面贯彻“识别和消除一切浪费”的理念,持续不断地优化流程与工作方式,达到高质量、低成本、无风险地快速交付客户价值的目的。

1.2.2 双环模型

自2009年Flickr(一个聚合全球知名热门图片分享网站)声称其网站每天部署10次之时起,“主干开发+持续集成+持续发布”已成为硅谷知名互联网公司应对VUCA环境的一种主流软件研发管理模式。这种变化的原动力并不是来自技术团队本身,而是来自业务与产品方的诉求。为了在VUCA环境中更快地了解海量用户,快速验证大量业务假设和解决方案,他们改变了业务探索的模式,并催生了软件研发管理模式的改变,两种模式相互促进,从而形成了互联网软件产品研发管理的双环模式,即“持续交付2.0”,如图1-10所示。

1-10{65%}

图1-10 持续交付2.0的双环模型

“持续交付2.0”是一种产品研发管理思维框架。它将精益创业与持续交付1.0相结合,强调业务与IT间的快速闭环,以“精益思想”为指导,全面贯彻“识别和消除一切浪费”的理念,通过一系列工作原则与实践,帮助企业以一种可持续方式,高质量、低成本、无风险地快速交付客户价值。

对企业来说,开发软件产品的目标是创造客户价值。因此,我们不应该仅仅关注快速开发软件功能,同时还应该关注我们所交付的软件的业务正确性,以及如何以有限的资源快速验证和解决业务问题。也就是说,不断探索发现真正要解决的业务问题,提出科学的目标,设计最小可行解决方案。通过快速实现解决方案并从真实反馈中收集数据,以验证该问题是否得以解决。这是一个从业务问题出发,到业务问题解决的完整业务闭环,简称为持续交付“8”字环。

它由两个相连的环组成:第一个环为“探索环”,其主要目标是识别和定义业务问题,并制订出最小可行解决方案进入第二个环;第二个环为“验证环”,其主要目标是以最快速度交付最小可行方案,可靠地收集真实反馈,并分析和验证业务问题的解决效果,以便决定下一步行动,如图1-10所示。

探索环包含4个可持续循环步骤,分别是提问、锚定、共创和精炼。

(1)提问,即定义问题。通过有针对性的提问,找出客户的具体需求,并找出具体需求后的原因,即具体需求后要解决的根本问题。在提问中形成团队期望达成的业务目标或者想要解决的业务问题。如果问题无法清晰定义,那么找到的答案自然就会有偏差。因此,在寻找答案之前,应该先清晰地定义问题。

(2)锚定,即定义结果目标指示器。针对问题进行信息收集,经过分析,去除干扰信息,识别问题假设,得到适当的衡量指标项,并用其描述现在的状况,同时讨论并定义我们接下来的行动所期望的结果。

(3)共创,即共同探索和创造解决或验证该问题的多种具有可行性的解决方案。

(4)精炼,即对所有的可行试验方案进行选择,找到最小可行性解决方案,它既可能是单个方案,也可能是多个方案的组合。

验证环也包含4个可持续循环的步骤,分别是构建、运行、监测和决策。

(1)构建:是指根据非数字化描述,将最小可行性解决方案准确地转换成符合质量要求的软件包。

(2)运行:是指将达到质量要求的软件包部署到生产环境或交到用户手中,并使之为用户提供服务。

(3)监测:是指收集生产系统中产生的数据,对系统进行监控,确保其正常运行。同时将业务数据以适当的形式及时呈现出来。

(4)决策:是指将收集到的数据信息与探索环得出的对应目标进行对比分析,做出决策,确定下一步的方向。

探索环就像是一部车子的前轮,把握前进方向。验证环则像车子的后轮,使车子平稳且驱动快速前进。它们之间相互促进,探索环产生的可行性方案规模越小,越能够提高验证环的运转速度;如果价值验证环能够提高运转速度,则有利于探索环尽早得到真实反馈,有利于快速决策,及时对前进方向进行验证或调整。

1.2.3 4个核心原则

“持续交付2.0”是指企业能够以可持续发展的方式,在高质量、低成本及无风险的前提下,不断缩短持续交付“8”字环周期,从而与企业外部频繁互动,获得及时且真实的反馈,最终创造更多客户价值的能力。下面逐一介绍缩短持续交付“8”字环周期的4个核心工作原则。

1.坚持少做

在咨询的过程中,最常听到的一句话就是:“我们最大的问题是人力不足。”无论公司实力如何,想做的事情永远超过自己的交付能力,需求永远做不完。然而,做得多就一定有效吗?我们应该抵住“通过大量计划来构建最佳功能”的诱惑,坚持少做,想办法对新创意尽早验证。

Moran在Do It Wrong Quickly
一书中写道:“Netflix认为,他们想做的事情中,可能有90%是错误的。”Ronny Kohavi等共同发布的文章“Online Experimentation at Microsoft”中也指出,在微软,“那些旨在改进关键指标而精心设计和开发的功能特性中,只有1/3左右成功地改进了关键指标”。

正如当年Mike Krieger(Instagram的联合创始人兼CTO)被问及“5个工程师如何支持4000万用户”时所说的那样——“少做,先做简单的事情”。

2.持续分解问题

复杂的业务问题中一定会包含很多不确定因素,它们会影响问题解决的速度和质量。在实施解决方案之前,通过对问题的层层分解,可以让团队更了解业务,更早识别出风险。企业应该坚信,即便是很大的课题或者大范围的变更,也可以将其分解为一系列小变更,快速解决,并得到反馈,从而尽早消除风险。与其设计一大堆特性,再策划一个持续数月的一次性发布,不如持续不断地尝试新想法,并各自独立发布给用户。

3.坚持快速反馈

当把问题分解以后,如果我们仍旧只是一味地埋头苦干,而忽视对每项已完成工作的结果反馈,那么就失去了由问题分解带来的另一半收益,确认风险降低或解除。只有通过快速反馈,我们才能尽早了解所完成工作的质量和效果。

4.持续改进并衡量

无论做了什么样的改进,如果无法以某种方式衡量它的结果,就无法证明真的得到了改进。在着手解决每个问题之前,我们都要找到适当的衡量方式,并将其与对应的功能需求放在同等重要的位置上,一起完成。

某数据公司就曾因无度量数据,而无法提出有效改进方向的情况。该系统是一个数据标注志愿者招募考试系统。虽然它被分成多个迭代,每个迭代都发布了很多功能,但是,由于没有实现产品人员所关注的数据收集与统计分析功能,使得团队仅知道人们可以使用这个系统完成工作,却无法知道是否能够高效完成工作,也很难提出下一步的产品优化方向。

1.2.4 持续交付七巧板

我们讨论了“持续交付2.0”的指导思想、工作理念和核心原则。大家很容易意识到,它对适应快速变化的市场环境和激烈的市场竞争是非常有效的。那么,企业如何让“持续交付2.0”成为一种组织能力,成为组织的DNA,持续发挥作用,从而领先竞争对手,成为自己的一种竞争优势呢?

持续交付双环模型的实施与改进将涉及企业内的多个部门与不同的角色,无法由某个部门独立实施,必须在整个组织范围内贯彻执行“持续交付2.0”的思想、理念与原则。企业需要在组织管理机制、基础设施以及软件系统架构3个方面付诸行动,而每一个方面都包含多项内容,如图1-11所示。

1-11{55%}

图1-11 持续交付七巧板

条条大路通罗马,而且,罗马也不是一天建成的。每个企业的实施路径可能各不相同,所需要的周期也各有长短,对各方面的能力需求也不完全一致。正如中国传统玩具七巧板一样,每个企业都应根据自己的意愿和诉求,拼出属于自己的持续交付实践地图。

1.3 小结

“持续交付2.0”建立在“持续交付1.0”的“可持续地快速发布软件服务”及精益创业的“最小化可行产品”两种理念基础之上,强调要以业务为导向,从一开始就将业务问题进行分解,并通过不断的科学探索与快速验证,减少浪费的同时,快速找到正确的业务前进方向,简称为“双环模型”。因此,其涉及组织中的多个团队,需要各个团队之间紧密合作,才能缩短“8”字环的周期,如图1-12所示。

1-12{70%}

图1-12 持续交付2.0的相关角色

“持续交付2.0”的4个核心工作原则是坚持少做、持续分解问题、坚持快速反馈和持续改进并衡量。只有这样,才能不断缩短持续交付“8”字环的运行周期,提升用户反馈速度,从而提高业务的敏捷性。这要求管理者跳出原有软件交付管理思维模式,摆脱“害怕失败”的恐惧感,拥抱“科学探索—快速验证”思维方法,快速试错,提升持续交付能力,进而发展现有业务,并快速开创新业务。

go sync.mutex 源代码分析

sync.Mutex是Go标准库中常用的一个排外锁。当一个 goroutine 获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放。

mutex

sync.Mutex的实现也是经过多次的演化,功能逐步加强,增加了公平的处理和饥饿机制。

 

初版的 Mutex

首先我们来看看Russ Cox在2008提交的第一版的Mutex实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type Mutex struct {
key int32;
sema int32;
}
func xadd(val *int32, delta int32) (new int32) {
for {
v := *val;
if cas(val, v, v+delta) {
return v+delta;
}
}
panic(“unreached”)
}
func (m *Mutex) Lock() {
if xadd(&m.key, 1) == 1 {
// changed from 0 to 1; we hold lock
return;
}
sys.semacquire(&m.sema);
}
func (m *Mutex) Unlock() {
if xadd(&m.key, -1) == 0 {
// changed from 1 to 0; no contention
return;
}
sys.semrelease(&m.sema);
}

可以看到,这简单几行就可以实现一个排外锁。通过cas对 key 进行加一, 如果key的值是从0加到1, 则直接获得了锁。否则通过semacquire进行sleep, 被唤醒的时候就获得了锁。

2012年, commit dd2074c8做了一次大的改动,它将waiter count(等待者的数量)和锁标识分开来(内部实现还是合用使用state字段)。新来的 goroutine 占优势,会有更大的机会获取锁。

获取锁, 指当前的gotoutine拥有锁的所有权,其它goroutine只有等待。

2015年, commit edcad863, Go 1.5中mutex实现为全协作式的,增加了spin机制,一旦有竞争,当前goroutine就会进入调度器。在临界区执行很短的情况下可能不是最好的解决方案。

2016年 commit 0556e262, Go 1.9中增加了饥饿模式,让锁变得更公平,不公平的等待时间限制在1毫秒,并且修复了一个大bug,唤醒的goroutine总是放在等待队列的尾部会导致更加不公平的等待时间。

目前这个版本的mutex实现是相当的复杂, 如果你粗略的瞄一眼,很难理解其中的逻辑, 尤其实现中字段的共用,标识的位操作,sync函数的调用、正常模式和饥饿模式的改变等。

本文尝试解析当前sync.Mutex的实现,梳理一下LockUnlock的实现。

源代码分析

根据Mutex的注释,当前的Mutex有如下的性质。这些注释将极大的帮助我们理解Mutex的实现。

互斥锁有两种状态:正常状态和饥饿状态。

在正常状态下,所有等待锁的goroutine按照FIFO顺序等待。唤醒的goroutine不会直接拥有锁,而是会和新请求锁的goroutine竞争锁的拥有。新请求锁的goroutine具有优势:它正在CPU上执行,而且可能有好几个,所以刚刚唤醒的goroutine有很大可能在锁竞争中失败。在这种情况下,这个被唤醒的goroutine会加入到等待队列的前面。 如果一个等待的goroutine超过1ms没有获取锁,那么它将会把锁转变为饥饿模式。

在饥饿模式下,锁的所有权将从unlock的gorutine直接交给交给等待队列中的第一个。新来的goroutine将不会尝试去获得锁,即使锁看起来是unlock状态, 也不会去尝试自旋操作,而是放在等待队列的尾部。

如果一个等待的goroutine获取了锁,并且满足一以下其中的任何一个条件:(1)它是队列中的最后一个;(2)它等待的时候小于1ms。它会将锁的状态转换为正常状态。

正常状态有很好的性能表现,饥饿模式也是非常重要的,因为它能阻止尾部延迟的现象。

在分析源代码之前, 我们要从多线程(goroutine)的并发场景去理解为什么实现中有很多的分支。

当一个goroutine获取这个锁的时候, 有可能这个锁根本没有竞争者, 那么这个goroutine轻轻松松获取了这个锁。
而如果这个锁已经被别的goroutine拥有, 就需要考虑怎么处理当前的期望获取锁的goroutine。
同时, 当并发goroutine很多的时候,有可能会有多个竞争者, 而且还会有通过信号量唤醒的等待者。

sync.Mutex只包含两个字段:

1
2
3
4
type Mutex struct {
state int32
sema uint32
}

state是一个共用的字段, 第0个 bit 标记这个mutex是否已被某个goroutine所拥有, 下面为了描述方便称之为state已加锁,或者mutex已加锁。 如果第0个 bit为0, 下文称之为state未被锁, 此mutex目前没有被某个goroutine所拥有。

第1个 bit 标记这个mutex是否已唤醒, 也就是有某个唤醒的goroutine要尝试获取锁。

第2个 bit 标记这个mutex状态, 值为1表明此锁已处于饥饿状态。

同时,尝试获取锁的goroutine也有状态,有可能它是新来的goroutine,也有可能是被唤醒的goroutine, 可能是处于正常状态的goroutine, 也有可能是处于饥饿状态的goroutine。

Lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
func (m *Mutex) Lock() {
// 如果mutext的state没有被锁,也没有等待/唤醒的goroutine, 锁处于正常状态,那么获得锁,返回.
// 比如锁第一次被goroutine请求时,就是这种状态。或者锁处于空闲的时候,也是这种状态。
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// 标记本goroutine的等待时间
var waitStartTime int64
// 本goroutine是否已经处于饥饿状态
starving := false
// 本goroutine是否已唤醒
awoke := false
// 自旋次数
iter := 0
// 复制锁的当前状态
old := m.state
for {
// 第一个条件是state已被锁,但是不是饥饿状态。如果时饥饿状态,自旋时没有用的,锁的拥有权直接交给了等待队列的第一个。
// 第二个条件是还可以自旋,多核、压力不大并且在一定次数内可以自旋, 具体的条件可以参考`sync_runtime_canSpin`的实现。
// 如果满足这两个条件,不断自旋来等待锁被释放、或者进入饥饿状态、或者不能再自旋。
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 自旋的过程中如果发现state还没有设置woken标识,则设置它的woken标识, 并标记自己为被唤醒。
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
// 到了这一步, state的状态可能是:
// 1. 锁还没有被释放,锁处于正常状态
// 2. 锁还没有被释放, 锁处于饥饿状态
// 3. 锁已经被释放, 锁处于正常状态
// 4. 锁已经被释放, 锁处于饥饿状态
//
// 并且本gorutine的 awoke可能是true, 也可能是false (其它goutine已经设置了state的woken标识)
// new 复制 state的当前状态, 用来设置新的状态
// old 是锁当前的状态
new := old
// 如果old state状态不是饥饿状态, new state 设置锁, 尝试通过CAS获取锁,
// 如果old state状态是饥饿状态, 则不设置new state的锁,因为饥饿状态下锁直接转给等待队列的第一个.
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 将等待队列的等待者的数量加1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// 如果当前goroutine已经处于饥饿状态, 并且old state的已被加锁,
// 将new state的状态标记为饥饿状态, 将锁转变为饥饿状态.
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// 如果本goroutine已经设置为唤醒状态, 需要清除new state的唤醒标记, 因为本goroutine要么获得了锁,要么进入休眠,
// 总之state的新状态不再是woken状态.
if awoke {
if new&mutexWoken == 0 {
throw(“sync: inconsistent mutex state”)
}
new &^= mutexWoken
}
// 通过CAS设置new state值.
// 注意new的锁标记不一定是true, 也可能只是标记一下锁的state是饥饿状态.
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果old state的状态是未被锁状态,并且锁不处于饥饿状态,
// 那么当前goroutine已经获取了锁的拥有权,返回
if old&(mutexLocked|mutexStarving) == 0 {
break
}
// 设置/计算本goroutine的等待时间
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 既然未能获取到锁, 那么就使用sleep原语阻塞本goroutine
// 如果是新来的goroutine,queueLifo=false, 加入到等待队列的尾部,耐心等待
// 如果是唤醒的goroutine, queueLifo=true, 加入到等待队列的头部
runtime_SemacquireMutex(&m.sema, queueLifo)
// sleep之后,此goroutine被唤醒
// 计算当前goroutine是否已经处于饥饿状态.
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 得到当前的锁状态
old = m.state
// 如果当前的state已经是饥饿状态
// 那么锁应该处于Unlock状态,那么应该是锁被直接交给了本goroutine
if old&mutexStarving != 0 {
// 如果当前的state已被锁,或者已标记为唤醒, 或者等待的队列中不为空,
// 那么state是一个非法状态
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw(“sync: inconsistent mutex state”)
}
// 当前goroutine用来设置锁,并将等待的goroutine数减1.
delta := int32(mutexLocked – 1<<mutexWaiterShift)
// 如果本goroutine是最后一个等待者,或者它并不处于饥饿状态,
// 那么我们需要把锁的state状态设置为正常模式.
if !starving || old>>mutexWaiterShift == 1 {
// 退出饥饿模式
delta -= mutexStarving
}
// 设置新state, 因为已经获得了锁,退出、返回
atomic.AddInt32(&m.state, delta)
break
}
// 如果当前的锁是正常模式,本goroutine被唤醒,自旋次数清零,从for循环开始处重新开始
awoke = true
iter = 0
} else { // 如果CAS不成功,重新获取锁的state, 从for循环开始处重新开始
old = m.state
}
}
}

Unlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func (m *Mutex) Unlock() {
// 如果state不是处于锁的状态, 那么就是Unlock根本没有加锁的mutex, panic
new := atomic.AddInt32(&m.state, -mutexLocked)
if (new+mutexLocked)&mutexLocked == 0 {
throw(“sync: unlock of unlocked mutex”)
}
// 释放了锁,还得需要通知其它等待者
// 锁如果处于饥饿状态,直接交给等待队列的第一个, 唤醒它,让它去获取锁
// 锁如果处于正常状态,
// new state如果是正常状态
if new&mutexStarving == 0 {
old := new
for {
// 如果没有等待的goroutine, 或者锁不处于空闲的状态,直接返回.
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 将等待的goroutine数减一,并设置woken标识
new = (old – 1<<mutexWaiterShift) | mutexWoken
// 设置新的state, 这里通过信号量会唤醒一个阻塞的goroutine去获取锁.
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false)
return
}
old = m.state
}
} else {
// 饥饿模式下, 直接将锁的拥有权传给等待队列中的第一个.
// 注意此时state的mutexLocked还没有加锁,唤醒的goroutine会设置它。
// 在此期间,如果有新的goroutine来请求锁, 因为mutex处于饥饿状态, mutex还是被认为处于锁状态,
// 新来的goroutine不会把锁抢过去.
runtime_Semrelease(&m.sema, true)
}
}

出个问题

最后我出一个问题,你可以根据Unlock的代码分析,下面的哪个答案正确?

如果一个goroutine g1 通过Lock获取了锁, 在持有锁的期间, 另外一个goroutine g2 调用Unlock释放这个锁, 会出现什么现象?

  • A、 g2 调用 Unlock panic
  • B、 g2 调用 Unlock 成功,但是如果将来 g1调用 Unlock 会 panic
  • C、 g2 调用 Unlock 成功,如果将来 g1调用 Unlock 也成功

运行下面的例子试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
“sync”
“time”
)
func main() {
var mu sync.Mutex
go func() {
mu.Lock()
time.Sleep(10 * time.Second)
mu.Unlock()
}()
time.Sleep(time.Second)
mu.Unlock()
select {}
}

Markdown基本语法总结

Markdown是一种纯文本格式的标记语言。通过简单的标记语法,它可以使普通文本内容具有一定的格式。

相比WYSIWYG编辑器

优点:
1、因为是纯文本,所以只要支持Markdown的地方都能获得一样的编辑效果,可以让作者摆脱排版的困扰,专心写作。
2、操作简单。比如:WYSIWYG编辑时标记个标题,先选中内容,再点击导航栏的标题按钮,选择几级标题。要三个步骤。而Markdown只需要在标题内容前加#即可

缺点:
1、需要记一些语法(当然,是很简单。五分钟学会)。
2、有些平台不支持Markdown编辑模式。

还好,简书是支持Markdown编辑模式的。

开启方式:设置->默认编辑器->Markdown编辑器

一、标题

在想要设置为标题的文字前面加#来表示
一个#是一级标题,二个#是二级标题,以此类推。支持六级标题。

注:标准语法一般在#后跟个空格再写文字,貌似简书不加空格也行。

示例:

# 这是一级标题
## 这是二级标题
### 这是三级标题
#### 这是四级标题
##### 这是五级标题
###### 这是六级标题

效果如下:

这是一级标题

这是二级标题

这是三级标题

这是四级标题

这是五级标题
这是六级标题

二、字体

  • 加粗

要加粗的文字左右分别用两个*号包起来

  • 斜体

要倾斜的文字左右分别用一个*号包起来

  • 斜体加粗

要倾斜和加粗的文字左右分别用三个*号包起来

  • 删除线

要加删除线的文字左右分别用两个~~号包起来

示例:

**这是加粗的文字**
*这是倾斜的文字*`
***这是斜体加粗的文字***
~~这是加删除线的文字~~

效果如下:

这是加粗的文字
这是倾斜的文字
这是斜体加粗的文字
这是加删除线的文字


三、引用

在引用的文字前加>即可。引用也可以嵌套,如加两个>>三个>>>
n个…
貌似可以一直加下去,但没神马卵用

示例:

>这是引用的内容
>>这是引用的内容
>>>>>>>>>>这是引用的内容

效果如下:

这是引用的内容

这是引用的内容

这是引用的内容

四、分割线

三个或者三个以上的 – 或者 * 都可以。

示例:

---
----
***
*****

效果如下:
可以看到,显示效果是一样的。





五、图片

语法:

![图片alt](图片地址 ''图片title'')

图片alt就是显示在图片下面的文字,相当于对图片内容的解释。
图片title是图片的标题,当鼠标移到图片上时显示的内容。title可加可不加

示例:

![blockchain](https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/
u=702257389,1274025419&fm=27&gp=0.jpg "区块链")

效果如下:

blockchain

上传本地图片直接点击导航栏的图片标志,选择图片即可

六、超链接

语法:

[超链接名](超链接地址 "超链接title")
title可加可不加

示例:

[shirdon](http://shirdon.com)
[百度](http://baidu.com)

效果如下:

shirdon
百度

注:Markdown本身语法不支持链接在新页面中打开,貌似简书做了处理,是可以的。别的平台可能就不行了,如果想要在新页面中打开的话可以用html语言的a标签代替。

<a href="超链接地址" target="_blank">超链接名</a>

示例
<a href="http://www.shirdon.com/u/1f5ac0cf6a8b" target="_blank">shirdon</a>

七、列表

  • 无序列表

语法:
无序列表用 – + * 任何一种都可以

- 列表内容
+ 列表内容
* 列表内容

注意:- + * 跟内容之间都要有一个空格

效果如下:

  • 列表内容
  • 列表内容
  • 列表内容
  • 有序列表

语法:
数字加点

1.列表内容
2.列表内容
3.列表内容

注意:序号跟内容之间要有空格

效果如下:

1.列表内容
2.列表内容
3.列表内容

  • 列表嵌套

上一级和下一级之间敲三个空格即可

  • 一级无序列表内容
    • 二级无序列表内容
    • 二级无序列表内容
    • 二级无序列表内容
  • 一级无序列表内容
    1. 二级有序列表内容
    2. 二级有序列表内容
    3. 二级有序列表内容
  1. 一级有序列表内容
    • 二级无序列表内容
    • 二级无序列表内容
    • 二级无序列表内容
  2. 一级有序列表内容
    1. 二级有序列表内容
    2. 二级有序列表内容
    3. 二级有序列表内容

八、表格

语法:

表头|表头|表头
---|:--:|---:
内容|内容|内容
内容|内容|内容

第二行分割表头和内容。
- 有一个就行,为了对齐,多加了几个
文字默认居左
-两边加:表示文字居中
-右边加:表示文字居右
注:原生的语法两边都要用 | 包起来。此处省略

示例:

姓名|技能|排行
--|:--:|--:
刘备|哭|大哥
关羽|打|二哥
张飞|骂|三弟

效果如下:

姓名 技能 排行
刘备 大哥
关羽 二哥
张飞 三弟

九、代码

语法:
单行代码:代码之间分别用一个反引号包起来

    `代码内容`

代码块:代码之间分别用三个反引号包起来,且两边的反引号单独占一行

(```)
  代码...
  代码...
  代码...
(```)

注:为了防止转译,前后三个反引号处加了小括号,实际是没有的。这里只是用来演示,实际中去掉两边小括号即可。

示例:

单行代码

`create database hero;`

代码块

(```)
    function fun(){
         echo "这是一句非常牛逼的代码";
    }
    fun();
(```)

效果如下:

单行代码

create database hero;

代码块

function fun(){
  echo "这是一句非常牛逼的代码";
}
fun();

十、流程图

```flow
st=>start: 开始
op=>operation: My Operation
cond=>condition: Yes or No?
e=>end
st->op->cond
cond(yes)->e
cond(no)->op
&```