我们不是研发,不会天天去关注代码

前段时间,项目实施人员告诉我,我写的 Ansible 脚本中有一处写死了版本号。并把代码截图给我看。我一看,这代码是老版本了。他代码应该没有更新。我这么跟他说。 然后他说出了一句出乎我意料的话:我们不是研发,不会天天去关注代码。 最后,我回了一句:要想自动化,就必须关注代码。屏幕背后的我,露出无奈与惋惜的表情。 惋惜的不是他有没有关注代码这件事情。而是他使用职位来限制住自己的能力。 不要笑话上面的同事,工作中不少这样的事情: 我是测试,那是研发的事情。 我是研发,那是运维人员的事情。 我是设计人员,不是研发。 前段时间,听另一个项目组的同事说:两周一迭代,前一周测试闲死,后一周开发闲死。 当时,我问了两个问题: 在后一周的时候,开发在干嘛? 在后一周的时候,产品经理在干嘛? 开发与产品经理都不是很忙的情况,为什么他们不可以参与测试呢?如果问他们,得到的回答可能是:因为那是测试的事情。 后来,我仔细想,那是XXX的事情,其实也不能完全怪他们。因为现实中,如果线上出现测试不到位的Bug,测试人员很可能会被 KPI。 最后,我才恍然大悟:那是XXX的事情的思维方式并不是员工原本的思维方式,而是这个管理制度下的结果。

2019-07-22 · 1 min · 18 words · 翟志军 Jack Zhai

如何设计 Ansible 的入门工作坊

本月在公司内部做了一次 Ansible 的入门工作坊。本文即对这次工作坊的设计过程进行一次总结。其他技术类的工作坊也可以参考。 设计过程大概过程如下文所述。 首先,我们需要确定参加本次工作坊的受众。他们是否具有最基本的前提。本次工作坊的受众有开发、测试、运维,还有毕业生。但是他们都会使用 shell。这已经满足最基本的前提。同时,了解受众后了,也就可以因材施教。 第二,分析工作坊的内容。Ansible 是一款上手非常容易的自动化运维工具。它的特点就是实操性非常强,不需要理解 Ansible 背后的概念就可以使用的工具。 笔者根据受众和教学内容的特点,得出本次工作坊的目标(教学目标): 知道 Ansible 是什么,并知道它的作用。 了解如何查文档。 能部署一个 Spring Boot 应用。 是不是很简单?其实不然。整个工作坊没有一个人能完成所有的任务。同时发现有运维和开发基础的同学会做得更快。 那接下来怎么实现这个目标呢?笔者使用的是任务驱动的方法。也就是受众通过做一个个任务,在任务中完成学习。同时,教师可以任务过程穿插讲相关的知识点。 以下为任务列表: 执行 ansible-playbook -i hosts playbook.yml 成功 创建用户 apps 及用户组 apps: user 模块: https://docs.ansible.com/ansible/latest/modules/user_module.html group 模块: https://docs.ansible.com/ansible/latest/modules/group_module.html 创建以下文件夹,并设置文件夹的用户和组为 apps: /apps,/apps/hello,/apps/hello/bin,/apps/hello/logs file 模块: https://docs.ansible.com/ansible/latest/modules/file_module.html 将 helloworld-0.0.2.jar copy 到 /apps/hello/bin 目录下,设置该 jar 文件的用户和用户组为 apps copy 模块: https://docs.ansible.com/ansible/latest/modules/copy_module.html 使用 template 模块将 app.service copy 到目标服务器的 /etc/systemd/system 中,并重命名 hello.service : template 模块: https://docs.ansible.com/ansible/latest/modules/template_module.html 启动 hello 服务 service 模块: https://docs.ansible.com/ansible/latest/modules/service_module.html 监听 hello 服务是否启动成功 wait_for 模块: https://docs.ansible.com/ansible/latest/modules/wait_for_module.html 为目标机器安装 JDK 1.8: 在本地仓库中创建 roles 目录 clone 代码:https://github.com/geerlingguy/ansible-role-java 到 roles 目录中 在 playbook.yml 文件中加入 ansible-role-java 的role 创建自定义 role: hello role 进入 roles 目录:cd roles 使用命令生成 role 模板:ansible-galaxy init hello 将 hello 的部署逻辑(在 playbook.yml 中)写入到 hello role 中 将 hello 部署到多台机器 需要修改 hosts 文件 多环境部署 任务的设计并不是随意的,而是有意的。比如: ...

2019-07-19 · 1 min · 146 words · 翟志军 Jack Zhai

小明说:代码是测试通过了,但是修改了一行代码,等和其他人的功能一起上吧

小明修复了 Web 后端的一个不大不小的 Bug。只修改了一行代码。并在 UAT 环境测试通过。 可是,当我问他为什么不发版时,他说: 代码是测试通过了,但是修改了一行代码,等和其他人的功能一起上吧 在我不长不短的职业生涯中,经常遇到这样的小明。我已经见怪不怪了。 笔者的观念是:如果变更的代码上线不会死人,能上就上。如果每次发版都很痛苦,那么先把发版难这个问题解决。至少也要朝着这个方向前进。 为什么我会这样觉得呢?因为: 程序员写出来的代码,只有真正运行在生产环境上了,才算完成工作。测试通过的代码不上线,就是库存。这在丰田称为“库存的浪费”。也就是不发布到生产环境,你为什么要写出来?还测试通过了。 如果一个分支停留太长时间,分支之间发生冲突的可能性就越大,而解决冲突这类操作对于产品的最终用户来说,是毫无价值的。用丰田生产方式的话说来,就是“动作上的无效劳动”。再者合并冲突过程容易再次引入缺陷。所以,应该避免分支停留过长时间。这个“过长”怎么定义,需要具体问题具体分析。 后记 其实,我很理解小明说出这样的话。大多是因为对部署没有足够的信心。对于没有自动化部署的团队来说,属于正常现象,你不能将责任推到一个人身上。而解决部署难的问题,不仅在管理上下功夫,还要在技术上下功能。

2019-07-07 · 1 min · 15 words · 翟志军 Jack Zhai

从文具的购买看一家企业的效率

Photo by **Leon Macapagal **from Pexels 团队每天都会面对着看板开早会。过完卡后,会有一些技术问题需要进行深入讨论。大家你一句我一句,实在说不清了,我就想从桌子上拿一支笔将想法画出来。 但是在会议室找不到可用的笔,然后跑到文印室也找不到。这样,几分钟过去了,5个人团队就干等着我找笔。实在找不到,我们只得打开投影仪,打开 Windows 系统自带的画图软件画起架构图。 在经历过几次这样的痛苦后,我决定找办公室负责采购的小姐姐说这事。小姐姐心好,从其它地方拿笔到我们经常早会的会议室。 最后小姐姐补充:本月的文具已购买,下月我再向经理审批再买哦。 我当时没有反应过来,没有理解她这句话的意思。 过了两个星期,我发现又没有笔可用了(后来发现是我在文印室没有找仔细)。我再次找到小姐姐。并跟她说:能不能多放一些笔? 小姐姐说:笔是统一放在文印室的,因为不知道哪个地方需要用。所以,你们有需要去文印室拿哦。 我说:文印室也没有了,没找到。 然后我实在没有忍住,一连串说了几句文具的成本是多低,节约人的时间,提高工作效率,可以节约更多的成本。 她似乎没有听进去:之前放了一支,你们又丢了。 我说:就一支,是不是别人拿到别的会议室了? 她说:不知道。我买文具是要经理审批的。 我这才想意识到,她没有决定权。问题出在经理那里。为什么经理不允许买呢? 从小姐姐那里了解到原来有一次小姐姐按往常一次提流程买笔和本子。谁知经理说:纸巾每月买可以理解,但是为什么笔和本子每个月都要买。 这就是现在为什么小姐姐很少买笔和本子的原因了。 最后,我也不提笔的事情了。因为我单独找经理谈这个事情,在当前的环境下,会被人说“跨级”。再者对小姐姐可能也不好。 谁知,过了两个星期,事业部的副经理从会议室跑出来,问:有没有见到大头笔? 我苦笑地说:没有。 苦笑是因为我猜到发生在我身上的事,也会发生在别人身上。只不过没想到,发生在了经理级别上而已。 后来,也不知道他有没有找到笔。但是那个会议室还是偶尔找不到笔。 不知道读者朋友看出来问题没有?笔者认为关键问题出在: 提效不一定非要花巨资买个 DevOps 平台,效率发生在每一个企业运营的细节。 软件工程是知识密集型工作,人与人之间需要高效沟通。那么笔、白板(或纸)就是即高效,成本又低的工具。说实在点,工作少出现一个沟通失误,节约下来的钱就可能买下整个公司几年的笔了。小姐姐没有认识到,经理也没有认识到。 企业文化让看到问题的人不敢提问题。 你没有想到吧,一个负责采购小姐姐的决定,也可能影响一家公司的效率?这里没有贬低小姐姐的意思。每个岗位都有它的意义。 这些问题怎么从根本上解决? 笔者认为,关键点是没有人从效率的角度考虑公司内部的行为。小姐姐考虑的是下次提流程时,经理不会问那样的问题。经理疑问了文具为什么用得这么快,但是并没有深究(几支笔的事情,当然没有必要深究)。其他人(可能)不想惹事,也不会找相关的人提“笔”的事情。在很多企业奉行谁提问题谁解决。有时我怀疑这句话对企业是有害的。 怎么解决呢?我只说一句:解铃还须系铃人。 一定能解决吗?我不知道。因为我也只是觉得可以解决。所以希望和更多人交流解决之道。 我把这些写出来,很有可能犯“政治错误”,得罪某些人。我还是要写出来。因为我相信还有不少企业发生类似的事情。不敢面对,怎么进步。

2019-05-27 · 1 min · 40 words · 翟志军 Jack Zhai

如果给你一支外墙清洗工程队,你会如何管?

本篇文章只是笔者看到外墙清洗工后,在管理方面思考的总结。笔者是外墙清洗行业的外行,所以,本文可能存在一些错误的假设。期望这些可能错误的假设不影响管理方面的思考。 引发思考的起因 某天上班,仰头看到大厦清洗外墙工人悬挂在20层楼的半空中作业,除了觉得他们危险外,脑海浮现一个问题。如果作为外墙清洗工的管理者,我们如何保证大厦的外墙的每一处被清洗干净了。 我们经常听管理者这么说: 我只要一个结果,不管过程。 可是这个“结果”是什么呢? 谁来定义“结果” 回到外墙清洗的管理,作为工程队的管理者,我们要的结果是什么呢? 面对这个问题,我们要马上回答的并不是“结果”的定义是什么。而是要问:这个问题本身应该由谁来回答。是由老板回答、管理者回答、还是由清洗工回答(我们假设管理者与老板这两种角色由不同的人担当)? 也就是说,对于不同角色的人,对于“结果”的定义是不一样的。 清洗外墙作为一门生意,结果当然是以最低成本赚最多钱。对于这一点似乎不用定义了。 但是,这个结果只是“老板”希望自己得到的结果。它与管理者希望清洗工给的结果是两回事。如下图所示。 至此,我们发现“谁来定义结果”这个问题,应该换个问法:谁来确定管理者想要的结果与老板想要的结果之间的关系? 笔者认为应该由管理者与老板共同确定。 如何确定不同层次结果之间的关系 管理者想要的结果与老板想要的结果处于不同的层次。那么如何确定不同层次结果之间的关系,这个问题由老板与管理者共同回答。 如果是把清洗外墙是一次性的生意,结果很准确,就是收到甲方的款。不论外墙是否真的被清洗干净了,因为贿赂验收人员我们一样能得到想要的结果。我们甚至可以设置一个部门专门搞定验收人员。这不是本文要讨论的范围。 如果基于企业长期发展的考虑,我们希望能把洗墙这项业务做好。最终会得到老板想要的结果。 本文,我们假设保证大厦外墙的每一处都被清洗干净是长期正作用于老板想要的结果的。如下图所示。 接下来,下文所说的“结果”均指管理者的结果。 如何验证结果 绕了一圈,终于把结果定义好了。现在咱们把“结果”定义为在文章开头就提了的: 保证大厦外墙的每一处都被清洗干净 但是我们如何验证这个结果呢?我们的管理者不可能也把自己挂在高高的外墙眼睁睁地检查清洗工洗过的每一处。就像产品经理不可能自己打 IDE 一行行的检查程序员的代码。 如果管理者无法做到,加一个监工不就行了?但是谁又来监督这个监工的工作呢?也就是谁来保证监工的工作做到位了呢? 还有一种办法:让工人相互监工。这样,花两个人的工资,顶三个人的活。可是,在企业里待过的人动下脑子都知道这个方案就不可靠。他们会串通的嘛。因为“偷懒”符合他们的共同利益。(在信息透明度高的情况下,让人相互监工是可行的。比如结对编程。) 以上都是臆造出来的解决方案。都是基于“监工”的模型。看看我们的身边,是不是也是这样? 回到我们的问题本身:如何确保大厦外墙的每一处都被清洗干净?我们换一种方式解决。 如果采用“激励”的方式呢?假如请了10个清洗工人,等最后清洗完成后,对这10个清洗工人的工作进行ABC评级。得A的人可以得到原来薪水的1.2倍,得B的人保持不变,得C的人是原来薪水的0.8倍。是不是感觉这套路很熟悉? “激励”的模型虽好,解决的是清洗工人的积极性问题,还是没有解决我们的根本问题:如何验证结果。 定期验收 也许我们可以把“每一处”的标准降低。在所有的清洗工中,选一个最合适的人作为清洗工组长,让其工资高于一般的清洗工人。组长的其中一个重要职责是定期对玻璃进行验收。至于定期的周期设为多长,涉及到工作的反馈周期了,属于另一个问题了。暂不讨论。 虽然这样管理者不用自己被挂在外墙,但是“保证大厦外墙的每一处都被清洗干净”的结果强依赖于组长的责任心。 这个模型的缺点是管理者想要的结果必须强依赖于一个很不稳定的因素:组长的责任心。 用对比法解决 突然有一天,我再看大厦的时候想到,用清洗前和清洗后的大厦照片对比不就可以了吗? 也就是在保证环境一致的情况下,在清洗前拍一次,在清洗后再拍一次。只要照片足够高清,你想对比多细节都可以。更进一步,我们甚至可以使用软件进行自动对比。 使用此种办法,除了解决以上方案的缺点,还使得“保证大厦外墙的每一处都被清洗干净”的结果从模糊变成准确被量化了。 这意味着什么? 更短的反馈周期:每天都可以当天的清洗情况。这对于能否按时完成工作非常重要。 更准确的定义“干净”:怎么样才算干净,一对比就知道。同时,清洗工之间绩效对比也有了。 成本更低的检查“每一处”:由于只需要坐在电脑前进行检查,成本会低很多。可能不需要多发一份组长的工资了。 最后,笔者想找同一建筑的两张图进行对比,但是实在找不到。读者朋友就脑补一下吧。 使用对比法就是最终答案了吗?不是的。也许,将来成本更低的办法是使用外墙清洗机器人会代替人工。也许这样更容易得到老板想要的结果。 更甚至于搞个 AI 外墙清洗,然后融个资?笔者调皮了。 后记 以上内容虽不能完全重现笔者的思考过程。但是大体思路就是这样的。这个思考过程,在软件工程方面,笔者认为是相通的。 产品经理必须思考做出来的软件功能是否正作用于老板想要的结果;必须想办法加快反馈;必须想办法更低成本的检查程序员的工作结果;必须想办法让所有人准确理解需求。 同时,本文还有很多方面没有讨论,比如我们是否需要以及如何关心每个人想要的结果;清洗工也是人,企业中的人文关怀问题等等。 最后,说明一下,我并不想挑战别人的专业。以上只是讨论。欢迎大家交流 参考 玻璃外墙清洗的准备工作与注意事项:http://www.fjyybj.com/news/517.html 外墙清洗机器人现身多幢大楼,清洗前后泾渭分明! https://juejin.im/post/5c73c3a251882562e5445024 人类真的是趋利避害的吗?:https://www.zhihu.com/question/60711385

2019-05-26 · 1 min · 59 words · 翟志军 Jack Zhai

如何对 Jenkins 共享库进行单元测试

Jenkins 共享库是除了 Jenkins 插件外,另一种扩展 Jenkins 流水线的技术。通过它,可以定义轻松的自定义步骤,还可以对现有的流水线逻辑进行一定程度的抽象与封装。至于如何写及如何使用它,读者朋友可以移步附录中的官方文档。 对共享库进行单元测试的原因 但是如何对它进行单元测试呢?共享库越来越大时,你不得不考虑的问题。因为如果你不在早期就开始单元测试,共享库后期可能就会发展成如下图所示的“艺术品”——能工作,但是脆弱到没有人敢动。 [图片来自网络,侵权必删] 这就是代码越写越慢的原因之一。后人要不断地填前人有意无意挖的坑。 共享库单元测试搭建 共享库官方文档介绍的代码仓库结构 (root) +- src # Groovy source files | +- org | +- foo | +- Bar.groovy # for org.foo.Bar class +- vars | +- foo.groovy # for global 'foo' variable | +- foo.txt # help for 'foo' variable +- resources # resource files (external libraries only) | +- org | +- foo | +- bar.json # static helper data for org.foo.Bar 以上是共享库官方文档介绍的代码仓库结构。整个代码库可以分成两部分:src 目录部分和 vars 目录部分。它们的测试脚手架的搭建是不一样的。 ...

2019-05-25 · 4 min · 665 words · 翟志军 Jack Zhai

使用 Jenkins + Ansible 实现 Springboot 自动化部署101

本文要点: 设计一条 Springboot 最基本的流水线:包括构建、制品上传、部署。 使用 Docker 容器运行构建逻辑。 自动化整个实验环境:包括 Jenkins 的配置,Jenkins slave 的配置等。 1. 代码仓库安排 本次实验涉及以下多个代码仓库: % tree -L 1 ├── 1-cd-platform # 实验环境相关代码 ├── 1-env-conf # 环境配置代码-实现配置独立 └── 1-springboot # Springboot 应用的代码及其部署代码 1-springboot 的目录结构如下: % cd 1-springboot % tree -L 1 ├── Jenkinsfile # 流水线代码 ├── README.md ├── deploy # 部署代码 ├── pom.xml └── src # 业务代码 所有代码,均放在 GitHub: https://github.com/cd-in-practice 2. 实验环境准备 笔者使用 Docker Compose + Vagrant 进行实验。环境包括以下几个系统: Jenkins * 1 Jenkins master,全自动安装插件、默认用户名密码:admin/admin。 Jenkins agent * 2 Jenkins agent 运行在 Docker 容器中,共启动两个。 Artifactory * 1 一个商业版的制品库。笔者申请了一个 30 天的商业版。 使用 Vagrant 是为了启动虚拟机,用于部署 Springboot 应用。如果你的开发机器无法使用 Vagrant,使用 VirtualBox 也可以达到同样的效果。但是有一点需要注意,那就是网络。如果在虚拟机中要访问 Docker 容器内提供的服务,需要在 DNS 上或者 hosts 上做相应的调整。所有的虚拟机的镜像使用 Centos7。 ...

2019-05-15 · 2 min · 408 words · 翟志军 Jack Zhai

基于 Jenkins 的 DevOps 平台应该如何设计凭证管理

背景 了解到行业内有些团队是基于 Jenkins 开发 DevOps 平台。而基于 Jenkins 实现的 DevOps 平台,就不得不考虑凭证的管理问题。 本文就此问题进行讨论,尝试找出相对合理的管理凭证的方案。 一开始我们想到的方案可能是这样的:用户在 DevOps 平台增加凭证后,DevOps 再将凭证同步到 Jenkins 上。Jenkins 任务在使用凭证时,使用的是存储在 Jenkins 上的凭证,而不是 DevOps 平台上的。 但是,仔细想想,这样做会存在以下问题: Jenkins 与 DevOps 平台之间的凭证数据会存在不一致问题。 存在一定的安全隐患。通过 Jenkins 脚本命令行很容易就把所有密码的明文拿到。哪天 Jenkins 被注入了,所有的凭证一下子就被扒走。 无法实现 Jenkins 高可用,因为凭证存在 Jenkins master 机器上。 那么,有没有更好的办法呢? 期望实现的目标 先定我们觉得更合理的目标,然后讨论如何实现。以下是笔者觉得合理的目标: 用户还是在 DevOps 管理自己的凭证。但是 DevOps 不需要将自己凭证同步到 Jenkins 上。Jenkins 任务在使用凭证时,从 DevOps 上取。 实现方式 Jenkins 有一个 Credentials Binding Plugin 插件,在 Jenkins pipeline 中的用法如下: withCredentials([usernameColonPassword(credentialsId: 'mylogin', variable: 'USERPASS')]) { sh ''' curl -u "$USERPASS" https://private.server/ > output ''' } withCredentials 方法做的事情就是从 Jenkins 的凭证列表中取出 id 为 mylogin 的凭证,并将值赋到变量名为 USERPASS 的变量中。接下来,你就可以在闭包中使用该变量了。 ...

2019-05-07 · 1 min · 160 words · 翟志军 Jack Zhai

Jenkins 自动安装插件

手工安装 Jenkins 插件的方法 通常,我们有两种方法安装 Jenkins 插件。第一种方法是到 Jenkins 插件管理页面搜索插件,然后安装。第二种方法是上传 Jenkins 插件的 hpi 文件安装。这两种方法能满足大多数人的需求。 第一种方法,如下图所示: 第二种方法,如下图所示: 但是对于需要保证 Jenkins 稳定或在 Jenkins 上进行二次开发的同学来说,以上方法是无法满足需求的。 第一种方法是无法指定插件的版本。第二种方式必须自己找到该插件的依赖树,一个个依赖的安装。是的,手工上传插件的这种方法,Jenkins 是不会自动下载依赖的。 自动安装插件的方法 那么,有什么方法能做到即指定插件的版本,又能自动下载它的依赖呢? 幸运的是,Jenkins 的 Docker 镜像的代码仓库里的 install-plugins.sh 脚本已经实现。只不过需要我们拿过来小小修改才能使用。笔者修改后创建了相应的代码仓库:jenkins-install-plugins-shell 。链接在文章末尾。 以下是 jenkins-install-plugins-shell 的使用方法: 将代码 clone 到 JENKINS_HOME 目录中。 cd $JENKINS_HOME git clone https://github.com/zacker330/jenkins-install-plugins-shell.git cd jenkins-install-plugins-shell 在 plugins.txt 中加入希望安装的插件 在 jenkins-install-plugins-shell 目录中,有一个 plugins.txt 文件,在文件中写入希望安装的插件及版本号。例如: ansible:1.0 powershell:1.3 执行安装 # Jenkins War 的路径,用于分析 export JENKINS_WAR_PATH=<Jenkins war文件的路径> chmod +x install-plugins.sh jenkins-support ./install-plugins.sh < plugins.txt 重启 Jenkins install-plugins 本质上做的事情就只是将插件从云端下载到 JENKINS_HOME 下的 plugins 目录中。要使安装的插件生效,还需要重启 Jenkins。 关于 Jenkins 插件的名称 Jenkins 插件有两个名称。一个叫 display name,一个叫 short name。比如 Ansible 插件的 disply name 为 Ansible plugin,short name 为 ansible。 ...

2019-04-27 · 1 min · 126 words · 翟志军 Jack Zhai

使用 Jenkins + Ansible 实现自动化部署 Nginx

本文介绍如何使用 Jenkins + Ansible 实现对 Nginx 的自动化部署。最终达到的效果有如下几点: 只要你将 Nginx 的配置推送到 GitHub 中,Jenkins 就会自动执行部署,然后目标服务器的 Nginx 配置自动生效。这个过程是幂等(idempotent)的,只要代码不变,执行多少遍,最终效果不变。 如果目标机器没有安装 Nginx,则会自动安装 Nginx。 自动设置服务器防火墙规则。 1. 实验环境介绍 本次实验使用 Docker Compose 搭建 Jenkins 及 Jenkins agent。使用 Vagrant 启动一台虚拟机,用于部署 Nginx。使用 Vagrant 是可选的,读者可以使用 VirtualBox 启动一个虚拟机。使用 Vagrant 完全是为了自动化搭建实验环境。 以下是整个实验环境的架构图: 注意,图中的 5123 <-> 80 代表将宿主机的 5123 端口请求转发到虚拟机中的 80 端口。 Vagrant:虚拟机管理工具,通过它,我们可以使用文本来定义、管理虚拟机。 Ansible:自动化运维工具 Docker Compose:它是一个用于定义和运行多容器 Docker 应用程序的工具。可以使用YAML文件来配置应用程序的服务。 2. 启动实验环境 克隆代码并进入文件夹 git clone https://github.com/zacker330/jenkins-ansible-nginx.git cd jenkins-ansible-nginx 构建 Jenkins agent 的镜像 需要自定义 Jenkins agent 镜像有两个原因: ...

2019-04-22 · 3 min · 432 words · 翟志军 Jack Zhai