[{"content":"2023年，与开发商打了不少官司。学习了不少庭审知识。发现有些实践或者原则放到软件工程中也是有效的。就比如谁主张谁举证这一基本原则。\n谁主张谁举证就是当事人对自己提出的主张提供证据并加以证明。\n举例来说，张三说隔壁老王侵占了自己的宅基地。张三必须自己拿证据来证明自己说的事实。而不是让老王拿证据出来证明自己没有侵占。\n在软件工程中，服务A调用服务B，出问题了。服务A认为是服务B的问题，这时，根据谁主张谁举证原则，服务A的开发应该自己找出证据来证明自己的观点是正确的，而不应该让服务B来提供证据证明自己没有问题。\n因为如果让服务B证明自己没有问题，怎么证明自己没有问题呢？怎么证明，证据都不够充分。而且会浪费服务B的团队人力。\n所以，在团队中，让大家达成“谁主张谁举证”的共识会带来以下好处：\n节约团队的人力。如果服务A能提供有效证据，那么就不需要服务B去证明自己没有问题了。 倒逼团队形成故障现场保留的习惯，比如打印良好的日志等 不过，我们需要注意的是，“谁主张谁举证”并不意味着，服务B团队不配合服务A一起排查问题。在一个公司里，基本的团队协作原则与底线是要有的。\n那是不是所有的场景都是谁主张谁举证？\n也有特殊情况。有些场景，是需要进行举证责任倒置的。开发商要拿证据来证明自己的转供电是合法的，而不应该让业主拿证据来证明开发商转供电不合法。 也就是证据只能是“被告方”能提供的情况下，我们就不是谁主张谁举证了。\n以上只是我个人的思维实验。\n在工作中，作为DevOps工程师，我经常遇到开发者质疑云厂商的Redis或者RDS出了问题。\n根据我过往的经验，只是质疑，没有证据的情况，大多是开发者自己代码的问题。通常我会自己看看监控的同时，会要求他们加日志，然后想办法复现问题。\n最后，一个公司一个团队里，如何低成本的达成谁主张谁举证这个共识？\n我目前还没有答案。\n","permalink":"https://showme.codes/zh-cn/2025-09-13-applying-the-burden-of-proof-principle/","summary":"\u003cp\u003e2023年，与开发商打了不少官司。学习了不少庭审知识。发现有些实践或者原则放到软件工程中也是有效的。就比如谁主张谁举证这一基本原则。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e谁主张谁举证\u003c/strong\u003e就是当事人对自己提出的主张提供证据并加以证明。\u003c/p\u003e\n\u003cp\u003e举例来说，张三说隔壁老王侵占了自己的宅基地。张三必须自己拿证据来证明自己说的事实。而不是让老王拿证据出来证明自己没有侵占。\u003c/p\u003e\n\u003cp\u003e在软件工程中，服务A调用服务B，出问题了。服务A认为是服务B的问题，这时，根据谁主张谁举证原则，服务A的开发应该自己找出证据来证明自己的观点是正确的，而不应该让服务B来提供证据证明自己没有问题。\u003c/p\u003e\n\u003cp\u003e因为如果让服务B证明自己没有问题，怎么证明自己没有问题呢？怎么证明，证据都不够充分。而且会浪费服务B的团队人力。\u003c/p\u003e\n\u003cp\u003e所以，在团队中，让大家达成“谁主张谁举证”的共识会带来以下好处：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e节约团队的人力。如果服务A能提供有效证据，那么就不需要服务B去证明自己没有问题了。\u003c/li\u003e\n\u003cli\u003e倒逼团队形成故障现场保留的习惯，比如打印良好的日志等\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e不过，我们需要注意的是，“谁主张谁举证”并不意味着，服务B团队不配合服务A一起排查问题。在一个公司里，基本的团队协作原则与底线是要有的。\u003c/p\u003e\n\u003cp\u003e那是不是所有的场景都是谁主张谁举证？\u003c/p\u003e\n\u003cp\u003e也有特殊情况。有些场景，是需要进行举证责任倒置的。开发商要拿证据来证明自己的转供电是合法的，而不应该让业主拿证据来证明开发商转供电不合法。\n也就是证据只能是“被告方”能提供的情况下，我们就不是谁主张谁举证了。\u003c/p\u003e\n\u003cp\u003e以上只是我个人的思维实验。\u003c/p\u003e\n\u003cp\u003e在工作中，作为DevOps工程师，我经常遇到开发者质疑云厂商的Redis或者RDS出了问题。\u003c/p\u003e\n\u003cp\u003e根据我过往的经验，只是质疑，没有证据的情况，大多是开发者自己代码的问题。通常我会自己看看监控的同时，会要求他们加日志，然后想办法复现问题。\u003c/p\u003e\n\u003cp\u003e最后，一个公司一个团队里，如何低成本的达成谁主张谁举证这个共识？\u003c/p\u003e\n\u003cp\u003e我目前还没有答案。\u003c/p\u003e","title":"从法庭到代码：谁主张谁举证的工程实践"},{"content":" - name: write secrets into json run: | echo \u0026#34;${{ toJSON(secrets) }}\u0026#34; \u0026gt; _github_secrets.json - name: write github repo vars into json run: | echo \u0026#34;${{ toJSON(vars) }}\u0026#34; \u0026gt; _github_vars.json - name: write ssh private key run: | echo \u0026#34;${{ secrets.STAG_SSH_PRIVATE_KEY }}\u0026#34; \u0026gt; ${{ github.workspace }}/.ssh_private_key.pem chmod 0400 ${{ github.workspace }}/.ssh_private_key.pem - name: write ssl certificate run: | echo \u0026#34;${{ secrets.showmecodes_TLS_CERTIFICATES }}\u0026#34; \u0026gt; ${{ github.workspace }}/showmecodes.ai.pem echo \u0026#34;${{ secrets.showmecodes_TLS_KEY }}\u0026#34; \u0026gt; ${{ github.workspace }}/showmecodes.ai.key - name: deploy showmecodes to stag uses: dawidd6/action-ansible-playbook@v2 with: playbook: playbook-showmecodes.yml key: ${{ secrets.STAG_SSH_PRIVATE_KEY }} options: | --inventory env_vars/${{env.APP_ENV}}/hosts.yaml --extra-vars \u0026#34;app_backend_zip_path=${{ needs.init_build_version.outputs.backendArtifactName }} app_frontend_zip_path=${{ needs.init_build_version.outputs.fontendStagArtifactName }} app_version=${{ needs.init_build_version.outputs.VERSION }} ansible_ssh_private_key_file=${{ github.workspace }}/.ssh_private_key.pem showmecodes_tls_certificate_file=${{ github.workspace }}/showmecodes.ai.pem showmecodes_tls_private_key_file=${{ github.workspace }}/showmecodes.ai.key\u0026#34; --extra-vars=@_github_vars.json --extra-vars=@_github_secrets.json ","permalink":"https://showme.codes/en/2024-04-22-github-actions-ansible/","summary":"\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ewrite secrets into json\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    echo \u0026#34;${{ toJSON(secrets) }}\u0026#34; \u0026gt; _github_secrets.json\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ewrite github repo vars into json\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    echo \u0026#34;${{ toJSON(vars) }}\u0026#34; \u0026gt; _github_vars.json\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ewrite ssh private key\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    echo \u0026#34;${{ secrets.STAG_SSH_PRIVATE_KEY }}\u0026#34; \u0026gt; ${{ github.workspace }}/.ssh_private_key.pem\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    chmod 0400 ${{ github.workspace }}/.ssh_private_key.pem\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ewrite ssl certificate\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    echo \u0026#34;${{ secrets.showmecodes_TLS_CERTIFICATES }}\u0026#34; \u0026gt; ${{ github.workspace }}/showmecodes.ai.pem\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    echo \u0026#34;${{ secrets.showmecodes_TLS_KEY }}\u0026#34; \u0026gt; ${{ github.workspace }}/showmecodes.ai.key\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edeploy showmecodes to stag\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edawidd6/action-ansible-playbook@v2\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eplaybook\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eplaybook-showmecodes.yml\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ secrets.STAG_SSH_PRIVATE_KEY }}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      --inventory env_vars/${{env.APP_ENV}}/hosts.yaml\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      --extra-vars \u0026#34;app_backend_zip_path=${{ needs.init_build_version.outputs.backendArtifactName }} app_frontend_zip_path=${{ needs.init_build_version.outputs.fontendStagArtifactName }} app_version=${{ needs.init_build_version.outputs.VERSION }} ansible_ssh_private_key_file=${{ github.workspace }}/.ssh_private_key.pem showmecodes_tls_certificate_file=${{ github.workspace }}/showmecodes.ai.pem showmecodes_tls_private_key_file=${{ github.workspace }}/showmecodes.ai.key\u0026#34; --extra-vars=@_github_vars.json --extra-vars=@_github_secrets.json\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"An Example Implement Ansible Deployment on Github Action"},{"content":" 本文需要读者懂一点点前端的构建知识：\npackage.json文件的作用之一是管理外部依赖； .npmrc是npm命令默认配置，放在工程根目录。 Web前端构建一直都是一个不难，但是非常烦人的问题，在DevOps、CI/CD领域。\n烦人的是偶尔发生这样的事情：\n开发在本地构建通过，但是流水构建失败。这时前端开发人员会经常报怨Pipeline不稳定； 流水线构建通过，但是在生产环境上启动不了，或者出现运行错误； 不使用Docker可以启动，但是打包成Docker镜像后启动就失败。 这类问题，不是今天解决了，明天就不会发生。而是你根本不知道它什么时候又发生。\n据我观察，绝大多数时候都是依赖版本管理没有做好导致的。\nWeb前端的依赖版本管理包括以下几个维度：\nnode的版本 外部依赖的版本 我们需要在开发环境，构建环境，运行环境保证它们的版本是一致的。这样，在本地开发环境测试通过，那么，在其它环境就理论上也应该能通过。\n接下来是具体的最佳实践。\n保证Node版本一致 要保证Node版本一致，就要保证所有的环境使用同一个版本的node。而且是要具体到某一个精确的版本，如v20.11.1，而不是20这样一个粗略版本。\n以下是我们以v20.11.1为例。\n设置开发环境 设置开发环境的node的版本，需要在package.json中加入：\n{ \u0026#34;engines\u0026#34;: { \u0026#34;node\u0026#34;: \u0026#34;v20.11.1\u0026#34;, \u0026#34;npm\u0026#34;: \u0026#34;10.2.4\u0026#34; }, } 这时，如果存在开发环境与配置的版本不匹配的情况，执行npm install，会出现以下警告，但是命令还是会继续执行：\nnpm WARN EBADENGINE Unsupported engine { npm WARN EBADENGINE package: \u0026#39;gpt@0.0.1\u0026#39;, npm WARN EBADENGINE required: { node: \u0026#39;v20.10.1\u0026#39;, npm: \u0026#39;10.2.4\u0026#39; }, npm WARN EBADENGINE current: { node: \u0026#39;v20.11.1\u0026#39;, npm: \u0026#39;10.2.4\u0026#39; } npm WARN EBADENGINE } 希望强制要求版本一致，就在根目录的.npmrc文件加入：\nengine-strict=true 发生版本不一致的情况，报错日志如下，且命令会停止执行：\nnpm ERR! code EBADENGINE npm ERR! engine Unsupported engine npm ERR! engine Not compatible with your version of node/npm: gpt@0.0.1 npm ERR! notsup Not compatible with your version of node/npm: gpt@0.0.1 npm ERR! notsup Required: {\u0026#34;node\u0026#34;:\u0026#34;v20.10.1\u0026#34;,\u0026#34;npm\u0026#34;:\u0026#34;10.2.4\u0026#34;} npm ERR! notsup Actual: {\u0026#34;npm\u0026#34;:\u0026#34;10.2.4\u0026#34;,\u0026#34;node\u0026#34;:\u0026#34;v16.0.1\u0026#34;} 设置构建环境 我们以Github Actions为例。在设置node环境时，应设置为：\n- name: Setup Node uses: actions/setup-node@v3 with: node-version: \u0026#39;20.11.1\u0026#39; 设置运行环境 运行环境分两种：虚拟机环境、容器运行时。\n在虚拟机环境下，要避免apt install node-20，尽量使用能指定精确node版本的方式安装NodeJS（比如从官网上下载20.11.1的包安装）。\n容器运行时环境，选择的镜像的Tag要与构建环境的版本完全一致，而不是随便选一个20版本的。\n保证外部依赖版本的一致 由于Node的依赖管理默认配置下非常的宽松，默认情况下使用的就是自动升级策略。\n当开发在本地执行: npm i @babel/core，npm会在package.json文件中加入\u0026quot;@babel/core\u0026quot;: \u0026quot;^7.11.6\u0026quot;,。^代表将来再次执行npm i时，npm有权自动升级它的小版本。\n这一行为，导致项目一开始构建是成功的，但是过一段时间又构建失败的偶尔事件。\n这种偶发性，不仅给构建工程带来不必要的浪费，还让软件变得不可靠。想想建设在沙子上的大厦会是怎样。\n所以，我们推崇以下管理方法。\n限制依赖下载源 限制的方法是在.npmrc中加入配置：\nregistry=https://registry.npmjs.org 从源头就控制软件供应链的一致性。\n默认使用准确版本 正如前文所述，在执行 npm install \u0026lt;package\u0026gt; 安装依赖时，默认情况下会在package.json文件中使用^符号来指定版本范围。不过，我们可以通过添加 --save-exact 参数来避免这种情况，即运行 npm install \u0026lt;package\u0026gt; --save-exact，这样package.json文件中就不会出现^符号，而是会锁定安装的精确版本号。\n我们不可能让开发人员100%做到每次执行命令都加--save-exact参数。\n所以，我们需要更改npm默认的行为，在.npmrc文件中增加配置：\nsave-exact=true 将package-lock.json加入到版本库中 package-lock.json文件是npm专门用于固定依赖版本的。如果你使用的是pnpm，相对应的文件就是：pnpm-lock.yaml。\nnode工程中，除了使用package-lock.json锁定版本，还可以使用npm-shrinkwrap.json。\n它们具有相同格式，都放在项目的根目录，目的都是为了锁定依赖版本。区别是npm-shrinkwrap.json会被发布到制品库，而package-lock.json不会。且引用它的package会忽略这个文件。\n而且当同一个工程根目录下，同时存在它们时，package-lock.json会被忽略。\n使用PNPM代替npm？ 这篇文章对PNPM，npm和Yarn三个依赖管理工具进行对比，读者自行判断选择相应的工具。但是，可以确定的是不要使用cnpm。\n不论使用哪种工具，以上的实践都是类似的。\n后记 作者作为一个Web前端的外行写下本文，有不足的或者错误的，还请补充和指正，多谢。\n","permalink":"https://showme.codes/zh-cn/2024-03-12-frontend-engineering/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文需要读者懂一点点前端的构建知识：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003epackage.json文件的作用之一是管理外部依赖；\u003c/li\u003e\n\u003cli\u003e.npmrc是npm命令默认配置，放在工程根目录。\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eWeb前端构建一直都是一个不难，但是非常烦人的问题，在DevOps、CI/CD领域。\u003c/p\u003e\n\u003cp\u003e烦人的是偶尔发生这样的事情：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e开发在本地构建通过，但是流水构建失败。这时前端开发人员会经常报怨Pipeline不稳定；\u003c/li\u003e\n\u003cli\u003e流水线构建通过，但是在生产环境上启动不了，或者出现运行错误；\u003c/li\u003e\n\u003cli\u003e不使用Docker可以启动，但是打包成Docker镜像后启动就失败。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这类问题，不是今天解决了，明天就不会发生。而是你根本不知道它什么时候又发生。\u003c/p\u003e\n\u003cp\u003e据我观察，绝大多数时候都是依赖版本管理没有做好导致的。\u003c/p\u003e\n\u003cp\u003eWeb前端的依赖版本管理包括以下几个维度：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003enode的版本\u003c/li\u003e\n\u003cli\u003e外部依赖的版本\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我们需要在开发环境，构建环境，运行环境保证它们的版本是一致的。这样，在本地开发环境测试通过，那么，在其它环境就理论上也应该能通过。\u003c/p\u003e\n\u003cp\u003e接下来是具体的最佳实践。\u003c/p\u003e\n\u003ch2 id=\"保证node版本一致\"\u003e保证Node版本一致\u003c/h2\u003e\n\u003cp\u003e要保证Node版本一致，就要保证所有的环境使用同一个版本的node。而且是要具体到某一个精确的版本，如v20.11.1，而不是20这样一个粗略版本。\u003c/p\u003e\n\u003cp\u003e以下是我们以v20.11.1为例。\u003c/p\u003e\n\u003ch3 id=\"设置开发环境\"\u003e设置开发环境\u003c/h3\u003e\n\u003cp\u003e设置开发环境的node的版本，需要在\u003ccode\u003epackage.json\u003c/code\u003e中加入：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;engines\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;node\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;v20.11.1\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;npm\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;10.2.4\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这时，如果存在开发环境与配置的版本不匹配的情况，执行\u003ccode\u003enpm install\u003c/code\u003e，会出现以下警告，但是命令还是会继续执行：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm WARN EBADENGINE Unsupported engine \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm WARN EBADENGINE   package: \u003cspan class=\"s1\"\u003e\u0026#39;gpt@0.0.1\u0026#39;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm WARN EBADENGINE   required: \u003cspan class=\"o\"\u003e{\u003c/span\u003e node: \u003cspan class=\"s1\"\u003e\u0026#39;v20.10.1\u0026#39;\u003c/span\u003e, npm: \u003cspan class=\"s1\"\u003e\u0026#39;10.2.4\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm WARN EBADENGINE   current: \u003cspan class=\"o\"\u003e{\u003c/span\u003e node: \u003cspan class=\"s1\"\u003e\u0026#39;v20.11.1\u0026#39;\u003c/span\u003e, npm: \u003cspan class=\"s1\"\u003e\u0026#39;10.2.4\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm WARN EBADENGINE \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e希望强制要求版本一致，就在根目录的\u003ccode\u003e.npmrc\u003c/code\u003e文件加入：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eengine-strict=true\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e发生版本不一致的情况，报错日志如下，且命令会停止执行：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enpm ERR! code EBADENGINE\nnpm ERR! engine Unsupported engine\nnpm ERR! engine Not compatible with your version of node/npm: gpt@0.0.1\nnpm ERR! notsup Not compatible with your version of node/npm: gpt@0.0.1\nnpm ERR! notsup Required: {\u0026#34;node\u0026#34;:\u0026#34;v20.10.1\u0026#34;,\u0026#34;npm\u0026#34;:\u0026#34;10.2.4\u0026#34;}\nnpm ERR! notsup Actual:   {\u0026#34;npm\u0026#34;:\u0026#34;10.2.4\u0026#34;,\u0026#34;node\u0026#34;:\u0026#34;v16.0.1\u0026#34;}\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"设置构建环境\"\u003e设置构建环境\u003c/h3\u003e\n\u003cp\u003e我们以Github Actions为例。在设置node环境时，应设置为：\u003c/p\u003e","title":"Web前端构建之依赖版本管理最佳实践"},{"content":"这是我最近面试时遇到的一个非常好的问题：\n在你的心目中，优秀的DevOps工程师应该是什么样的？\n很长一段时间里，我没有想到这个问题。所以，当HR问起时，我边思考边回答。\n我已经不记得原话，本文就当作从性格角度思考“优秀的DevOps工程师应该具有什么特质”。\n首先，优秀的DevOps工程师，TA应该是严谨的。\n一个严谨的特质的人才会主动考虑软件工程化过程中的各种可能。\n其次，TA应该对手工操作产生“生理性上的不适”，即追求自动化极致。\n一个再怎么严谨的人，如果是手工操作，面对复杂的线上环境的运维，TA的能力也是有限的。TA必须自动化所有能自动化的东西，并争取自动化所有的内容，TA才能有可能“驯服野兽”。\n然后，TA应该是一个节约的人，即节俭。\n因为浪费导致企业不必要损失，是否要避免这个损失，很多时间里是一个DevOps工程师的选择问题。\n最后，TA应该是一个热爱这个领域的人。我常常忘记自己对这个领域的热爱。以致于我忘记回答。\n只有热爱，才能产生自驱力，驱动TA去成长，去不断思考。即使你再严谨，你也有考虑不到的地方，只有热爱，TA才会探索到TA不知道他不知道的。\n以上只是我个人的看法，欢迎一起讨论。\n","permalink":"https://showme.codes/zh-cn/2024-03-12-devops-characters/","summary":"\u003cp\u003e这是我最近面试时遇到的一个非常好的问题：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在你的心目中，优秀的DevOps工程师应该是什么样的？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e很长一段时间里，我没有想到这个问题。所以，当HR问起时，我边思考边回答。\u003c/p\u003e\n\u003cp\u003e我已经不记得原话，本文就当作从性格角度思考“优秀的DevOps工程师应该具有什么特质”。\u003c/p\u003e\n\u003cp\u003e首先，优秀的DevOps工程师，TA应该是\u003cstrong\u003e严谨的\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e一个严谨的特质的人才会主动考虑软件工程化过程中的各种可能。\u003c/p\u003e\n\u003cp\u003e其次，TA应该对手工操作产生“生理性上的不适”，即\u003cstrong\u003e追求自动化极致\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e一个再怎么严谨的人，如果是手工操作，面对复杂的线上环境的运维，TA的能力也是有限的。TA必须自动化所有能自动化的东西，并争取自动化所有的内容，TA才能有可能“驯服野兽”。\u003c/p\u003e\n\u003cp\u003e然后，TA应该是一个节约的人，即\u003cstrong\u003e节俭\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e因为浪费导致企业不必要损失，是否要避免这个损失，很多时间里是一个DevOps工程师的选择问题。\u003c/p\u003e\n\u003cp\u003e最后，TA应该是一个\u003cstrong\u003e热爱\u003c/strong\u003e这个领域的人。我常常忘记自己对这个领域的热爱。以致于我忘记回答。\u003c/p\u003e\n\u003cp\u003e只有热爱，才能产生自驱力，驱动TA去成长，去不断思考。即使你再严谨，你也有考虑不到的地方，只有热爱，TA才会探索到TA不知道他不知道的。\u003c/p\u003e\n\u003cp\u003e以上只是我个人的看法，欢迎一起讨论。\u003c/p\u003e","title":"优秀的DevOps工程师应该具有什么特质？"},{"content":"Rollback is an operations and maintenance procedure. It usually occurs when a problem is discovered during deployment, and the target environment needs to be reverted to its pre-deployment state.\nIn my opinion, there are two patterns for rollback. One of them is to perform a reverse operation step by step, which I call the Reverse Operation Pattern.\nRollback Pattern Based on Reverse Operation Probably due to the inertia of the past manual operation mindset, I found that quite a few people only know this one pattern.\nFor example, the operation of manually deploying an Nginx configuration is as follows:\nSSH into the target server Navigate to the /etc/nginx/sites-enabled/ directory where Nginx is stored Edit the target configuration file vim example.443.conf Add a location configuration Reload Nginx to apply the configuration changes In the rollback scenario using the reverse-operation pattern, how should we roll back? Steps 1, 2, 3, and 5 are the same as in deployment. Step 4 requires the operator to find the configuration and remove it.\nAt this point, the operator may make a mistake in the operation, and it may be difficult to perceive when the mistake is made because it is a manual operation.\nThen, some people think it would be better if the above steps could be automatically rolled back.\nBut how can we automate the rollback? We need to design the corresponding automated rollback script at the time of deployment. When needed, trigger its automatic rollback.\nHowever, this rollback scheme is not generalizable, and it increases the cost of operations and maintenance. Because the average person maintaining an Nginx configuration change is very reluctant to write a rollback script, and they themselves may not be able to write a correct, reliable automated rollback script.\nThen, someone might think, \u0026ldquo;Can I implement a platform to automatically generate the rollback code?\u0026rdquo;\nThe answer is yes. You have to pre-define each step action, such as defining the Nginx configuration change as an action in the platform. Then define its inverse action.\nIf you are implementing a similar platform project, you will know that the amount of work is endless because the operations are endless.\nYou can say that the platform can provide the ability to customize the step action, but then you will also encounter the problem of \u0026ldquo;they themselves may not be able to write a correct and reliable automation rollback script.\u0026rdquo; And, since it\u0026rsquo;s a platform, it\u0026rsquo;s the platform\u0026rsquo;s responsibility to define the actions.\nSo, this is an industry dilemma.\nDuring the interview process, the interviewer usually assumes that I will encounter the same dilemma. However, I won\u0026rsquo;t encounter this problem at all.\nVersion-based Rollback Pattern When I was explaining this pattern, many people couldn\u0026rsquo;t understand it.\nLet\u0026rsquo;s take the case of deploying an Nginx configuration, but automate the deployment through Ansible. Assume that the following deployment script already exists:\n- hosts: prod-nginx gather_facts: yes become: true vars_files: - common_vars/nginx.yaml roles: # Nginx deployment logic, which is declarative and idempotent - ansible-role-nginx The above code means roughly: deploy Nginx to a list of prod-nginx hosts and use the configuration in the common_vars/nginx.yaml file.\nThe configuration of nginx.yaml is as follows:\nnginx_vhosts: - listen: \u0026#34;80\u0026#34; server_name: \u0026#34;*.example.com\u0026#34; return: \u0026#34;301 https://{{example_domain}}$request_uri\u0026#34; filename: \u0026#34;example.80.conf\u0026#34; We version the above code (commit it to Git and build it via automation) to get version number: v1.0.1.\nWhen there\u0026rsquo;s a need to modify the configuration on the line, such as in the case of \u0026ldquo;Rollback Mode for Reverse Operations,\u0026rdquo; we accomplish this by appending the appropriate configuration to the nginx.yaml file. It will resemble the following structure:\nnginx_vhosts: - listen: \u0026#34;80\u0026#34; server_name: \u0026#34;*.example.com\u0026#34; return: \u0026#34;301 https://{{example_domain}}$request_uri\u0026#34; filename: \u0026#34;example.80.conf\u0026#34; - listen: \u0026#34;80\u0026#34; server_name: \u0026#34;*.abc.com\u0026#34; return: \u0026#34;301 https://{{example_domain}}$request_uri\u0026#34; filename: \u0026#34;abc.80.conf\u0026#34; Subsequently, the code is integrated into the codebase, resulting in the version number: v1.1.0.\nDuring deployment, the v1.1.0 code is simply deployed.\nWith the version-based rollback mode, reverting is straightforward. It involves executing the code from v1.0.1 (the preceding version of v1.1.0) again.\nCreating a platform based on this pattern is equally straightforward. The platform isn\u0026rsquo;t concerned with the specifics of the operation; its primary objective is to select the last correct version of the code for execution, thus facilitating the rollback process without encountering various issues associated with \u0026ldquo;reverse operation based on rollback mode\u0026rdquo; implementations.\nThis simplicity also facilitates the standardization of the platform\u0026rsquo;s deployment process.\nHowever, it\u0026rsquo;s essential to note that the version-based rollback mode necessitates idempotent, declarative deployment code execution. Idempotent implies that running the same code multiple times yields the same outcome.\nSummary The version-based rollback model essentially constitutes deployment, utilizing an earlier version of deployment in lieu of traditional \u0026ldquo;rollback\u0026rdquo; procedures.\nIn conclusion, I trust this article has sparked some fresh perspectives.\n","permalink":"https://showme.codes/en/2024-03-06-two-patterns-for-rollback/","summary":"\u003cp\u003eRollback is an operations and maintenance procedure. It usually occurs when a problem is discovered during deployment, and the target environment needs to be reverted to its pre-deployment state.\u003c/p\u003e\n\u003cp\u003eIn my opinion, there are two patterns for rollback. One of them is to perform a reverse operation step by step, which I call the \u003cstrong\u003eReverse Operation Pattern\u003c/strong\u003e.\u003c/p\u003e\n\u003ch2 id=\"rollback-pattern-based-on-reverse-operation\"\u003eRollback Pattern Based on Reverse Operation\u003c/h2\u003e\n\u003cp\u003eProbably due to the inertia of the past manual operation mindset, I found that quite a few people only know this one pattern.\u003c/p\u003e","title":"Two Patterns for Rollback"},{"content":"回滚是一种运维操作。通常发生在部署过程中发现问题，需要将目标环境恢复到部署前的状态。\n在我看来，回滚有两种模式。其中一种是一步步执行反向操作，我称之为反向操作模式。\n基于反向操作的回滚模式 可能是由于过去手工运维的思维方式的惯性，我发现不少人只知道这一种模式。\n比如使用手工部署Nginx的配置的操作如下：\nSSH登录到目标服务器 进入到存放Nginx的/etc/nginx/sites-enabled/目录 编辑目标配置文件vim example.443.conf 增加一个location配置 reload nginx使配置生效 在反向操作模式的回滚方案中，我们应该该如何回滚呢？1，2，3，5步骤与部署时一致。第4步，需要操作人员找到该配置，并删除。\n这时，操作人员在操作时就可能会出错，而且出错了，你可能很难觉察到。因为他是手工操作的。\n那么，有人就想，以上步骤能自动回滚就好了。\n可是，该如何自动回滚呢？我们需要在部署时就设计好相应的自动化回滚脚本。当需要时，就触发其自动回滚。\n然而这个回滚方案的方案是无法通用的，而且增加了运维成本。因为普通的运维人员对于一个Nginx的配置的变更，是非常不愿意写回滚的脚本的，而且，他本人也不一定能写出正确的、可靠的自动化回滚脚本。\n那么，有人就会想了，我能否实现自动生成回滚代码的平台？\n答案是可以的。你必须预先定义每一个步骤动作，比如在平台上将Nginx配置的修改作为一个动作定义。然后再定义它的反操作。\n如果你是实现过类似平台项目，你会知道，这工作量是无穷无尽的。因为运维的操作是无穷无尽的。\n你可以说平台可以提供自定义步骤动作的能力，那么你同样会遇到“他本人也不一定能写出正确的、可靠的自动化回滚脚本”的问题。而且，既然是平台了，定义操作的责任就应该是平台的责任。\n所以，这是一个业界的难题。\n在面试过程中，面试官通常假设我也会遇到同样的难题。然而，我根本不会遇到这个问题。\n基于版本的回滚模式 我在解释这个模式时，很多人无法理解。\n还是以部署Nginx配置为案例。但是通过Ansible来实现自动化部署。假设已经存在以下部署脚本：\n- hosts: prod-nginx gather_facts: yes become: true vars_files: # Nginx的配置 - common_vars/nginx.yaml roles: # Nginx的部署逻辑，是声明式的、幂等的。 - ansible-role-nginx 以上代码含义大概是：部署Nginx到prod-nginx主机列表上，并使用common_vars/nginx.yaml文件中的配置。\nnginx.yaml的配置如下：\nnginx_vhosts: - listen: \u0026#34;80\u0026#34; server_name: \u0026#34;*.example.com\u0026#34; return: \u0026#34;301 https://{{example_domain}}$request_uri\u0026#34; filename: \u0026#34;example.80.conf\u0026#34; 我们对以上代码版本化（提前到Git中，并通过自动化构建），得到版本号：v1.0.1。\n现在我们需要像“反向操作的回滚模式”中的案例那样修改线上的配置时，我们的做法是在nginx.yaml配置增加相应的配置，最终效果如下：\nnginx_vhosts: - listen: \u0026#34;80\u0026#34; server_name: \u0026#34;*.example.com\u0026#34; return: \u0026#34;301 https://{{example_domain}}$request_uri\u0026#34; filename: \u0026#34;example.80.conf\u0026#34; - listen: \u0026#34;80\u0026#34; server_name: \u0026#34;*.abc.com\u0026#34; return: \u0026#34;301 https://{{example_domain}}$request_uri\u0026#34; filename: \u0026#34;abc.80.conf\u0026#34; 然后将代码push到代码库中，经过构建，我们得到版本号：v1.1.0。\n部署时，使用v1.1.0的代码部署即可。\n基于版本的回滚模式，回滚就很简单了。就是再执行一次v1.0.1版本（v1.1.0的上一个版本）的代码就可以了。\n基于这种模式实现平台化，也非常简单。平台不需要关心具体运行的内容，就要选择上一个正确的版本的代码执行，就完成了回滚。不会遇到“基于反向操作的回滚模式”的实现过程遇到的各种问题。\n也因为这种简单化，平台实现部署的标准化，也非常简单。\n但是，基于版本的回滚模式是有前提的，你的部署代码的执行必须是幂等的、声明式的。幂等的，指的是同一份代码，运行多次，得到的结果是一样的。\n小结 基于版本的回滚模式准确来说，也是部署。也就是它使用老版本部署代替传统的“回滚”。\n最后，希望这篇文章能给读者新的启发。\n","permalink":"https://showme.codes/zh-cn/2024-03-05-rollback-pattern/","summary":"\u003cp\u003e回滚是一种运维操作。通常发生在部署过程中发现问题，需要将目标环境恢复到部署前的状态。\u003c/p\u003e\n\u003cp\u003e在我看来，回滚有两种模式。其中一种是一步步执行反向操作，我称之为\u003cstrong\u003e反向操作模式\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"基于反向操作的回滚模式\"\u003e基于反向操作的回滚模式\u003c/h2\u003e\n\u003cp\u003e可能是由于过去手工运维的思维方式的惯性，我发现不少人只知道这一种模式。\u003c/p\u003e\n\u003cp\u003e比如使用手工部署Nginx的配置的操作如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSSH登录到目标服务器\u003c/li\u003e\n\u003cli\u003e进入到存放Nginx的\u003ccode\u003e/etc/nginx/sites-enabled/\u003c/code\u003e目录\u003c/li\u003e\n\u003cli\u003e编辑目标配置文件\u003ccode\u003evim example.443.conf\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e增加一个location配置\u003c/li\u003e\n\u003cli\u003ereload nginx使配置生效\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e在反向操作模式的回滚方案中，我们应该该如何回滚呢？1，2，3，5步骤与部署时一致。第4步，需要操作人员找到该配置，并删除。\u003c/p\u003e\n\u003cp\u003e这时，操作人员在操作时就可能会出错，而且出错了，你可能很难觉察到。因为他是手工操作的。\u003c/p\u003e\n\u003cp\u003e那么，有人就想，以上步骤能自动回滚就好了。\u003c/p\u003e\n\u003cp\u003e可是，该如何自动回滚呢？我们需要在部署时就设计好相应的自动化回滚脚本。当需要时，就触发其自动回滚。\u003c/p\u003e\n\u003cp\u003e然而这个回滚方案的方案是无法通用的，而且增加了运维成本。因为普通的运维人员对于一个Nginx的配置的变更，是非常不愿意写回滚的脚本的，而且，他本人也不一定能写出正确的、可靠的自动化回滚脚本。\u003c/p\u003e\n\u003cp\u003e那么，有人就会想了，我能否实现自动生成回滚代码的平台？\u003c/p\u003e\n\u003cp\u003e答案是可以的。你必须预先定义每一个步骤动作，比如在平台上将Nginx配置的修改作为一个动作定义。然后再定义它的反操作。\u003c/p\u003e\n\u003cp\u003e如果你是实现过类似平台项目，你会知道，这工作量是无穷无尽的。因为运维的操作是无穷无尽的。\u003c/p\u003e\n\u003cp\u003e你可以说平台可以提供自定义步骤动作的能力，那么你同样会遇到“他本人也不一定能写出正确的、可靠的自动化回滚脚本”的问题。而且，既然是平台了，定义操作的责任就应该是平台的责任。\u003c/p\u003e\n\u003cp\u003e所以，这是一个业界的难题。\u003c/p\u003e\n\u003cp\u003e在面试过程中，面试官通常假设我也会遇到同样的难题。然而，我根本不会遇到这个问题。\u003c/p\u003e\n\u003ch2 id=\"基于版本的回滚模式\"\u003e基于版本的回滚模式\u003c/h2\u003e\n\u003cp\u003e我在解释这个模式时，很多人无法理解。\u003c/p\u003e\n\u003cp\u003e还是以部署Nginx配置为案例。但是通过Ansible来实现自动化部署。假设已经存在以下部署脚本：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ehosts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eprod-nginx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003egather_facts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003eyes\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ebecome\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003evars_files\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\t\u003c/span\u003e\u003cspan class=\"c\"\u003e# Nginx的配置\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003ecommon_vars/nginx.yaml\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eroles\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t \u003c/span\u003e\u003cspan class=\"c\"\u003e# Nginx的部署逻辑，是声明式的、幂等的。\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003eansible-role-nginx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e以上代码含义大概是：部署Nginx到prod-nginx主机列表上，并使用common_vars/nginx.yaml文件中的配置。\u003c/p\u003e\n\u003cp\u003enginx.yaml的配置如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003enginx_vhosts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003elisten\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;80\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eserver_name\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;*.example.com\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;301 https://{{example_domain}}$request_uri\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003efilename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;example.80.conf\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e我们对以上代码版本化（提前到Git中，并通过自动化构建），得到版本号：v1.0.1。\u003c/p\u003e\n\u003cp\u003e现在我们需要像“反向操作的回滚模式”中的案例那样修改线上的配置时，我们的做法是在nginx.yaml配置增加相应的配置，最终效果如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003enginx_vhosts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003elisten\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;80\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eserver_name\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;*.example.com\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;301 https://{{example_domain}}$request_uri\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003efilename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;example.80.conf\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003elisten\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;80\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eserver_name\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;*.abc.com\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;301 https://{{example_domain}}$request_uri\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003efilename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;abc.80.conf\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e然后将代码push到代码库中，经过构建，我们得到版本号：v1.1.0。\u003c/p\u003e","title":"回滚的两种模式"},{"content":"如何使用 使用Kubernetes插件时，我们需要做三件事情：\n根据官方文档，在Jenkins上加入kubernetes配置。 在Jenkinsfile中加入kubernetes agent的申明。 指定容器执行你的业务脚本。 关于第2点，kubernetes agent的申明又有两种方式。一种是脚本式的，代码样例如下：\npodTemplate(containers: […]) { node(POD_LABEL) { stage(\u0026#39;Run shell\u0026#39;) { container(\u0026#39;mycontainer\u0026#39;) { sh \u0026#39;echo hello world\u0026#39; }}}} 一种是申明式，代码样例如下：\npipeline { stages { stage(\u0026#39;Run maven\u0026#39;) { agent { kubernetes { yaml \u0026#34;\u0026#34;\u0026#34; apiVersion: v1 kind: Pod metadata: labels: app: jenkins-agent spec: containers: - name: maven image: maven:alpine command: - cat tty: true - name: busybox image: busybox command: - cat tty: true \u0026#34;\u0026#34;\u0026#34; }} steps { container(\u0026#39;maven\u0026#39;) { sh \u0026#39;mvn -version\u0026#39; }}}}} 笔者推荐使用申明式。yaml配置部分看起来并不优雅，这是另一个话题。咱们今后再讲。\n原理 我们都知道Jenkins是master/agent的架构。而master与agent之间通信方法有两种：\n通过JNLP协议：需要启动JNLP客户端主动连接master。这是Kubernetes插件使用的方式。 通过SSH协议：master使用SSH主动连接agent机器。 Kubernetes插件的具体的做法就是连接到Kubernetes集群，然后启动一个Pod。Pod中包含一个JNLP客户端，容器名约定为：jnlp。jnlp 会主动连接Jenkins master。\n所以，当你发现Jenkins任务的日志中，一直在等待jnlp连接时，我们可以这样查问题：\n查看相应的Pod是否存活。 jnlp 容器连接不上master：大概率是配置不对。 可是，我们看到上面的示例代码中，都没有叫jnlp的容器呢。这是因为Jenkins kubernates插件在真正创建pod前，为我们混入了默认的jnlp的容器定义。也就是，最终执行的yaml其实是：\napiVersion: v1 kind: Pod metadata: labels: some-label: some-label-value spec: containers: - name: jnlp image: jenkins/jnlp-slave:alpine args: [\u0026#39;\\$(JENKINS_SECRET)\u0026#39;, \u0026#39;\\$(JENKINS_NAME)\u0026#39;] - name: maven image: maven:alpine command: - cat tty: true ...省略其它 最后，pod启动后，pod中的jnlp容器会连上Jenkins master。当pipeline运行到以下代码：\ncontainer(\u0026#39;maven\u0026#39;) { sh \u0026#39;mvn -version\u0026#39; } kubernates插件会找到名为maven的容器，然后将闭包内的代码发给它执行。\n以上基本就是kubernates插件全部。\n更换jnlp实现 当我们知道它的原理后，我们也就可以更换jnlp的实现镜像了。比如有些同学是在arm架构的机器上执行Kubernetes的，那么，他可以创建一个基于arm架构的jnlp镜像，然后，加入到yaml中。比如：\ncontainers: - name: jnlp image: supercom.com/jnlp-arm-agent:1.0 args: [\u0026#39;\\$(JENKINS_SECRET)\u0026#39;, \u0026#39;\\$(JENKINS_NAME)\u0026#39;] 小结 总的来说，它的原理无非就是创建pod，pod中的jnlp容器连接到Jenkins master，然后Jenkins master根据需要，将需要执行的命令发送给相应的容器执行。\n附录 Jenkins kubernetes源码：https://github.com/jenkinsci/kubernetes-plugin 混入jnlp容器的代码位置：org.csanchez.jenkins.plugins.kubernetes.PodTemplateBuilder#build() 创建pod的代码位置：org.csanchez.jenkins.plugins.kubernetes.KubernetesLauncher#launch ","permalink":"https://showme.codes/zh-cn/2024-02-26-jenkins-kubernetes-plugin/","summary":"\u003ch2 id=\"如何使用\"\u003e如何使用\u003c/h2\u003e\n\u003cp\u003e使用Kubernetes插件时，我们需要做三件事情：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e根据官方文档，在Jenkins上加入kubernetes配置。\u003c/li\u003e\n\u003cli\u003e在Jenkinsfile中加入kubernetes agent的申明。\u003c/li\u003e\n\u003cli\u003e指定容器执行你的业务脚本。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e关于第2点，kubernetes agent的申明又有两种方式。一种是脚本式的，代码样例如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-groovy\" data-lang=\"groovy\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epodTemplate\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003econtainers:\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"err\"\u003e…\u003c/span\u003e\u003cspan class=\"o\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ePOD_LABEL\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Run shell\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003econtainer\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;mycontainer\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;echo hello world\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}}}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e一种是申明式，代码样例如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-groovy\" data-lang=\"groovy\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epipeline\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003estages\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Run maven\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003eagent\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ekubernetes\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"n\"\u003eyaml\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                apiVersion: v1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                kind: Pod\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                metadata:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                  labels:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    app: jenkins-agent\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                spec:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                  containers:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                  - name: maven\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    image: maven:alpine\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    command:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    - cat\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    tty: true\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                  - name: busybox\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    image: busybox\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    command:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    - cat\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    tty: true\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003esteps\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtainer\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;maven\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;mvn -version\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}}}}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e笔者推荐使用申明式。yaml配置部分看起来并不优雅，这是另一个话题。咱们今后再讲。\u003c/p\u003e","title":"Jenkins kubernetes插件的原理"},{"content":" 完整代码在文末\n背景 前段时间，线上系统出现了两次持续时间比较长的事故。这两次事故暴露我在某些方面的不足。同时，也意识到在SRE这个领域，经验的重要性。\n事故过程中，我们发现大量的FullGC。当时，我们想到了要dump内存出来分析，可惜发现没有加-XX:HeapDumpPath参数。同时，我们也发现，如果dump出来了，我们也没法拿到dump出来的文件。因为我们的应用是跑在K8s中的。\n方案调研 经复盘，我们得到一个action：在Java应用出现OOM时，将内存dump出来，并持久化，并且方便分析。\n这个action可以细分为两个任务：\nOOM时，dump内存出来； 提供一种途径方便分析。 经过权衡，任务2的优先级是可以降低的。puvad只要把任务1做好就可以。所以，这两个任务最终变成：在Java应用出现OOM时，将内存dump到NAS中。\n笔者在网上搜索一通，看到的方案基本就是启动一个sidecar容器，与应用共享一个目录。然后监控这个目录，发现内容就上传到s3这类对象存储中。\n这种方案的问题在于：\nsidecar在传输过程，有出现问题的风险； 为了OOM这个小概率事件启动一个sidecar，资源有点浪费。 个人觉得，Java应用的Pod应该只负责将OOM时的内存dump到NAS即可，其它事情应该由其它Pod完成。\n具体实现 以下方案是基于Helm自动化部署。如果你使用的是其它自动化部署工具，思路大体相同。\n准备NFS服务 这部分不是本文范畴。\nJava应用启动时参数配置 在Dockerfile中必须将变量$JAVA_OPTS加入到启动参数中。\nFROM openjdk:11.0.12-jre-buster COPY target/app.jar /app.jar CMD java -jar $JAVA_OPTS /app.jar 加入InitContainers 作用：创建符合指定规则的Dump目录（注意DUMP_FOLDER变量的定义）。如下代码，在init容器启动后，它会创建目录：/nfs/dump/default/jvm-oom-example/10.233.66.38 。在应用出现OOM，内存文件会被dump在此目录下。\ninitContainers: - name: init image: registry.cn-shenzhen.aliyuncs.com/aliacs-app-catalog/busybox:1.30.1 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo $DUMP_FOLDER;mkdir -p $DUMP_FOLDER\u0026#39;] {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} {{- end }} env: - name: MY_NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: MY_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: MY_POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: DUMP_FOLDER value: \u0026#34;/nfs/dump/$(MY_POD_NAMESPACE)/{{ include \u0026#34;app.fullname\u0026#34; . }}/$(MY_POD_IP)\u0026#34; 配置应用容器 我们要做的，其实就是设置JAVA_OPTS环境变量。这里要注意的是JAVA_OPTS可以由三部分组成的：\n内存大小设置，比如：-Xmx640M Xms640M 这类； GC算法设置，比如：-XX:+UseSerialGC JVM日志设置，比如：-XX:ErrorFile=/dump/hs_err_pid%p.log -XX:HeapDumpPath=/dump。 1,2部分应该是由用户决定。第3部分是由平台决定的。\n所以，我们的配置JAVA_OPTS分成两部分：DUMP_ARGS 和 用户的JVM配置。代码如下：\ncontainers: - name: {{ .Chart.Name }} {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} {{- end }} env: - name: MY_NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: MY_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: MY_POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: DUMP_FOLDER value: \u0026#34;/nfs/dump/$(MY_POD_NAMESPACE)/{{ include \u0026#34;app.fullname\u0026#34; . }}/$(MY_POD_IP)\u0026#34; - name: DUMP_ARGS value: \u0026#34;-XX:ErrorFile=$(DUMP_FOLDER)/hs_err_pid.log -XX:HeapDumpPath=$(DUMP_FOLDER) -XX:+HeapDumpOnOutOfMemoryError\u0026#34; - name: JAVA_OPTS value: \u0026#34;{{.Values.javaOpts}} $(DUMP_ARGS)\u0026#34; 最终效果 [vagrant@k8s-3 jvm-oom]$ pwd /persistentvolumes/dump/default/jvm-oom [vagrant@k8s-3 jvm-oom]$ tree . ├── 10.233.66.37 └── 10.233.66.38 └── java_pid6.hprof 小结 这个方案并不是没有缺点。比如每次Pod启动都会创建一个目录，不论是否出现OOM。当然，这个缺点的解决方案也很简单，另启一个Pod负责清理就好了。\n完成代码地址：https://github.com/zacker330/jvm-oom-example\n","permalink":"https://showme.codes/zh-cn/2024-2-26-jvm-oom-kubernetes/","summary":"\u003cblockquote\u003e\n\u003cp\u003e完整代码在文末\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"背景\"\u003e背景\u003c/h3\u003e\n\u003cp\u003e前段时间，线上系统出现了两次持续时间比较长的事故。这两次事故暴露我在某些方面的不足。同时，也意识到在SRE这个领域，经验的重要性。\u003c/p\u003e\n\u003cp\u003e事故过程中，我们发现大量的FullGC。当时，我们想到了要dump内存出来分析，可惜发现没有加\u003ccode\u003e-XX:HeapDumpPath\u003c/code\u003e参数。同时，我们也发现，如果dump出来了，我们也没法拿到dump出来的文件。因为我们的应用是跑在K8s中的。\u003c/p\u003e\n\u003ch3 id=\"方案调研\"\u003e方案调研\u003c/h3\u003e\n\u003cp\u003e经复盘，我们得到一个action：在Java应用出现OOM时，将内存dump出来，并持久化，并且方便分析。\u003c/p\u003e\n\u003cp\u003e这个action可以细分为两个任务：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eOOM时，dump内存出来；\u003c/li\u003e\n\u003cli\u003e提供一种途径方便分析。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e经过权衡，任务2的优先级是可以降低的。puvad只要把任务1做好就可以。所以，这两个任务最终变成：在Java应用出现OOM时，将内存dump到NAS中。\u003c/p\u003e\n\u003cp\u003e笔者在网上搜索一通，看到的方案基本就是启动一个sidecar容器，与应用共享一个目录。然后监控这个目录，发现内容就上传到s3这类对象存储中。\u003c/p\u003e\n\u003cp\u003e这种方案的问题在于：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003esidecar在传输过程，有出现问题的风险；\u003c/li\u003e\n\u003cli\u003e为了OOM这个小概率事件启动一个sidecar，资源有点浪费。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e个人觉得，Java应用的Pod应该只负责将OOM时的内存dump到NAS即可，其它事情应该由其它Pod完成。\u003c/p\u003e\n\u003ch3 id=\"具体实现\"\u003e具体实现\u003c/h3\u003e\n\u003cp\u003e以下方案是基于Helm自动化部署。如果你使用的是其它自动化部署工具，思路大体相同。\u003c/p\u003e\n\u003ch4 id=\"准备nfs服务\"\u003e准备NFS服务\u003c/h4\u003e\n\u003cp\u003e这部分不是本文范畴。\u003c/p\u003e\n\u003ch4 id=\"java应用启动时参数配置\"\u003eJava应用启动时参数配置\u003c/h4\u003e\n\u003cp\u003e在Dockerfile中必须将变量$JAVA_OPTS加入到启动参数中。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eFROM openjdk:11.0.12-jre-buster\nCOPY target/app.jar /app.jar\nCMD java -jar $JAVA_OPTS /app.jar\n\u003c/code\u003e\u003c/pre\u003e\u003ch4 id=\"加入initcontainers\"\u003e加入InitContainers\u003c/h4\u003e\n\u003cp\u003e作用：创建符合指定规则的Dump目录（注意DUMP_FOLDER变量的定义）。如下代码，在init容器启动后，它会创建目录：/nfs/dump/default/jvm-oom-example/10.233.66.38 。在应用出现OOM，内存文件会被dump在此目录下。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003einitContainers:\n- name: init\n  image: registry.cn-shenzhen.aliyuncs.com/aliacs-app-catalog/busybox:1.30.1\n  command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo $DUMP_FOLDER;mkdir -p $DUMP_FOLDER\u0026#39;]\n  {{- with .Values.volumeMounts }}\n  volumeMounts:\n  {{- toYaml . | nindent 12 }}\n  {{- end }}\n  env:\n  - name: MY_NODE_NAME\n      valueFrom:\n      fieldRef:\n          fieldPath: spec.nodeName\n  - name: MY_POD_NAME\n      valueFrom:\n      fieldRef:\n          fieldPath: metadata.name\n  - name: MY_POD_NAMESPACE\n      valueFrom:\n      fieldRef:\n          fieldPath: metadata.namespace\n  - name: MY_POD_IP\n      valueFrom:\n      fieldRef:\n          fieldPath: status.podIP\n  - name: DUMP_FOLDER\n      value: \u0026#34;/nfs/dump/$(MY_POD_NAMESPACE)/{{ include \u0026#34;app.fullname\u0026#34; . }}/$(MY_POD_IP)\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003ch4 id=\"配置应用容器\"\u003e配置应用容器\u003c/h4\u003e\n\u003cp\u003e我们要做的，其实就是设置JAVA_OPTS环境变量。这里要注意的是JAVA_OPTS可以由三部分组成的：\u003c/p\u003e","title":"K8s工程化：K8s中的Java应用出现OOM后怎么办？"},{"content":"“本质”类的文章，通常很难带流量。而且写起来非常吃力。\n那我为什么还要写？写作是对自己的锻炼。写作是让自己的思想更有深度的一种有效方式。\n如果你觉得这篇文章对你有帮助，也你麻烦你转发这篇文章，这是对我的帮助。谢谢。\nKubernetes 的包管理器的本质 “Helm 是 Kubernetes 的包管理器”。Helm的官方网站如是说。\n那什么是“Kubernetes 的包管理器”？\n我们假设需要在没包管理器的场景下部署资源，你需要一个个文件手工地执行kubectl apply -f abc.yaml，abc.yaml就是Kubernetes的资源的定义文件。\n文件内容如下：\n--- apiVersion: apps/v1 kind: Deployment metadata: name: abc labels: app.kubernetes.io/name: abc spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: abc 当需要卸载资源呢？你又需要手工执行kubectl delete -f abc.yaml。\n所以每次发布，你都必须有一个发布记录，记录下哪些YAML要执行apply，哪些yaml要执行delete。而且delete后，你还要记得将那个文件从文件夹中删除。\n如果每次手工执行，工作量大不说，还很容易出错。所以，有人会想到使用Shell脚本或者Python脚本来解决这些问题。\n当你通过Shell脚本或者Python脚本能自动化解决以上问题时，实际上就等于实现了一个Kubernetes 的包管理器。\n当我们真正理解以上所说的Kubernetes资源的部署问题后，你就明白了Kubernetes 的包管理器其实就两个核心功能：\n自动化执行Kubenetes资源更新； 跟踪Kubenetes资源更新记录（本质还是版本化）。 我们在选择包管理器时，务必要从这两个角度考虑。像Grafana公司Tanka，并不是一开始就实现“跟踪Kubenetes资源更新记录”功能，具体可以看：https://github.com/grafana/tanka/issues/88 。\nHelm是如何实现包管理的 注：本文讲的是Helm3。Helm2与Helm3存在较大差异。\nHelm的包：Chart 假如存在一个微服务x，我们将其部署到Kubernetes中，需要准备Deployment、HPA、Service的这三种资源的YAML文件。这三个文件，统一放在一个文件夹中。\nHelm本身是一个命令行工具。通过package子命令，可以将整个文件夹打包成一个tgz的压缩包。打包命令为：helm package x-service --version 1.0 。打包结果是一个tgz包。如下图： 这个tgz包，我们称之为Chart包。本质上它就是Kubernetes的资源文件的一个集合。\n我们可以将Chart包上传到Nexus这类制品管理工具进行版本化控制。这涉及到Chart的管理的工程实践，不在本文范围。\n在有了Chart包以后，我们可以通过命令helm install \u0026lt;release\u0026gt; \u0026lt;chart路径\u0026gt;将svc安装到指定的Kubernetes集群上。如x-svc的部署指令将会是：helm install x-svc ./x-svc-1.0.tgz。\nrelease是Helm的一个概念，即发布名。每执行一次helm install，对于Helm来说就是创建一个release。通常我们使用应用名作为发布名。\nrelease这个概念在资源变更跟踪中环节非常重要。后面会反复使用此概念。\n使用模板技术解决Chart的规模性问题 实际工作中，我们还会有y-svc、z-svc……n个服务。我们是不是每个服务要创建一个Chart？另外，每个服务都将被部署到三个环境中，那么，是不是每个环境还要单独又创建一个Chart？最终，我们需要服务与环境两个维度进行排列组合个Chart。\n如果不能很好解决这个问题。Chart的数量会爆炸式增长。Helm如何解决这个问题呢？\n它通过模板技术解决。换句话说，就是将Chart中资源文件中的容易变化的部分配置抽离出来变成变量，不变的部分变成模板。\n变量部分配置统一放在Chart包中的values.yaml文件中。所以，这部分配置，我们通常也称为values配置，或者values文件。\n这样，我们的Chart包的结构就变成如下（实际还有一些别的文件，但是不是本文讨论范围）：\n对于Chart中不变的部分，Helm使用gotemplate模板语言进行描述。就是说我们可以在deployments.yaml中直接写gotemplate模板语言了，如代码1：\napiVersion: apps/v1 kind: Deployment metadata: name: {{ .Values.name }} labels: {{- include \u0026#34;demo.labels\u0026#34; . | nindent 4 }} ...篇幅有限，省略 resources: {{- toYaml .Values.resources | nindent 12 }} 我们无意挑起模板语言的战争，我们对gotemplate没有好感。你需要小心翼翼地去维护模板文件中的空格数量。如代码1的最后一行，它的意思是指该YAML块缩进12个空格。\n关键问题我们该如何确定它应该是12，而不是10呢？而且，如果重构这部分代码，我又要重新算一次空格的数量！\n使用gotemplate作为它的模板语言是它的最大错误。我们可能需要另外写一篇文章介绍规避这个问题的方法。\n在写Helm的gotemplate模板时，建议不要写太复杂的逻辑，代码宁可重复，甚至另创建一个新的Chart。\n执行资源变更 当values与Chart都已经准备好之后，我们通过以下命令，即可将x-svc的所有的资源部署到指定的namespace中：\nhelm install x-svc ./svc-chart-1.0.tgz -f x-svc-value.yaml 注意，一个不存在的服务，首次部署时是要执行install子命令。将来更新时，就只能执行upgrade子命令了。\n以此类推，y-svc的部署命令就是：\nhelm install y-svc ./svc-chart-1.0.tgz -f y-svc-value.yaml 在执行install成功后，如果你需要修改该release，你需要执行upgrade指令，如下：\nhelm upgrade y-svc ./svc-chart-2.0.tgz -f y-svc-value.yaml 但是，helm是如何知道是要执行创建/变更资源，还是要执行删除资源呢？svc-chart-2.0.tgz比1.0版本可能少了deployment资源。\n这就涉及到资源变更的跟踪了。\n资源变更跟踪 在介绍“资源变更跟踪”前，我们先介绍几个重要的相关子命令：\nupgrade：更新已存在的release。如：helm upgrade y-svc ./svc-chart-1.0.tgz -f y-svc-value.yaml； list：列出所有的已经安装的release； rollback: 将指定的release进行回滚。甚至可以指定回滚到某个版本，命令：helm rollback \u0026lt;RELEASE\u0026gt; [REVISION] [flags]； history: 列出release的发布记录。 有些同学可能发现问题了：执行helm命令时，即没有默认的，也没有显示指定的release持久化方式，这些release信息是记录在哪里的？\n同时，这又与我们上文说的“资源变更跟踪”有什么关系？\n它们是相关的。Helm的核心原理就在此：\n当首次部署时，使用install，这时，Helm会直接在指定命名空间（默认是default）下，创建一个helm.sh/release类型的secret。secret的名称定义为：sh.helm.release.v1.release.v1。secret的内容是这次执行的所有的Kubernetes资源的YAML内容。 当使用upgrade更新时，Helm从sh.helm.release.v1.release.v1的secret取出所有的YAML资源内容与本次将要执行更新的YAML资源内容进行对比，计算出本次更新需要执行的操作，是删除，变更。源码：https://github.com/helm/helm/blob/main/pkg/action/upgrade.go#L286 当upgrade执行成功，Helm会创建名为sh.helm.release.v1.release.v2的secret。当你看到这个v2的时候，你就已经知道了。Helm是通过结合secret的名称约定和secret的内容来记录下每一次发布的。当下次upgrade时，Helm会取v2的secret，然后执行更新，并创建v3的secret。以此类推。 为了展示的更友好，Helm把这些底层都隐藏下来了，所以，当你执行history指令时，你看到的将是：\n截图取自Helm官网\n至此，整个Helm的本质，已经介绍完。剩下细节可以通过查文档学习了。\n小结 虽然本文标题写的是Helm的本质，其实写的是Kubernetes的包管理器的本质：\n自动化执行Kubenetes资源更新； 跟踪Kubenetes资源更新记录（本质还是版本化）。 你可以拿这两次去评估Kustomize或者另的包管理工具。\n","permalink":"https://showme.codes/zh-cn/2024-2-26-theory-of-helm/","summary":"\u003cp\u003e“本质”类的文章，通常很难带流量。而且写起来非常吃力。\u003c/p\u003e\n\u003cp\u003e那我为什么还要写？写作是对自己的锻炼。写作是让自己的思想更有深度的一种有效方式。\u003c/p\u003e\n\u003cp\u003e如果你觉得这篇文章对你有帮助，也你麻烦你转发这篇文章，这是对我的帮助。谢谢。\u003c/p\u003e\n\u003ch2 id=\"kubernetes-的包管理器的本质\"\u003eKubernetes 的包管理器的本质\u003c/h2\u003e\n\u003cp\u003e“Helm 是 Kubernetes 的包管理器”。Helm的官方网站如是说。\u003c/p\u003e\n\u003cp\u003e那什么是“Kubernetes 的包管理器”？\u003c/p\u003e\n\u003cp\u003e我们假设需要在没包管理器的场景下部署资源，你需要一个个文件手工地执行\u003ccode\u003ekubectl apply -f abc.yaml\u003c/code\u003e，abc.yaml就是Kubernetes的资源的定义文件。\u003c/p\u003e\n\u003cp\u003e文件内容如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nn\"\u003e---\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eapiVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eapps/v1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ekind\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDeployment\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eabc\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nt\"\u003elabels\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\t\u003c/span\u003e\u003cspan class=\"nt\"\u003eapp.kubernetes.io/name\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eabc\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nt\"\u003ereplicas\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nt\"\u003eselector\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\t\u003c/span\u003e\u003cspan class=\"nt\"\u003ematchLabels\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\t\t\u003c/span\u003e\u003cspan class=\"nt\"\u003eapp.kubernetes.io/name\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eabc\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e当需要卸载资源呢？你又需要手工执行\u003ccode\u003ekubectl delete -f abc.yaml\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e所以每次发布，你都必须有一个发布记录，记录下哪些YAML要执行apply，哪些yaml要执行delete。而且delete后，你还要记得将那个文件从文件夹中删除。\u003c/p\u003e\n\u003cp\u003e如果每次手工执行，工作量大不说，还很容易出错。所以，有人会想到使用Shell脚本或者Python脚本来解决这些问题。\u003c/p\u003e\n\u003cp\u003e当你通过Shell脚本或者Python脚本能自动化解决以上问题时，实际上就等于实现了一个Kubernetes 的包管理器。\u003c/p\u003e\n\u003cp\u003e当我们真正理解以上所说的Kubernetes资源的部署问题后，你就明白了Kubernetes 的包管理器其实就两个核心功能：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e自动化执行Kubenetes资源更新；\u003c/li\u003e\n\u003cli\u003e跟踪Kubenetes资源更新记录（本质还是版本化）。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我们在选择包管理器时，务必要从这两个角度考虑。像Grafana公司Tanka，并不是一开始就实现“跟踪Kubenetes资源更新记录”功能，具体可以看：https://github.com/grafana/tanka/issues/88 。\u003c/p\u003e\n\u003ch2 id=\"helm是如何实现包管理的\"\u003eHelm是如何实现包管理的\u003c/h2\u003e\n\u003cp\u003e注：本文讲的是Helm3。Helm2与Helm3存在较大差异。\u003c/p\u003e\n\u003ch3 id=\"helm的包chart\"\u003eHelm的包：Chart\u003c/h3\u003e\n\u003cp\u003e假如存在一个微服务x，我们将其部署到Kubernetes中，需要准备Deployment、HPA、Service的这三种资源的YAML文件。这三个文件，统一放在一个文件夹中。\u003c/p\u003e\n\u003cp\u003eHelm本身是一个命令行工具。通过package子命令，可以将整个文件夹打包成一个tgz的压缩包。打包命令为：\u003ccode\u003ehelm package x-service --version 1.0\u003c/code\u003e 。打包结果是一个tgz包。如下图：\n\u003cimg alt=\"Pasted image 20221213140013.png\" loading=\"lazy\" src=\"https://upload-images.jianshu.io/upload_images/292372-c272b0d600a1417f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\"\u003e\u003c/p\u003e\n\u003cp\u003e这个tgz包，我们称之为Chart包。本质上它就是Kubernetes的资源文件的一个集合。\u003c/p\u003e\n\u003cp\u003e我们可以将Chart包上传到Nexus这类制品管理工具进行版本化控制。这涉及到Chart的管理的工程实践，不在本文范围。\u003c/p\u003e\n\u003cp\u003e在有了Chart包以后，我们可以通过命令\u003ccode\u003ehelm install \u0026lt;release\u0026gt; \u0026lt;chart路径\u0026gt;\u003c/code\u003e将svc安装到指定的Kubernetes集群上。如x-svc的部署指令将会是：\u003ccode\u003ehelm install x-svc ./x-svc-1.0.tgz\u003c/code\u003e。\u003c/p\u003e","title":"Kubernetes包管理器Helm的本质"},{"content":"背景 Prometheus有两个最基本的组件：一个是Prometheus程序，一个是Alertmanager程序。\n它们的职责分工很明确：\nPrometheus程序负责：定时拉取监控指标数据、存储指标数据、根据告警规则发起告警通知； Alertmanager程序负责：负责告警通知的路由，即当接收到Prometheus程序的通知后，该将通知以何种方式通知给谁。 Prometheus程序的配置最核心的配置是：\n# ... # 当指标数据符合什么规则进行告警通知。 # 在其它文件定义，这里只是引用该文件的路径。 rule_files: [ - \u0026lt;filepath_glob\u0026gt; ... ] # 从哪里，该如何拉取指标 scrape_configs: [ - \u0026lt;scrape_config\u0026gt; ... ] # ... Alertmanager程序的配置最核心的配置是：\n# ... # 告警通知路由规则 route: [- \u0026lt;route_config\u0026gt;-] # 告警通知的接收者列表，部分监控告警平台也称之为channel receivers: [- \u0026lt;receivers\u0026gt;-] # ... 在实际工作中，Prometheus和Alertmanager的配置会非常大。\n严谨的软件工程要求我们在真正部署这些配置前，对其进行有效性和正确性的检查。否则SRE/DevOps的工程效率就会很低，因为你需要手工调试庞大的配置。\n所以，我们需要有一种高效率的方式来保证配置的有效性和正确性。\n保证Prometheus程序配置的有效性和正确性 promtool Prometheus程序提供了一个叫promtool的命令行程序。解压Prometheus的程序包后，你会发现它和Prometheus程序放在一个文件夹中。\npromtool提供了一些子命令来保证Prometheus程序配置的有效性和正确性：\n# 校验Prometheus配置的有效性，它支持--lint=\u0026#34;duplicate-rules\u0026#34;参数，用于检查重复的rule配置 check config [\u0026lt;flags\u0026gt;] \u0026lt;config-files\u0026gt;... # 校验rule配置的有效性 check rules [\u0026lt;flags\u0026gt;] \u0026lt;rule-files\u0026gt;... # 执行rules单元测试用例 test rules \u0026lt;test-rule-file\u0026gt;... 至于有效性检查，只需要执行check子命令即可，不需要过多说明。\npromtool的test rules子命令可以实现rule配置的单元测试，具体命令如下：\n./promtool test rules test.yml test.yaml是单元测试描述文件。promtool是支持同时指定多个单元测试文件的，如：./promtool test rules test.yml test1.yml test2.yml\n单元测试描述文件内容的格式如下：\n# Prometheus的rule配置文件路径 rule_files: - rule1.yml # 评估的间隔时长 evaluation_interval: 1m # 单元测试列表 tests: - interval: 1m input_series: alert_rule_test: promql_expr_test: tests下的每个用例由4个字段组成：\ninput_series：测试用例的测试数据，即指标的时序数据； interval：代表每个时序数据之间的间隔时长； alert_rule_test：告警规则的测试用例； promql_expr_test：promql表达式的测试用例。我们可以使用它进行调试我们的promsql。 接下来将详细介绍它们。\n测试用例数据 测试用例数据的定义格式如下：\ninput_series: - series: \u0026#39;up{job=\u0026#34;prometheus\u0026#34;, instance=\u0026#34;localhost:9090\u0026#34;}\u0026#39; values: \u0026#39;0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\u0026#39; - series: \u0026#39;up{job=\u0026#34;node_exporter\u0026#34;, instance=\u0026#34;localhost:9100\u0026#34;}\u0026#39; values: \u0026#39;1+0x6 0 0 0 0 0 0 0 0\u0026#39; - series: \u0026#39;go_goroutines{job=\u0026#34;prometheus\u0026#34;, instance=\u0026#34;localhost:9090\u0026#34;}\u0026#39; values: \u0026#39;10+10x2 30+20x5\u0026#39; - series: \u0026#39;go_goroutines{job=\u0026#34;node_exporter\u0026#34;, instance=\u0026#34;localhost:9100\u0026#34;}\u0026#39; values: \u0026#39;10+10x7 10+30x4\u0026#39; 每一条input_serie由两个字段组成：\nseries：指标时序数据的key values：指标的value。其中的每一个值之间的间隔时长是interval的值 为了简化values的值的定义，你可以一种扩展符号来定义其值。语法如下：\na+bxc代表：a a+b a+(2*b) a+(3*b) … a+(c*b)；这一个a值是起始值的序列，然后a以b的(0..c)的倍数进行递增； a-bxc表示：这一个a值是起始值的序列，然后a以b的(0..c)的倍数进行递减； _下划线表示：序列中的某次指标值没有被抓取到； stale表示：过期的样本数据。 以下是一些来自官方文档的例子： -2+4x3表示：-2 2 6 10 1-2x4表示：1 -1 -3 -5 -7 1x4表示：1 1 1 1 1 1 _x3 stale表示：1 _ _ _ stale 1+0x6 0 0 0 0 0 0 0 0表示： 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 10+10x2 30+20x5表示：10 20 30 30 50 70 90 110 130 测试Promsql表达式 在写Prometheus告警规则时，一个很大的痛点就是无法简单的验证自己写的promsql的正确性。我们在告警规则的单元测试中验证后，再配置到真正的rule文件中。\npromsql的测试用例的写法如下：\npromql_expr_test: - expr: go_goroutines \u0026gt; 5 # 要测试的promsql表达式，它将会从测试数据中查询数据 eval_time: 4m # 评估时长。从测试数据的第0秒开始算。 # 如果interval是1m，那么4m代表的是测试数据的第4个数值 exp_samples: # 执行promsql表达式后，预期得到的数据结果 - labels: \u0026#39;go_goroutines{job=\u0026#34;prometheus\u0026#34;,instance=\u0026#34;localhost:9090\u0026#34;}\u0026#39; value: 50 - labels: \u0026#39;go_goroutines{job=\u0026#34;node_exporter\u0026#34;,instance=\u0026#34;localhost:9100\u0026#34;}\u0026#39; value: 50 告警规则的单元测试 在单元测试描述文件中，添加Promsql表达式的测试用例的同时，我们还可以添加告警规则的测试用例，代码样例如下：\nalert_rule_test: - eval_time: 10m alertname: InstanceDown exp_alerts: - exp_labels: severity: page instance: localhost:9090 job: prometheus exp_annotations: summary: \u0026#34;Instance localhost:9090 down\u0026#34; description: \u0026#34;localhost:9090 of job prometheus has been down for more than 5 minutes.\u0026#34; eval_time：规则评估时长； alertname：告警名，要求与Prometheus的告警规则中的alertname一致； exp_labels：预期收到的告警通知中的label值； exp_annotations：预期收到的告警通知中的annotation值。 保证Alertmanager程序配置的有效性和正确性 amtool 与Prometheus程序类似，Alertmanager程序提供了一个叫amtool的命令行程序。\n我们关注它的两个子命令：\nconfig routes test：验证配置的正确性 check-config \u0026lt;config.yaml\u0026gt;：验证配置的有效性 config routes test子命令介绍 amtool不像promtool那样支持在YAML文件中定义测试用例，以下是它的命令样例： amtool config routes test --config.file=config.yaml --verify.receivers=team-X-pager service=database owner=team-X\n--config.file参数指定了配置文件的路径。\n除了支持指定配置的路径，还可以通过参数--alertmanager.url指定使用某个运行中的Alertmanager的配置。\n--verify.receivers指定期望返回的receiver列表，使用逗号分隔。\n该子命令的最后是标签集，由key=value的格式组成，并使用空格分隔。例子中service=database owner=team-X，代表的是{service=\u0026quot;database\u0026quot;,owner=\u0026quot;team-X\u0026quot;}\n为了更好的可视化，还可以加一个--tree的参数，效果如下：\n% amtool config routes test --config.file=config.yaml --tree --verify.receivers=team-X-pager service=database owner=team-X Matching routes: . └── default-route └── {service=\u0026#34;database\u0026#34;} └── {owner=\u0026#34;team-X\u0026#34;} receiver: team-X-pager 如果验证失败，该命令返回非0结果。\ncheck-config子命令介绍 它的运行效果如下：\n% amtool check-config config.yaml Checking \u0026#39;config.yaml\u0026#39; SUCCESS Found: - global config - route - 1 inhibit rules - 5 receivers - 1 templates SUCCESS 如果验证失败，该命令返回非0结果。\n可视化告警通知路由 Prometheus官网提供了一个告警通知路由的在线可视化编辑器。\n将配置粘贴至编辑框中，然后在“Match Label Set”中输入告警的标签，最后下方会显示通知的路由路径。如下图，实心红点即是匹配了该label的receiver：\npro\n可视化工具在路由配置调试阶段非常有用。减小了路由配置的难度。但是，需要注意：不要将任何敏感配置上传到公网。\n如何集成到CI/CD Pipeline中 以上介绍的是两个命令最原始的使用方法，即手工运行。我们需要将其集成到CI/CD pipeline中，以实现工程化。\n集成方式一般有两：\n在Pipeline中增加一个执行promtool和amtool的阶段 集成构建工具中，比如集成到Bazel中。 当然有一些DevOps平台如果需要深度集成，可以将promtool的amtool的实现代码引入到自己的DevOps平台的代码中。\n工程化的挑战 另一个工程化的挑战，就是以上的配置文件之间存在引用，如下prometheus的rule文件中的expr字段的值，实际上是被prometheus-unitesting.yml文件引用。\n如果不对这个引用关系进行治理，这些配置的维护成本将会非常高。\n由于YAML文件天生不具备变量定义的功能。可以采用类似Jsonnet、CUE这样的支持编程的配置语言代替YAML。\n这个话题比较大，不在本文讨论范围。\n","permalink":"https://showme.codes/zh-cn/2024-2-26-prometheus-engineering/","summary":"\u003ch1 id=\"背景\"\u003e背景\u003c/h1\u003e\n\u003cp\u003ePrometheus有两个最基本的组件：一个是Prometheus程序，一个是Alertmanager程序。\u003c/p\u003e\n\u003cp\u003e它们的职责分工很明确：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePrometheus程序负责：定时拉取监控指标数据、存储指标数据、根据告警规则发起告警通知；\u003c/li\u003e\n\u003cli\u003eAlertmanager程序负责：负责告警通知的路由，即当接收到Prometheus程序的通知后，该将通知以何种方式通知给谁。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/prometheus-config-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003ePrometheus程序的配置最核心的\u003ca href=\"https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration\"\u003e配置\u003c/a\u003e是：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# ... \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 当指标数据符合什么规则进行告警通知。\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 在其它文件定义，这里只是引用该文件的路径。\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003erule_files\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e- \u003cspan class=\"l\"\u003e\u0026lt;filepath_glob\u0026gt; ... ]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 从哪里，该如何拉取指标\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003escrape_configs\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e- \u003cspan class=\"l\"\u003e\u0026lt;scrape_config\u0026gt; ... ]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# ...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAlertmanager程序的配置最核心的\u003ca href=\"https://prometheus.io/docs/alerting/latest/configuration/#configuration\"\u003e配置\u003c/a\u003e是：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# ...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 告警通知路由规则\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eroute\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e- \u003cspan class=\"l\"\u003e\u0026lt;route_config\u0026gt;-]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 告警通知的接收者列表，部分监控告警平台也称之为channel\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ereceivers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e- \u003cspan class=\"l\"\u003e\u0026lt;receivers\u0026gt;-]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# ...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e在实际工作中，Prometheus和Alertmanager的配置会非常大。\u003c/p\u003e\n\u003cp\u003e严谨的软件工程要求我们在真正部署这些配置前，对其进行有效性和正确性的检查。否则SRE/DevOps的工程效率就会很低，因为你需要手工调试庞大的配置。\u003c/p\u003e\n\u003cp\u003e所以，我们需要有一种高效率的方式来保证配置的有效性和正确性。\u003c/p\u003e\n\u003ch1 id=\"保证prometheus程序配置的有效性和正确性\"\u003e保证Prometheus程序配置的有效性和正确性\u003c/h1\u003e\n\u003ch2 id=\"promtool\"\u003epromtool\u003c/h2\u003e\n\u003cp\u003ePrometheus程序提供了一个叫promtool的命令行程序。解压Prometheus的程序包后，你会发现它和Prometheus程序放在一个文件夹中。\u003c/p\u003e\n\u003cp\u003epromtool提供了一些子命令来保证Prometheus程序配置的有效性和正确性：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 校验Prometheus配置的有效性，它支持--lint=\u0026#34;duplicate-rules\u0026#34;参数，用于检查重复的rule配置\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echeck config \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u0026lt;flags\u0026gt;\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u0026lt;config-files\u0026gt;...\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 校验rule配置的有效性\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echeck rules \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u0026lt;flags\u0026gt;\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u0026lt;rule-files\u0026gt;...\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 执行rules单元测试用例\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003etest\u003c/span\u003e rules \u0026lt;test-rule-file\u0026gt;...\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e至于有效性检查，只需要执行check子命令即可，不需要过多说明。\u003c/p\u003e","title":"SRE-DevOps不得不懂的：Prometheus的配置工程化"},{"content":"\n半年多前，我们从传统的Ansible自动化部署迁移到了云原生部署。我们没有通过Rancher或者KubeSphere这些平台的可视化界面部署，而是选择了Helm这个命令行工具。原因有以下几点：\n坚持一切版本化，一切自动化的原则； Helm在声明式思维方面相对其它工具更友好； 方便配置与制品分离； Helm目前有两个版本：v2和v3。幸运的是，我们正准备大规模使用时，v3版本发布。所以，我们没有经历升级之苦。特此说明以下最佳实践基于Helm3。\n注：本文针对对Helm有一定基础的同学，如果没有基础，可以先收藏。\n正片开始：\n自行版本化chart maven、npm等构建工具的包会有一个唯一的官方源，但是，Helm的chart包似乎没有，你会遇到很多不同的源。这对chart的版本控制非常不利，因为你不知道哪天，远端的源就不见了。所以，最好的做法，使用helm pull命令将chart下载本地，然后指定一个版本上传制品库Nexus的Helm仓库中。上传命令为：\ncurl -s -u '${USER_PASS}' --upload-file ${chart_name}-${charts_version}.tgz http://xxx-nexus.com/repository/helm-repo/ 使用upgrade —install子命令部署应用 刚开始学习Helm时，我们通常使用helm install来安装chart。但是，第二次执行helm install，就会报错，因为K8s中已经存在了该chart的release了。这个过程对流水线是不友好的，所以，在流水线，我们使用的是helm upgrade —install xx ./xx.tgz来部署。\n尽早标准化应用，标准化chart 如果存在100个微服务，我们是不是要创建100个chart呢？事实上，一开始，我们团队就是这样的。这是因为我们的微服务一开始不够标准化，所以，chart也跟着不同。后来，我们逐渐标准化了应用。chart也变成了标准。也就是所有的后端服务使用的是同一个chart。这样做的还有利于提高我们创建新的微服务的速度。\n所谓标准化，指的是pod对外提供服务的端口号、优雅停机、设置环境变量的方法等等这些通用的领域的配置都应该是统一的。\n尽量少使用if-else判断 以chart中，我们应该尽量少使用if-else判断。有时，宁愿多写几个YAML也不要在同一个文件嵌套if-else。因为要尽可能的让chart本身所见即所得。\n使用template子命令快速调试chart 当我们在开始chart时，每次修改都要执行一次helm upgrade来验证正确性是很不经济的。Helm提供了template子命令，用于验证我们的chart的语法的正确性。示例：helm template \u0026lt;chart的地址\u0026gt;。\n定义一个全局的values.yaml chart中的values.yaml文件为我们提供了chart的默认配置。同时，我们可以在执行helm upgrade —install部署chart时，加入-f values.yaml来指定另外的values文件，比如：\nhelm upgrade --install -f ./abc.yaml abc ./abc-chart.tgz 但是，有些配置，是全局性的，比如mysql的url。我们不希望它重复写在不同的应用的配置中。所以，我们定义一个全局的values.yaml。比如：global-value.yaml。helm的命令将变成：\nhelm upgrade --install -f ./global-value.yaml -f ./abc.yaml abc ./abc-chart.tgz 利用helm的-f参数的顺序实现配置的优先级 当全局values文件与应用的values存在配置冲突的时候，通过会采用应用的values文件中的配置。需要注意的是 -f 参数的顺序。后一个 -f 参数的配置会覆盖前一个-f参数的配置。\n多版本的实现 过去，我们通常是一个应用一个版本。但是，现在我们更多的是一个应用线上同时存在多个版本。所以，一个chart能同时部署多个版本的应用。\nhelm upgrade --install -f ./global-value.yaml -f ./abc.yaml --set \u0026#39;image.tag={1.2.1,1.2.3}\u0026#39; abc ./abc-chart.tgz chart中的deployment文件：\n{{/* globle变量缓存全局变量, 遍历tag的同时，再将全局变量变回 */}} {{- $global := . -}} {{- range .Values.image.tag }} {{- $version := . -}} {{- with $global }} --- apiVersion: apps/v1 kind: Deployment metadata: name: {{ include \u0026#34;abc.fullname\u0026#34; . }}-{{ $version }} labels: version: {{ $version }} spec: # 注意此处，我们可以针对不同的版本设置不同的副本数 {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include \u0026#34;abc.selectorLabels\u0026#34; . | nindent 6 }} version: {{ $version }} template: ...省略无关代码... containers: - name: {{ .Chart.Name }} ...省略无关代码... image: \u0026#34;{{ .Values.image.repository }}:{{ $version | default .Chart.AppVersion }}\u0026#34; {{- end }} {{- end }} 所有的内容都通过chart进行部署 也许你会觉得创建一个namespace就是一个命令的事情，不需要使用helm了。但是，基于持续交付的原则——一切版本化，一切自动化——我们强烈建议，任何操作都应该通过helm进行。比如，我们可以专门创建一个管理Kubernetes的chart。这个chart中，我们就可以实现根据配置创建namespace。再说说Istio这个流行的网格服务的框架。它本身提供了，istioctl命令行进行部署。但是，我们还是建议你使用helm的方式进行部署。因为这样，你才能获得更多的可控性。\n","permalink":"https://showme.codes/zh-cn/2024-2-26-helm-best-practice/","summary":"\u003cp\u003e\u003cimg alt=\"helm-best-practice\" loading=\"lazy\" src=\"/assets/images/helm-best-practice.png\"\u003e\u003c/p\u003e\n\u003cp\u003e半年多前，我们从传统的Ansible自动化部署迁移到了云原生部署。我们没有通过Rancher或者KubeSphere这些平台的可视化界面部署，而是选择了Helm这个命令行工具。原因有以下几点：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e坚持一切版本化，一切自动化的原则；\u003c/li\u003e\n\u003cli\u003eHelm在声明式思维方面相对其它工具更友好；\u003c/li\u003e\n\u003cli\u003e方便配置与制品分离；\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eHelm目前有两个版本：v2和v3。幸运的是，我们正准备大规模使用时，v3版本发布。所以，我们没有经历升级之苦。特此说明以下最佳实践基于Helm3。\u003c/p\u003e\n\u003cp\u003e注：本文针对对Helm有一定基础的同学，如果没有基础，可以先收藏。\u003c/p\u003e\n\u003cp\u003e正片开始：\u003c/p\u003e\n\u003ch3 id=\"自行版本化chart\"\u003e自行版本化chart\u003c/h3\u003e\n\u003cp\u003emaven、npm等构建工具的包会有一个唯一的官方源，但是，Helm的chart包似乎没有，你会遇到很多不同的源。这对chart的版本控制非常不利，因为你不知道哪天，远端的源就不见了。所以，最好的做法，使用helm pull命令将chart下载本地，然后指定一个版本上传制品库Nexus的Helm仓库中。上传命令为：\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ecurl -s -u '${USER_PASS}' --upload-file ${chart_name}-${charts_version}.tgz http://xxx-nexus.com/repository/helm-repo/ \u003c/code\u003e\u003c/p\u003e\n\u003ch3 id=\"使用upgrade-install子命令部署应用\"\u003e使用upgrade —install子命令部署应用\u003c/h3\u003e\n\u003cp\u003e刚开始学习Helm时，我们通常使用helm install来安装chart。但是，第二次执行helm install，就会报错，因为K8s中已经存在了该chart的release了。这个过程对流水线是不友好的，所以，在流水线，我们使用的是\u003ccode\u003ehelm upgrade —install xx ./xx.tgz\u003c/code\u003e来部署。\u003c/p\u003e\n\u003ch3 id=\"尽早标准化应用标准化chart\"\u003e尽早标准化应用，标准化chart\u003c/h3\u003e\n\u003cp\u003e如果存在100个微服务，我们是不是要创建100个chart呢？事实上，一开始，我们团队就是这样的。这是因为我们的微服务一开始不够标准化，所以，chart也跟着不同。后来，我们逐渐标准化了应用。chart也变成了标准。也就是所有的后端服务使用的是同一个chart。这样做的还有利于提高我们创建新的微服务的速度。\u003c/p\u003e\n\u003cp\u003e所谓标准化，指的是pod对外提供服务的端口号、优雅停机、设置环境变量的方法等等这些通用的领域的配置都应该是统一的。\u003c/p\u003e\n\u003ch3 id=\"尽量少使用if-else判断\"\u003e尽量少使用if-else判断\u003c/h3\u003e\n\u003cp\u003e以chart中，我们应该尽量少使用if-else判断。有时，宁愿多写几个YAML也不要在同一个文件嵌套if-else。因为要尽可能的让chart本身所见即所得。\u003c/p\u003e\n\u003ch3 id=\"使用template子命令快速调试chart\"\u003e使用template子命令快速调试chart\u003c/h3\u003e\n\u003cp\u003e当我们在开始chart时，每次修改都要执行一次helm upgrade来验证正确性是很不经济的。Helm提供了template子命令，用于验证我们的chart的语法的正确性。示例：helm template \u0026lt;chart的地址\u0026gt;。\u003c/p\u003e\n\u003ch3 id=\"定义一个全局的valuesyaml\"\u003e定义一个全局的values.yaml\u003c/h3\u003e\n\u003cp\u003echart中的values.yaml文件为我们提供了chart的默认配置。同时，我们可以在执行helm upgrade —install部署chart时，加入-f values.yaml来指定另外的values文件，比如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003ehelm upgrade --install -f ./abc.yaml abc ./abc-chart.tgz\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e但是，有些配置，是全局性的，比如mysql的url。我们不希望它重复写在不同的应用的配置中。所以，我们定义一个全局的values.yaml。比如：global-value.yaml。helm的命令将变成：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003ehelm upgrade --install -f ./global-value.yaml -f ./abc.yaml abc ./abc-chart.tgz\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"利用helm的-f参数的顺序实现配置的优先级\"\u003e利用helm的-f参数的顺序实现配置的优先级\u003c/h3\u003e\n\u003cp\u003e当全局values文件与应用的values存在配置冲突的时候，通过会采用应用的values文件中的配置。需要注意的是 -f 参数的顺序。后一个 -f 参数的配置会覆盖前一个-f参数的配置。\u003c/p\u003e\n\u003ch3 id=\"多版本的实现\"\u003e多版本的实现\u003c/h3\u003e\n\u003cp\u003e过去，我们通常是一个应用一个版本。但是，现在我们更多的是一个应用线上同时存在多个版本。所以，一个chart能同时部署多个版本的应用。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003ehelm upgrade --install -f ./global-value.yaml -f ./abc.yaml  --set \u0026#39;image.tag={1.2.1,1.2.3}\u0026#39;  abc ./abc-chart.tgz\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003echart中的deployment文件：\u003c/p\u003e","title":"云原生部署之Helm最佳实践"},{"content":"应用的规范定义是一个权衡的过程，你不能一下把规范定义得太死，太死了导致无法很好的在不同团队推广，最后可能导致规范失去信用。你也不能把规范定义得太泛，导致人们不知道如何下手。\n在经历了传统部署（使用Ansible自动化部署应用到虚拟机）和Kubernetes的部署（使用Helm实现自动化部署）后，我们总结出一套云原生应用规范。它无关语言，无关框架，无关部署方式。\n定义此云原生应用规范，我们有以下几个目的：\n节约人员沟通成本：你不需要像以前那样需要反复的问对方的服务的端口； 节约运维成本：因为应用是标准的，所以，对于所有的应用，只需要使用统一的部署方式、统一的监控方式； 节约开发新应用的成本：根据规范，我们可以搭建各种语言或者框架的工程的脚手架； 以下是规范正文：\n业务端口规范 所有的Pod或部署在虚拟机上的应用要求：\nhttp协议的服务使用8000端口 grpc协议的服务使用9000端口 如果有其它协议可以在此添加 所有的Service：使用80端口\n实践Tips1：遗留工作通常没有统一的端口，我们可以在部署环节通过环境变量来覆盖应用本身的端口来实现统一端口的目的。\n实践Tips2：对于虚拟机上的部署，过去，一台机器上我们常常部署多个应用，所以要求每个应用的端口都不能相同。我们的做法，缩小虚拟机的配置，一个虚拟机只部署一个应用。\n监控端口规范 监控端口统一使用：30000。监控端口与业务端口分离是基于安全的考虑而设计。而且监控端口只允许内部访问。\n提供Prometheus监控接口/private/prom：返回 prometheus标准数据结构； 提供优雅停机接口/private/shutdown：POST请求即代表发起停机操作； 提供健康检查的接口/private/health：http code返回200代表健康； 提供应用ready接口(可以与健康检查接口相同)/private/ready：http code返回200代表ready； 提供实时修改日志级别的接口：/private/loggers 。这个接口，我们可以参考Java的Logback框架的实现。 Docker镜像规范 提供 curl 命令行，因为我们需要使用命令进行优雅机器：curl -XPOST \u0026lt;http://127.0.0.1:30000/private/shutdown; 应用需要提供应用进程的环境配置入口。比如JVM应用需要提供 JAVA_OPTS 的环境变量设置 实践Tips：实际工作中，可创建一些基础镜像方便开发人员使用。\n日志规范 日志要求统一输出到console。输入日志统一使用json结构。json结构中必须包含字段：\n@timestamp: 日志打印时间 thread_name：进程名 level：日志级别 appId: 应用标识，同一个namespace全局唯一 namespace：用于隔离app，租户的功能 env：环境标识 ver：应用版本 msg：帮助debug问题的 traceId: 其实就是traceId 同时，我们建议使用以下通用字段：\nevent：代表事件，建议不要使用带空格的字符串 method：方法名 result：代表执行结果，可以是方法的返回结果，也可以http方法的response req：代表请求参数体 以下是日志示例：\n{\u0026quot;@timestamp\u0026quot;:\u0026quot;2021-07-15T17:24:07.912+08:00\u0026quot;,\u0026quot;userName\u0026quot;:\u0026quot;Foobar\u0026quot;,\u0026quot;thread_name\u0026quot;:\u0026quot;http-nio-8080-exec-150\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;appId\u0026quot;:\u0026quot;UserService\u0026quot;,\u0026quot;env\u0026quot;:\u0026quot;prod\u0026quot;,\u0026quot;ver\u0026quot;:\u0026quot;v1.0-5598\u0026quot;,\u0026quot;event\u0026quot;:\u0026quot;register_user\u0026quot;}\n实践Tips1：在实际工作，我们需要为不同的语言实现符合此日志规范的框架。\n实践Tips2：日志规范需要配合日志处理环节考虑，在日志处理环节没有准备好之前，保持原样是更明智的选择。\n小结 此规范已经在我们团队实践一年多。正在向其它团队延伸的过程。不敢说它是一套面面俱到的规范，但是它是一套能在一些团队进行落地的规范。\n每个人都存在认知不足的情况，我也是人，所以我也不例外。此规范只是版本1.0。将来发现不足，持续改进。\n","permalink":"https://showme.codes/zh-cn/2024-2-26-cloud-native-specification/","summary":"\u003cp\u003e应用的规范定义是一个权衡的过程，你不能一下把规范定义得太死，太死了导致无法很好的在不同团队推广，最后可能导致规范失去信用。你也不能把规范定义得太泛，导致人们不知道如何下手。\u003c/p\u003e\n\u003cp\u003e在经历了传统部署（使用Ansible自动化部署应用到虚拟机）和Kubernetes的部署（使用Helm实现自动化部署）后，我们总结出一套云原生应用规范。它无关语言，无关框架，无关部署方式。\u003c/p\u003e\n\u003cp\u003e定义此云原生应用规范，我们有以下几个目的：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e节约人员沟通成本：你不需要像以前那样需要反复的问对方的服务的端口；\u003c/li\u003e\n\u003cli\u003e节约运维成本：因为应用是标准的，所以，对于所有的应用，只需要使用统一的部署方式、统一的监控方式；\u003c/li\u003e\n\u003cli\u003e节约开发新应用的成本：根据规范，我们可以搭建各种语言或者框架的工程的脚手架；\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e以下是规范正文：\u003c/p\u003e\n\u003ch3 id=\"业务端口规范\"\u003e\u003cstrong\u003e业务端口规范\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e所有的Pod或部署在虚拟机上的应用要求：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ehttp协议的服务使用8000端口\u003c/li\u003e\n\u003cli\u003egrpc协议的服务使用9000端口\u003c/li\u003e\n\u003cli\u003e如果有其它协议可以在此添加\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e所有的Service：使用80端口\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实践Tips1：遗留工作通常没有统一的端口，我们可以在部署环节通过环境变量来覆盖应用本身的端口来实现统一端口的目的。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实践Tips2：对于虚拟机上的部署，过去，一台机器上我们常常部署多个应用，所以要求每个应用的端口都不能相同。我们的做法，缩小虚拟机的配置，一个虚拟机只部署一个应用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"监控端口规范\"\u003e\u003cstrong\u003e监控端口规范\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e监控端口统一使用：30000。监控端口与业务端口分离是基于安全的考虑而设计。而且监控端口只允许内部访问。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e提供Prometheus监控接口\u003ccode\u003e/private/prom\u003c/code\u003e：返回 prometheus标准数据结构；\u003c/li\u003e\n\u003cli\u003e提供优雅停机接口\u003ccode\u003e/private/shutdown\u003c/code\u003e：POST请求即代表发起停机操作；\u003c/li\u003e\n\u003cli\u003e提供健康检查的接口\u003ccode\u003e/private/health\u003c/code\u003e：http code返回200代表健康；\u003c/li\u003e\n\u003cli\u003e提供应用ready接口(可以与健康检查接口相同)\u003ccode\u003e/private/ready\u003c/code\u003e：http code返回200代表ready；\u003c/li\u003e\n\u003cli\u003e提供实时修改日志级别的接口：/private/loggers 。这个接口，我们可以参考Java的Logback框架的实现。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"cloud native specification\" loading=\"lazy\" src=\"/assets/images/cloud-native-specification.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"docker镜像规范\"\u003e\u003cstrong\u003eDocker镜像规范\u003c/strong\u003e\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e提供 curl 命令行，因为我们需要使用命令进行优雅机器：\u003ccode\u003ecurl -XPOST \u0026lt;http://127.0.0.1:30000/private/shutdown\u003c/code\u003e;\u003c/li\u003e\n\u003cli\u003e应用需要提供应用进程的环境配置入口。比如JVM应用需要提供 JAVA_OPTS 的环境变量设置\u003c/li\u003e\n\u003c/ol\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实践Tips：实际工作中，可创建一些基础镜像方便开发人员使用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"日志规范\"\u003e\u003cstrong\u003e日志规范\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e日志要求统一输出到console。输入日志统一使用json结构。json结构中必须包含字段：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e@timestamp: 日志打印时间\u003c/li\u003e\n\u003cli\u003ethread_name：进程名\u003c/li\u003e\n\u003cli\u003elevel：日志级别\u003c/li\u003e\n\u003cli\u003eappId: 应用标识，同一个namespace全局唯一\u003c/li\u003e\n\u003cli\u003enamespace：用于隔离app，租户的功能\u003c/li\u003e\n\u003cli\u003eenv：环境标识\u003c/li\u003e\n\u003cli\u003ever：应用版本\u003c/li\u003e\n\u003cli\u003emsg：帮助debug问题的\u003c/li\u003e\n\u003cli\u003etraceId: 其实就是traceId\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e同时，我们建议使用以下通用字段：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eevent：代表事件，建议不要使用带空格的字符串\u003c/li\u003e\n\u003cli\u003emethod：方法名\u003c/li\u003e\n\u003cli\u003eresult：代表执行结果，可以是方法的返回结果，也可以http方法的response\u003c/li\u003e\n\u003cli\u003ereq：代表请求参数体\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e以下是日志示例：\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e{\u0026quot;@timestamp\u0026quot;:\u0026quot;2021-07-15T17:24:07.912+08:00\u0026quot;,\u0026quot;userName\u0026quot;:\u0026quot;Foobar\u0026quot;,\u0026quot;thread_name\u0026quot;:\u0026quot;http-nio-8080-exec-150\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;appId\u0026quot;:\u0026quot;UserService\u0026quot;,\u0026quot;env\u0026quot;:\u0026quot;prod\u0026quot;,\u0026quot;ver\u0026quot;:\u0026quot;v1.0-5598\u0026quot;,\u0026quot;event\u0026quot;:\u0026quot;register_user\u0026quot;}\u003c/code\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实践Tips1：在实际工作，我们需要为不同的语言实现符合此日志规范的框架。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实践Tips2：日志规范需要配合日志处理环节考虑，在日志处理环节没有准备好之前，保持原样是更明智的选择。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e此规范已经在我们团队实践一年多。正在向其它团队延伸的过程。不敢说它是一套面面俱到的规范，但是它是一套能在一些团队进行落地的规范。\u003c/p\u003e\n\u003cp\u003e每个人都存在认知不足的情况，我也是人，所以我也不例外。此规范只是版本1.0。将来发现不足，持续改进。\u003c/p\u003e","title":"可落地的云原生应用规范"},{"content":"DDD是领域驱动设计的简写。前段时间听群友说行业里少有DDD的代码案例，进而对DDD没有一个感性的认识。我想这是行业里普遍存在的现象吧。所以，我就有了写此文的想法。\n本文开篇介绍了行业里比较普遍的代码风格，接着，我采用DDD风格对其进行修改。\n我无意说服读者要按照我认为的DDD的风格来写代码，只是想告诉大家，这个世界上，还存在另一种代码风格。\n如果各位觉得这样的风格好，可以尝试一下。非常欢迎大家反馈，平时太少人和我交流这些了。\n文章标题说的是“同事的代码”，其实只是为了让此文更具传播，没别的意思。\n如果你觉得此文对你有帮助，麻烦转发。干货好文不易。谢谢。\n本文虽是以Java语言为案例演示，也希望对其它语言的读者朋友有帮助。\n行业里普遍的代码风格，简称A风格 代码结构如下：\n├── domain domain模块被同事认为是用于存放专门和DB打交道的类的地方 - src/main/java/com/xx/domain/account/repository/AbcLoginInfoRepository.java - src/main/java/com/xx/domain/account/AbcLoginInfo.java ├── repository-impl - 包路径太长省略/AbcLoginInfoRepositoryImlp.java ├── server - src/main/java/com/xx/server/login/LoginService.java - src/main/java/com/xx/server/login/LoginController.java - src/main/java/com/xx/server/login/AuthCodeVo.java - src/main/java/com/xx/server/login/UserInfoVo.java - src/main/java/com/xx/config/AbcWebMvcConfigurer.java Server模块 A风格下，整个业务系统的业务逻辑都在此模块中。\nLoginController.java 实现http服务：\n@Controller @RequestMapping public class LoginController { @Autowired LoginService loginService; // 省略一些不重要的代码 @GetMapping(value = \u0026#34;/login\u0026#34;) @ResponseBody public UserInfoVo login(String code) throws IOException { UserInfoVo userInfoVo = loginService.login(code, httpServletResponse); httpServletResponse.sendRedirect(\u0026#34;/\u0026#34;); return userInfoVo; } @GetMapping(value = \u0026#34;/logout\u0026#34;) @ResponseBody public boolean logout() { return loginService.logout(httpServletRequest,httpServletResponse); } } UserInfoVo.java是返回给前端的用户信息的结构体：\npublic class UserInfoVo { private String id; private String userType; // 省略一些其它字段 // 省略一些getter setter方法 } AuthCodeVo.java是用于存储一些认证过程中的数据的结构体\npublic class AuthCodeVo { private String token; private Integer expiresIn; // 省略一些getter setter方法 } A风格的特点是：除了VO，行业里，还有各种O，如PO、DTO、DO。\n刚入行的小伙伴很难分清各种O，所以，只有跟着前辈的老代码依葫芦画瓢。进而导致大家对于Java代码的印象：不就是各种O之间的转换嘛。\n这里并不是说DDD风格下的代码没有O。在DDD风格下，O本身是有业务逻辑方法的，并不只是一堆字段、getter和setter方法。\nAbcWebMvcConfigurer.java这个类用于实现对所有的请求的拦截，以实现统一认证：\n@Configuration public class AbcWebMvcConfigurer implements WebMvcConfigurer { @Autowired LoginService loginService; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserAuthInterceptorRegistry()) // 省略代码 } class UserAuthInterceptorRegistry implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (loginService.isLoginSuccess(request)) { return true; } response.sendRedirect(\u0026#34;登录页面的url\u0026#34;); return false; } } } AbcLoginInfo.java，AbcLoginInfoRepository.java，AbcLoginInfoRepositoryImlp.java 三个文件实现了登录信息的存储。其中AbcLoginInfo.java只是用户的信息及getter和setter方法，典型的贫血型模型。AbcLoginInfoRepository是AbcLoginInfo对象的持久化接口，而AbcLoginInfoRepositoryImlp是该接口的实现。\n登录逻辑LoginService 这个就是登录服务直接实现逻辑所在。源代码将近200行代码\n@Service public class LoginService { // 省略一些不重要的代码 public UserInfoVo login(String code, HttpServletResponse response) { AuthCodeVo authCodeVo = authCode(code); // 省略部分代码 UserInfoVo userInfoVo = getUserInfo(authCodeVo.getAccessToken(), authCodeVo.getExpiresIn()); // 省略部分代码 LoginInfo loginInfo =loginInfoRepository.findByUid(userInfoVo.getUid()); if (loginInfo == null) { loginInfo = new LoginInfo(); } setLoginInfo(loginInfo, authCodeVo, userInfoVo); loginInfoRepository.save(loginInfo); addLoginCookie(loginInfo, response); return userInfoVo; } private void addLoginCookie(LoginInfo loginInfo, HttpServletResponse response) { Cookie tokenCookie = new Cookie(TOKEN_COOKIE, loginInfo.getAccessToken()); response.addCookie(tokenCookie); } public boolean isLoginSuccess(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { return false; } String token = null; String uid = null; // 此处省略代码，即从cookies中取出token和uid将设置到变量中。 LoginInfo loginInfo = loginInfoRepository.findByUid(uid); // 此处对acessToken和过期时间进行校验 if (token.equals(loginInfo.getAccessToken()) \u0026amp;\u0026amp; new Date().compareTo(loginInfo.getExpiresDate()) \u0026lt; 0) { return true; } return false; } public boolean logout(HttpServletRequest request, HttpServletResponse response) { Cookie[] cookies = request.getCookies(); // 对cookie进行过期处理 return true; } private LoginInfo setLoginInfo(LoginInfo loginInfo, AuthCodeVo authCodeVo, UserInfoVo userInfoVo) { long nowTime = System.currentTimeMillis(); // 根据过期时长计算过期时间，并设置到LoginInfo中 Date expiresDate = new Date(nowTime + authCodeVo.getExpiresIn() * 1000); // 此处省略一些拿authCodeVo和userInfoVo中的信息set到loginInfo的代码 return loginInfo; } public AuthCodeVo authCode(String code) { Map\u0026lt;String, String\u0026gt; params = new HashMap\u0026lt;\u0026gt;(); // 省略params参数的组装的代码 // 请求access token的地址，并拿到AuthCodeVo结构体的内容 Map\u0026lt;String, Object\u0026gt; resultMap = restTemplate.postForObject(ACCESS_TOKEN_URL, null, Map.class, params); AuthCodeVo authCodeVo = new AuthCodeVo(); // 将resultMap中的值set到authCodeVo中 return authCodeVo; } public UserInfoVo getUserInfo(String accessToken, Integer expiresIn) { Map\u0026lt;String, Object\u0026gt; params = new HashMap\u0026lt;\u0026gt;(); // 省略params参数的组装的代码 // 请求用户的信息的地址，并拿到用户信息。注意这里直接使用restTemplate这个技术实现。 Map\u0026lt;String, Object\u0026gt; resultMap = restTemplate.getForObject(PROFILE_URL, Map.class, params); UserInfoVo userInfoVo = new UserInfoVo(); // 将resultMap中的值set到userInfoVo中 return userInfoVo; } } A风格小结 小结一下A风格的代码：\n登录的主逻辑放在LoginService中； LoginService即处理http请求技术逻辑（cookie的操作），也处理业务逻辑（登录信息的持久化、登录判断、token过期时间设置）； LoginService存放在Server模块； 所有的实体、各种O中，只有字段，getter和setter方法。这导致lombok这样的代码生成库大量被使用，因为A风格觉得为每个字段写getter和setter方法是必须，但是又是浪费时间的事情。 我们暂不讨论A风格的问题，接着看DDD风格的代码。\nDDD风格的代码，简称D风格 代码仓库结构：\n├── domain - domain是用于存放整个业务系统的核心逻辑 ├── abc-o2-auth - 存放所有abc-o2的相关逻辑，下文详细介绍 ├── server - src/main/java/com/xx/server/login/LoginController.java - src/main/java/com/xx/config/AbcWebMvcConfigurer.java ├── repository-impl - 包路径太长省略/AccountRepositoryImpl.java ├── spring-ioc-impl - spring的IoC的实现，D风格下，IoC的实现也应该是可以被低成本地替换的 ├── tech-lib - 公共技术逻辑的接口 ├── tech-lib-impl - 公共技术逻辑的接口的实现 abc-o2-auth模块 我们所有o2-auth的所有的逻辑放在这个新的模块中。考虑到将来可能需要实现新的认证逻辑。以下是该模块的Java代码的结构：\n./abc-o2-auth/src/main/java/com/xxx/domain/o2 ├── AccessToken.java ├── AccessTokenFetcher.java ├── AccessTokenFetcherImpl.java ├── Account.java ├── AccountProfile.java ├── AccountProfileFetcher.java ├── AccountProfileFetcherImpl.java ├── AuthConfig.java └── repository └── AccountRepository.java 登录逻辑Account Account.java是整个o2-auth的认证方式的核心逻辑。源代码将近330行。\n虽然它是一个实体，但它不是整个业务系统的核心逻辑，所以，没有被放到整个业务系统的domain模块中。\nAccount类中所有的技术逻辑都被抽象成接口。\n公共的技术接口，比如Json数据的操作接口json-util，我们统一放在tech-lib模块中。公共的技术接口的实现，目前统一放在tech-lib-impl中。\n当然，也可以以更小粒度的模块来实现解耦，比如创建一个json-util-jackson-impl模块来实现json-util接口。\nP.S. 为什么不直接使用Jackson呢？你想想，当发生像Fastjson那样的安全事故时，你该如何快速的更换json的实现？如果按照D风格，是不是就很容易更换了。\n当Account中的逻辑被真正运行时，需要用到这些技术接口的具体实现时，就从InstanceFactory实例工厂类的getInstance静态方法获取。InstanceFactory是什么，我们下面再说。\n当前，你只需要知道，通过InstanceFactory的getInstance静态方法可以拿到接口的实现实例就可以了。\n采用D风格，在写业务逻辑时，就不需要关心技术逻辑的实现了。这样就能很好的解决“无法写单元测试”的问题。\n@Entity @Table(name = \u0026#34;abc_o2_accounts\u0026#34;) public class Account { // 省略所有的字段，getter和setter代码 public static Optional\u0026lt;Account\u0026gt; login(String code) { // AccessTokenFetcher是accessToken的拉取接口 // 因为accessToken需要请求第三方系统 AccessTokenFetcher accessTokenFetcher = InstanceFactory.getInstance(AccessTokenFetcher.class); Optional\u0026lt;AccessToken\u0026gt; accessTokenOptional = accessTokenFetcher.auth(code); if (accessTokenOptional.isEmpty()) { throw new LoginBizException(\u0026#34;401\u0026#34;); } // AccountProfileFetcher是accountProfile的拉取接口 AccountProfileFetcher accountProfileFetcher = InstanceFactory.getInstance(AccountProfileFetcher.class); Optional\u0026lt;AccountProfile\u0026gt; accountProfileOptional = accountProfileFetcher.fetch(accessTokenOptional.get().getAccessToken(), accessTokenOptional.get().getExpiresIn()); if (accountProfileOptional.isEmpty()) { throw new LoginBizException(\u0026#34;401\u0026#34;); } // 登录成功后，将登录信息持久化 AccountRepository accountRepository = InstanceFactory.getInstance(AccountRepository.class); Optional\u0026lt;Account\u0026gt; accountOptional = accountRepository.findByUid(accountProfileOptional.get().getUid()); if (accountOptional.isEmpty()) { Account account = buildBy(accessTokenOptional.get(),accountProfileOptional.get()); account.save(); return Optional.of(account); } else { Account account = accountOptional.get(); account.update(accessTokenOptional.get(), accountProfileOptional.get()); return Optional.of(account); } } // 登录的url是从配置中获取的。至于是从数据库，还是Etcd配置中心获取，登录核心逻辑并不关心， // 而由AuthConfig的实现决定。这样，将来我们想换配置中心，成本就很低了。 public static String loginUrl(){ AuthConfig authConfig = InstanceFactory.getInstance(AuthConfig.class); return authConfig.getLoginUrlWithRedirect(); } // 再次review此代码时，发现这个方法叫isLoggedIn更能体现方法内的逻辑。 public static boolean isLoginSuccess(String token, String uid) { AccountRepository accountRepository = InstanceFactory.getInstance(AccountRepository.class); Optional\u0026lt;Account\u0026gt; accountOptional = accountRepository.findByUid(uid); if (accountOptional.isEmpty()) { return false; } return StringUtils.equals(token, accountOptional.get().getAccessToken()) \u0026amp;\u0026amp; new Date().before(accountOptional.get().getExpiresDate()); } public AccountProfile getProfile() { AccountProfile result = new AccountProfile(); // 将Account中的信息设置到AccountProfile中，因为前端只需要Account中的部分信息 return result; } private void update(AccessToken accessToken, AccountProfile accountProfile) { Date expiresDate = calExpiredDate(accessToken.getExpiresIn()); // 省略部分更新Account对象的代码。 // save方法即保存此对象 save(); } // 计算出最新的过期时间 private static Date calExpiredDate(int expiresIn) { long nowTime = System.currentTimeMillis(); return new Date(nowTime + expiresIn * 1000L); } // D风格的代码的一大特点：行为跟着数据走。 // 因为数据结构在Account类中，所以数据的持久化方法save也应该放在Account类中。 // 虽然底层实现都是accountRepository.save(xxx) // A风格下，持久化方法放在LoginService中，而数据结构放在另一个类中。 private void save() { AccountRepository accountRepository = InstanceFactory.getInstance(AccountRepository.class); accountRepository.save(this); } private static Account buildBy(AccessToken accessToken, AccountProfile accountProfile) { Account result = new Account(); // calExpiredDate方法的实现放在AccessToken类中更合理，我们只需要调用accessToken。getExpiresDate()。 // 因为根据过期时长计算过期日期的逻辑应该属于AccessToken，而不属于Account Date expiresDate = calExpiredDate(accessToken.getExpiresIn()); // 省略根据accessToken和accountProfile构建一个Account实例 return result; } } Server模块 LoginController.java 只负责调用Account实体的的login方法和操作Cookie这类、HTTP服务相关的技术逻辑。\n@Controller @RequestMapping public class LoginController { @GetMapping(value = \u0026#34;/login\u0026#34;) @ResponseBody public AccountProfile login(String code, HttpServletResponse response) throws IOException { Optional\u0026lt;Account\u0026gt; accountOptional = Account.login(code); if (accountOptional.isPresent()) { responseLoginCookie(accountOptional.get().getAccessToken(), accountOptional.get().getUid(), response); response.sendRedirect(\u0026#34;/\u0026#34;); return accountOptional.get().getProfile(); } // 此处省略部分代码 } @GetMapping(value = \u0026#34;/logout\u0026#34;) @ResponseBody public boolean logout(HttpServletRequest request, HttpServletResponse response) { Cookie[] cookies = request.getCookies(); // 遍历Cookie，并设置cookie过期 return true; } private void responseLoginCookie(String accessToken, String uid, HttpServletResponse response) { // 登录成功，设置cookie } } 以下是D风格AbcWebMvcConfigurer的代码：\n@Configuration public class AbcWebMvcConfigurer implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserAuthInterceptorRegistry()) // 部分代码省略 } class UserAuthInterceptorRegistry implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Cookie[] cookies = request.getCookies(); //省略代码 String token = null; String uid = null; // 从Cookie中取值，并设置到token和uid变量中 if (Account.isLoginSuccess(token, uid)) { return true; } response.sendRedirect(Account.loginUrl()); return false; } } } D风格的AbcWebMvcConfigurer.java 与A风格的区别是：\n关于Cookie的操作，A风格放在LoginService类中，而D风格放在AbcWebMvcConfigurer。因为D风格认为Cookie的操作属于HTTP服务行为，不属于核心业务。另，UserAuthInterceptorRegistry，可以考虑移到abc-o2-auth模块中； 关于是否已经登录的判断逻辑，A风格放在LoginService类中，而D风格放在Account类中。因为是否已经登录的判断逻辑，D风格认为属于abc-o2-auth模块的核心逻辑，而不属于server模块。 InstanceFactory实例工厂的魔法 在D风格中会大量使用InstanceFactory静态类，它使我们能做到与IoC的实现的解耦。\nInstanceFactory代码来自https://github.com/dayatang/dddlib 。\nDDDLib是我的恩师所创建。我在十年前跟他学习到的DDD。大家可以start并从该仓库学习到DDD的一些代码样例。\nD风格小结 小结一下D风格：\n登录的主逻辑放在abc-o2-auth中模块的Account实体中。D风格中的实体类包含各种业务方法，是充血型模型。每一类的设计都有业务含义的，不仅仅只是一个数据结构； 在写代码时，时刻在思考：这是技术逻辑，还是业务逻辑？这是核心业务逻辑，还是非核心逻辑。 篇外话 5年前，我还是一名Java程序员的时候，我一直按照DDD风格要求自己。\n但是，软件行业的绝大数公司才不管你写的代码好，还是坏。更不会管这代码在几年后还能否被维护，维护成本是多少。\n这是DDD风格不流行的原因之一。另一个原因就是：根本没有几个人知道这样写代码。所以，本文算是一篇科普文。\n(全文完)\n","permalink":"https://showme.codes/zh-cn/2024-02-26-ddd-coworker-code/","summary":"\u003cp\u003eDDD是领域驱动设计的简写。前段时间听群友说行业里少有DDD的代码案例，进而对DDD没有一个感性的认识。我想这是行业里普遍存在的现象吧。所以，我就有了写此文的想法。\u003c/p\u003e\n\u003cp\u003e本文开篇介绍了行业里比较普遍的代码风格，接着，我采用DDD风格对其进行修改。\u003c/p\u003e\n\u003cp\u003e我无意说服读者要按照我认为的DDD的风格来写代码，只是想告诉大家，这个世界上，还存在另一种代码风格。\u003c/p\u003e\n\u003cp\u003e如果各位觉得这样的风格好，可以尝试一下。非常欢迎大家反馈，平时太少人和我交流这些了。\u003c/p\u003e\n\u003cp\u003e文章标题说的是“同事的代码”，其实只是为了让此文更具传播，没别的意思。\u003c/p\u003e\n\u003cp\u003e如果你觉得此文对你有帮助，麻烦转发。干货好文不易。谢谢。\u003c/p\u003e\n\u003cp\u003e本文虽是以Java语言为案例演示，也希望对其它语言的读者朋友有帮助。\u003c/p\u003e\n\u003ch1 id=\"行业里普遍的代码风格简称a风格\"\u003e行业里普遍的代码风格，简称A风格\u003c/h1\u003e\n\u003cp\u003e代码结构如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e├── domain \n    domain模块被同事认为是用于存放专门和DB打交道的类的地方\n\t- src/main/java/com/xx/domain/account/repository/AbcLoginInfoRepository.java\n\t- src/main/java/com/xx/domain/account/AbcLoginInfo.java\n├── repository-impl\n\t-  包路径太长省略/AbcLoginInfoRepositoryImlp.java\n├── server\n\t - src/main/java/com/xx/server/login/LoginService.java\n\t - src/main/java/com/xx/server/login/LoginController.java\n\t - src/main/java/com/xx/server/login/AuthCodeVo.java\n\t - src/main/java/com/xx/server/login/UserInfoVo.java\n\t - src/main/java/com/xx/config/AbcWebMvcConfigurer.java\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"server模块\"\u003eServer模块\u003c/h2\u003e\n\u003cp\u003eA风格下，整个业务系统的业务逻辑都在此模块中。\u003c/p\u003e\n\u003cp\u003eLoginController.java 实现http服务：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Controller\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@RequestMapping\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLoginController\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Autowired\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eLoginService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eloginService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"c1\"\u003e// 省略一些不重要的代码\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@GetMapping\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalue\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/login\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@ResponseBody\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eUserInfoVo\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003elogin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ecode\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003ethrows\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eIOException\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eUserInfoVo\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003euserInfoVo\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eloginService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003elogin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecode\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ehttpServletResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003ehttpServletResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esendRedirect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003euserInfoVo\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@GetMapping\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalue\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/logout\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@ResponseBody\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003eboolean\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003elogout\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eloginService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003elogout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ehttpServletRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003ehttpServletResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eUserInfoVo.java是返回给前端的用户信息的结构体：\u003c/p\u003e","title":"我是如何将同事的代码改成DDD风格的"},{"content":"笔者2011年入行时，运气好，遇到了我的恩师simon杨。\n当时，我们几个还不知道什么叫SSH（Spring、Struts、Hibernate）的毕业生和一个高级程序员基于DDDLib就开始实践领域驱动设计。现在想想还是觉得不可思议。一毕业就开始接触这门DDD技艺。\n我记得当时simon杨经常谈如何利用抽象、解耦，在不增加复杂性的同时实现简单性、一致性、灵活性、可扩展性。至于如何实现CRUD，那是具体实现的问题，可以放在最后做。当他谈到某个精妙的设计时，眼神里都满是光。\n在他的教导下，我就开始似懂非懂地读《领域驱动设计》、《企业应用架构模式》、《敏捷软件开发——原则、模式与实践》等这些高层设计类的书籍。\n也许你会说，对于一个刚毕业的人学习这些“高层”设计，是不是太早了，毕竟你连CRUD都还不熟练。\n在我工作10年之后看来，越早学习这些，越好。当你习惯使用CRUD的思维方式来看所有的问题时，你的思维方式已经固化，是很难接受DDD这套思维方式的。我尝试过说服不同工作经验的人使用DDD，无果。\n2013年，在国内，也只有Jdon网站讨论DDD。而我们已经跟着simon杨在项目上实践DDD两年了。\n2014年左右，为了验证了我是否真的理解DDD，我利用一次比赛的机会采用DDD的方式设计了一个BlackJack的游戏的核心。到此，我才算是对DDD有了一个更深入的理解。\n但是，在2014年以后到现在的2021年，我就再也没有接触过DDD的项目了。而我，只能在自己的工作内容范围内实践，比如某个功能、某个模块。\n值得说的是，我最近3年做的是DevOps相关的工作，但是DDD的思维方式依然能帮助我很好的完成工作。\n比如在实现流水线时，将构建工具逻辑与流水线流程逻辑解耦、在设计 版本号时，将构建工具本身的版本号与流水线流程生成的版本号分离。\n如果你不熟悉DevOps也没有关系，你只需要知道DDD贯穿着整个系统设计的每一个细节。\n现在市场上把DDD吹得很高大上，似乎只有在架构上做DDD，才叫DDD。又或者非得和微服务关联在一起，才叫DDD。\n其实不然，DDD的原意是领域驱动设计。只要你是使用领域知识来驱动你的设计，这个设计可以是方法级别的设计、类级别的设计、前端UI的设计等一切设计，你都可以叫DDD。\n也许，这样的话听起来就像“色即是空，空即是色”一样让人不知所云。\n没办法，10年了，我也没有找到非常好的让人一下就懂这种思维方式的方法。\n如果非得要我介绍一下DDD的思维是什么，我觉得就是：不停地问问题是什么，解决方案是什么。然后优先从问题域着手设计。在确定问题域设计得差不多了，才开始实现解决方案。\n这里要提个问，如果拿到一个需求时，先设计MySQL的表结构，再根据表结构导出类。这样的方式符合DDD的思维方式吗？如果不符合，与DDD的思维方式有什么区别？\n最后，我想说的是《领域驱动设计》我读过3遍了，也实践这么多年了，我依然记不清“战略设计”、“柔性设计”等这些术语的准确定义。我也建议各位不要去记那些术语的定义，更不要去争论那些术语的定义，而是要从DDD的原意开始去理解DDD。也就是每当遇到一个术语，你就提问：它是帮助你领域驱动设计的呢。\n","permalink":"https://showme.codes/zh-cn/2024-02-26-ddd-ten-years/","summary":"\u003cp\u003e笔者2011年入行时，运气好，遇到了我的恩师simon杨。\u003c/p\u003e\n\u003cp\u003e当时，我们几个还不知道什么叫SSH（Spring、Struts、Hibernate）的毕业生和一个高级程序员基于\u003ca href=\"https://github.com/dayatang/dddlib\"\u003eDDDLib\u003c/a\u003e就开始实践领域驱动设计。现在想想还是觉得不可思议。一毕业就开始接触这门DDD技艺。\u003c/p\u003e\n\u003cp\u003e我记得当时simon杨经常谈如何利用抽象、解耦，在不增加复杂性的同时实现简单性、一致性、灵活性、可扩展性。至于如何实现CRUD，那是具体实现的问题，可以放在最后做。当他谈到某个精妙的设计时，眼神里都满是光。\u003c/p\u003e\n\u003cp\u003e在他的教导下，我就开始似懂非懂地读《领域驱动设计》、《企业应用架构模式》、《敏捷软件开发——原则、模式与实践》等这些高层设计类的书籍。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"ddd book\" loading=\"lazy\" src=\"/assets/images/ddd-book.png\"\u003e\u003c/p\u003e\n\u003cp\u003e也许你会说，对于一个刚毕业的人学习这些“高层”设计，是不是太早了，毕竟你连CRUD都还不熟练。\u003c/p\u003e\n\u003cp\u003e在我工作10年之后看来，越早学习这些，越好。当你习惯使用CRUD的思维方式来看所有的问题时，你的思维方式已经固化，是很难接受DDD这套思维方式的。我尝试过说服不同工作经验的人使用DDD，无果。\u003c/p\u003e\n\u003cp\u003e2013年，在国内，也只有Jdon网站讨论DDD。而我们已经跟着simon杨在项目上实践DDD两年了。\u003c/p\u003e\n\u003cp\u003e2014年左右，为了验证了我是否真的理解DDD，我利用一次比赛的机会采用DDD的方式设计了一个\u003ca href=\"https://github.com/zacker330/blackjackgame\"\u003eBlackJack的游戏\u003c/a\u003e的核心。到此，我才算是对DDD有了一个更深入的理解。\u003c/p\u003e\n\u003cp\u003e但是，在2014年以后到现在的2021年，我就再也没有接触过DDD的项目了。而我，只能在自己的工作内容范围内实践，比如某个功能、某个模块。\u003c/p\u003e\n\u003cp\u003e值得说的是，我最近3年做的是DevOps相关的工作，但是DDD的思维方式依然能帮助我很好的完成工作。\u003c/p\u003e\n\u003cp\u003e比如在实现流水线时，将构建工具逻辑与流水线流程逻辑解耦、在设计 版本号时，将构建工具本身的版本号与流水线流程生成的版本号分离。\u003c/p\u003e\n\u003cp\u003e如果你不熟悉DevOps也没有关系，你只需要知道DDD贯穿着整个系统设计的每一个细节。\u003c/p\u003e\n\u003cp\u003e现在市场上把DDD吹得很高大上，似乎只有在架构上做DDD，才叫DDD。又或者非得和微服务关联在一起，才叫DDD。\u003c/p\u003e\n\u003cp\u003e其实不然，DDD的原意是领域驱动设计。只要你是使用领域知识来驱动你的设计，这个设计可以是方法级别的设计、类级别的设计、前端UI的设计等一切设计，你都可以叫DDD。\u003c/p\u003e\n\u003cp\u003e也许，这样的话听起来就像“色即是空，空即是色”一样让人不知所云。\u003c/p\u003e\n\u003cp\u003e没办法，10年了，我也没有找到非常好的让人一下就懂这种思维方式的方法。\u003c/p\u003e\n\u003cp\u003e如果非得要我介绍一下DDD的思维是什么，我觉得就是：不停地问问题是什么，解决方案是什么。然后优先从问题域着手设计。在确定问题域设计得差不多了，才开始实现解决方案。\u003c/p\u003e\n\u003cp\u003e这里要提个问，如果拿到一个需求时，先设计MySQL的表结构，再根据表结构导出类。这样的方式符合DDD的思维方式吗？如果不符合，与DDD的思维方式有什么区别？\u003c/p\u003e\n\u003cp\u003e最后，我想说的是《领域驱动设计》我读过3遍了，也实践这么多年了，我依然记不清“战略设计”、“柔性设计”等这些术语的准确定义。我也建议各位不要去记那些术语的定义，更不要去争论那些术语的定义，而是要从DDD的原意开始去理解DDD。也就是每当遇到一个术语，你就提问：它是帮助你领域驱动设计的呢。\u003c/p\u003e","title":"这10年，我所经历的领域驱动设计（DDD）"},{"content":"\n学习英文最大的两个问题：\n正确的学习方法 坚持 正确的学习方法，我是自认为是已经掌握了的。但是，我很难坚持下来。\nEffortless English是我follow最长时间的英文教程。我断断续续听了有一年了。\n有一段时间，我真正地做到按照课程里的听，并练习。然后在那段时间，我的大脑会不自觉地使用英文描述生活中的事情。这种状态是真正把英文内化的一个状态。\n在两天前，我看了别人推荐《把你的英语用起来！》，书中提到了“坚持这件小事”，我才发现，在过去，我的问题出在坚持。\n在看了《把你的英语用起来！》我没有坚持下来的原因是：\n给自己的目标期望太高了； 没有养成正面的自我激励模式。 通常我们认为坚持会是一件痛苦的事情。然而，这是一个错误的认知。如果它是一件快乐的事情，坚持是不需要痛苦了。\n所以，接下来，我需要针对性解决以上两个问题。\n说回Effortless English课程的学习方法。\n该课程由A.J. Hoge录制的纯音频的全英文课程（没有写作和阅读）。它由一系列的由易到难的mini-story组成。\n每次课一个story，它又分成三部分内容（三个音频），每部分要达到的目标有所不同：\n第一部分：几分钟mini-story陈述 目标：能听懂故事的内容 第二部分：介绍Mini-story中的重点单词 不需要刻意的记忆这些单词，因为在重复听和练习过程中，自然就记住了它们 第三部分：采用不同的时态对同一个story进行复述 目的是让你自然形成英文语法的语感 AJ会在课程中提问。问题包括两类：\n问你懂的（如果你不懂，说明你没有听懂第一部分）。针对这类问题，你要做的就是快速回答。回答通常只需要一两个单词。 问你不懂的（需要你猜测的）。针对这类问题，你要立刻作出猜测性的回答。 这个过程像是锻炼你的大脑肌肉，对英文问题的回答形成肌肉记忆。\n而且，AJ会从各个角度对同一件事情进行提问，问发生地点、发生时间、谁参与了……。这个过程，很像父母在教两岁左右的小孩学习说话。\n以上是AJ Hoge每节课的pattern。但是随着Level的上升，课程里会逐渐出现复述、跟读的练习。\n复述过程是最容易放弃的阶段。\n说完内容，说说学习计划。\n每次课都要坚持一个星期。每天至少一个小时（早上30分钟，晚上30分钟）。 如果遇到难的课程，延长学习该课的时长，直到你真正掌握该课。 最后，我想说，Effortless English能非常好地锻炼你英文听说的真正能力，如果你学习英文的主要目标是为了雅思拿个好成绩，这个课程不能在短时间内提升你的分数。\n","permalink":"https://showme.codes/zh-cn/2024-2-19-effortless-english/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/effortless-english.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e学习英文最大的两个问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e正确的学习方法\u003c/li\u003e\n\u003cli\u003e坚持\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e正确的学习方法，我是自认为是已经掌握了的。但是，我很难坚持下来。\u003c/p\u003e\n\u003cp\u003eEffortless English是我follow最长时间的英文教程。我断断续续听了有一年了。\u003c/p\u003e\n\u003cp\u003e有一段时间，我真正地做到按照课程里的听，并练习。然后在那段时间，我的大脑会不自觉地使用英文描述生活中的事情。这种状态是真正把英文内化的一个状态。\u003c/p\u003e\n\u003cp\u003e在两天前，我看了别人推荐《把你的英语用起来！》，书中提到了“坚持这件小事”，我才发现，在过去，我的问题出在坚持。\u003c/p\u003e\n\u003cp\u003e在看了《把你的英语用起来！》我没有坚持下来的原因是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e给自己的目标期望太高了；\u003c/li\u003e\n\u003cli\u003e没有养成正面的自我激励模式。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e通常我们认为坚持会是一件痛苦的事情。然而，这是一个错误的认知。如果它是一件快乐的事情，坚持是不需要痛苦了。\u003c/p\u003e\n\u003cp\u003e所以，接下来，我需要针对性解决以上两个问题。\u003c/p\u003e\n\u003cp\u003e说回Effortless English课程的学习方法。\u003c/p\u003e\n\u003cp\u003e该课程由A.J. Hoge录制的纯音频的全英文课程（没有写作和阅读）。它由一系列的由易到难的mini-story组成。\u003c/p\u003e\n\u003cp\u003e每次课一个story，它又分成三部分内容（三个音频），每部分要达到的目标有所不同：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e第一部分：几分钟mini-story陈述\n目标：能听懂故事的内容\u003c/li\u003e\n\u003cli\u003e第二部分：介绍Mini-story中的重点单词\n不需要刻意的记忆这些单词，因为在重复听和练习过程中，自然就记住了它们\u003c/li\u003e\n\u003cli\u003e第三部分：采用不同的时态对同一个story进行复述\n目的是让你自然形成英文语法的语感\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAJ会在课程中提问。问题包括两类：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e问你懂的（如果你不懂，说明你没有听懂第一部分）。针对这类问题，你要做的就是快速回答。回答通常只需要一两个单词。\u003c/li\u003e\n\u003cli\u003e问你不懂的（需要你猜测的）。针对这类问题，你要立刻作出猜测性的回答。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这个过程像是锻炼你的大脑肌肉，对英文问题的回答形成肌肉记忆。\u003c/p\u003e\n\u003cp\u003e而且，AJ会从各个角度对同一件事情进行提问，问发生地点、发生时间、谁参与了……。这个过程，很像父母在教两岁左右的小孩学习说话。\u003c/p\u003e\n\u003cp\u003e以上是AJ Hoge每节课的pattern。但是随着Level的上升，课程里会逐渐出现复述、跟读的练习。\u003c/p\u003e\n\u003cp\u003e复述过程是最容易放弃的阶段。\u003c/p\u003e\n\u003cp\u003e说完内容，说说学习计划。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e每次课都要坚持一个星期。每天至少一个小时（早上30分钟，晚上30分钟）。\u003c/li\u003e\n\u003cli\u003e如果遇到难的课程，延长学习该课的时长，直到你真正掌握该课。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e最后，我想说，Effortless English能非常好地锻炼你英文听说的真正能力，如果你学习英文的主要目标是为了雅思拿个好成绩，这个课程不能在短时间内提升你的分数。\u003c/p\u003e","title":"Effortless English英文学习小结"},{"content":"前段时间在面试的时候，面试官问到：你是如何将DDD（领域驱动设计）应用到基础设施的？\n我很惊讶，终于有人问我这个问题了。\n在过去从事基础设施（DevOps、SRE、运维）的这5年里，我经常说起DDD是一种思维模式，可以应用到任何的领域，包括基础设施的设计。\n但是，从来没有人像这位面试官问起我具体的做法。\n为什么没有人问？原因大概是这两个概念通常是不会放在一起的。大多数开发不会深入理解基础设施的设计，而大多数从事基础设施设计的人是不会接触到DDD。而且，开发人员对于DDD的理解，也仅局限于用它开发业务系统。\n我就是那少部分人，即做基础设施的设计，又觉得自己懂DDD的人。\n说回问题本身。\n我所理解的DDD 我首先会向提问的人澄清我所理解的DDD。\n为什么要这样呢？很久以前有一次面试，因为我说我擅长DevOps，面试官就认为我不懂GitOps。然后在这个点上他就认为我不适合，不再问我DevOps方面的问题。我只能说没有缘份。\n我是这样解释DDD的：\n就像开发一个象棋游戏，不论你要开发手机端，还是web端，象棋规则本身都是不变的。这个规则本身就可以理解为“领域”。 其它所有的技术（包括架构）都是具体实现，它们应该由“领域”来驱动设计。\n当时的解释与以上的解释大差不差。\n将DDD应用到基础设施设计的具体做法 那么该如何将DDD应用到基础设施的设计呢？\nDDD的思维方式要求我们首先问：我们要设计的软件的领域（核心）问题是什么？\n基础设施的领域问题是什么？我的回答是配置。\n我认为基础设施的搭建、维护，本质就是配置的设计、部署、维护。\n寻找领域（本质）问题的能力是DDD的核心能力。\n为了让读者更好理解，我们以一个一个基于云上的虚拟机的分布式系统为例。它的基础设施就包括：vpc、LB、MQ、DB等。\n要搭建、维护这一套基础设施。\n根据“配置管理是基础设施设计的核心问题”，我首先将基础设施的所有的配置放在清单代码（并不一定是一个文件）中，如下：\nvpc: # .... LB: # ... DB: # ... MQ: # ... APP1: mq_addr: \u0026#34;{{ MQ.addr }}\u0026#34; db_host: \u0026#34;{{ DB.addr }}\u0026#34; APP2: mq_addr: \u0026#34;{{ MQ.addr }}\u0026#34; db_host: \u0026#34;{{ DB.addr }}\u0026#34; app1_addr: \u0026#34;{{ APP1.addr }}\u0026#34; 从清单中，你看不出它使用何种部署方式、部署顺序。你只知道APP1引用了MQ和DB，APP2会调用APP2这样纯粹的领域知识。\n现实通常是多个环境，所以，我一开始就会将不同环境的值从清单上抽离到独立的文件夹中。\n第二步，我才会考虑如何部署它们。这时，我通过两个工具实现：\nTerraform负责云基础设施； Ansible负责业务基础设施。 Ansible是可以直接读取我们的YAML格式配置文件。而Terraform代码引用YAML代码，就没有那么方便了。\n所以，我决定使用Jsonnet这门配置语言来统一所有的配置，这样不同的配置之间就可以相互引用了。\n配置之间的相互引用功能是配置管理的核心功能。\n假如某一天，我们需要将云虚拟机的基础设施，迁移到K8s呢？\n最上层的清单配置是不需要做什么变更的，因为它和你的具体的基础设施实现是无关的，你只需要更改底层的部分配置。\n比如，你需要将Ansible部署工具改成Helm，那么，你需要做的就是写一套通用的Helm chart，然后chart values配置引用上层的APP的配置即可。这时，你会发现，配置的标准化，我们在一开始实现了，而不需要后期返工。\n最终我们的基础设施的配置的架构，可以简化成下图：\n小结 我是如何将DDD应用到基础设施的：\n基于自己对基础设施的理解，将“配置管理”定义为这个领域的核心问题； 分析配置管理的所带来的具体问题；这一步通常由领域专家来做。本文我是直接略过这一环节； 根据第二步，决定使用代码去管理配置； 根据基础设施的上下文（是否使用云，是否云原生）选择工具，读取配置，然后实现部署与维护。 不论你是否赞同使用基础设施即代码，我的基础设施的设计都是由配置管理（领域）这个问题驱动。\n","permalink":"https://showme.codes/zh-cn/2024-01-27-ddd-infra-integrated/","summary":"\u003cp\u003e前段时间在面试的时候，面试官问到：你是如何将DDD（领域驱动设计）应用到基础设施的？\u003c/p\u003e\n\u003cp\u003e我很惊讶，终于有人问我这个问题了。\u003c/p\u003e\n\u003cp\u003e在过去从事基础设施（DevOps、SRE、运维）的这5年里，我经常说起DDD是一种思维模式，可以应用到任何的领域，包括基础设施的设计。\u003c/p\u003e\n\u003cp\u003e但是，从来没有人像这位面试官问起我具体的做法。\u003c/p\u003e\n\u003cp\u003e为什么没有人问？原因大概是这两个概念通常是不会放在一起的。大多数开发不会深入理解基础设施的设计，而大多数从事基础设施设计的人是不会接触到DDD。而且，开发人员对于DDD的理解，也仅局限于用它开发业务系统。\u003c/p\u003e\n\u003cp\u003e我就是那少部分人，即做基础设施的设计，又觉得自己懂DDD的人。\u003c/p\u003e\n\u003cp\u003e说回问题本身。\u003c/p\u003e\n\u003ch2 id=\"我所理解的ddd\"\u003e我所理解的DDD\u003c/h2\u003e\n\u003cp\u003e我首先会向提问的人澄清我所理解的DDD。\u003c/p\u003e\n\u003cp\u003e为什么要这样呢？很久以前有一次面试，因为我说我擅长DevOps，面试官就认为我不懂GitOps。然后在这个点上他就认为我不适合，不再问我DevOps方面的问题。我只能说没有缘份。\u003c/p\u003e\n\u003cp\u003e我是这样解释DDD的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e就像开发一个象棋游戏，不论你要开发手机端，还是web端，象棋规则本身都是不变的。这个规则本身就可以理解为“领域”。\n其它所有的技术（包括架构）都是具体实现，它们应该由“领域”来驱动设计。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e当时的解释与以上的解释大差不差。\u003c/p\u003e\n\u003ch2 id=\"将ddd应用到基础设施设计的具体做法\"\u003e将DDD应用到基础设施设计的具体做法\u003c/h2\u003e\n\u003cp\u003e那么该如何将DDD应用到基础设施的设计呢？\u003c/p\u003e\n\u003cp\u003eDDD的思维方式要求我们首先问：我们要设计的软件的领域（核心）问题是什么？\u003c/p\u003e\n\u003cp\u003e基础设施的领域问题是什么？我的回答是\u003cstrong\u003e配置\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e我认为基础设施的搭建、维护，本质就是配置的设计、部署、维护。\u003c/p\u003e\n\u003cp\u003e寻找领域（本质）问题的能力是DDD的核心能力。\u003c/p\u003e\n\u003cp\u003e为了让读者更好理解，我们以一个一个基于云上的虚拟机的分布式系统为例。它的基础设施就包括：vpc、LB、MQ、DB等。\u003c/p\u003e\n\u003cp\u003e要搭建、维护这一套基础设施。\u003c/p\u003e\n\u003cp\u003e根据“配置管理是基础设施设计的核心问题”，我首先将基础设施的所有的配置放在清单代码（并不一定是一个文件）中，如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003evpc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# ....\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eLB\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# ...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eDB\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# ...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eMQ\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# ...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eAPP1\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003emq_addr\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{ MQ.addr }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003edb_host\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{ DB.addr }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eAPP2\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003emq_addr\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{ MQ.addr }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003edb_host\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{ DB.addr }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eapp1_addr\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{ APP1.addr }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e从清单中，你看不出它使用何种部署方式、部署顺序。你只知道APP1引用了MQ和DB，APP2会调用APP2这样纯粹的领域知识。\u003c/p\u003e\n\u003cp\u003e现实通常是多个环境，所以，我一开始就会将不同环境的值从清单上抽离到独立的文件夹中。\u003c/p\u003e\n\u003cp\u003e第二步，我才会考虑如何部署它们。这时，我通过两个工具实现：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eTerraform负责云基础设施；\u003c/li\u003e\n\u003cli\u003eAnsible负责业务基础设施。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAnsible是可以直接读取我们的YAML格式配置文件。而Terraform代码引用YAML代码，就没有那么方便了。\u003c/p\u003e","title":"如何将DDD应用到基础设施设计？"},{"content":"导言 正如标题所言，我是一名程序员。2023年5月之前，我是一个法盲，不懂起诉的流程，更不懂开庭的步骤。\n但在过去的一年中，我以各种身份参与庭审多次，包括：\n以被告的公民代理的身份，为被告辩护，参加庭审：1次 以原告的公民代理的身份，为原告辩护，参加庭审：1次 以原告的身份起诉开发商：2次 以起诉人身份参加二审：1次 以旁听的身份参加庭审：2次 多次为小区其他业主免费写起诉状、答辩状、再审申请书等多份文书。\n但是结果是什么呢？后文会说。如果你只想知道官司输赢，可以直接划到文章最后。\n本文比较长，你可以挑选自己感兴趣的部分开始。\n如果您觉得本文有意义，还请转发给其他遇到相同问题的业主，以帮助更多的人。如果帮到你，还请用实际行动赞赏本文。\n你我的权益，需要法制社会来保护，也需要你我的努力。\n背景介绍 我是2017年购买的商品房，坐落在一个18线的县城。开发商是市里的一家房地产开发商。\n但是直到现在小区还是由物业代抄电表，代收电费交给开发商。\n在2020年和2022年的两次水灾中，其它实现了一户一表的小区，供电局很快就恢复供电了。但是我们小区停电了半个多月，因为开发商需要自己找人去修该变压器。\n这里有两个背景知识：\n开发商需要将小区的变压器的产权无偿移交给供电局，变压器的维护才由供电局负责； 一户一表：说大白话就是由业主与供电局直接发生供用电关系，而不是由开发商代缴电费。 业主是2020后才知道小区并不是一户一表的。所以，部分小区业主从那时开始拒绝“交电费”。开发商只能为这部分业主“垫付”电费。\n2023年5月，开发商不再为业主“垫付”电费。小区因此被供电局停电。\n接下来，小区业主与开发商、镇政府、供电局、住建局等多方，进行长时间地“拉扯”（中间的故事可以再写一篇文章，但是考虑到篇幅，本文不详写）。\n虽然现在小区有电用，但至今小区依然没有实现一户一表。这就是多方拉扯的结局。懂的都懂。\n其间，我开始研究业主与开发商之间的合同《商品房买卖合同》（由国家住建部和国家工商局2014年制定的格式合同），商品房验收条件中，关于电的部分，合同上的原文是这样的：\n供电：交付时纳入城市供电网络并正式供电。\n也就是合同里并没有写明“一户一表”。也请读者朋友拿出自己的合同，找到相关条文确认。因为合同一字之差就差个十万八千里。\n根据这条合同条文，我们不能以“未实现一户一表”来起诉开发商。只能起诉开发商没有实现“交付时纳入城市供电网络并正式供电”。\n本文为了方便，会将两“一户一表”与“交付时纳入城市供电网络并正式供电”混用，但是它们意思大致相同。注意，在庭审过程，它们不是同一事物。\n这里再次提醒读者：起诉开发商，必须以合同为依据起诉。\n以上简单介绍了背景知识（实际情况更复杂）。接下来解答一些读者会提出来疑问：\n为什么要写下这些 在前几天，我刚刚结束了二审（判决结果还没有出），我决定将整个经历写下来。原因有二：\n一是因为我这人记性不好，想记录下这段有意义的经历；二是希望为法制社会做出一点点微薄的贡献。\n为什么不找律师？ 我在找律师前做了很多功课，比如起诉开发商涉及的法律条文、开发商会如何抗辩等。但是，在找过多家律师所后，我最终决定不找律师。\n原因有：\n本县城的律师不愿意为我们打官司，有律师认为钱太少，有律师听到小区名字就说不打。部分业主甚至愿意与律师平分违约金。 隔壁县城的律师找了两个，我个人判断不可靠。当我拿出《合同》与他们讨论其中的条文时，他们并没有给出令我满意的答案，而且整个市的律师都在同一个律师协会里，懂的都懂； 省会的律师不太可能受理我们这种小金额官司。连他们的差旅费，我们可能都给不起； 小区里并不是所有的人都愿意打这个官司（这个是关键）。 我以个人名义打官司，就不会有以上问题了。\n为什么不是起诉物业？ 说白了，小区物业就是开发商的一家子公司。所以很多业主经常误以为物业就是开发商，毕竟“交电费”是直接交给的物业的。\n小区的几次停电，部分业主跑去物业那里“闹”，这是找错对象了。因为我们小区的供电主体是开发商，不是物业。\n再多说一句，如果物业没有代缴电费的权力（具体要看物业合同），所以他们也无法起诉你不交电费给他们。\n为什么不是起诉开发商不移交变压器？ 这在上文已经说明了，要以合同为依据来起诉。合同里并没有写“移交变压器”的条文。\n集体诉讼，还是一个个单独诉讼？ 在没有接触真正的诉讼流程前，我也以为我们是可以集体诉讼的。\n但是在学习后，发现中国是没有集体诉讼的概念的。只能单独诉讼。如果我错了，还请纠正我。多谢。\n假如有律师接受整个小区对开发商的诉讼，律师也是分别和每一位业主签法务合同。律师只不过是批量操作，并不是真正意义的集体诉讼。\n我可以代理其他人的诉讼吗？ 如果没有集体诉讼，那么，我将面临两个问题：\n我是自己起诉开发商，还是和其业主一起？ 我可以代理其他业主的起诉吗？并不是每一个业主都有时间。 对于问题1，经过深思熟虑，我做出的策略是：我以个人名义单独起诉。\n因为我完全没有诉讼经验，不知道法官和开发商会如何抗辩。即使我本人这次官司输了，其他人也可以以我的诉讼经验发起另一次诉讼，直到打赢。\n关于问题2，你是可以以公民代理的身份代理你的邻居或者亲戚的诉讼。中国不允许没有律师执照的人为其他人代理诉讼，公民代理算是对这机制的补充。如果我理解错了，还请纠正，谢谢。\n公民代理的方法是：\n取得证明你们关系。 如果是邻居关系，就拿双方的购房合同或者房产证去当地居委会开证明； 如果是亲戚关系，就拿双方的户口去公安局、居委会或者村委会。可能每个地方不一样，你需要咨询当地的居委会或者村委会； 准备授权委托书，即你的邻居或者亲戚将案件委托给你的证明； 在提交起诉状或者开庭前向法庭提交双方身份证复印件、委托书和关系证明。 以上是我个人的总结。但是，还是建议有需求的读者，请咨询当地法院。\n题外话，我在给邻居代理时，发生了两件“有趣”的事情。\n一件是当我给其中邻居代理房产证逾期的诉讼时，负责立案的”漂亮“的小姐姐，气急败坏似地不给任何理由地拒绝了我的代理。这超出了我认知。她不是在法院知法犯法吗？\n另一件也是这个“漂亮”的小姐姐，我在立案时，故意刁难我，本来可以使用微信线上交纳诉讼费的，却跟我说法院规定月底最后三天，需要线下去银行柜台汇款。\n当时，我不清楚，天真去线下交了，浪费了我大量时间。后来有一次也是月底，我看到其他所有人都不需要线下汇款。我想法院应该是没有任何理由拒绝我线上支付的。如果有懂相关法律的读者可以告诉我。\n我也是法盲，该如何学习？ 正如本文所言，在这之前，我是法盲，但是为了告赢开发商，我通过以下三种方法学习打官司：\n打12348法律援助的电话，向值班的律师请教法律相关知识。建议是晚上8点半左右人相对较少，提问前请打好草稿； 在中国裁判文书网 - 最高人民法院上查找相似的案件； 通过AI模拟庭审过程中可能出现抗辩、学习诉讼策略； 通过搜索网站查找相关法律条文； 在B站上学习别人的庭审经验。 发起诉讼 以什么依据起诉开发商什么？ 首先，我们起诉开发商的原因是希望能实现一户一表。\n但是，我们不能以一个“希望”来起诉。通过对《商品房买卖合同》的研究，我找到了起诉开发商的条文：\n供电：交付时纳入城市供电网络并正式供电。\n即开发商没有实现合同里的这条约定。\n同时，合同里有写，如果没有实现约定，那么，“出卖人按日计算买受人支付全部房价款万分之2的违约金”。\n根据以上内容，我们就可以以开发商没有履行合同约定来起诉开发商。\n但是，这与“一户一表”有什么关系呢？后文会有说明。\n写起诉状和准备证据 确定起诉的诉求和依据后，就可以开始考虑起诉状了。\n在写起诉状之前，你需要选择进行上诉的法院。比如你在上海工作，但房产在广东，这时，你应该在广东发起上诉，而不是在上海。\n接着就是写起诉状。\n作为原告，我一开始和很多人一样，也以为起诉状里要把被告触犯的法律一条条列在起诉状中。\n其实，那不是必须的。正相反，在法院立案庭接收立案前提下，起诉状应该越简单越好。\n这就像原告和被告打牌，如果你作为原告一开始就把打法和底牌告诉被告，你还怎么打？\n所以，我的起诉状很简单，大概内容如下：\n诉讼请求：要求被告支付合同违约金xxxx元，并立即履行合同约定； 事实与理由是：开发商从xxxx年至今未履行《合同》的“供电：交付时纳入城市供电网络并正式供电。”的约定，根据约定，出卖人按日计算买受人支付全部房价款万分之2的违约金。根据总房价计算，开发商应该支付xxxx元违约金。 该起诉状的模板的链接，我在本系列文章后续文章发布。\n关于诉讼请求中的“立即履行合同约定”，我所在法院要求我去掉，理由是他们无法执行。但是，我咨询过的一个律师说是可以写的。如果有了解的读者，可以后台留言。\n但是，在我之后进行起诉的业主，我就不建议此策略了。因为其他人并没有像我一样有时间去学习法律和庭审。\n所以，后面起诉的业主，我帮他们写起诉状时，就基于我第一次庭审的经验，直接将所有的论点和论据都写在起诉状中，相当于明牌和对方打。\n这样做的目的：\n经过一次我与开发商的第一次官司，我已经知道对方能打什么牌了，所以，可以直接针对性的写诉状、设计诉讼策略； 提前以文字的方式写清楚，避免业主在开庭时被在法庭上的人玩文字游戏。谁知道法官会不会偏袒开发商？ 这个明牌起诉状的模板，在本系列文章后续文章发布。\n除了起诉状，你还需要准备证据目录。它的目的是为了证明你的诉求的合法性的，或者证明对方是违约了。我的证据目录包括：原告身份证复印件、原告身份证复印件、被告法定代表人身份证、原告与被告签署的《商品房买卖合同》、开发商关于电费的《起诉状》。\n证据目录模板，在本系列文章后续文章发布。\n读者会有疑问在这里：我怎么拿到开发商的营业执照复印件或者他们法定代表人的身份证？\n我们小区比较特殊。开发商之前在起诉业主不交电费时，证据目录就必须有以上两个文件。所以，我们是拿开发商起诉业主时的证据再去起诉开发商。我不建议读者这样操作。\n细心的读者注意到其中一个证据“开发商关于电费的《起诉状》”，即开发商起诉部分业主不交电费时的起诉状。这个案件发生在我起诉开发商之前。\n这个起诉状明显不是律师写的，白纸黑字写着：小区尚未实现一户一表。也就是他们自己自认了没有实现一户一表。这是一个非常关键的证据。\n有读者可能已经想到了：只要证明“一户一表”与合同的“供电”约定是同一件事，那么，这个案子就赢了。\n我在庭审中采用了这一策略，但是事实并没有这么简单。\n诉讼金额大小确定 在写起诉状时，有一个很重要点需要考虑：诉讼金额大小。即你要起诉开发商应该支付多少违约金。\n举个例子可能更好理解。比如开发商是2017年1月1日开始，直到当前（2024年1月1日）一直在违约。你并不需要一次性选择起诉开发商这一整段时间的违约，你可以只起诉其中一段时间违约，例如只起诉2017年1月1日往后的100天违约。\n如果是律师，在可能的情况下，律师当然会觉得违约金越多越好。因为违约金越多，律师费就越多。\n但是，我是非专业的，第一次起诉违约金越多，越有害。我考虑的点如下：\n如果我输了，我就失去了下一次起诉的机会，而且我认为我输的可能性很大； 违约金越多，要交给法院的受理费就越高； 如果我赢了这次，我可以再起诉剩余的。 在本案中，我只提出8千多违约金的诉讼请求，这样我只需要向法院（好像是）交纳25块钱的受理费。\n这样做，还可以带来另外的好处：留给开发商一个难题——请律师吗？如果再有一个业主提起诉讼，开发商该如何选择？\n可惜，开发商也不笨，并不是每个案件都请律师。在我们小区的多次诉讼中，只针对4个诉讼请了同一个律师。\n按照市场价，估计开发商需要支付1万2的律师费。\n开庭前 在我上交起诉状和证据目录后，法院受理了就只要等法院的开庭通知。起诉状和证据目录分别打印3份，其中两份分别给法院和被告，自己留一份。\n在等待过程中，我开始查阅大量的资料，着手准备法庭上的提问环节和辩论环节。\n我准备从两个方面进行论证：\n想办法证明是开发商的原因没有将变压器移交给供电局，进而导致违约； 想办法证明“一户一表”与合同的“供电”约定是同一件事； 但是，事实证明，以上两个方向都是错了，法官不会站在你这边帮你去论证。\n所以，我也不细讲这部分了。下文说到一审判决你就明白了。\n开庭过程 开庭过程大概有以下过程：\n法官会确认双方的身份，及原告的诉求； 双方对证据进行举证质证：就是告诉法官你是否认可对方提供的证据的真实性、合法性、与本案的关联性； 法官进行法庭调查，并给出双方争议焦点：也就是法官会问双方问题，你需要如实回答； 原被告双方进行相互提问； 原被告进行辩论； 原被告进行最后的陈述。 在本次开庭前，被告提交答辩状和他们的证据目录。答辩状内容大概如下：\n被答辩人（我，业主）自《合同》签订至今已经过了6年，已经超过诉讼时效； 因当地电力分公司的交付标准为合表电价户方式，由物业代为收取各户电费。答辩人（开发商）按时向电力管理部门交纳全小区电费。小区全体业主基于用电和缴费事实，与答辩人形成事实上的供用电合同关系； 本县城普通存在合表电价户问题，本小区目前是第一批将实施抄表到户的小区。 开发商给到的证据有：\n最近4个月电费通知单，2018年供电抄表结算复核单：证明该公司使用供电部门的正式供电情况； 最近3个月缴纳电费银行转账凭证：证明该公司向供电部门缴纳供电电费的事实； 最近3个月供电部门向开发商开具的电费发票：证明供电部门对公司正式供电产生费用并开具发票的事实； 两个当地房地产行业协会的开发照片：证明当地小区的供电情况。 我对于以上证据的举证质证：合法性和真实认可，关联性不认可。\n提问环节，我向开发商提问了多个问题，用于证明小区未完成移交电表的原因是开发商。开发商都没有正面回答。而法官也没有要求开发商正面回答。\n整个庭审下来，都感觉是法官在袒护开发商。当然，只是我感觉。\n以下是两个一审时的案情关键点。\n案情关键点：诉讼时效 大白话翻译过来就是开发商说我已经过了诉讼时效了，他们没有违约。大白话解释一个“诉讼时效”，就是开发商从2017年1月1日违约了，但是到2020年1月1日的过程中，你不起诉，即代表你自愿放弃将来起诉的权益。\n在法庭调查阶段，法官问我：你是什么时候知道开发商没有实现合同中的“供电”约定的？\n我就说我是在xxxx年x月左右（诉讼时效内），当时小区停电后，我才知道，所以才不交电费给开发商。\n案情关键点：是否正常用电 在法庭调查阶段，法官还问：你们现在是正常用电吗？现在电价是多少？\n我回答：不算正常用电，每年水灾时，我们都停电很长时间。电价不知道是多少，因为电表不是供电局的。\n后来，我发现二审法官也这样问。\n我一开始不明白他们为什么问这个问题。现在回头看，最好的回答可能是：\n能否正常用电与开发商是否违约没有关系，如果开发商买汽油用发电机供电给我们用？我们是否属于正常用电？ 业主交给开发商的钱与开发商交给供电局的钱不是一回事，小区业主每月交10万电给开发商，而同样的用电量，开发商可能就只用交8万给供电局，目前小区业主交给开发商的电费按阶梯收费的，而开发商与供电局之间并没有按阶梯收费，统一5毛4。如果开发商存在供电盈利，需要提供转供电资质证明。 如果开发商是通过这个差价赚钱的，哪个开发商愿意实现一户一表。\n现在看来，我在一审过程中，明显经验不足。把以上内容提供出来，仅供大家参考。\n《前期物业合同》是其中一个关键点 《前期物业合同》是业主与开发商签订《商品房买卖合同》时就签订的。《前期》中有一条：电费由前期物业代缴。\n请各位读者留意这条。如果有这一条，我们的官司会难打很多。但是，我们小区大概19年的时候，成立了业委会。\n在《合同》中，明确写有：在成立业委会后，前期物业合同自动失效。而业委会与物业新签订的物业合同里没有授权物业代缴电费。\n所以，当前开发商没有委托物业代收电费的权力。建议读者留意你们的物业合同，并尽早成立业委会。\n一审判决 在我起诉开发商之前，开发商起诉了一批“没有交电费”的业主时，法官采用小额诉讼。该案件判决业主输了。这个案件，我也可以单独拿出来写一篇。\n小额诉讼的案件是一审和终审了。不服判决，只能申请再审。而再审走的是检察程序了。\n法官没有想到我们业主有人提起再审流程。导致法院很被动。因为再审意味法院存在错判，进而影响法院的KPI。\n所以，我们后面的案件的金额虽然很小，但是都不以小额诉讼来处理。\n最终法院给出一审判决中，认定的事实有：\n现原告居住该房屋，并能正常供电使用； 小区变压器在开发商名下，供电局为小区正常供电，用电后由开发商向供电局缴纳小区电费，再由开发商委托物业向业主代抄电表，业主根据抄表明细缴费，还未能实现一户一表。 xxxx年x月x日，原告认为被告交房时供电未纳入城市供电网络，停止向被告委托的物业缴纳电费。 法院向供电局咨询：“如何理解实现接入城市供电网络和安装开发商供电设施配套并完成验收，供电公司正常供电，但是变压器在开发商名下，未能实现一户一表，现用电是由开发商向电力部门缴纳小区电费，再由开发商委托物业向业主代抄电表，业主根据抄表明细缴费，是否属于实现接入城市供电网络”。供电局回复：1. 实现接入城市供电网络非电网企业通用词无法释义；2. 开发商与供电企业存在供用电合同关系，即该开发商的配电设施已经接入公用配电线路。 法院基于认定的事实，认为：\n《合同》合法有效； 原告未过诉讼时效； 按理被告应将小区电力设施移交供电部门，业主也能正常使用供电部门输出的电。双方在合同约定交房条件为“供电：交付时纳入城市供电网络并正式供电”，对该约定的含义，本院致函给供电局。由于原、被告在合同约定不明确，现原告诉称被告交房时供电未纳入城市供电网络构建违约，要求支付违约金的请求，证据不足，本院不予支付。 所以，一审就是我们输了。\n但是我并没有放弃，向中级人民法院提起二审。如果是你，你会如何提起二审。\n一审开庭前，法官挂断我们的电话 一审中，我想到去住建局调取证据——小区内部的电力规划图——用于证明开发商没有按规划图建设。因为事实就是没有按规划图建设。所以，变压器无法移交给供电局。\n可是，当我们第一次打电话给法官时，法官说：他们没有权力去调取，那是国家机密。然后“嘭”的一声挂断了我们的电话。\n我当时就懵了，法官还有这操作的？\n然后，我上网查，发现，就因为那是国家机密，只能是法院去调取。\n简直不敢相信法官竟然说出这样的话。\n接着，我们开始打第二次电话。书记员接电话说：法官出去办事了，你晚点再打电话过来。\n后来，我们向律师说明此事。律师说，这是经常的事。\n提起二审 在拿到一审判决书时，我是非常兴奋的。只要法院给出有漏洞的判决书，我就有机会。\n在写二审上诉书时，我意识到一审中我的主要思路是从“纳入城市供电网络”这个方向进行举证证明开发商违约。\n所以，二审中，我变换思路，以以下思路进行：\n从“正式供电”方向，使用国家国标和法律法规证明小区目前非正式用电，即针对合同中的“正式用电”来上诉； 采用举证倒置的方法，开发商要拿出证据证明商品房符合验收条件：纳入城市供电网络并正式供电。 这里简单说一下二审的相关知识：\n在拿到一审判决书后，你需要在一定的时间内提起二审上诉，否则等于放弃二审上诉；这个时长是多少，请仔细看判决书； 二审上诉状，你需要根据一审判决中，一审法院认定的事实和判决进行一点点反驳。 二审中，你也可以提交新证据； 二审过程 二审过程总共花了将近半小时。\n开庭前 在开庭前，我拿到开发商的答辩状。内容如下图：\n法官让指出不认同的事实 二审法官首先确认双方身份，询问是否有新证据。双方均无新证据。\n二审法官首先让双方分别指出不认同的一审判决的事实。\n我没有料想到法官会有这么一出。因为我的二审上诉状中已经写了。\n随后我从一审判决书找出不认同的事实。\n这里提醒下没有经验的读者：遇到没有料想到的，不要慌，你可以向法官要求更多的时间。\n而开发商的律师也没有料想到这么一出，也一下子找不出。看来对方律师也经验不足。\n法庭调查阶段 进入法庭调查阶段。法官又提问了一审中法官的提问：上诉人，你们现在用电正常吗？现在电价多少？\n我根本不会想到法官会问这个问题，因为在一审中法官已经提问，且我二审上诉状中，我从法律上证明什么是“正常供电”，从常识逻辑上说明正常用电与开发商是否违约是两码事。\n此时，我严重怀疑法官根本没有看我的上诉状。\n无奈下，我只能回答：我们目前不属于正常用电，发生水灾时，别的小区都恢复用电，就我们小区需要半个月才恢复。电价是多少我们也不知道，因为电表不是供电局的。\n就这样，没了。法官根本不会针对我的上诉状来调查。\n针对是否正常用电，我现在总结下来，我觉得可能这样回答更好：\n能否正常用电与开发商是否违约没有关系，如果开发商买汽油用发电机供电给我们用？我们是否属于正常用电？ 业主交给开发商的钱与开发商交给供电局的钱不是一回事，小区业主每月交10万电给开发商，而同样的用电量，开发商可能就只用交8万给供电局，目前小区业主交给开发商的电费按阶梯收费的，而开发商与供电局之间并没有按阶梯收费，统一5毛4。如果开发商存在供电盈利，需要提供转供电资质证明。 法官接下来问开发商：现在你们县城存在多少这种供电情况？\n开发商：我们县城基本都是以这种方式供电。\n此时，我激动了，直接插话：其它小区的情况与本案无关。\n法官问开发商：现在小区，就这几位起诉，对吗？\n开发商：是的。\n法官总结争议焦点：开发商是否应该支付违约金。\n提问阶段 接着法官开始让双方针对争议焦点提问。\n当时，我一下没有反应过来（我也没有准备），浪费了一个好的机会。\n我应该向开发商提问：请解释合同中的“供电”约定，具体指的是什么？然后找突破口。\n同时，从一审经验中，我也发现了，即使涉及案件的关键点，对方不回答，或者回答是假的，法官也大概率不会追究。对的，开发商在提问环节说谎。法官不care。\n这是超出了我认知的事情。\n辩论阶段 经历过这么多次的辩论阶段，我发现这是我作为原告，在法庭唯一能长篇幅表达自己论点的机会。\n并不像在电影里，双方可以针对论点进行相互论证反驳。\n我遇到的情况是，法官可能根本就不给你机会辩论（说白了，就是想办法不让你说话）。我不确定这是不是法官在袒护开发商。\n所以，我总结的经验是：不要等辩论环节说出你的论点，在起诉状里，你就要把所有的论点证据写清楚。\n在过去的几次庭审里，只要允许辩论，开发商就翻车，因为他们辩不过。\n这次辩论阶段，开发商又翻车了。\n我首先发表：\n供电局并不是为小区正常供电，而是为开发商。业主和供电局之间没有供用电合同关系； 开发商没有供电资质；如果有，应该提供证据； 根据国家相差标准，“供电服务”是有明确规定的，规定必须是小区业主与供电局签订合同后，小区才算由供电局提供供电服务，也就不符合《合同》上的正式供电； 应该实施举证倒举原则：开发商应该提供商品房“纳入城市供电网络并正式供电”的证据。 就是对我的上诉状的总结。\n开发商的律师抗辩：\n之前发生水灾，那是不可抗力因素导致； 是因为小区业主不按时缴纳电费，导致小区停电； 整个县城的小区，供电公司都采取合表电价户供电方式。（大概原话） 我抗辩：\n发生水灾停电是不可抗力，但是停电如此长时间，不是不可抗力因素。其它正式供电的小区，供电局很快就恢复电力； 如果小区是供电局正式供电，不会发生这样的情况； 其它小区供电方式的情况与本案无关。 这时，法官宣布进入最后陈述阶段。辩论就这么结束了。\n二审上诉状具体内容 以下是二审上诉状的具体内容有，它们也可以作为读者的一审的上诉状的内容。如果暂时不感兴趣，可以跳到最后。\n一、一审判决认定的“供电局为小区正常供电”，与事实不符。 根据国家标准GB/T 28583-2012《供电服务规范》4.6：供电企业应当加快推进城乡电网“一户一表”改造工程，逐步实现抄表及收费到户和6.5.1：给用户供电前，供电企业应当按照有关规定，遵循平等自愿、协商一致、诚实信用的原则，与用户签订供用电合同。\n证明国家标准对于“供电服务”有明确的规定。而供电局作为提供供电服务的企业，并未与小区代表的业委会或小区业主签订供用电合同。证明供电局没有为小区提供供电服务，也就没有为小区正常供电。\n二、一审判决认定的“业主能正常使用供电部门输出的电”与事实不符，且存在逻辑漏洞。 一审判决忽略了电力可以由多种渠道提供的事实，业主能正常用电不能等同于使用的是供电部门输出的电。这个逻辑很简单，假如开发商在中国石化加油站买了汽油，然后通过汽油发电机发电给业主使用，那么，一审法院是不是会认定业主能正常使用中国石化加油站输出的电？同样的，开发商从供电局购买电力，并由开发商的配电设施输出给业主使用，不应被认定为业主使用的是供电部门输出的电。\n且一审判决中强调开发商与供电局之间的供用电合同关系，却不提上诉人及小区所有业主与供电局不存在供用电合同关系这一事实。\n根据《电力法》第二十五条：供电营业区的设立、变更，由供电企业提出申请，电力管理部门依据职责和管理权限，会同同级有关部门审查批准后，发给《电力业务许可证》。供电营业区设立、变更的具体办法，由国务院电力管理部门制定。证明供电局是区域唯一合法的供电单位。\n根据1996年国务院令[第196号]《电力供应与使用条例》第二十条 供电方式应当按照安全、可靠、经济、合理和便于管理的原则，由电力供应与使用 双方根据国家有关规定以及电网规划、用电需求和当地供电条件等因素协商确定。 \u0026hellip;\u0026hellip;非经供电企业委托，任何单位不得擅自向外供电；第三十八条违反本条例规定，有下列行为之一的，由电力管理部门责令改正，没收违法所得，可以并处违法所得５倍以下的罚款：\u0026hellip;\u0026hellip; （三）擅自向外转供电的。\n再根据被上诉人未完成变压器移交给供电局的事实、被上诉人无转供电资质的事实。\n业主无法自行将自购商品房接入城市供电网络或者公用配电线路，如果不使用开发商的非法“转供电”，则无法正常生活。被上诉人，即不是合法供电企业，也没有转供电资质。业主目前是被迫使用开发商的非法“转供电”，这不能算正常用电，更不是国家标准认定的正式用电。\n再根据《供电服务规范》4.6和6.5.1，上诉人没有与供电局签订供用电合同，所以，上诉人使用的不是供电局的供电服务，这也就证明“业主能正常使用供电部门输出的电”与事实不符。\n同时，供电局的《复函》中提到的“开发商供电设施配套并完成验收，供电公司正常供电”。只能证明供电公司与开发商供电设施配套之间验收成功，并不代表开发商供电设施配套到入户的供电设施验收成功；也只能证明供电公司是正常供电给开发商，不能证明业主使用的是供电公司输出的电。开发商供电设施的产权属于开发商，也能证明此事实。\n三、一审判决认定的“原、被告在合同的约定不明确”，与事实不符。事实与理由如下： （一）、供电局不具有《商品买卖合同（预售）》的释义权。\n《商品买卖合同（预售）》的封面证明了其释义权是中华人民共和国住房和城乡建设部与中华人民共和国国家工商行政管理总局。所以，《商品买卖合同（预售）》释义权不在供电局。一审判决中不能以供电局的释义来解释“实现接入城市供电网络”，更不能以“实现接入城市供电网络非电网企业通用词无法释义”为由，认定“原、被告在合同的约定不明确”。\n（二）、作为房地产开发商的被上诉人应该知道，也必须知道商品房是买卖合同的标的物和商品房的交付条件。\n根据《合同法》第一百三十条 买卖合同是出卖人转移标的物的所有权于买受人，买受人支付价款的合同。商品房作为《商品买卖合同（预售）》买卖合同的标的物，“供电：纳入城市供电网络并正式供电”是标的物的基础设施的交付条件之一，即商品房被纳入城市供电网络并正式供电是质量条件之一。作为房地产开发商的被上诉人应该知道，也必须知道。\n根据《合同法》第一百一十一条质量不符合约定的，应当按照当事人的约定承担违约责任，再根据《商品买卖合同（预售）》中约定的逾期交付责任，上诉人在一审中要求被上诉人支付违约金，即合理，也合法。\n（三）、供电局是范围唯一供电营业机构，商品房的交付条件“供电：纳入城市供电网络并正式供电”即代表商品房应该，也只能由供电局直接供电。作为房地产开发商的被上诉人应该知道，也必须知道这一事实。\n根据《电力法》第二十五条：供电企业在批准的供电营业区内向用户供电。供电营业区的划分，应当考虑电网的结构和供电合理性等因素。一个供电营业区内只设立一个供电营业机构。\n又根据《合同法》第三十五条：当事人采用合同书形式订立合同的，双方当事人签字或者盖章的地点为合同成立的地点。\n再根据《城镇一户一表改造的若干规定》第二条工程改造的原则中，规定一户一表的改造应与城市配电网改造相结合；进户线的改造应与户内配线相结合。\n商品房的交付条件“供电：纳入城市供电网络并正式供电”即代表商品房应该，也只能由供电局直接供电。作为房地产开发商的被上诉应该知道，也必须知道这一事实。\n（四）、“纳入城市供电网络”只是手段，“正式供电”是目的。\n根据《电力法》第二十五条，能为上诉人购买的商品房合法供电的，只能是供电局。证明商品房就必须由供电局直接供电。\n根据国标GB/T 28583-2012《供电服务规范》4.6：供电企业应当加快推进城乡电网“一户一表”改造工程，逐步实现抄表及收费到户、6.5.1节：给用户供电前，供电企业应当按照有关规定，遵循平等自愿、协商一致、诚实信用的原则，与用户签订供电用合同。证明购买商品房的业主不与供电局签订供用电合同，就无法使用供电局的供电服务。\n因此，商品房要使用供电局的供电服务（即正式供电），就必须业主与供电局签订供电用合同（即一户一表）。换句话说，一户一表是供电局正式供电给商品房的先决条件。\n商品房要得到正式供电，那么纳入（接入）城市供电网络、城市配电网络、公用配电线路、城市供电系统，都是实现正式供电的手段。\n只要商品房不是被供电局直接供电，无论使用什么手段，商品房都没有达到《商品买卖合同（预售）》中约定的交付条件。\n（五）、房地产开发商，应该知道，也必须知道商品房必须按一户一表要求建设，并为商品房实现一户一表。\n根据国家电力公司发布实施《城镇一户一表改造的若干规定》第五项工程改造资金，第4条：新建居民住宅一律按新标准和一户一表要求进行建设。所需资金应列入住宅建设投资之中；第二条工程改造的原则中，规定：在一户一表改造的同时，实现集中抄表；第四条工程改造的步骤中的第三步：把企事业单位自供职工住宅及由其转供电的居民住宅改造为一户一表，抄表到户。\n作为房地产开发商，应该知道，也必须知道商品房必须按一户一表要求建设，并为商品房实现一户一表。\n四、一审判决适用法律错误 一审判决认定“原、被告在合同的约定不明确”，并认定原告证据不足，所以驳回原告的诉讼请求。\n根据《合同法》第六十二条 当事人就有关合同内容约定不明确，依照本法第六十一条的规定仍不能确定的，适用下列规定：（一）质量要求不明确的，按照国家标准、行业标准履行；没有国家标准、行业标准的，按照通常标准或者符合合同目的的特定标准履行。\n在“实现接入城市供电网络”定义不明确的情况下，合同也应该按国家标行业标准履行。根据《电力法》第二十五条，上诉人所购买的商品房并未得到供电局的正式供电，即被上诉人合同违约。\n关于证据不足，一审法院应考虑到上诉人作为小区内200多户的消费者之一，无法提供自己与供电局没有签订供用电合同关系的证据，属于举证困难情况。被上诉人作为商品房商家，有责任提供为商品房实现正式供电的证明。所以，提议法院对举证责任进行倒置，应该由被上诉人提供证明上诉人所购买的商品房已经接入城市供电网络并正式供电。\n插曲 在我们的二审结束后，该法庭接着是开发商与另一个业主关于房产证逾期办理的案件。\n我们就旁听了该庭审。\n有趣的是，法官似乎对本案不感兴趣，倒是对我们的县城的其它类似的案件感兴趣。庭审过程10分钟不到就结束了。\n问了开发商这些问题：\n目前小区里只有这一个业主起诉你们吗？ 之前的那个再审案件（也是业主起诉开发商房产证逾期，小额诉讼程序，业主赢了，开发商提起再审），一审法院为什么不受理？ 之前的那个再审申请书，是怎么写的？ 这些问题都不会被记录在庭审记录里\n读者，你们觉得二审法官为什么会这些问题？\n二审小结 二审结果，还没有出来。我也不知道判决书会如何写。\n邻居问我有多少把握？\n我心里认为，上诉状里，我已经把一审判决的所有的论点都反驳了，并且找到明确的法律条文来反驳，按道理应该是100%赢了。\n我回答：只有70%的机会吧。\n谁知道呢？在过往的一审（小额程序里）经历里，法官居然拿没有经过我们质证的证据来进行判决。是的，你没有看错（懂的人都懂）。我不想在这里多说。\n二审即终审，与判决结果 二审的结果并没有出乎我的意料：维持原判。所以是我输了这场官司。\n只是我输得非常不服气。\n在二审中，我变换思路，以以下思路进行辩论：\n从“正式供电”方向，使用国家国标和法律法规证明小区目前非正式用电，即针对合同中的“正式用电”来上诉； 采用举证倒置的方法，开发商要拿出证据证明商品房符合验收条件：纳入城市供电网络并正式供电。 但是，中院在判决书中根本不提这些。而是认定我们尝试证明“一户一表”等同于“纳入城市供电网络并正式供电”的论据，是我们的上诉的理由。\n二审判决书中，关键部分（经过脱敏的）原文 本院二审期间，当事人没有提交新证据。经查，一审查明事实清楚，本院予以确认。\n本院认为，上诉人与被上诉人签订的《商品房买卖合同(预售)》系双方真实意思表示,内容未违反法律、行政法规，双方均应遵照履行。《商品房买卖合同(预售)》第十条约定交付时供电条件为:纳入城市供电网络并正式供电。虽然上诉人认为《商品买卖合同(预售)》的释义权归中华人民共和国住房和城乡建设部与中华人民共和国国家工商行政管理总局，供电局不具有释义权，但供电公司作为负责管辖区范围内供电服务和电网运维工作的机构，一审法院向供电局发函咨询“纳入城市供电网络”的释义并无不妥。上诉人认为案涉商品房要实现正式供电，必须按“一户一表”进行建设，但上诉人与被上诉人签订的 《商品买卖合同(预售)》并未明确约定商品房交付时要配备“一户一表”的供电设施，本院对上诉人的该上诉理由不予采纳。供电局出具的复函中已确认案涉小区开发商与供电企业存在供用电合同关系，且开发商的配电设施已经接入公用配电线路。现业主也能正常使用供电部门输出的电，上诉人主张被上诉人未按照《商品买卖合同(预售)》约定供电的理由不能 成立，本院对上诉人的主张不予支持。\n综上所述，xxx的上诉请求不能成立，应予驳回; 一审判决认定事实清楚，适用法律正确，应予维持。依照《中华人民共和国民事诉讼法》第一百七十七条第一款第一项规定，判决如下: 驳回上诉，维持原判。\n以下是二审判决书中的其它部分（已脱敏）原文 xxx上诉请求: 一、撤销原审判决;并依法改判 为被上诉人向上诉人支付违约金支付违约金 xxxxx 元;\n二、本案一审、二审诉讼费用由被上诉人承担。\n事实和理由:一审判 决认定的“供电局为小区正常供电”，与事实不符。\n一、供电局作为提供供电服务的企业，并未按照《供电服务规范》的规定与小区代表的业委会或小区业主签订供用电合同。证明供电局没有为小区提供供电服务，也就没有为小区正常供电。\n二、一审判决忽略了电力可以由多种渠道提供的事实，业主能正常用电不能等同于使用的是供电部门输出的电。开发商从供电局购买电力，并由开发商的配电设施输出给业主使用，不应被认定为业主使用的是供电部门输出的电。业主目前是被迫使用开发商的非法“转供电”，这不能算正常用电，更不是国家标准 认定的正式用电。同时，供电局的《复函》中提到的“开发商供电设施配套并完成验收，供电公司正常供电”。只能证明供 电公司与开发商供电设施配套之间验收成功，并不代表开发商供电设施配套到入户的供电设施验收成功;也只能证明供电公司是 正常供电给开发商，不能证明业主使用的是供电公司输出的电。开发商供电设施的产权属于开发商，也能证明此事实。\n三、县供电局不具有《商品买卖合同(预售)》的释义权。一审判决 中不能以供电局的释义来解释“实现接入城市供电网络”， 更不能以“实现接入城市供电网络非电网企业通用词无法释义” 为由，认定原、被告在合同的约定不明确”。因房屋未能正常供电，说明被上诉人交付的商品房有质量问题，再根据《商品买卖 合同(预售)》中约定的逾期交付责任，上诉人在一审中要求被 上诉人支付违约金，即合理，也合法。商品房要符合交付条件是 要“供电”且是纳入城市供电网络并正式供电，即只能由供电局直接供电。这就要求商品房必须按一户一表要求建设，并为商品房实现一户一表，现案涉商品房并没有符合这一条件。四、 提议法院对举证责任进行倒置，应该由被上诉人提供证明上诉人所购买的商品房已经接入城市供电网络，并正式供电。综上所述， 一审判决认定事实和适用法律错误，为了维护上诉人的合法权 益，请二审法院查明事实，支持上诉人的上诉请求。\n房地产投资有限责任公司辩称，房地产投 资有限责任公司辩称，一审法院认定事实正确，上诉人认为一审认定事实不符没有任何法律依据。上诉人说的国家标准只是标准，不是法律的强制性规定，双方在合同中也没有约定，“一户一表”是国家标准中希望供电企业加快推进的改造工程。上诉人偷换概念，一审认定中“原被告的约定不明确”与“证据不足” 并没有逻辑关系，一审判决符合法律规定。上诉人提出举证责任 倒置不符合法律规定，举证责任倒置一般存在与特殊的侵权案件 中，不是可以随意适用的。综上，答辩人认可一审的判决，请求二审驳回上诉人的诉请，维持原判。\nxxx向一审法院起诉请求:\n依法判决被告立即 支付违约金 xxxx 元; 本案诉讼费由被告承担。 一审法院认定事实:原告xxx和xxx是夫妻关系。xxxx 年，原告xxx与被告开发商签订了《商品房买卖合同(预售)》，合同约定原告xxx购买被告开发商开发的一期 x 栋 x 单元 x 层 x 号房屋，房屋总价款为 xxxxx 元。关于商品房相关设施设备交付条件:合同第十条约定“(一)基础设施设备。 1\u0026hellip;\u0026hellip;.。2.供电:交付时纳入城市供电网络并正式供电。\u0026hellip;\u0026hellip;。 如果在约定期限内基础设施设备未达到交付使用条件，双方同意按照下列第 1 种方式处理:(1)以上设施中第 1、2、3、4 项在 约定交付日未达到交付条件的，出卖人按照本合同第十二条的约 定承担逾期交付责任。\u0026hellip;\u0026hellip;。第十二条:逾期交付责任:\u0026hellip;\u0026hellip;。 买受人要求继续履行合同的，合同继续履行，出卖人按日计算向 买受人支付全部房价款万分之二的违约金。合同签订后，原告交清房款及相关费用。被告开发商向原告xxx交付 x 栋 x 单元 x 层 x 号房屋。现原告居住该房屋，并能正常供电使用。至原告起诉时，被告开发商开发的小区的变压器在被告名下，供电局为小区正常供电，用电后是由被告开发商向供电局缴纳小区电费，再由被 告开发商委托物业向业主代抄电表，业主根据抄表明细缴费，还未能实现一户一表。xxx 年 x 月，原告认为被告在交房时供 电未纳入城市供电网络，停止向被告委托的物业公司缴纳电费。 xxx 年 x 月，被告曾向该院提起诉讼要求原告给付电费。xxxx 年 x 月 x 日，原告诉至该院。\n本案在审理过程中，向电力投资集团有限责任公司供电局咨询:“如何理解实现接入城市供电网络和安装开发商 供电设施配套并完成验收，供电公司正常供电，但变压器在开发商名下，未能实现一户一表，现用电是由开发商向电力部门缴纳小区电费，再由开发商委托物业向业主代抄电表，业主根据抄表明细缴费，是否属于实现接入城市供电网络”。供电局复函该院，《复函》:(1)实现接入城市供电网络非电网企业通用词无 法释义;(2)开发商供电设施配套并完成验收，供电公司正常供电，但变压器在开发商名下，未能实现一户一表，现用电是由开 发商向电力部门缴纳小区电费，在由开发商委托物业向业主代抄 电表，业主根据抄表明细缴费的理解:若开发商向供电企业缴纳电费，说明开发商与供电企业存在供用电合同关系，即该开发商的配电设施已经接入公用配电线路。\n以上事实，有原告身份证复印件、被告《营业执照》复印件、 《结婚证》、《商品房买卖合同(预售)》、被告起诉原告要求给付 电费的民事起诉状、被告提供的被告最近 4 个月的电费通知单、 缴纳电费的转账凭证、被告交纳电费的发票、房地产协会 《关于房地产开发小区供电情况的说明》、照片、该院向 供电局咨询的《复函》及庭审笔录等证据材料所证实。\n审法院认为，原告xxx与被告开发商签订的 商品房销售合同系双方当事人真实意思表示，内容与形式不违反法律、行政法规的强制性规定，该合同合法有效。原告认为小区 的供电未接入城市供电网络而提起诉讼，被告辩称原告的诉讼已 超过诉讼时效，该院认为根据《中华人民共和国民法典》第一百 八十八条:“诉讼时效期间自权利人知道或者应当知道权利受到 损害以及义务人之日起计算。法律另有规定的，依照其规定。” 由于双方签订的是格式合同条款，被告未能提供证据证明原告从签订合同之日起就清楚告知被告建设的小区的供电模式，因此， 对于被告的辩称意见，该院不予支持。原告诉称被告小区的供电方式未符合《商品房买卖合同》中的接入城市供电网络并正式供 电的约定，要求被告支付违约金。被告辩称已经完成供电设备的安装并验收合格，电力公司对小区正式供电，被告已经履约完成 关于交付时纳入城市供电网络并正式供电的合约，请求法院驳回 原告的诉讼请求。该院认为，按理被告应将小区电力设施移交供 电部门，业主可以直接向供电部门缴纳电费。现被告未将电力设 施移交供电部门，业主也能正常使用供电部门输出的电。双方在 合同约定交房条件为“供电:交付时纳入城市供电网络并证实供电”，对该约定内容的含义，该院致函新电力集团供电局，该局复函说明:(1)实现接入城市供电网络非电网企业通用词无 法释义;(2)开发商向供电企业缴纳电费，说明开发商与供电企 业存在供用电合同关系，即该开发商的配电设施已经接入公用配 电线路。由于原、被告在合同的约定不明确，现原告诉称被告交房时供电未纳入城市供电网络构成违约，要求支付违约金的请求，证据不足，该院不予支持。综上，依照《最高人民法院关于 适用《中华人民共和国民事诉讼法》的解释》第九十条之规定， 判决:驳回原告xxx的诉讼请求。案件受理费减半收取25元，由原告xxx负担。\n就这样完了？ 肯定就不能这么完了。\n经过一审二审，得到了“整个县城几乎都没有实现一户一表”的证据，通过这个证据向电力部门和城建部投诉。\n本小区里还有其他人也需要上诉，我会根据之前经验再次优化诉讼过程，优化内容：\n在下次上诉类似的案件，明确写清论据论点； 将“纳入城市供电网络并正式供电”的请求写入到诉求中（在一审就写了，但是立案时，庭长要求去掉）； 在提问阶段，让开发商解释“供电”条文； 在法庭调查阶段，如果法官提问是否正常用电，反问法官什么是正常用电？ 开发商从供电局花0.54买的电，然后从业主那收0.56是否违约？ 向省电网公司咨询“纳入城市供电网络并正式供电”的定义。 向国家住建部咨询该合同的解释 在住建部官方网站（搜索关键定：商品房买卖合同），已经有类似的提问：\n《商品房买卖合同》（GF-2014-0171）第四章，第十条“商品房相关设施设备交付条件（一）基础设施设备：1、供水、排水：交付时供水、排水配套设施齐全，并与城市公共供水、排水管网连接。使用自建设施供水的，供水的水质符合国家规定的饮用水卫生标准；2、供电：交付时纳入城市供电网络并正式供电；”咨询点：第一：供水“与城市公共供水管网连接”的认定标准是什么？第二：供电：“纳入城市供电网络”的认定标准什么？第三：商住楼每户业主（产权人）既不是自来水公司的用户，也不是电力公司的用户，之间都没有建立供用水和供用电的合同关系，属于被“转供水”和被“转供电”的非永久性状态，此种情况属于纳入还是没有纳入？\n住建部的回复如下：\n“与城市公共供水管网连接”的具体认定标准，建议咨询当地城市供水行政主管部门；“纳入城市供电网络”的具体认定标准，建议咨询当地电力管理部门。\n","permalink":"https://showme.codes/zh-cn/2024-01-22-owners-and-developers-fight-lawsuit/","summary":"\u003ch2 id=\"导言\"\u003e导言\u003c/h2\u003e\n\u003cp\u003e正如标题所言，我是一名程序员。2023年5月之前，我是一个法盲，不懂起诉的流程，更不懂开庭的步骤。\u003c/p\u003e\n\u003cp\u003e但在过去的一年中，我以各种身份参与庭审多次，包括：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e以被告的公民代理的身份，为被告辩护，参加庭审：1次\u003c/li\u003e\n\u003cli\u003e以原告的公民代理的身份，为原告辩护，参加庭审：1次\u003c/li\u003e\n\u003cli\u003e以原告的身份起诉开发商：2次\u003c/li\u003e\n\u003cli\u003e以起诉人身份参加二审：1次\u003c/li\u003e\n\u003cli\u003e以旁听的身份参加庭审：2次\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e多次为小区其他业主免费写起诉状、答辩状、再审申请书等多份文书。\u003c/p\u003e\n\u003cp\u003e但是结果是什么呢？后文会说。如果你只想知道官司输赢，可以直接划到文章最后。\u003c/p\u003e\n\u003cp\u003e本文比较长，你可以挑选自己感兴趣的部分开始。\u003c/p\u003e\n\u003cp\u003e如果您觉得本文有意义，还请转发给其他遇到相同问题的业主，以帮助更多的人。如果帮到你，还请用实际行动赞赏本文。\u003c/p\u003e\n\u003cp\u003e你我的权益，需要法制社会来保护，也需要你我的努力。\u003c/p\u003e\n\u003ch2 id=\"背景介绍\"\u003e背景介绍\u003c/h2\u003e\n\u003cp\u003e我是2017年购买的商品房，坐落在一个18线的县城。开发商是市里的一家房地产开发商。\u003c/p\u003e\n\u003cp\u003e但是直到现在小区还是由物业代抄电表，代收电费交给开发商。\u003c/p\u003e\n\u003cp\u003e在2020年和2022年的两次水灾中，其它实现了一户一表的小区，供电局很快就恢复供电了。但是我们小区停电了半个多月，因为开发商需要自己找人去修该变压器。\u003c/p\u003e\n\u003cp\u003e这里有两个背景知识：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e开发商需要将小区的变压器的产权无偿移交给供电局，变压器的维护才由供电局负责；\u003c/li\u003e\n\u003cli\u003e一户一表：说大白话就是由业主与供电局直接发生供用电关系，而不是由开发商代缴电费。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e业主是2020后才知道小区并不是一户一表的。所以，部分小区业主从那时开始拒绝“交电费”。开发商只能为这部分业主“垫付”电费。\u003c/p\u003e\n\u003cp\u003e2023年5月，开发商不再为业主“垫付”电费。小区因此被供电局停电。\u003c/p\u003e\n\u003cp\u003e接下来，小区业主与开发商、镇政府、供电局、住建局等多方，进行长时间地“拉扯”（中间的故事可以再写一篇文章，但是考虑到篇幅，本文不详写）。\u003c/p\u003e\n\u003cp\u003e虽然现在小区有电用，但至今小区依然没有实现一户一表。这就是多方拉扯的结局。懂的都懂。\u003c/p\u003e\n\u003cp\u003e其间，我开始研究业主与开发商之间的合同《商品房买卖合同》（由国家住建部和国家工商局2014年制定的格式合同），商品房验收条件中，关于电的部分，合同上的原文是这样的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e供电：交付时纳入城市供电网络并正式供电。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e也就是合同里并没有写明“一户一表”。也请读者朋友拿出自己的合同，找到相关条文确认。因为合同一字之差就差个十万八千里。\u003c/p\u003e\n\u003cp\u003e根据这条合同条文，我们不能以“未实现一户一表”来起诉开发商。只能起诉开发商没有实现“交付时纳入城市供电网络并正式供电”。\u003c/p\u003e\n\u003cp\u003e本文为了方便，会将两“一户一表”与“交付时纳入城市供电网络并正式供电”混用，但是它们意思大致相同。\u003cstrong\u003e注意，在庭审过程，它们不是同一事物\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e这里再次提醒读者：起诉开发商，必须以合同为依据起诉。\u003c/p\u003e\n\u003cp\u003e以上简单介绍了背景知识（实际情况更复杂）。接下来解答一些读者会提出来疑问：\u003c/p\u003e\n\u003ch3 id=\"为什么要写下这些\"\u003e为什么要写下这些\u003c/h3\u003e\n\u003cp\u003e在前几天，我刚刚结束了二审（判决结果还没有出），我决定将整个经历写下来。原因有二：\u003c/p\u003e\n\u003cp\u003e一是因为我这人记性不好，想记录下这段有意义的经历；二是希望为法制社会做出一点点微薄的贡献。\u003c/p\u003e\n\u003ch3 id=\"为什么不找律师\"\u003e为什么不找律师？\u003c/h3\u003e\n\u003cp\u003e我在找律师前做了很多功课，比如起诉开发商涉及的法律条文、开发商会如何抗辩等。但是，在找过多家律师所后，我最终决定不找律师。\u003c/p\u003e\n\u003cp\u003e原因有：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e本县城的律师不愿意为我们打官司，有律师认为钱太少，有律师听到小区名字就说不打。部分业主甚至愿意与律师平分违约金。\u003c/li\u003e\n\u003cli\u003e隔壁县城的律师找了两个，我个人判断不可靠。当我拿出《合同》与他们讨论其中的条文时，他们并没有给出令我满意的答案，而且整个市的律师都在同一个律师协会里，懂的都懂；\u003c/li\u003e\n\u003cli\u003e省会的律师不太可能受理我们这种小金额官司。连他们的差旅费，我们可能都给不起；\u003c/li\u003e\n\u003cli\u003e小区里并不是所有的人都愿意打这个官司（这个是关键）。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我以个人名义打官司，就不会有以上问题了。\u003c/p\u003e\n\u003ch3 id=\"为什么不是起诉物业\"\u003e为什么不是起诉物业？\u003c/h3\u003e\n\u003cp\u003e说白了，小区物业就是开发商的一家子公司。所以很多业主经常误以为物业就是开发商，毕竟“交电费”是直接交给的物业的。\u003c/p\u003e\n\u003cp\u003e小区的几次停电，部分业主跑去物业那里“闹”，这是找错对象了。因为我们小区的供电主体是开发商，不是物业。\u003c/p\u003e\n\u003cp\u003e再多说一句，如果物业没有代缴电费的权力（具体要看物业合同），所以他们也无法起诉你不交电费给他们。\u003c/p\u003e\n\u003ch3 id=\"为什么不是起诉开发商不移交变压器\"\u003e为什么不是起诉开发商不移交变压器？\u003c/h3\u003e\n\u003cp\u003e这在上文已经说明了，要以合同为依据来起诉。合同里并没有写“移交变压器”的条文。\u003c/p\u003e\n\u003ch3 id=\"集体诉讼还是一个个单独诉讼\"\u003e集体诉讼，还是一个个单独诉讼？\u003c/h3\u003e\n\u003cp\u003e在没有接触真正的诉讼流程前，我也以为我们是可以集体诉讼的。\u003c/p\u003e\n\u003cp\u003e但是在学习后，发现中国是没有集体诉讼的概念的。只能单独诉讼。如果我错了，还请纠正我。多谢。\u003c/p\u003e\n\u003cp\u003e假如有律师接受整个小区对开发商的诉讼，律师也是分别和每一位业主签法务合同。律师只不过是批量操作，并不是真正意义的集体诉讼。\u003c/p\u003e\n\u003ch3 id=\"我可以代理其他人的诉讼吗\"\u003e我可以代理其他人的诉讼吗？\u003c/h3\u003e\n\u003cp\u003e如果没有集体诉讼，那么，我将面临两个问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e我是自己起诉开发商，还是和其业主一起？\u003c/li\u003e\n\u003cli\u003e我可以代理其他业主的起诉吗？并不是每一个业主都有时间。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e对于问题1，经过深思熟虑，我做出的策略是：我以个人名义单独起诉。\u003c/p\u003e\n\u003cp\u003e因为我完全没有诉讼经验，不知道法官和开发商会如何抗辩。即使我本人这次官司输了，其他人也可以以我的诉讼经验发起另一次诉讼，直到打赢。\u003c/p\u003e\n\u003cp\u003e关于问题2，你是可以以公民代理的身份代理你的邻居或者亲戚的诉讼。中国不允许没有律师执照的人为其他人代理诉讼，公民代理算是对这机制的补充。如果我理解错了，还请纠正，谢谢。\u003c/p\u003e\n\u003cp\u003e公民代理的方法是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e取得证明你们关系。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003e如果是邻居关系，就拿双方的购房合同或者房产证去当地居委会开证明；\u003c/li\u003e\n\u003cli\u003e如果是亲戚关系，就拿双方的户口去公安局、居委会或者村委会。可能每个地方不一样，你需要咨询当地的居委会或者村委会；\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003e准备授权委托书，即你的邻居或者亲戚将案件委托给你的证明；\u003c/li\u003e\n\u003cli\u003e在提交起诉状或者开庭前向法庭提交双方身份证复印件、委托书和关系证明。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e以上是我个人的总结。但是，还是建议有需求的读者，请咨询当地法院。\u003c/p\u003e\n\u003cp\u003e题外话，我在给邻居代理时，发生了两件“有趣”的事情。\u003c/p\u003e\n\u003cp\u003e一件是当我给其中邻居代理房产证逾期的诉讼时，负责立案的”漂亮“的小姐姐，气急败坏似地不给任何理由地拒绝了我的代理。这超出了我认知。她不是在法院知法犯法吗？\u003c/p\u003e\n\u003cp\u003e另一件也是这个“漂亮”的小姐姐，我在立案时，故意刁难我，本来可以使用微信线上交纳诉讼费的，却跟我说法院规定月底最后三天，需要线下去银行柜台汇款。\u003c/p\u003e\n\u003cp\u003e当时，我不清楚，天真去线下交了，浪费了我大量时间。后来有一次也是月底，我看到其他所有人都不需要线下汇款。我想法院应该是没有任何理由拒绝我线上支付的。如果有懂相关法律的读者可以告诉我。\u003c/p\u003e","title":"程序员想告赢开发商一户一表违约"},{"content":"Overview In this document, I\u0026rsquo;ll describe my solution from the following parts:\nPart1 Architecture: describe the desired state of the architecture Part2 Implementation: Code Structure Introduce How to Build it Deploy Nginx Controller using Helm Part1: Architecture We assume that the project has a project named: health. Here\u0026rsquo;s the architecture graph, which draw by Excalidraw\nNetwork Architecture I created 4 subnets that are evenly distributed to 2 Availability Zones. Each availability zone has 2 subnets, one is public subnet,and another one is private subnet. The public subnet goes out through the Internet gateway and the private subnet goes out through the NAT gateway.\nLog We also want to log events in CloudWatch for the cluster, so we create a log group for the eks cluster.\nSecurity In order to ensure network security, we create some security groups and rules for eks cluster.\nPart2: Implementation Code Structure Why Monorepo A monorepo is a single repository containing multiple distinct projects, with well-defined relationships.\nThis is a monorepo. So everything of code in it. It\u0026rsquo;s one of reason we choose Bazel as our build tool.\nMany benefits from monorepo as below:\nNo overhead to create new projects:Use the existing CI setup, and no need to publish versioned packages if all consumers are in the same repo. Atomic commits across projects: Everything works together at every commit. There\u0026rsquo;s no such thing as a breaking change when you fix everything in the same commit. One version of everything: No need to worry about incompatibilities because of projects depending on conflicting versions of third party libraries. Developer mobility: Get a consistent way of building and testing applications written using different tools and technologies. Developers can confidently contribute to other teams’ applications and verify that their changes are safe. Structure Introduce % tree -L 5 . ├── BUILD.bazel ├── README.md ├── WORKSPACE # It\u0026#39;s a kind of Bazel project\u0026#39;s file that to defined external dependencies. ├── charts # It stores all Helm charts. │ ├── BUILD.bazel │ └── nginx-ingress # It\u0026#39;s downloaded from Nginx official ├── doc # It stores all docs in the monorepo. Each subfolder presents a project. │ ├── BUILD.bazel │ └── health │ ├── BUILD.bazel │ ├── architecture.excalidraw │ ├── architecture.svg │ └── terraform-graph.svg ├── docker-compose.yaml # It\u0026#39;s useful on local environment. ├── environments # It stores all the configuration of 3 environments. # Each environment folder has the same structure, by convention. │ ├── BUILD.bazel │ ├── production │ │ └── BUILD.bazel │ ├── staging # Store the configuration code of the staging environment for all projects. Each directory represents a project. # for more details, we can see it on test folder. │ │ ├── BUILD.bazel │ │ └── health │ │ ├── BUILD.bazel │ │ ├── app │ │ │ ├── BUILD.bazel │ │ │ └── nginx-ingress.jsonnet │ │ ├── infra │ │ │ ├── BUILD.bazel │ │ │ └── main.tf.jsonnet │ │ ├── secrets.libsonnet │ │ ├── secrets.libsonnet.secret │ │ └── vars.libsonnet │ ├── template │ │ ├── BUILD.bazel │ │ ├── aws # It stores all templates of aws terraform resource. │ │ │ ├── BUILD.bazel │ │ │ ├── eip.libsonnet │ │ │ ├── security_group_rule.libsonnet # security groups and rules for eks │ │ │ ├── example-main.tf.json │ │ │ ├── eks_cluster.libsonnet │ │ │ ├── eks_node_group.libsonnet │ │ │ ├── iam_role.libsonnet │ │ │ ├── iam_role_policy_attachment.libsonnet │ │ │ ├── internet_gateway.libsonnet │ │ │ ├── main.libsonnet │ │ │ ├── nat_gateway.libsonnet │ │ │ ├── route_table.libsonnet │ │ │ ├── route_table_association.libsonnet │ │ │ ├── subnet.libsonnet │ │ │ ├── tags.libsonnet │ │ │ └── vpc.libsonnet │ │ ├── azure # It stores all templates of azure terraform resource. │ │ │ └── BUILD.bazel │ │ └── gcp # It stores all templates of gcp terraform resource. │ │ └── BUILD.bazel │ └── test # Store the configuration code of the test environment for all projects. Each directory represents a project. │ ├── BUILD.bazel │ └── health # health project │ ├── BUILD.bazel │ ├── app # applications in health project │ │ ├── BUILD.bazel │ │ └── nginx-ingress.jsonnet # the values file, which in json format. │ ├── infra │ │ ├── BUILD.bazel │ │ └── main.tf.jsonnet # terraform main file │ ├── secrets.libsonnet # It\u0026#39;s already ignored by gitignore. Here is just a example that show you that a file stores some secrets. │ ├── secrets.libsonnet.secret # It is the file that is encrypted by git-secrets and will eventually be committed to the git repository. │ └── vars.libsonnet # All variables of this project. The variable in it can be referenced by anywhere. └── tools # Store some useful scripts ├── BUILD.bazel └── delete-vpc.sh Where is HCL? As you notice that there\u0026rsquo;s not exists any HCL file in our repo. Because I choose another solution to declare our infrastructure.\nTerraform provides 2 kinds of syntax to declare resources:\nHCL syntax: Most Terraform configurations are written in it. JSON syntax: Terraform also supports an alternative syntax that is JSON-compatible. However, compared to the HCL language, JSON format used by fewer people. The reason I choose JSON syntax is that the combine of Jsonnet and Bazel has many benefits as below:\nJsonnet is a complete programing language in configuration domain, which supports generating many kinds of configuration file, open source from Google. It can solve many configuration problems natively. And HCL did not do better than Jsonnet. For example, nested loop function is easy use in Jsonnet, but HCL not. For details as below: HCL\u0026rsquo;s way: https://blog.boltops.com/2020/10/06/terraform-hcl-nested-loops/ Jsonnet\u0026rsquo;s way: Array Comprehensions section in https://jsonnet.org/ref/language.html Slow speed of compile and test is painful when your project is big. The ways of reducing painful of them include: cached build, distributed build and build on demand. And Bazel supports them all well. Of course, there\u0026rsquo;s a Bazel rule that supports building Jsonnet. We\u0026rsquo;re able to write unit testing easily. So, finally, the tools we are using are:\nTerraform Jsonnet Bazel Another articles about Jsonnet:\nFractal Application Streamlining Terraform configuration with Jsonnet What about Secret? We use git-secret for hiding our secrets in the monorepo. And the reveal secret key we can save it into GitHub Actions secrets for building.\nTerraform Graph We can generate a graph with command: terraform graph | dot -Tsvg \u0026gt; graph.svg.\nIt will be updated before each commit by pre-commit, if you installed pre-commit.\nHow to Build it on local development environment You can follow these two steps to build the code at first time:\ninstall Bazelisk run bazel test //... \u0026amp;\u0026amp; bazel build //... in root of project NOTE: You should install pre-commit that to make sure something is ok before you commit.\nWhat\u0026rsquo;s the Output after Build After you build it successfully, you can get a main.tf.json in the bazel folder where is located in bazel-bin/environments/test/health/infra.\nWe give an example of main.tf.json in environments/template/aws/example-main.tf.json .\non GitHub Actions We use GitHub Actions as our CI/CD platform. All things of it defined in .github/workflows folder.\nDeploy Nginx Controller using Helm Helm is a deployment tool for Kubernetes. The deployment command is helm install -n \u0026lt;release ns\u0026gt; -f \u0026lt;values file\u0026gt; \u0026lt;helm release name\u0026gt; \u0026lt;chart path\u0026gt;.\nThe values file of Nginx Controller is written by Jsonnet also. So we have to build them before the deployment.\non Local Development Environment You can follow these steps to deploy application to Kubernetes:\nconfig your kubernetes config in your ~/.kube/config file. run command bazel test //... \u0026amp;\u0026amp; bazel build //... in the root of this repo, then you would get a Helm values file with JSON format. deploy it with helm install -n nginx-ingress -f bazel-bin/environments/test/health/app/nginx-ingress.json nginx-ingress ./charts/nginx-ingress Verify it After finishing the deployment, we can get an endpoint of nginx-controller that exposed by NLB. A few minutes later, the endpoint is available, so that you can access it.\n","permalink":"https://showme.codes/en/2024-01-20-eks-jsonnet-terraform-bazel/","summary":"\u003ch1 id=\"overview\"\u003eOverview\u003c/h1\u003e\n\u003cp\u003eIn this document, I\u0026rsquo;ll describe my solution from the following parts:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePart1 Architecture: describe the desired state of the architecture\u003c/li\u003e\n\u003cli\u003ePart2 Implementation:\n\u003cul\u003e\n\u003cli\u003eCode Structure Introduce\u003c/li\u003e\n\u003cli\u003eHow to Build it\u003c/li\u003e\n\u003cli\u003eDeploy Nginx Controller using Helm\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"part1-architecture\"\u003ePart1: Architecture\u003c/h1\u003e\n\u003cp\u003eWe assume that the project has a project named: health. Here\u0026rsquo;s the architecture graph, which draw by \u003ca href=\"https://excalidraw.com/\"\u003eExcalidraw\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/eks-bazel-arch.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"network-architecture\"\u003eNetwork Architecture\u003c/h3\u003e\n\u003cp\u003eI created 4 subnets that are evenly distributed to 2 Availability Zones. Each availability zone has 2 subnets, one is public subnet,and another one is private subnet.\nThe public subnet goes out through the Internet gateway and the private subnet goes out through the NAT gateway.\u003c/p\u003e","title":"Setting up EKS with Bazel, Jsonnet and Terraform"},{"content":"本文是关于如何使用Bazel搭建Springboot 3.1.0工程（基于JDK17）。为什么使用Bazel，而不是使用Maven或者Gradle？可以看我之前关于Bazel的介绍文章。\n前期准备 在根目录加入.bazelversion文件，并加入6.2.0，指定当前工程使用的Bazel的版本。这样，Bazel命令自动使用该版本的Bazel进行构建。\n在根目录加入.bazelrc文件，并指定构建和测试时使用JDK17，内容如下：\nbuild --java_language_version=17 --java_runtime_version=17 --tool_java_language_version=17 --tool_java_runtime_version=17 test --java_language_version=17 --java_runtime_version=17 --tool_java_language_version=17 --tool_java_runtime_version=17 外部依赖准备 在根目录中创建以下两个文件：\nWORKSPACE：在Bazel中，所有的外部依赖统一定义WORKSPACE文件中； BUILD.bazel：内容留空即可，用于告诉Bazel当前目录也是一个Package。 Bazel本身是支持多语言的。所以，我们需要特定语言的rule来帮助我们在WORKSPACE中定义外部依赖。\n对于Java工程，我们使用rules_jvm_external进行外部依赖的管理。它的使用步骤如下：\n步骤1：在WORKSPACE中增加rules_jvm_external配置 以下配置指定了rules_jvm_external的下载位置，并进行rule的初始化：\nload(\u0026#34;@bazel_tools//tools/build_defs/repo:http.bzl\u0026#34;, \u0026#34;http_archive\u0026#34;) RULES_JVM_EXTERNAL_TAG = \u0026#34;4.5\u0026#34; RULES_JVM_EXTERNAL_SHA = \u0026#34;\u0026lt;sha hash value\u0026gt;\u0026#34; http_archive( name = \u0026#34;rules_jvm_external\u0026#34;, strip_prefix = \u0026#34;rules_jvm_external-%s\u0026#34; % RULES_JVM_EXTERNAL_TAG, sha256 = RULES_JVM_EXTERNAL_SHA, url = \u0026#34;https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip\u0026#34; % RULES_JVM_EXTERNAL_TAG, ) load(\u0026#34;@rules_jvm_external//:repositories.bzl\u0026#34;, \u0026#34;rules_jvm_external_deps\u0026#34;) rules_jvm_external_deps() load(\u0026#34;@rules_jvm_external//:setup.bzl\u0026#34;, \u0026#34;rules_jvm_external_setup\u0026#34;) rules_jvm_external_setup() load(\u0026#34;@rules_jvm_external//:defs.bzl\u0026#34;, \u0026#34;maven_install\u0026#34;) maven_install( artifacts = [ # The project\u0026#39;s dependencies \u0026#34;junit:junit:4.12\u0026#34;, \u0026#34;org.hamcrest:hamcrest-library:1.3\u0026#34;, ], repositories = [ # Private repositories are supported through HTTP Basic auth # \u0026#34;http://username:password@localhost:8081/artifactory/my-repository\u0026#34;, \u0026#34;https://maven.aliyun.com/repository/public\u0026#34;, ], ) 以上采用了非Bzlmod的管理rule。\n步骤2：初始化maven_install.json rules_jvm_external通过maven_install.json对Java依赖的版本进行固定。类似前端工程通过package-lock.json文件，用于固定依赖的版本。\n因为是新工程，需要在根目录执行以下命令生成maven_install.json：\nbazel run @maven//:pin 然后在WORKSPACE中的maven_install语句加入：\nload(\u0026#34;@maven//:defs.bzl\u0026#34;, \u0026#34;pinned_maven_install\u0026#34;) pinned_maven_install() 并在maven_install的参数列表中增加maven_install_json参数，效果如下：\nmaven_install( # artifacts, repositories, ... maven_install_json = \u0026#34;//:maven_install.json\u0026#34;, ) 步骤3：加入Springboot的外部依赖 修改WORKSPACE中maven_install的artifacts的参数，加入Springboot 3.1.0所需的依赖：\nSPRING_BOOT_VERSION = \u0026#34;3.1.0\u0026#34; SPRING_VERSION = \u0026#34;6.0.9\u0026#34; maven_install( artifacts = [ # log \u0026#34;org.slf4j:slf4j-api:2.0.7\u0026#34;, \u0026#34;ch.qos.logback:logback-classic:1.4.6\u0026#34;, # template engine \u0026#34;org.springframework.boot:spring-boot-starter-thymeleaf:%s\u0026#34; % SPRING_BOOT_VERSION, # spring \u0026#34;org.springframework.boot:spring-boot-autoconfigure:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework.boot:spring-boot-configuration-processor:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework.data:spring-data-jpa:%s\u0026#34; % \u0026#34;3.1.0\u0026#34;, \u0026#34;org.springframework.boot:spring-boot-test-autoconfigure:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework.boot:spring-boot-starter-test:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework.boot:spring-boot-starter-validation:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework.boot:spring-boot-test:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework.boot:spring-boot:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework.boot:spring-boot-starter:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework.boot:spring-boot-starter-web:%s\u0026#34; % SPRING_BOOT_VERSION, \u0026#34;org.springframework:spring-webmvc:%s\u0026#34; % SPRING_VERSION, \u0026#34;org.springframework:spring-beans:%s\u0026#34; % SPRING_VERSION, \u0026#34;org.springframework:spring-context:%s\u0026#34; % SPRING_VERSION , \u0026#34;org.springframework:spring-test:%s\u0026#34; % SPRING_VERSION, \u0026#34;org.springframework:spring-web:%s\u0026#34; % SPRING_VERSION, \u0026#34;org.springframework:spring-core:%s\u0026#34; % SPRING_VERSION, \u0026#34;org.springframework:spring-orm:%s\u0026#34; % SPRING_VERSION, \u0026#34;org.springframework:spring-tx:%s\u0026#34; % SPRING_VERSION, \u0026#34;jakarta.servlet:jakarta.servlet-api:6.0.0\u0026#34;, \u0026#39;javax.annotation:javax.annotation-api:1.3.2\u0026#39;, ... 执行以下命令更新maven_install.json文件：\nbazel run @unpinned_maven//:pin 至此目录结构如下：\n\u0026gt; $ tree . ├── .bazelrc ├── .bazelversion ├── .gitignore ├── BUILD.bazel ├── WORKSPACE ├── maven_install.json 创建Maven工程结构 本例中，我们在根目录创建一个server模块来对外提供服务，最终效果图如下： 可以看出，server模块的目录结构与常规的Maven工程的结构相同。在Bazel并不一定需要采用Maven工程的结构，只是为了保持Java工程的习惯。\n为达以上效果，我们需要做以下事情：\n配置rules_spring 由于Springboot的代码需要使用Springboot loader进行启动，Springboot程序的打包逻辑与普通的Java程序不同。这意味着，Bazel原生的 java_binary 无法正常启动Springboot程序。\n其它构建工具Maven/Gradle是通过plugin完成对Springboot工程的打包。\n而在Bazel通过rules_spring实现相同的功能。具体方法是在WORKSPACE中加入rules_spring：\nhttp_archive( name = \u0026#34;rules_spring\u0026#34;, sha256 = \u0026#34;\u0026lt;hash value\u0026gt;\u0026#34;, urls =[\u0026#34;https://github.com/salesforce/rules_spring/releases/download/2.3.0/rules-spring-2.3.0.zip\u0026#34;, ], ) 配置Java构建 在Bazel，构建逻辑写在BUILD.bazel文件中。本案例的server/src/main/java/BUILD.bazel的内容如下：\n# load rule that you can use it load(\u0026#34;@rules_spring//springboot:springboot.bzl\u0026#34;, \u0026#34;springboot\u0026#34;) package(default_visibility = [\u0026#34;//visibility:public\u0026#34;]) app_deps = [ \u0026#34;@maven//:org_thymeleaf_thymeleaf\u0026#34;, \u0026#34;@maven//:com_fasterxml_jackson_core_jackson_annotations\u0026#34;, \u0026#34;@maven//:org_springframework_spring_beans\u0026#34;, \u0026#34;@maven//:org_springframework_spring_core\u0026#34;, \u0026#34;@maven//:org_springframework_boot_spring_boot_starter_thymeleaf\u0026#34;, \u0026#34;@maven//:org_springframework_boot_spring_boot_loader_tools\u0026#34;, \u0026#34;@maven//:org_springframework_boot_spring_boot_loader\u0026#34;, \u0026#34;@maven//:org_springframework_boot_spring_boot\u0026#34;, \u0026#34;@maven//:org_springframework_boot_spring_boot_autoconfigure\u0026#34;, \u0026#34;@maven//:org_springframework_boot_spring_boot_starter_web\u0026#34;, \u0026#34;@maven//:org_springframework_spring_context\u0026#34;, \u0026#34;@maven//:org_springframework_spring_webmvc\u0026#34;, \u0026#34;@maven//:org_springframework_boot_spring_boot_starter_validation\u0026#34;, \u0026#34;@maven//:jakarta_servlet_jakarta_servlet_api\u0026#34;, \u0026#34;@maven//:org_springframework_spring_web\u0026#34;, \u0026#34;@maven//:ch_qos_logback_logback_classic\u0026#34;, \u0026#34;@maven//:org_slf4j_slf4j_api\u0026#34; ] # define a lib contains all java files, in this case java_library( name = \u0026#34;lib\u0026#34;, srcs = glob([\u0026#34;**/*.java\u0026#34;]), deps = app_deps, # include the all resources resources = [\u0026#34;//server/src/main/resources:server-resources\u0026#34;], ) springboot( name = \u0026#34;springboot\u0026#34;, # specify the main class boot_app_class = \u0026#34;codes.showme.server.Main\u0026#34;, # refrence the library java_library = \u0026#34;:lib\u0026#34;, # build failed is there\u0026#39;s any duplicated classes dupeclassescheck_enable = True, dupeclassescheck_ignorelist = \u0026#34;//server:springboot_dupeclass_allowlist.txt\u0026#34;, ) 配置Resources 本例中，我们采用了Thymeleaf模板引擎进行前端渲染。所以，我们在server/src/main/resources/templates/pages中增加Thymeleaf模板。\n为了告诉Springboot Thymeleaf的模板的位置，在application.yml配置以下内容：\nspring: thymeleaf: mode: HTML # prefix: file:\u0026lt;path of the templates\u0026gt; prefix: classpath:/templates/pages/ cache: false content-type: text/html encoding: UTF-8 suffix: .html check-template-location: true application: name: bazel-springboot main: banner-mode: \u0026#34;off\u0026#34; 最后，再在server/src/main/resources/BUILD.bazel中配置server-resources target：\nfilegroup( name = \u0026#34;server-resources\u0026#34;, srcs = glob([ \u0026#34;application.yml\u0026#34;, \u0026#34;templates/pages/**/*\u0026#34;, ]), visibility = [\u0026#34;//visibility:public\u0026#34;], ) 至此，基础工程已经配置完成。看起来需要配置很多内容。但是这些配置就是在工程开始时配置一次。今后修改就不需要配置这么多内容了。\n文章最后提供了本文的代码模板。使用该模板即可节约大量配置时间。\n构建并启动工程 基础工程已经配置完成后，剩下的就是在此基础上构建新功能，并执行调试。\n我们通过以下命令进行构建：\nbazel build //... 如果希望在本地启动并调试，运行以下命令：\nbazel run //server/src/main/java:springboot 如果运行在生产环境，建议使用Bazel打包好的bazel-bin/server/src/main/java/springboot.jar，或者将其打包到Docker镜像中。\n总结 本教程遗留了以下几个问题需要处理：\n未集成ORM的能力； 未集成前端JS/CSS相关的能力。 以上能力会在接下来的教程中实现。\n完整的工程地址：https://github.com/zacker330/bazel-springboot-project-template\n补充 rules_spring提供了类冲突检测能力，构建时出现如下异常，时代表工程中存在重复的依赖。这一检测能力对软件工程的稳定性非常有益。遇到这种情况，你有两种选择：\n移除其中一个依赖； 在dupeclassescheck_ignorelist的文件中配置允许重复。 Exception: Found duplicate classes in the packaged springboot jar Spring Boot packaging has failed for bazel-out/darwin-fastbuild/bin/server/src/main/java/springboot.jar because multiple copies of the same class, but with different hashes, were found: class jakarta/servlet/annotation/ServletSecurity$EmptyRoleSemantic.class jar processed_jakarta.servlet-api-6.0.0.jar hash b31cc341ef8131abf1c791b152880c53 jar processed_tomcat-embed-core-10.1.8.jar hash 54513d067d21bf5a31fe942473085bec class jakarta/servlet/annotation/ServletSecurity$TransportGuarantee.class jar processed_jakarta.servlet-api-6.0.0.jar hash eb31c9cca28bba1ba7f3e6fc5839e828 jar processed_tomcat-embed-core-10.1.8.jar hash 0acd10bfd01aa6cec1b89cdd0fcbd0f9 class jakarta/servlet/annotation/ServletSecurity.class jar processed_jakarta.servlet-api-6.0.0.jar hash 9b7a0b ","permalink":"https://showme.codes/zh-cn/2024-01-19-bazel-springboot/","summary":"\u003cp\u003e本文是关于如何使用Bazel搭建Springboot 3.1.0工程（基于JDK17）。为什么使用Bazel，而不是使用Maven或者Gradle？可以看我之前关于Bazel的介绍文章。\u003c/p\u003e\n\u003ch2 id=\"前期准备\"\u003e前期准备\u003c/h2\u003e\n\u003cp\u003e在根目录加入\u003ccode\u003e.bazelversion\u003c/code\u003e文件，并加入\u003ccode\u003e6.2.0\u003c/code\u003e，指定当前工程使用的Bazel的版本。这样，Bazel命令自动使用该版本的Bazel进行构建。\u003c/p\u003e\n\u003cp\u003e在根目录加入\u003ccode\u003e.bazelrc\u003c/code\u003e文件，并指定构建和测试时使用JDK17，内容如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ebuild --java_language_version\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e17\u003c/span\u003e --java_runtime_version\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e17\u003c/span\u003e --tool_java_language_version\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e17\u003c/span\u003e --tool_java_runtime_version\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e17\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003etest\u003c/span\u003e  --java_language_version\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e17\u003c/span\u003e --java_runtime_version\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e17\u003c/span\u003e --tool_java_language_version\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e17\u003c/span\u003e --tool_java_runtime_version\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e17\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"外部依赖准备\"\u003e外部依赖准备\u003c/h2\u003e\n\u003cp\u003e在根目录中创建以下两个文件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWORKSPACE：在Bazel中，所有的外部依赖统一定义WORKSPACE文件中；\u003c/li\u003e\n\u003cli\u003eBUILD.bazel：内容留空即可，用于告诉Bazel当前目录也是一个Package。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBazel本身是支持多语言的。所以，我们需要特定语言的rule来帮助我们在WORKSPACE中定义外部依赖。\u003c/p\u003e\n\u003cp\u003e对于Java工程，我们使用\u003ca href=\"https://github.com/bazelbuild/rules_jvm_external\"\u003erules_jvm_external\u003c/a\u003e进行外部依赖的管理。它的使用步骤如下：\u003c/p\u003e\n\u003ch3 id=\"步骤1在workspace中增加rules_jvm_external配置\"\u003e步骤1：在WORKSPACE中增加rules_jvm_external配置\u003c/h3\u003e\n\u003cp\u003e以下配置指定了rules_jvm_external的下载位置，并进行rule的初始化：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@bazel_tools//tools/build_defs/repo:http.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;http_archive\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRULES_JVM_EXTERNAL_TAG\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;4.5\u0026#34;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRULES_JVM_EXTERNAL_SHA\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u0026lt;sha hash value\u0026gt;\u0026#34;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ehttp_archive\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;rules_jvm_external\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003estrip_prefix\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;rules_jvm_external-\u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"n\"\u003eRULES_JVM_EXTERNAL_TAG\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003esha256\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eRULES_JVM_EXTERNAL_SHA\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003eurl\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://github.com/bazelbuild/rules_jvm_external/archive/\u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e.zip\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"n\"\u003eRULES_JVM_EXTERNAL_TAG\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@rules_jvm_external//:repositories.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;rules_jvm_external_deps\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erules_jvm_external_deps\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@rules_jvm_external//:setup.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;rules_jvm_external_setup\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erules_jvm_external_setup\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@rules_jvm_external//:defs.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;maven_install\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emaven_install\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003eartifacts\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e# The project\u0026#39;s dependencies\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"s2\"\u003e\u0026#34;junit:junit:4.12\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"s2\"\u003e\u0026#34;org.hamcrest:hamcrest-library:1.3\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e],\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003erepositories\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e# Private repositories are supported through HTTP Basic auth  \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e# \u0026#34;http://username:password@localhost:8081/artifactory/my-repository\u0026#34;,    \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"s2\"\u003e\u0026#34;https://maven.aliyun.com/repository/public\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e],\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e以上采用了非Bzlmod的管理rule。\u003c/p\u003e","title":"Bazel使用案例：构建Springboot工程"},{"content":"前言 Github Actions是Github提供的一个CICD Pipeline服务。除了Pipeline，它还提供Secret和简单的配置管理。\n本文并不是它的一个完整介绍和知识的罗列。而是我在实际使用Github Actions后，对Github Actions的“共享问题”的解决方案的总结。\n不要小看这个问题，它是所有的Pipeline平台（包括Gitlab CI）都会遇到的问题。只要对这一问题深入理解，所有的平台一通百通。\n提示1：下文可能会是Workflows和Pipeline两个术语共用。因为它们本质上就是同一个东西，只是不同平台不同的叫法。 提示2：下文可能会共用DevOps平台和Pipeline平台，虽然它们可能是完全不同的平台，但是在本文中，它们都是能提供Pipeline的平台。\n共享问题 只要是Pipeline平台，都会遇到共享问题。那么，什么是共享问题？\n共享问题就是Pipeline中不同的位置之间共享资源，以实现不重复执行、生成准确结果的目标。\n定义听起来有些枯燥。我们列举一个有共享的场景，就比较好理解了：\n比如对于一个单仓库，它同时包含多个前端工程，这些前端工程同时依赖于一个common模板。其它前端工程只有等它构建完成，并取得构建，才能开始自己的构建。如果common模板的构建结果不能被其它工程构建共享使用，就会存在构建结果不一致、重复构建的问题； 比如一个Pipeline中，版本号是有特定格式的，需要在第一步骤计算出来后，其它步骤取得这个版本号，进行打包工作。如果没有实现版本号在多个步骤之间共享，很可能会导致版本号不一致问题。 我们稍微对共享问题进行抽象和理解，根据共享的范围，共享问题，可以分为：\nWorkflows之间进行共享； Workflow内的Jobs之间进行共享； Job内的Step之间进行共享。 根据共享的内容，可以分为：\n共享源码； 共享制品； 共享变量。 Github Actionsr制品的定义 在Github Actions中的制品（Artifact）的概念和我们平时所说的“制品”有一定的区别。在Github Actions中，制品指的是Job生成的文件或者文件夹。\n我们平时所说的，更广意的“制品”，在Github Actions叫Packages。\nWorkflows之间的共享制品 一般只有在大型项目才会存在Workflows之间的共享。而我个人是不建议将依赖Pipeline实现大型项目的构建的，而是依赖构建工具本身的能力。\n由于笔者时间有限，不再亲身做实验，本节内容，请读者自行测试。\n如果Workflow是由workflow_run事件触发的情况下，它们就可以直接使用actions/upload-artifact和actions/download-artifact两个actions来实现制品的共享。相关文档：https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts#downloading-artifacts-during-a-workflow-run\n有趣的是Github Actions提供了一种reusable的Workflow概念。说到底是一种模板化Workflow的方式。但是这种方式不适合用来实现共享制品。因为它并不是共享制品。相关文档：https://docs.github.com/en/actions/using-workflows/reusing-workflows#using-outputs-from-a-reusable-workflow\nJobs之间共享制品 在同一个Workflow中，多个Job进行制品共享。如下代码：\njobs: // 生成制品的job build: runs-on: ubuntu-latest steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and export uses: docker/build-push-action@v5 with: context: . tags: myimage:latest outputs: type=docker,dest=/tmp/myimage.tar - name: Upload artifact uses: actions/upload-artifact@v3 with: name: myimage path: /tmp/myimage.tar // 使用制品的job use: runs-on: ubuntu-latest needs: build steps: - name: Download artifact uses: actions/download-artifact@v3 with: name: myimage path: /tmp - name: Load image run: | docker load --input /tmp/myimage.tar docker image ls -a 以上案例来自：https://docs.docker.com/build/ci/github-actions/share-image-jobs/\n注意: actions/download-artifact和actions/upload-artifact截止到2024年1月，它们都已经升级v4的版本。\n好奇心强的同学就要问：\n这些制品会存放在哪里？存多久？ 制品大小有限制吗？ 需要收费吗？ 答案在这里：https://github.com/actions/upload-artifact?tab=readme-ov-file#limitations\n关于Workflow存储数据为制品的更多信息：https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts\nJobs之间共享变量 我有一个习惯，每使用一种Pipeline平台，我首先，会问该如何自定义版本吗？因为我的设计的版本号都会增加git的commitID前8位。\n为什么要这么做？是因为过去，我看到的DevOps平台似乎都无法做到通过制品反查相应的源代码的能力。而在版本号上增加commitID是实现这一能力的最低成本方案。\n如果平台没有提供这样的能力，我就通过自定义版本号来实现。实现方法就是在一个step生成版本号，其它step共享这一版本号。\n但是，在Github Actions里，想实现这样的功能，是一件非常痛苦的事情。\n以下是定义共享变量的代码：\njobs: init_build_version: name: init build version runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: init version Properties id: properties shell: bash run: | VERSION=\u0026#34;$(echo $(date +\u0026#39;v%Y.%m.%d\u0026#39;)_${{ github.run_number }}_$(git rev-parse --short=8 HEAD))\u0026#34; echo \u0026#34;VERSION=$VERSION\u0026#34; \u0026gt;\u0026gt; \u0026#34;$GITHUB_OUTPUT\u0026#34; ... define other vars outputs: VERSION: ${{ steps.properties.outputs.VERSION }} ... other outputs 以下是其它Job使用共享变量的方法\njobs: build_devops: name: package runs-on: ubuntu-latest # required needs: - init_build_version steps: # .. other steps - name: create a release draft id: create_prerelease uses: softprops/action-gh-release@v1 with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ needs.init_build_version.outputs.VERSION }} name: ${{ needs.init_build_version.outputs.VERSION }} draft: true prerelease: false files: | # some files prepare to be uploaded 在Job中，通过needs.init_build_version.outputs来引用另一个Job的output。\n这样的解决方案的问题：\n大量的重复代码 这个Pipeline无法在本地验证和独立测试 官方文档：https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs\nStep之间共享变量 同一个Job中多个step之间，通过环境变量进行共享变量。以下展示的是由一个step生成一个变量，再由后一step使用：\njobs: steps: - name: define env shell: bash run: | VERSION=\u0026#34;$(echo $(date +\u0026#39;v%Y.%m.%d\u0026#39;)_${{ github.run_number }}_$(git rev-parse --short=8 HEAD))\u0026#34; echo \u0026#34;VERSION=$VERSION\u0026#34; \u0026gt;\u0026gt; $GITHUB_ENV - name: use-env run: echo \u0026#39;${{ env.VERSION }}\u0026#39; 当然，如果你不需要生成变量，而是固定的变量，则可以直接在Workflows顶层定义环境变量：\nenv: APP_ENV: \u0026#34;stag\u0026#34; jobs: # use that env variable via ${{ env.APP_ENV }} Step之间共享制品 因为同一个Job下的step是共享一个Workspace的，所以，使用Step之间的制品，就和使用本地文件一样使用。\n对Github Actions的看法 比较主观，仅供参考：\n选择YAML作为Pipeline的DSL，让人有“简单”的错觉，实际使用起来，非常的痛苦； Debug过程非常耗时：因为你没有简单办法在本地进行Debug； 总之，它的开发体验非常差。但是从另一方面看，也是做DevOps产品公司的机会。\n","permalink":"https://showme.codes/zh-cn/2024-01-12-github-actions-share-things/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003eGithub Actions是Github提供的一个CICD Pipeline服务。除了Pipeline，它还提供Secret和简单的配置管理。\u003c/p\u003e\n\u003cp\u003e本文并不是它的一个完整介绍和知识的罗列。而是我在实际使用Github Actions后，对Github Actions的“共享问题”的解决方案的总结。\u003c/p\u003e\n\u003cp\u003e不要小看这个问题，它是所有的Pipeline平台（包括Gitlab CI）都会遇到的问题。只要对这一问题深入理解，所有的平台一通百通。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e提示1：下文可能会是Workflows和Pipeline两个术语共用。因为它们本质上就是同一个东西，只是不同平台不同的叫法。\n提示2：下文可能会共用DevOps平台和Pipeline平台，虽然它们可能是完全不同的平台，但是在本文中，它们都是能提供Pipeline的平台。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"共享问题\"\u003e共享问题\u003c/h2\u003e\n\u003cp\u003e只要是Pipeline平台，都会遇到共享问题。那么，什么是共享问题？\u003c/p\u003e\n\u003cp\u003e共享问题就是Pipeline中不同的位置之间共享资源，以实现不重复执行、生成准确结果的目标。\u003c/p\u003e\n\u003cp\u003e定义听起来有些枯燥。我们列举一个有共享的场景，就比较好理解了：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e比如对于一个单仓库，它同时包含多个前端工程，这些前端工程同时依赖于一个common模板。其它前端工程只有等它构建完成，并取得构建，才能开始自己的构建。如果common模板的构建结果不能被其它工程构建共享使用，就会存在构建结果不一致、重复构建的问题；\u003c/li\u003e\n\u003cli\u003e比如一个Pipeline中，版本号是有特定格式的，需要在第一步骤计算出来后，其它步骤取得这个版本号，进行打包工作。如果没有实现版本号在多个步骤之间共享，很可能会导致版本号不一致问题。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我们稍微对共享问题进行抽象和理解，根据共享的范围，共享问题，可以分为：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eWorkflows之间进行共享；\u003c/li\u003e\n\u003cli\u003eWorkflow内的Jobs之间进行共享；\u003c/li\u003e\n\u003cli\u003eJob内的Step之间进行共享。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e根据共享的内容，可以分为：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e共享源码；\u003c/li\u003e\n\u003cli\u003e共享制品；\u003c/li\u003e\n\u003cli\u003e共享变量。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"github-actionsr制品的定义\"\u003eGithub Actionsr制品的定义\u003c/h3\u003e\n\u003cp\u003e在Github Actions中的制品（Artifact）的概念和我们平时所说的“制品”有一定的区别。在Github Actions中，制品指的是Job生成的文件或者文件夹。\u003c/p\u003e\n\u003cp\u003e我们平时所说的，更广意的“制品”，在Github Actions叫Packages。\u003c/p\u003e\n\u003ch3 id=\"workflows之间的共享制品\"\u003eWorkflows之间的共享制品\u003c/h3\u003e\n\u003cp\u003e一般只有在大型项目才会存在Workflows之间的共享。而我个人是不建议将依赖Pipeline实现大型项目的构建的，而是依赖构建工具本身的能力。\u003c/p\u003e\n\u003cp\u003e由于笔者时间有限，不再亲身做实验，本节内容，请读者自行测试。\u003c/p\u003e\n\u003cp\u003e如果Workflow是由workflow_run事件触发的情况下，它们就可以直接使用\u003ccode\u003eactions/upload-artifact\u003c/code\u003e和\u003ccode\u003eactions/download-artifact\u003c/code\u003e两个actions来实现制品的共享。相关文档：https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts#downloading-artifacts-during-a-workflow-run\u003c/p\u003e\n\u003cp\u003e有趣的是Github Actions提供了一种reusable的Workflow概念。说到底是一种模板化Workflow的方式。但是这种方式不适合用来实现共享制品。因为它并不是共享制品。相关文档：https://docs.github.com/en/actions/using-workflows/reusing-workflows#using-outputs-from-a-reusable-workflow\u003c/p\u003e\n\u003ch3 id=\"jobs之间共享制品\"\u003eJobs之间共享制品\u003c/h3\u003e\n\u003cp\u003e在同一个Workflow中，多个Job进行制品共享。如下代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ejobs\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003e// 生成制品的job\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ebuild\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-latest\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003esteps\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eSet up Docker Buildx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edocker/setup-buildx-action@v3\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eBuild and export\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edocker/build-push-action@v5\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e.\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003emyimage:latest\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003eoutputs\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003etype=docker,dest=/tmp/myimage.tar\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eUpload artifact\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/upload-artifact@v3\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003emyimage\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/tmp/myimage.tar\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003e// 使用制品的job\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003euse\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-latest\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eneeds\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ebuild\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003esteps\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDownload artifact\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/download-artifact@v3\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003emyimage\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/tmp\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eLoad image\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e          docker load --input /tmp/myimage.tar\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e          docker image ls -a         \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e以上案例来自：https://docs.docker.com/build/ci/github-actions/share-image-jobs/\u003c/p\u003e","title":"DevOps架构师是如何看待Github Actions的共享制品解决方案的？"},{"content":"ORM框架为什么不香？ 对ORM框架的偏见 看了一些MyBaties与Hibernate进行对比的文章。可能是因为一些Hibernate历史原因，国内对于Hibernate普遍存在偏见，我摘抄了几点：\nhibernate是全自动，而mybatis是半自动 hibernate完全可以通过对象关系模型实现对数据库的操作，拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。而mybatis仅有基本的字段映射，对象数据以及对象实际关系仍然需要通过手写sql来实现和管理。\nsql直接优化上，mybatis要比hibernate方便很多 由于mybatis的sql都是写在xml里，因此优化sql比hibernate方便很多。而hibernate的sql很多都是自动生成的，无法直接维护sql\n应用场景 MyBatis 适合需求多变的互联网项目，例如电商项目、金融类型、旅游类、售票类项目等。 Hibernate 适合需求明确、业务固定的项目，例如 OA 项目、ERP 项目和 CRM 项目等。\n也不知道是不是因为这些对Hibernate的偏见，导致大家对ORM框架也普遍存在偏见。\n现状是不论大小公司，国内清一色地使用MyBaties。有时，我都不敢说，我喜欢使用ORM框架。\n本文并不是一篇为Hibernate洗地的文章，而是介绍另一款比较小众的ORM框架：Ebean。\n领域问题分析 介绍Ebean之前，我们需要弄清楚一个问题：为什么会有MyBaties和ORM这些框架？对于这个问题，我们无从下手，那么，我们将问题倒置：如果没有这些框架，会怎么样？\n问题倒置的好处是我们立马就有了可下手的方向。我们找到了不使用框架的情况下，Java代码与数据库进行交互的代码：\npublic static void viewTable(Connection con) throws SQLException { String query = \u0026#34;select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES\u0026#34;; try (Statement stmt = con.createStatement()) { ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String coffeeName = rs.getString(\u0026#34;COF_NAME\u0026#34;); int supplierID = rs.getInt(\u0026#34;SUP_ID\u0026#34;); float price = rs.getFloat(\u0026#34;PRICE\u0026#34;); int sales = rs.getInt(\u0026#34;SALES\u0026#34;); int total = rs.getInt(\u0026#34;TOTAL\u0026#34;); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } } 这样的代码存在什么问题呢？\n代码不易于维护：你需要知道每个字段在数据库中的类型，才知道该调用ResultSet的哪个方法； 代码重复：像COF_NAME这样的字段名，在整个代码仓库可能会飘落得到处都是； 不安全：手工拼装SQL带来的安全问题，不须多言。 以上三个问题，我们称之为ORM领域核心问题。\n从目前市面上的解决方案来看，解决这些核心问题的方案，至少需要包含以下三个能力：\n自动映射：在数据库与Java对象之间自动进行字段类型映射，而不是手工进行映射； 自动生成SQL：根据Java API自动生成SQL，而不是手写； 自动执行：自动执行，而不是手工直接操作JDBC接口。 MyBaties如何解决核心问题 MyBaties通过Mapper实现自动映射、自动执行。但是并没有实现自动生成SQL，也正是它称之为Mapper的原因。\npublic interface PersonMapper { @Insert(\u0026#34;Insert into person(name) values (#{name})\u0026#34;) public Integer save(Person person); @Select( \u0026#34;Select personId, name from Person where personId=#{personId}\u0026#34;) @Results(value = { @Result(property = \u0026#34;personId\u0026#34;, column = \u0026#34;personId\u0026#34;), @Result(property=\u0026#34;name\u0026#34;, column = \u0026#34;name\u0026#34;), @Result(property = \u0026#34;addresses\u0026#34;, javaType = List.class, column = \u0026#34;personId\u0026#34;, many=@Many(select = \u0026#34;getAddresses\u0026#34;)) }) public Person getPersonById(Integer personId); // ... } 我个人很好奇，为什么MyBatise没有使用JPA规范，而是自己又创造一种注解。\nMyBaties另一种通过XML的配置方式配置的，本文就不介绍了。想想当年，Spring也是使用XML进行配置Bean的，现在好像已经没有人这么干了。\n说到底，MyBaties也是一个ORM框架。MyBatis-Plus插件的流行程度正好证明了这一点。所以，大家没有必要对ORM框架抱有偏见。:P\nJPA小传 在介绍Ebean前，我们回顾一下JPA的历史。\nJPA全称：Java Persistent API（Java持久化API）。它只是规范，并不是具体技术，其中Hibernate应该是最出名的实现之一了。Ebean也是具体实现之一。\n值得注意的是我们应该可以认定这个规范没有限制我们只能用它将数据持久化到数据库（思路要打开）。即使，我们绝大多数时候，只用它持久化数据到数据库中。\n它的版本历史如下：\n2006.5.11: JPA1.0作为JSR220规范的一部分发布。Ebean同年11月发布Bate测试版本； 2009年：JPA2.0发布； 2013年和2017年：JPA2.1和JPA2.2分别发布； 2019年：JPA更名为Jakarta Persistence。 2020年和2022年：Jakarta Persistence3.0和3.1版本分别发布。 此部分内容来自： https://handwiki.org/wiki/Java_Persistence_API https://en.wikipedia.org/wiki/Jakarta_Persistence\nEbean：一款被低估的ORM框架 Ebean最早于2006年11月13日发布了Bate测试版本。然后v1.0.0版本，在2008年11月24日，由它的作者Rob Bygrave发布到了SourceForge。\n后来Ebean迁到了Github。目前最新版本是2023年11月22日。从Github组织来看，Ebean的主要维护人只有：Rob Bygrave。18年的坚持，不得不佩服作者的毅力。\n但这也成为我认为Ebean目前最大的问题：如果作者突然有个什么三长两短？社区应该如何应对。即使，它目前有将近100个contributor。\n个人在2011左右接触到Play Framework的时候，了解到Ebean。Play框架使用Ebean作为它的JPA实现。当时就被它优秀的设计所吸引。\n但是真正让我使用的是：它的设计非常符合我的DDD口味，同时鼓励充血模型的实体。\nEbean是如何实现自动映射的 在上文中，我们已经介绍了ORM领域核心问题：自动映射。这是JPA规范要解决的最重要的问题之一。Ebean实现了JPA规范定义的注解。\n用户在字段上加上JPA的注解，然后在真正需要映射的时候，Ebean自动进行映射。以下是定义一个实体Person，它对应的表名是people。实体字段与数据库字段也有相应的映射：\n@Entity @Table(name=\u0026#34;people\u0026#34;) public class Person { @Id @GeneratedValue private int id; @Column(name=\u0026#34;first_name\u0026#34;, length=10) private String firstName; @Column(name=\u0026#34;last_name\u0026#34;, length=10) private String lastName; 在实体类上中定义Java类与数据库之间的映射关系，最大的好处就是DDL语句和数据库迁移SQL可以被工具自动生成。\n假如你在Person类中增加一个email的字段，Ebean的的DDL特性就可以为你生成相应的建表语句。而Ebean的Migration工具就为你生成相应的alert语句。当然，这些语句的执行时机，还是由用户控制。\nJPA本身提供了大量注解，Ebean还扩展了一些有用的注解：\n@DbJson注解，自动将对象转成JSON进行存储。如果你所使用的数据库支持JSON模式，你会非常喜欢这个注解；有了这个注解，你可能就不需要值对象注解了； @WhenCreated注解：自动设置对象的创建时间； @WhenModified注解：自动设置对象的修改时间； @DbMap注解：自动将Map结构，构建数据库映射到不同的数据类型，如果是Postgre就映射到HSTORE，其它数据库则映射到VARCHAR。 @SoftDelete 软删除注解：当调用实体的delete方法时，只是软删除。只需要在实体中增加一个字段： @SoftDelete boolean deleted; 更多相关信息：https://ebean.io/docs/mapping/\nEbean是如何自动生成SQL并执行的 以下我们通过一个实例来展示Ebean相关的能力。\n// Database是Ebean与数据库进行交互的主要接口 @Autowired Database database; @Test public void crud() { Person customer = new Person(); customer.setFirstName(\u0026#34;Jack\u0026#34;); customer.setLastName(\u0026#34;J\u0026#34;); // 这是为了让大家对Ebean的database类有一个感性认知 database.save(customer); // 实际应用中，我通常是在Person类中定义一个save方法，并在内容调用database.save(this)。 // 最终就是实现这样的调用效果：customer.save() // 批量执行存储。你猜这里应该是生成一条语句，还是多条语句？ database.saveAll(customerList); // 根据ID查询对象。Ebean生成相应的select-where语句并执行 Customer customerA = database.find(Customer.class, 1); // 当然你也可以只查询其中一个字段的值，Ebean将生成并执行： // select first_name from people where id=1; database.find(Customer.class).select(\u0026#34;first_name\u0026#34;).where().idEq(1).findSingleAttribute(); customerA.setFirstName(\u0026#34;Jane\u0026#34;); // Ebean会识别出customerA要做的是修改，而不是创建新的记录。所以，生成alert语句并执行。 database.save(customerA); // 当然，少不了大家关心的能否执行原生SQL String sql = \u0026#34;select id, first_name from customer where first_name like ?\u0026#34;; Customer customer = database.findNative(Customer.class, sql) .setParameter(\u0026#34;Jo%\u0026#34;) .findOne(); // 另，有时，我们会想念DTO，则可以这么写： List\u0026lt;CustomerDto\u0026gt; beans = database.findDto(CustomerDto.class, \u0026#34;select id, first_name from customer where first_name = :name\u0026#34;) .setParameter(\u0026#34;name\u0026#34;, \u0026#34;Rob\u0026#34;) .findList(); // CustomerDto是需要提前定义好的。 // 删除记录 database.delete(customer); } 至此，已经把Ebean的解决方案介绍完成，由于篇幅有限，还请感兴趣的同学到官网学习。\nEbean的实体类增强技术 在Ebean的官网或者一些网上的文章，你会发现只要实体类继承了Ebean的BaseModel类，都会自动多出save方法以及其它方法。这Lombok与类似，只要加一个@Setter注解，类中就自动出多了相应的setter方法。\n又或者，你会看到：\nPerson contact = new QContact() .firstName.equalTo(\u0026#34;rob\u0026#34;) .findOne(); QContact类是由Ebean生成的（在实体类前加一个Q字母代表查询类），方便用户使用链式调用来查询自己想要数据。而不是需要像database.find(Customer.class).select(\u0026quot;first_name\u0026quot;)这样手工写字段名。\n发生以上的魔法是因为Ebean使用了增强（Enhancement）技术。这项技术必须嵌入到我们的IDE和构建工具中，否则相关代码的编译都不可能通过。\nEnhancement技术虽然让我们少写代码，但是我们也要认清这门技术所带来的成本：它使我们的开发环境强依赖相应的插件。比如Maven必须要安装它的插件才能构建通过、IDE必须安装插件才能正常写代码。\n幸运的是，我们可以选择不使用它的编译时生成的代码。IDE也就不需要安装相应的插件了。\n我个人宁愿自己在实体中手写save方法，也不使用这项技术生成。另一个重要的考虑因素是：我不希望领域实体类依赖于具体实现技术。\n然而，运行时，还是必须加上agent，即-javaagent:\u0026lt;路径\u0026gt;/ebean-agent.jar，以便Ebean对实体进行脏检查和懒加载支持。\n使用经验 以下是一些个人的使用经验，仅供参考：\nJPA的所有API，并不是每一个都必须用到。比如字段上的@Basic(fetch=FetchType.LAZY)懒式加载，我就不建议使用。因为在实际工作中，你不能确保每个人都理解懒式加载的应用场景；比如它的JPQL，我们完全没有必要又另学一种SQL，再者Java API的调用方式才应该是推荐； 使用Java配置类对Ebean进行配置，而不是使用官网介绍properties配置。只有这样才足够灵活应对将来的多数据源需求； 在实体类中不要直接使用Ebean的技术，而是在实体类中调用repository接口，再由repository的实现调到Ebean。 如果各位想看更多的Ebean的文章，请点赞并转发。\n","permalink":"https://showme.codes/zh-cn/2024-01-02-ebean/","summary":"\u003ch1 id=\"orm框架为什么不香\"\u003eORM框架为什么不香？\u003c/h1\u003e\n\u003ch2 id=\"对orm框架的偏见\"\u003e对ORM框架的偏见\u003c/h2\u003e\n\u003cp\u003e看了一些MyBaties与Hibernate进行对比的文章。可能是因为一些Hibernate历史原因，国内对于Hibernate普遍存在偏见，我摘抄了几点：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ehibernate是全自动，而mybatis是半自动\u003c/li\u003e\n\u003c/ol\u003e\n\u003cblockquote\u003e\n\u003cp\u003ehibernate完全可以通过对象关系模型实现对数据库的操作，拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。而mybatis仅有基本的字段映射，对象数据以及对象实际关系仍然需要通过手写sql来实现和管理。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003esql直接优化上，mybatis要比hibernate方便很多\u003c/li\u003e\n\u003c/ol\u003e\n\u003cblockquote\u003e\n\u003cp\u003e由于mybatis的sql都是写在xml里，因此优化sql比hibernate方便很多。而hibernate的sql很多都是自动生成的，无法直接维护sql\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003col start=\"3\"\u003e\n\u003cli\u003e应用场景\u003c/li\u003e\n\u003c/ol\u003e\n\u003cblockquote\u003e\n\u003cp\u003eMyBatis 适合需求多变的互联网项目，例如电商项目、金融类型、旅游类、售票类项目等。\nHibernate 适合需求明确、业务固定的项目，例如 OA 项目、ERP 项目和 CRM 项目等。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e也不知道是不是因为这些对Hibernate的偏见，导致大家对ORM框架也普遍存在偏见。\u003c/p\u003e\n\u003cp\u003e现状是不论大小公司，国内清一色地使用MyBaties。有时，我都不敢说，我喜欢使用ORM框架。\u003c/p\u003e\n\u003cp\u003e本文并不是一篇为Hibernate洗地的文章，而是介绍另一款比较小众的ORM框架：Ebean。\u003c/p\u003e\n\u003ch2 id=\"领域问题分析\"\u003e领域问题分析\u003c/h2\u003e\n\u003cp\u003e介绍Ebean之前，我们需要弄清楚一个问题：为什么会有MyBaties和ORM这些框架？对于这个问题，我们无从下手，那么，我们将问题倒置：如果没有这些框架，会怎么样？\u003c/p\u003e\n\u003cp\u003e问题倒置的好处是我们立马就有了可下手的方向。我们找到了不使用框架的情况下，Java代码与数据库进行交互的代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003estatic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eviewTable\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eConnection\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003econ\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003ethrows\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eSQLException\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003equery\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003etry\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eStatement\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estmt\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003econ\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ecreateStatement\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"n\"\u003eResultSet\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ers\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estmt\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eexecuteQuery\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003equery\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ecoffeeName\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;COF_NAME\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esupplierID\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetInt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;SUP_ID\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"kt\"\u003efloat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eprice\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetFloat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;PRICE\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esales\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetInt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;SALES\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003etotal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetInt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;TOTAL\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecatch\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSQLException\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"n\"\u003eJDBCTutorialUtilities\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintSQLException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这样的代码存在什么问题呢？\u003c/p\u003e","title":"Ebean：一款被低估的ORM框架"},{"content":"PostgreSQL15后，Public Schema的权限发生了变化：普通用户默认在Public schema中不再有CREATE的权限。当他们执行CREATE TABLE命令时，就会报以下错误：\nERROR: permission denied for schema public 所以，我们需要为该用户再分配权限。命令如下：\nGRANT USAGE, CREATE on SCHEMA PUBLIC to \u0026lt;username\u0026gt;; 因为某些应用程序的sql的migration是自动的，你可能还需要为用户分配更多权限，命令如下：\ngrant all on database \u0026lt;db_name\u0026gt; to \u0026lt;username\u0026gt;; ALTER DATABASE \u0026lt;db_name OWNER to \u0026lt;username\u0026gt;; ","permalink":"https://showme.codes/zh-cn/2024-01-01-postgresql15-public-schema-permission/","summary":"\u003cp\u003ePostgreSQL15后，Public Schema的权限发生了变化：普通用户默认在Public schema中不再有CREATE的权限。当他们执行CREATE TABLE命令时，就会报以下错误：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eERROR: permission denied for schema public\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e所以，我们需要为该用户再分配权限。命令如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eGRANT USAGE, CREATE on SCHEMA PUBLIC to \u0026lt;username\u0026gt;;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e因为某些应用程序的sql的migration是自动的，你可能还需要为用户分配更多权限，命令如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egrant all on database \u0026lt;db_name\u0026gt; to \u0026lt;username\u0026gt;;\nALTER DATABASE  \u0026lt;db_name OWNER to  \u0026lt;username\u0026gt;;\n\u003c/code\u003e\u003c/pre\u003e","title":"PostgreSQL15 Public Schema没有权限问题解决"},{"content":"最近在做一个产品，需要用邮箱服务和邮件发送服务。本文以Mxroute和Sendgrid为例介绍邮箱服务和邮件发送服务的配置。但是所有的这类产品，思路都应该是一致的。\nMxroute是邮箱服务，类似Web服务，只不过，它是专门为邮箱协议而设计的。Sendgrid就是邮件发送服务，也就是你需要批量向一堆邮箱发送邮件时，就需要用邮件发送服务。本质上Sendgrid与Mxroute是两回事。但是，通常我们先配置邮箱服务，再配置邮件发送服务。\n本文只为记录一下，将来忘记了可以重新拾起。\n配置邮箱服务 首先，邮箱服务需要MX类型的域名解析记录。这能让邮箱服务能在整个互联网被解析到。\n每一个在Mxroute付费的用户，都会被分配到一个独立的MX域名，如first.mxrouting.net 。他们应该会发邮件给你，你需要留意。\n在Mxroute上配置的步骤如下：\n创建一个域名。比如example.com。如果你使用的是子域名，也可以是mail.example.com。 拿到DKIM Keys等信息。在Mxroute的左边菜单中可以找到链接 在域名提供商中，再配置以下这些DNS记录\n记录类型 name content 优先级 MX _dmac v=DMARC1; p=none CNAME mail \u0026lt;mxroute分配的独立MX域名\u0026gt; MX mail \u0026lt;mxroute分配的独立MX域名\u0026gt; 10 MX mail \u0026lt;mxroute分配的独立MX域名\u0026gt; 20 TXT mail \u0026lt;从mxroute上获取\u0026gt; TXT x._domainkey \u0026lt;从mxroute上获取\u0026gt; 如果你使用的是子域名，那么，还需要在 _dmas和x.domainkey 后加上 . 。例如mail子域名，就是 x.domainkey.mail。\n通过以上配置，只证明我们的“邮箱服务器”已经配置好了。现在在上面创建账号，并进行测试了。如果你可以向这个账号收发邮件，就证明，你的邮箱服务已经配置完成。\n配置邮件发送服务 当你有了一个邮箱账号后，你就可以Sendgrid上配置了。登录后，从左边菜单“Senders”进入列表页。 然后再点击按钮“Create new Sender”，即可创建。这部分就不细说了。因为太简单了。\n","permalink":"https://showme.codes/zh-cn/2023-12-31-email-service-setup/","summary":"\u003cp\u003e最近在做一个产品，需要用邮箱服务和邮件发送服务。本文以\u003ca href=\"https://mxroute.com/\"\u003eMxroute\u003c/a\u003e和\u003ca href=\"https://sendgrid.com/\"\u003eSendgrid\u003c/a\u003e为例介绍邮箱服务和邮件发送服务的配置。但是所有的这类产品，思路都应该是一致的。\u003c/p\u003e\n\u003cp\u003eMxroute是邮箱服务，类似Web服务，只不过，它是专门为邮箱协议而设计的。Sendgrid就是邮件发送服务，也就是你需要批量向一堆邮箱发送邮件时，就需要用邮件发送服务。本质上Sendgrid与Mxroute是两回事。但是，通常我们先配置邮箱服务，再配置邮件发送服务。\u003c/p\u003e\n\u003cp\u003e本文只为记录一下，将来忘记了可以重新拾起。\u003c/p\u003e\n\u003ch2 id=\"配置邮箱服务\"\u003e配置邮箱服务\u003c/h2\u003e\n\u003cp\u003e首先，邮箱服务需要MX类型的域名解析记录。这能让邮箱服务能在整个互联网被解析到。\u003c/p\u003e\n\u003cp\u003e每一个在Mxroute付费的用户，都会被分配到一个独立的MX域名，如first.mxrouting.net 。他们应该会发邮件给你，你需要留意。\u003c/p\u003e\n\u003cp\u003e在Mxroute上配置的步骤如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e创建一个域名。比如example.com。如果你使用的是子域名，也可以是mail.example.com。\n\u003cimg loading=\"lazy\" src=\"/assets/images/mail-service-setup-1.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e拿到DKIM Keys等信息。在Mxroute的左边菜单中可以找到链接\n\u003cimg loading=\"lazy\" src=\"/assets/images/mail-service-setup-2.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e在域名提供商中，再配置以下这些DNS记录\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003e记录类型\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003ename\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003econtent\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003e优先级\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMX\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e_dmac\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ev=DMARC1; p=none\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eCNAME\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003email\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026lt;mxroute分配的独立MX域名\u0026gt;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMX\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003email\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026lt;mxroute分配的独立MX域名\u0026gt;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMX\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003email\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026lt;mxroute分配的独立MX域名\u0026gt;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTXT\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003email\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026lt;从mxroute上获取\u0026gt;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTXT\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ex._domainkey\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026lt;从mxroute上获取\u0026gt;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e如果你使用的是子域名，那么，还需要在 _dmas和x.domainkey 后加上 .\u003csubdomain\u003e 。例如mail子域名，就是 x.domainkey.mail。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e通过以上配置，只证明我们的“邮箱服务器”已经配置好了。现在在上面创建账号，并进行测试了。如果你可以向这个账号收发邮件，就证明，你的邮箱服务已经配置完成。\u003c/p\u003e\n\u003ch2 id=\"配置邮件发送服务\"\u003e配置邮件发送服务\u003c/h2\u003e\n\u003cp\u003e当你有了一个邮箱账号后，你就可以Sendgrid上配置了。登录后，从左边菜单“Senders”进入列表页。\n\u003cimg loading=\"lazy\" src=\"/assets/images/mail-service-setup-3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e然后再点击按钮“Create new Sender”，即可创建。这部分就不细说了。因为太简单了。\u003c/p\u003e","title":"使用Mxroute和Sendgrid实现邮箱服务和邮件发送服务"},{"content":"\n安全漏洞是软件工程化能力的试金石 2021年年底，Log4j的漏洞陆续被公开。因为该框架被大量的开源软件依赖，所以，漏洞影响面非常大。\n面对这个漏洞，我们遇到的第一个问题是：如何知道我们哪些工程使用了Log4j？\n在我看来，这个漏洞是企业软件工程化的一颗非常好的试金石。因为：\n如何第一时间了解到这个漏洞，反应这家企业的安全能力； 如何第一时间能找到所有使用了Log4j的位置，体现了这家企业第三方软件依赖管理能力； 替换Log4j的速度，体现企业的持续集成、持续部署的能力。 Google的开源软件安全漏洞扫描工具 今天介绍的OSC-Scanner，能加强我们第1项和第2项能力。\nOSV-Scanner是Google在2022年12月13日推出的一款免费的安全扫描工具。它具有以下特点：\n支持多生态系统，包括：Go、PyPI、RubyGens、Linux、Maven等16个生态系统； 同时支持直接依赖的扫描和间接依赖的扫描； 采用标准的漏洞记录格式； 从当前最大的开源软件漏洞数据库（https://osv.dev/）获取信息。这也是DenpencyTrack和Flutter安全工具的漏洞数据库。 OSV-Scanner是一款命令行工具，我们可以将它集成到我们的构建工具或者CICD Pipeline中。目前它已经被集成到Scorecard中。Scorecard是一款为开发源软件的安全健康度打分的开源软件。我们可以在Github Actions中使用它：https://github.com/ossf/scorecard/tree/main?tab=readme-ov-file#scorecard-github-action\nOSV-Scanner的安装 Windows：\nscoop install osv-scanner Mac Homebrew:\nbrew install osv-scanner 也可以直接下载二进制包：https://github.com/google/osv-scanner/releases\n具体安装文档：https://google.github.io/osv-scanner/installation/\nOSV-Scanner的使用 Keras是一个使用Python编写的开源人工神经网络库。我们以它为例。命令行里运行以下命令：\n./osv-scanner_1.3.6_linux_amd64 --format json keras/ 输出内容说明：keras存在一个“潜在内存泄漏”的漏洞。\n当拿到json结果后，我们的DevOps平台就可以进行一些告警监控的操作。\n后记 osv-scanner目前需要连osv.dev，才能使用。但是，已经开放实验功能，允许用户离线使用osv-scanner。这是自建DevOps平台的福音！\n","permalink":"https://showme.codes/zh-cn/2023-12-25-google-osv/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/xkcd-dependency.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"安全漏洞是软件工程化能力的试金石\"\u003e安全漏洞是软件工程化能力的试金石\u003c/h2\u003e\n\u003cp\u003e2021年年底，Log4j的漏洞陆续被公开。因为该框架被大量的开源软件依赖，所以，漏洞影响面非常大。\u003c/p\u003e\n\u003cp\u003e面对这个漏洞，我们遇到的第一个问题是：如何知道我们哪些工程使用了Log4j？\u003c/p\u003e\n\u003cp\u003e在我看来，这个漏洞是企业软件工程化的一颗非常好的试金石。因为：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e如何第一时间了解到这个漏洞，反应这家企业的安全能力；\u003c/li\u003e\n\u003cli\u003e如何第一时间能找到所有使用了Log4j的位置，体现了这家企业第三方软件依赖管理能力；\u003c/li\u003e\n\u003cli\u003e替换Log4j的速度，体现企业的持续集成、持续部署的能力。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"google的开源软件安全漏洞扫描工具\"\u003eGoogle的开源软件安全漏洞扫描工具\u003c/h2\u003e\n\u003cp\u003e今天介绍的OSC-Scanner，能加强我们第1项和第2项能力。\u003c/p\u003e\n\u003cp\u003eOSV-Scanner是Google在2022年12月13日推出的一款免费的安全扫描工具。它具有以下特点：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e支持多生态系统，包括：Go、PyPI、RubyGens、Linux、Maven等16个生态系统；\u003c/li\u003e\n\u003cli\u003e同时支持直接依赖的扫描和间接依赖的扫描；\u003c/li\u003e\n\u003cli\u003e采用标准的漏洞记录格式；\u003c/li\u003e\n\u003cli\u003e从当前最大的开源软件漏洞数据库（https://osv.dev/）获取信息。这也是DenpencyTrack和Flutter安全工具的漏洞数据库。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eOSV-Scanner是一款命令行工具，我们可以将它集成到我们的构建工具或者CICD Pipeline中。目前它已经被集成到Scorecard中。Scorecard是一款为开发源软件的安全健康度打分的开源软件。我们可以在Github Actions中使用它：https://github.com/ossf/scorecard/tree/main?tab=readme-ov-file#scorecard-github-action\u003c/p\u003e\n\u003ch2 id=\"osv-scanner的安装\"\u003eOSV-Scanner的安装\u003c/h2\u003e\n\u003cp\u003eWindows：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003escoop install osv-scanner\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eMac Homebrew:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ebrew install osv-scanner\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e也可以直接下载二进制包：https://github.com/google/osv-scanner/releases\u003c/p\u003e\n\u003cp\u003e具体安装文档：https://google.github.io/osv-scanner/installation/\u003c/p\u003e\n\u003ch2 id=\"osv-scanner的使用\"\u003eOSV-Scanner的使用\u003c/h2\u003e\n\u003cp\u003eKeras是一个使用Python编写的开源人工神经网络库。我们以它为例。命令行里运行以下命令：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./osv-scanner_1.3.6_linux_amd64 --format json keras/\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/osv-scanner-keras.png\"\u003e\u003c/p\u003e\n\u003cp\u003e输出内容说明：keras存在一个“潜在内存泄漏”的漏洞。\u003c/p\u003e\n\u003cp\u003e当拿到json结果后，我们的DevOps平台就可以进行一些告警监控的操作。\u003c/p\u003e\n\u003ch2 id=\"后记\"\u003e后记\u003c/h2\u003e\n\u003cp\u003eosv-scanner目前需要连osv.dev，才能使用。但是，已经开放实验功能，允许用户离线使用osv-scanner。这是自建DevOps平台的福音！\u003c/p\u003e","title":"使用Google OSV工具扫描依赖安全漏洞"},{"content":"有同学在知乎上提问：“线上无事故，运维还重要吗？”，描述如下：\n本人运维行业，本部门在近几年一直保持效率增长且极少出现重大saas生产事故，并且为其他部门输出提升方法以及友好协同提升，但是最近从各层面接到反馈说对运维的投入减少，着实想不通，线上出了事故要运维背锅，产品出了bug要运维陪着到最晚，为什么把线上环境搞得稳定了，却不重视运维岗了？\n这是原贴：https://www.zhihu.com/question/497361582\n以上提问的是一个运维的同学。言下之义是不出事故，没有人知道运维重要。\n这位同学的的感受，过去几年，我感同深受。我相信因为这个标题而点进这篇博客的同学，也有同样的感受。\n但是，为什么出事故后，是运维重要呢？而不是测试、开发或者手机端开发呢？\n通常是因为运维这个角色：\n线上环境，他们最清楚，通常也只有他们有权限操作线上环境，可以紧急加一个数据库索引； 他们掌握了部署能力，可以发起回滚操作； 有权限查看各个组件的情况，并诊断根因； 为团队准备基础设施能力，如金丝雀发布能力； 搭建告警监控系统、CMDB、DevOps平台等。 等等 但是，这些与是否出事故，有多大的关联性呢？我们应该统计各种事故的根因的类型的比例，才有答案。\n就目前而言，我们并不能说因为我们看重运维，就不出事故。\n以上的问题是从个人感受出发的提问。只是更深层次问题的表象。\n从企业层面上，我的疑问是：为什么在企业里，稳定性建设通常都是一阵阵的。即出一次事故，就立个项，就加班加点去完成“稳定性”项目。\n比起讨论个人感受，从企业层面讨论这个问题，似乎更有趣。\n其实，除了稳定性，软件的质量建设也是一阵阵的。想想，不是吗？不出Bug，没有人知道测试重要。\n也许这是所有企业的正常表现。就像人的身体，痛风（一种慢性病）不发作时，你是不会感受它的存在，也自然就不会想到要去治疗或者预防它。然而，如果平时不注意饮食和锻炼，痛风经常复发。\n线上事故就如同企业的痛风。企业应对“痛风”，容易好了伤疤忘了痛。\n虽说可能是所有企业的正常表现，但不是一种健康的表现。\n预防痛风，只能通过健康的生活方式如：\n限制或避免饮酒，尤其是啤酒。 限制或者避免饮用含糖饮料，尤其是含高果糖玉米糖浆的饮料。 限制肉类摄入量，尤其是红肉、内脏和海鲜。 保持健康的体重。如果您需要减肥，请避免断食或过快地减肥，因为这可能会暂时增加尿酸水平。 增加水和低脂乳制品的摄入量。这些有预防痛风的作用。 一个人应对痛风的健康表现应该是采用健康的生活方式。\n说回企业的稳定性建设，也是一样的道理。\n稳定性不是通过“一阵阵的运动”或者“一阵阵的表演”来建设的，而是通过平时健康的企业活动来实现（我无意指导别人的企业，这只是我个人的思考）。\n当然，现实中，对于有些人，要维持健康的生活方式是一件很难的事情（想想有身边有多少人做到早睡早起），而另一些人是一件很自然的事。为什么呢？\n相同的，一家企业为什么无法自然地做到健康的企业活动？一定要出事故，才知道X的重要性呢？（X代表任何东西）\n这个问题就很大了。希望对各位读者有启发。\n","permalink":"https://showme.codes/zh-cn/2023-11-21-incident-important-person/","summary":"\u003cp\u003e有同学在知乎上提问：“线上无事故，运维还重要吗？”，描述如下：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e本人运维行业，本部门在近几年一直保持效率增长且极少出现重大saas生产事故，并且为其他部门输出提升方法以及友好协同提升，但是最近从各层面接到反馈说对运维的投入减少，着实想不通，线上出了事故要运维背锅，产品出了bug要运维陪着到最晚，为什么把线上环境搞得稳定了，却不重视运维岗了？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这是原贴：https://www.zhihu.com/question/497361582\u003c/p\u003e\n\u003cp\u003e以上提问的是一个运维的同学。言下之义是不出事故，没有人知道运维重要。\u003c/p\u003e\n\u003cp\u003e这位同学的的感受，过去几年，我感同深受。我相信因为这个标题而点进这篇博客的同学，也有同样的感受。\u003c/p\u003e\n\u003cp\u003e但是，为什么出事故后，是运维重要呢？而不是测试、开发或者手机端开发呢？\u003c/p\u003e\n\u003cp\u003e通常是因为运维这个角色：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e线上环境，他们最清楚，通常也只有他们有权限操作线上环境，可以紧急加一个数据库索引；\u003c/li\u003e\n\u003cli\u003e他们掌握了部署能力，可以发起回滚操作；\u003c/li\u003e\n\u003cli\u003e有权限查看各个组件的情况，并诊断根因；\u003c/li\u003e\n\u003cli\u003e为团队准备基础设施能力，如金丝雀发布能力；\u003c/li\u003e\n\u003cli\u003e搭建告警监控系统、CMDB、DevOps平台等。\u003c/li\u003e\n\u003cli\u003e等等\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e但是，这些与是否出事故，有多大的关联性呢？我们应该统计各种事故的根因的类型的比例，才有答案。\u003c/p\u003e\n\u003cp\u003e就目前而言，我们并不能说因为我们看重运维，就不出事故。\u003c/p\u003e\n\u003cp\u003e以上的问题是从个人感受出发的提问。只是更深层次问题的表象。\u003c/p\u003e\n\u003cp\u003e从企业层面上，我的疑问是：为什么在企业里，稳定性建设通常都是一阵阵的。即出一次事故，就立个项，就加班加点去完成“稳定性”项目。\u003c/p\u003e\n\u003cp\u003e比起讨论个人感受，从企业层面讨论这个问题，似乎更有趣。\u003c/p\u003e\n\u003cp\u003e其实，除了稳定性，软件的质量建设也是一阵阵的。想想，不是吗？不出Bug，没有人知道测试重要。\u003c/p\u003e\n\u003cp\u003e也许这是所有企业的正常表现。就像人的身体，痛风（一种慢性病）不发作时，你是不会感受它的存在，也自然就不会想到要去治疗或者预防它。然而，如果平时不注意饮食和锻炼，痛风经常复发。\u003c/p\u003e\n\u003cp\u003e线上事故就如同企业的痛风。企业应对“痛风”，容易好了伤疤忘了痛。\u003c/p\u003e\n\u003cp\u003e虽说可能是所有企业的正常表现，但不是一种健康的表现。\u003c/p\u003e\n\u003cp\u003e预防痛风，只能通过健康的生活方式如：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e限制或避免饮酒，尤其是啤酒。\u003c/li\u003e\n\u003cli\u003e限制或者避免饮用含糖饮料，尤其是含高果糖玉米糖浆的饮料。\u003c/li\u003e\n\u003cli\u003e限制肉类摄入量，尤其是红肉、内脏和海鲜。\u003c/li\u003e\n\u003cli\u003e保持健康的体重。如果您需要减肥，请避免断食或过快地减肥，因为这可能会暂时增加尿酸水平。\u003c/li\u003e\n\u003cli\u003e增加水和低脂乳制品的摄入量。这些有预防痛风的作用。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e一个人应对痛风的健康表现应该是采用健康的生活方式。\u003c/p\u003e\n\u003cp\u003e说回企业的稳定性建设，也是一样的道理。\u003c/p\u003e\n\u003cp\u003e稳定性不是通过“一阵阵的运动”或者“一阵阵的表演”来建设的，而是通过平时健康的企业活动来实现（我无意指导别人的企业，这只是我个人的思考）。\u003c/p\u003e\n\u003cp\u003e当然，现实中，对于有些人，要维持健康的生活方式是一件很难的事情（想想有身边有多少人做到早睡早起），而另一些人是一件很自然的事。为什么呢？\u003c/p\u003e\n\u003cp\u003e相同的，一家企业为什么无法自然地做到健康的企业活动？一定要出事故，才知道X的重要性呢？（X代表任何东西）\u003c/p\u003e\n\u003cp\u003e这个问题就很大了。希望对各位读者有启发。\u003c/p\u003e","title":"不出事故，没有人知道你重要"},{"content":"说到构建工具，不同语言技术栈的人，想起的构建工具不同。\nJava程序员想到的是Maven，前端程序员想的是NPM或者Webpack、Android程序员想到的是Gradle、Rust程序想到的是Cargo、C++程序员想到的是Make等等。\n然而这些工具在Bazel面前，层次有些低。所以，我愿称Bazel是构建工具之王。\nP.S. Android平台的构建，2020年已经开始了迁移到Bazel的工作。 具体地址：https://blog.bazel.build/2020/11/12/aosp_migrating_to_bazel.html\nBazel介绍 Bazel是Google在2015年开源的一款构建工具。\n目前使用Bazel的知名公司有：Esty、Canva、Databricks、Dropbox、Huawei、Line、LinkedIn、Stripe、Twitter、Tinder、Uber、VMware、Wix等。具体可以看：https://bazel.build/community/users 。\n其中Twitter是从自家的Pants迁移到的Bazel的，具体迁移过程介绍：https://opensourcelive.withgoogle.com/events/bazelcon2020/watch?talk=day1-talk2\nFacebook使用的是其自研的Buck2，但是，其与Bazel使用的是相同的远程执行的API。\n除了公司，某些著名的开源软件也使用Bazel构建，包括自动化测试领域的Selenium，AI领域的TensorFlow，容器编排领域的Kubernetes等。具体还有：https://bazel.build/community/users#open-source-projects-using-Bazel\n相对于其它构建工具，它的显著的特点有：\n支持多语言； 支持远程分布式构建； 支持增量构建； 支持强大的密闭性； 支持构建缓存； 支持并行构建。 假设存在一个复杂的软件工程 假设存在一个软件工程中，它包含5部分：Web前端、Android端、Java后端、Go后端、嵌入式端。\n作为Java后端的程序员，他们修改了一个API。但是他作为个人，他无法预知到底发生了哪些影响。\n所以，他把这个问题交给了持续集成（CI），让它去发现集成问题。\n在过去很长一段时间里，行业里只有一种CI模式，我称之为传统的CI模式。\n殊不知，还有另一种模式。\n传统的CI模式 目前行业里比较传统的CI架构，通常如下： 在这样的架构下，实现CI的步骤如下：\n开发人员提交代码； Gitlab检测到开发人员提交代码，然后触发Jenkins controller执行； Jenkins controller根据该代码仓库预先设计的pipeline执行； Jenkins controller根据pipeline中的任务所需要的构建环境，将任务分配给不同的Jenkins agent； 在agent构建完成后，将制品release到制品仓库中。 如果开发者希望验证自己写的代码，就必须将代码commit到Gitlab中。因为整个验证环境被定义在CI环境的Pipeline中。而且这个过程，越大的工程，集成速度越慢。开发者也无法在本地进行全量验证。\n作为Pipeline的维护者，他需要清楚知道哪些任务是可以并行执行的，并手工配置并行，这样才能加快构建速度。比如前端构建和后端构建可以并行进行。\n也就是说在传统的CI模式下，开发者的效率会随着软件的规模越大而降低。换句话，这样的模式，开发效率无法scale。\n案例 希望以下案例可以给你一个感性的认知。下图是Google在2010年到2015年的周commit数量。绿线代表commit总数，黄线是人数。我们取离我们最近的2015年的数据来讨论。2015年的代码量如下： 在这个代码量下，每周能达到300左右的commit。如下图： 根据持续集成的原则，每一个commit都必须构建通过。20亿行代码一次全量构建需要多久？\n我们以一个开源项目作参考。apitable是一个开源的数据表格项目，它有200万左右的代码，全量构建一次需要20分钟左右。那么，根据不准确的类推，20亿行代码，全量构建一次需要：20/2,000,000 * 2000,000,000=200,000,000分钟，也就是13天左右。\n在传统的CI模式下，是尽量避免执行全量构建这样庞大的代码量的。所以，传统CI模式下，通常是多仓库模式管理代码。\n那么Bazel呢？Bazel如果真要构建这样庞大的代码量，估计也够呛。但是由于Bazel天然支持并行构建、构建缓存和增量构建，所以，Bazel通常不会遇到真正意义的全量构建的情况。\n为什么其它公司不使用Bazel 也许有人会问：为什么阿里2018年新增的代码行(https://zhuanlan.zhihu.com/p/54435171)就有12亿，不也没有使用Bazel吗？\n这个是一个好问题。\n但是，无法简单的回答这个问题，而是需要深入到各自组织内部才能分析清楚。个人觉得可以从以下维度分析：\n在代码仓库上工作的人员的规模：同样的代码量，不同的组织需要不同数量的人维护； 代码管理方式：阿里使用多仓库的管理办法，不需要统一的版本号； 持续集成的程度不同：阿里可能不需要对每一个commit跑一次全量。 为什么Bazel会颠覆你对CI的认知 Bazel是如何解决传统CI模式下开发效率无法scale的问题呢？其主要通过它的六个特性来解决。\n首先，Bazel支持远程分布式构建。\n在一个使用Bazel构建的仓库中，开发者写好代码后，不用commit代码到Git仓库，只要在本地命令行执行bazel run --remote_executor=grpc://localhost:8980 //... ，代码仓库中所有构建和测试任务都将运行在远程执行服务器。远程执行服务器越多，构建速度越快。\n这一特性可以明显地提高开发者本地的开发效率。因为开发者在本地就可以执行全量构建和全量测试。\n传统CI模式下，无法提升开发者本地的开发效率。\n第二，Bazel支持增量构建和增量测试（精准测试）。\n开发者在本地执行build命令时，Bazel检测出修改了a.java文件，所以，Bazel只将构建a.java的任务及其相关的构建任务给远程执行服务器执行。这就是增量构建。\n如果开发者执行test命令，Bazel则能检测出被影响的测试，然后只运行这些测试。其实这就是精准测试了。在Bazel中，精准测试实现起来并不难。\n传统CI模式下，它是不关心增量构建和增量测试的。所以，每次运行都是全量。这是一种极大的浪费。\n第三，支持构建缓存。\n当程序员A执行完成构建后，Bazel会将所有的构建结果缓存起来。另一个程序员在同样的代码基础上执行相同的构建时，Bazel会直接读取缓存，而不再重复执行构建。\n传统CI模式不关心构建缓存。\n第四，支持强大的密闭性。\n传统CI模式通过构建节点提供构建环境。当要执行mvn构建命令时，它会依赖构建环境是否已经安装好Maven。当要对Prometheus的配置进行验证时，就需要构建环境提供promtool命令行程序。\n而Bazel提供一种叫工具链的机制，在执行到相应的任务时，Bazel通过该机制实现根据操作系统下载相应版本的构建工具。而不需要开发者操心构建工具的问题。\n同时这一机制也是实现分布式构建的基础。\n另，行业里构建工具之间的速度对比的报告，往往没有将构建环境准备的时间计算在内，这是不合理的对比方式。因为Bazel在真正开始构建前，会自动准备构建环境，以保证环境的密闭性。比如准备NodeJS环境或者Go环境。\n感兴趣的同学可以看看自Gradle(https://blog.gradle.org/gradle-vs-bazel-jvm)的报告：\n密闭性的另一个好处是提升构建的准确性。现实中经常出现的现象——程序员向DevOps平台抱怨“为什么我本地构建可以，在DevOps平台上构建就不行”——就可以得到很好的解决。\n第五、支持多语言。\n与针对单一语言而设计的构建工具不同，Bazel提供了一种叫rule的扩展机制，无限支持不同的语言。\n当然，Java程序员不关心go程序员写的代码，但是，同一个软件工程下，不论哪门语言程序员都必须关心他写的代码是否对其他人写的代码有影响。\n现实中，不同语言之间的引用关系是一定存在的。比如Java程序员改一个protobuf的定义，而go程序引用了这个定义。传统CI模式是无法感知到这个引用关系的，自然就无法容易的实现分布式构建和增量构建。\n在Bazel中，不同语言之间的构建任务也是可以相互引用的。\n第六、支持并行构建。\nBazel使用声明式的语言描述构建任务，使得它可以自动分析出哪些构建任务是可以并行执行的。\n传统CI模块下，需要人工维护并行任务。这不仅需要人力，还不一定比Bazel做得好。\n总之，有了Bazel之后，很多以前在CI pipeline中做的事情，就可以放在Bazel中实现了。\nBazel与IaC的关系 当基础设施被写成代码（Infrastructure as Code）后，实际上是需要构建和测试才能真正上线的。虽然Terraform提供了plan命令，方便开发人员在真正部署前就知道真正部署的内容，但是，整个基础设施不仅仅用Terraform，还会使用其它工具。\n这些工具的配置又该如何在真正部署前进行自动构建测试呢？\n由于Bazel支持多语言、密闭性和扩展机制，你可以通过Bazel实现对于基础设施的构建。\n这是我通过扩展Bazel实现对Prometheus配置和告警规则进行单元测试的案例: https://github.com/zacker330/rules_prometheus 。\nBazel与单仓库的关系 单仓库指的是将多语言、多项目的代码放在同一个代码仓库中，而不是分成多个代码仓库。它有很多好处，我们可以另开一篇文章讨论。\n代码的规模大到一定的程度，一定会遇到各种问题。而单仓库是解决这些问题的良方。大家可以看看Google的解决方案：https://www.youtube.com/watch?v=W71BTkUbdqE\n如果是单仓库，就必须使用类似Bazel能很好支持单仓库的工具。\n但是使用Bazel，就一定要推广单仓库吗？不一定。原来的多代码仓库下，也可以使用Bazel。\n后记 写本文的目的，并不是鼓励大家无脑地在自己的工程中使用Bazel。因为现实中，一个构建工具的引入，除了考虑构建速度，还有其它的因素需要考虑。\n接下来，我还会花很多时间在Bazel上。如果对Bazel或者构建工具感兴趣的同学，可以加群交流：\n","permalink":"https://showme.codes/zh-cn/2023-11-20-bazel-king-of-build-tool/","summary":"\u003cp\u003e说到构建工具，不同语言技术栈的人，想起的构建工具不同。\u003c/p\u003e\n\u003cp\u003eJava程序员想到的是Maven，前端程序员想的是NPM或者Webpack、Android程序员想到的是Gradle、Rust程序想到的是Cargo、C++程序员想到的是Make等等。\u003c/p\u003e\n\u003cp\u003e然而这些工具在Bazel面前，层次有些低。所以，我愿称Bazel是构建工具之王。\u003c/p\u003e\n\u003cp\u003eP.S. Android平台的构建，2020年已经开始了迁移到Bazel的工作。 具体地址：https://blog.bazel.build/2020/11/12/aosp_migrating_to_bazel.html\u003c/p\u003e\n\u003ch2 id=\"bazel介绍\"\u003eBazel介绍\u003c/h2\u003e\n\u003cp\u003eBazel是Google在2015年开源的一款构建工具。\u003c/p\u003e\n\u003cp\u003e目前使用Bazel的知名公司有：Esty、Canva、Databricks、Dropbox、Huawei、Line、LinkedIn、Stripe、Twitter、Tinder、Uber、VMware、Wix等。具体可以看：https://bazel.build/community/users 。\u003c/p\u003e\n\u003cp\u003e其中Twitter是从自家的Pants迁移到的Bazel的，具体迁移过程介绍：https://opensourcelive.withgoogle.com/events/bazelcon2020/watch?talk=day1-talk2\u003c/p\u003e\n\u003cp\u003eFacebook使用的是其自研的\u003ca href=\"https://engineering.fb.com/2023/04/06/open-source/buck2-open-source-large-scale-build-system/\"\u003eBuck2\u003c/a\u003e，但是，其与Bazel使用的是相同的远程执行的API。\u003c/p\u003e\n\u003cp\u003e除了公司，某些著名的开源软件也使用Bazel构建，包括自动化测试领域的Selenium，AI领域的TensorFlow，容器编排领域的Kubernetes等。具体还有：https://bazel.build/community/users#open-source-projects-using-Bazel\u003c/p\u003e\n\u003cp\u003e相对于其它构建工具，它的显著的特点有：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e支持多语言；\u003c/li\u003e\n\u003cli\u003e支持远程分布式构建；\u003c/li\u003e\n\u003cli\u003e支持增量构建；\u003c/li\u003e\n\u003cli\u003e支持强大的密闭性；\u003c/li\u003e\n\u003cli\u003e支持构建缓存；\u003c/li\u003e\n\u003cli\u003e支持并行构建。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"假设存在一个复杂的软件工程\"\u003e假设存在一个复杂的软件工程\u003c/h2\u003e\n\u003cp\u003e假设存在一个软件工程中，它包含5部分：Web前端、Android端、Java后端、Go后端、嵌入式端。\u003c/p\u003e\n\u003cp\u003e作为Java后端的程序员，他们修改了一个API。但是他作为个人，他无法预知到底发生了哪些影响。\u003c/p\u003e\n\u003cp\u003e所以，他把这个问题交给了持续集成（CI），让它去发现集成问题。\u003c/p\u003e\n\u003cp\u003e在过去很长一段时间里，行业里只有一种CI模式，我称之为传统的CI模式。\u003c/p\u003e\n\u003cp\u003e殊不知，还有另一种模式。\u003c/p\u003e\n\u003ch2 id=\"传统的ci模式\"\u003e传统的CI模式\u003c/h2\u003e\n\u003cp\u003e目前行业里比较传统的CI架构，通常如下：\n\u003cimg loading=\"lazy\" src=\"/assets/images/ci-traditional-architechure.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在这样的架构下，实现CI的步骤如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e开发人员提交代码；\u003c/li\u003e\n\u003cli\u003eGitlab检测到开发人员提交代码，然后触发Jenkins controller执行；\u003c/li\u003e\n\u003cli\u003eJenkins controller根据该代码仓库预先设计的pipeline执行；\u003c/li\u003e\n\u003cli\u003eJenkins controller根据pipeline中的任务所需要的构建环境，将任务分配给不同的Jenkins agent；\u003c/li\u003e\n\u003cli\u003e在agent构建完成后，将制品release到制品仓库中。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e如果开发者希望验证自己写的代码，就必须将代码commit到Gitlab中。因为整个验证环境被定义在CI环境的Pipeline中。而且这个过程，越大的工程，集成速度越慢。开发者也无法在本地进行全量验证。\u003c/p\u003e\n\u003cp\u003e作为Pipeline的维护者，他需要清楚知道哪些任务是可以并行执行的，并手工配置并行，这样才能加快构建速度。比如前端构建和后端构建可以并行进行。\u003c/p\u003e\n\u003cp\u003e也就是说在传统的CI模式下，开发者的效率会随着软件的规模越大而降低。换句话，这样的模式，开发效率无法scale。\u003c/p\u003e\n\u003ch2 id=\"案例\"\u003e案例\u003c/h2\u003e\n\u003cp\u003e希望以下案例可以给你一个感性的认知。下图是Google在2010年到2015年的周commit数量。绿线代表commit总数，黄线是人数。我们取离我们最近的2015年的数据来讨论。2015年的代码量如下：\n\u003cimg loading=\"lazy\" src=\"/assets/images/linesofcodeofgoogle.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在这个代码量下，每周能达到300左右的commit。如下图：\n\u003cimg loading=\"lazy\" src=\"/assets/images/google-commit-per-week.png\"\u003e\u003c/p\u003e\n\u003cp\u003e根据持续集成的原则，每一个commit都必须构建通过。20亿行代码一次全量构建需要多久？\u003c/p\u003e\n\u003cp\u003e我们以一个开源项目作参考。apitable是一个开源的数据表格项目，它有200万左右的代码，全量构建一次需要20分钟左右。那么，根据不准确的类推，20亿行代码，全量构建一次需要：\u003ccode\u003e20/2,000,000 * 2000,000,000=200,000,000\u003c/code\u003e分钟，也就是13天左右。\u003c/p\u003e\n\u003cp\u003e在传统的CI模式下，是尽量避免执行全量构建这样庞大的代码量的。所以，传统CI模式下，通常是多仓库模式管理代码。\u003c/p\u003e\n\u003cp\u003e那么Bazel呢？Bazel如果真要构建这样庞大的代码量，估计也够呛。但是由于Bazel天然支持并行构建、构建缓存和增量构建，所以，Bazel通常不会遇到真正意义的全量构建的情况。\u003c/p\u003e\n\u003ch2 id=\"为什么其它公司不使用bazel\"\u003e为什么其它公司不使用Bazel\u003c/h2\u003e\n\u003cp\u003e也许有人会问：为什么阿里2018年新增的代码行(\u003ca href=\"https://zhuanlan.zhihu.com/p/54435171\"\u003ehttps://zhuanlan.zhihu.com/p/54435171\u003c/a\u003e)就有12亿，不也没有使用Bazel吗？\u003c/p\u003e\n\u003cp\u003e这个是一个好问题。\u003c/p\u003e\n\u003cp\u003e但是，无法简单的回答这个问题，而是需要深入到各自组织内部才能分析清楚。个人觉得可以从以下维度分析：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在代码仓库上工作的人员的规模：同样的代码量，不同的组织需要不同数量的人维护；\u003c/li\u003e\n\u003cli\u003e代码管理方式：阿里使用多仓库的管理办法，不需要统一的版本号；\u003c/li\u003e\n\u003cli\u003e持续集成的程度不同：阿里可能不需要对每一个commit跑一次全量。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"为什么bazel会颠覆你对ci的认知\"\u003e为什么Bazel会颠覆你对CI的认知\u003c/h2\u003e\n\u003cp\u003eBazel是如何解决传统CI模式下开发效率无法scale的问题呢？其主要通过它的六个特性来解决。\u003c/p\u003e\n\u003cp\u003e首先，Bazel支持远程分布式构建。\u003c/p\u003e\n\u003cp\u003e在一个使用Bazel构建的仓库中，开发者写好代码后，不用commit代码到Git仓库，只要在本地命令行执行\u003ccode\u003ebazel run --remote_executor=grpc://localhost:8980 //...\u003c/code\u003e ，代码仓库中所有构建和测试任务都将运行在远程执行服务器。远程执行服务器越多，构建速度越快。\u003c/p\u003e\n\u003cp\u003e这一特性可以明显地提高开发者本地的开发效率。因为开发者在本地就可以执行全量构建和全量测试。\u003c/p\u003e\n\u003cp\u003e传统CI模式下，无法提升开发者本地的开发效率。\u003c/p\u003e\n\u003cp\u003e第二，Bazel支持增量构建和增量测试（精准测试）。\u003c/p\u003e\n\u003cp\u003e开发者在本地执行build命令时，Bazel检测出修改了a.java文件，所以，Bazel只将构建a.java的任务及其相关的构建任务给远程执行服务器执行。这就是增量构建。\u003c/p\u003e\n\u003cp\u003e如果开发者执行test命令，Bazel则能检测出被影响的测试，然后只运行这些测试。其实这就是精准测试了。在Bazel中，精准测试实现起来并不难。\u003c/p\u003e\n\u003cp\u003e传统CI模式下，它是不关心增量构建和增量测试的。所以，每次运行都是全量。这是一种极大的浪费。\u003c/p\u003e","title":"Bazel作为构建工具之王，将会颠覆你对CI的认知"},{"content":"​年底了，事故频发。但是都听说是因为循环依赖导致。所以，我决定来写写依赖管理领域中，通常不被重视的循环依赖问题。\n循环依赖(circular dependencies)的定义 来自维基百科的定义：\n在软件工程中，循环依赖是两个或多个模块之间的关系，这些模块直接或间接地相互依赖才能正常运行。此类模块也称为相互递归。\n循环依赖是依赖管理领域中经常出现的一种现象，如下图：\n循环依赖的不同层次以及后果 循环依赖可以发生在两个层次：\n源码之间相互引用依赖； 服务之间相互调用依赖。 源码之间产生循环依赖带来的问题是：构建工具不知道应该从何处开始构建。因为构建工具无从下手。\n构建工具底线这时就体现出来了，如果它可以忽略其中一个节点，“勉强”构建出一个制品，那么这个制品估计你也不敢用，因为该制品存在不确定性。这是源码层次中，循环依赖的后果。\n而服务之间调用的循环依赖就更麻烦了。平时非常难发现，是不会出事故的，但一出事故就会雪崩。\n因为你无法单独启动循环依赖中的任何一个服务，而循环依赖中的任何一个服务挂了，其它所有的节点都会同时挂。\n循环依赖的环越大，影响面越大。\n为什么会出现循环依赖 既然循环依赖从任何一个节点都法进行构建或者启动，那么它又为什么会产生？难道定义依赖的人不知道吗？Code Review的人不知道吗？\n因为循环依赖是软件系统在长时间发展中，不加以合理的依赖管理所导致的。如下图。一开始只是B的V1版本依赖A的V1版本，最后变成B的V2版本与A的V3版本相互依赖。\n软件系统发展的时间足够长，交接的人数足够多，软件依赖关系的规模早就超出了人类能处理的限度。\n如何从根上就避免循环依赖 用人力的办法解决循环依赖，是不现实的。依赖管理这种吃力又不讨好，还拿不上台面的术语，没有人会去做。更不会得到KPI的“赏识”。\n可以预料得到，如果对依赖管理治理不当，那么每几年，就可能出现一次循环依赖的事故。不发生事故的原因可能只有一个：软件的规模还不够大。\n所以，我们必须想办法从根上避免循环依赖，即从依赖的定义的地方开始治理。\n源代码层次，构建工具就可以帮我们依赖。只要出现循环依赖，构建就不通过。例如Make工具：\nmake: Circular main.asm.o \u0026lt;- main.asm dependency dropped. 但是，如果是多仓库管理源代码的模式下，你可能还是很难避免循环依赖的情况。如果是单仓库，就不会出现这样的情况。\n而服务之间的调用关系，在不同的公司定义的位置不一样。这也决定了查找循环依赖的成本。\n服务之间的调用关系的定义，本质上属于配置管理的范畴。因为你总要在某个地方定义这些依赖。\n服务之间的循环依赖查找，本质上就是配置管理领域中，查找配置之间的相互引用关系。\n这说明配置管理的方式决定了查找服务之间循环依赖的成本。\n什么样的配置管理方式才能低成本的实现查找循环依赖呢？\n在这里，我提出我的方案：使用Jsonnet定义所有的配置（某些case无法覆盖），然后通过Bazel进行构建。\nJsonnet是一门专为配置定义而设计语言，语言只有一页A4纸； Bazel是一款支持增量构建、分布构建、构建缓存、支持多语言的构建工具。 通过这个方案，Bazel会自动构建一个软件依赖关系图，同时检测其中是否存在循环关系。只要循环关系存在，构建就不通过，当然就无法上线了。这样就从根上就避免了循环依赖。\n如果想了解更多具体的落地方式，请关注我。并转发本文，让更多人看到这种神奇的配置管理方式。\n","permalink":"https://showme.codes/zh-cn/2023-11-11-sre-dimond-incident/","summary":"\u003cp\u003e​年底了，事故频发。但是都听说是因为循环依赖导致。所以，我决定来写写依赖管理领域中，通常不被重视的循环依赖问题。\u003c/p\u003e\n\u003ch2 id=\"循环依赖circular-dependencies的定义\"\u003e循环依赖(circular dependencies)的定义\u003c/h2\u003e\n\u003cp\u003e来自维基百科的定义：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在软件工程中，循环依赖是两个或多个模块之间的关系，这些模块直接或间接地相互依赖才能正常运行。此类模块也称为相互递归。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e循环依赖是依赖管理领域中经常出现的一种现象，如下图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%962.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"循环依赖的不同层次以及后果\"\u003e循环依赖的不同层次以及后果\u003c/h2\u003e\n\u003cp\u003e循环依赖可以发生在两个层次：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e源码之间相互引用依赖；\u003c/li\u003e\n\u003cli\u003e服务之间相互调用依赖。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e源码之间产生循环依赖带来的问题是：构建工具不知道应该从何处开始构建。因为构建工具无从下手。\u003c/p\u003e\n\u003cp\u003e构建工具底线这时就体现出来了，如果它可以忽略其中一个节点，“勉强”构建出一个制品，那么这个制品估计你也不敢用，因为该制品存在不确定性。这是源码层次中，循环依赖的后果。\u003c/p\u003e\n\u003cp\u003e而服务之间调用的循环依赖就更麻烦了。平时非常难发现，是不会出事故的，但一出事故就会雪崩。\u003c/p\u003e\n\u003cp\u003e因为你无法单独启动循环依赖中的任何一个服务，而循环依赖中的任何一个服务挂了，其它所有的节点都会同时挂。\u003c/p\u003e\n\u003cp\u003e循环依赖的环越大，影响面越大。\u003c/p\u003e\n\u003ch2 id=\"为什么会出现循环依赖\"\u003e为什么会出现循环依赖\u003c/h2\u003e\n\u003cp\u003e既然循环依赖从任何一个节点都法进行构建或者启动，那么它又为什么会产生？难道定义依赖的人不知道吗？Code Review的人不知道吗？\u003c/p\u003e\n\u003cp\u003e因为循环依赖是软件系统在长时间发展中，不加以合理的依赖管理所导致的。如下图。一开始只是B的V1版本依赖A的V1版本，最后变成B的V2版本与A的V3版本相互依赖。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%963.png\"\u003e\u003c/p\u003e\n\u003cp\u003e软件系统发展的时间足够长，交接的人数足够多，软件依赖关系的规模早就超出了人类能处理的限度。\u003c/p\u003e\n\u003ch2 id=\"如何从根上就避免循环依赖\"\u003e如何从根上就避免循环依赖\u003c/h2\u003e\n\u003cp\u003e用人力的办法解决循环依赖，是不现实的。依赖管理这种吃力又不讨好，还拿不上台面的术语，没有人会去做。更不会得到KPI的“赏识”。\u003c/p\u003e\n\u003cp\u003e可以预料得到，如果对依赖管理治理不当，那么每几年，就可能出现一次循环依赖的事故。不发生事故的原因可能只有一个：软件的规模还不够大。\u003c/p\u003e\n\u003cp\u003e所以，我们必须想办法从根上避免循环依赖，即从依赖的定义的地方开始治理。\u003c/p\u003e\n\u003cp\u003e源代码层次，构建工具就可以帮我们依赖。只要出现循环依赖，构建就不通过。例如Make工具：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emake: Circular main.asm.o \u0026lt;- main.asm dependency dropped.\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e但是，如果是多仓库管理源代码的模式下，你可能还是很难避免循环依赖的情况。如果是单仓库，就不会出现这样的情况。\u003c/p\u003e\n\u003cp\u003e而服务之间的调用关系，在不同的公司定义的位置不一样。这也决定了查找循环依赖的成本。\u003c/p\u003e\n\u003cp\u003e服务之间的调用关系的定义，本质上属于配置管理的范畴。因为你总要在某个地方定义这些依赖。\u003c/p\u003e\n\u003cp\u003e服务之间的循环依赖查找，本质上就是配置管理领域中，查找配置之间的相互引用关系。\u003c/p\u003e\n\u003cp\u003e这说明配置管理的方式决定了查找服务之间循环依赖的成本。\u003c/p\u003e\n\u003cp\u003e什么样的配置管理方式才能低成本的实现查找循环依赖呢？\u003c/p\u003e\n\u003cp\u003e在这里，我提出我的方案：使用Jsonnet定义所有的配置（某些case无法覆盖），然后通过Bazel进行构建。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eJsonnet是一门专为配置定义而设计语言，语言只有一页A4纸；\u003c/li\u003e\n\u003cli\u003eBazel是一款支持增量构建、分布构建、构建缓存、支持多语言的构建工具。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e通过这个方案，Bazel会自动构建一个软件依赖关系图，同时检测其中是否存在循环关系。只要循环关系存在，构建就不通过，当然就无法上线了。这样就从根上就避免了循环依赖。\u003c/p\u003e\n\u003cp\u003e如果想了解更多具体的落地方式，请关注我。并转发本文，让更多人看到这种神奇的配置管理方式。\u003c/p\u003e","title":"听说最近的事故都是循环依赖导致的？"},{"content":"前天与一个大佬交流。想起自己在6年多前在团队里做的一次小小的效能提升。\n改进前 在同一个产品团队，同时有前端工程师和后端工程师。他们经常需要共同协作完成features。\n前端是一个传统的多页应用。前端渲染是由后端的velocity模板引擎实现的。\n打包后，最终执行就是一个jar包： vm文件后缀名是velocity模板文件。它们内容大概是这样的：\n\u0026lt;html\u0026gt; \u0026lt;body\u0026gt; Hello $customer.Name! \u0026lt;table\u0026gt; #foreach( $mud in $mudsOnSpecial ) #if ( $customer.hasPurchased($mud) ) \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt; $flogger.getPromo( $mud ) \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; #end #end \u0026lt;/table\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 其中一些非html代码就是Velocity Template Language。\n默认情况下，一个前端工程师是不懂这门模板语言的。而且，这种模板语言对浏览器不友好，不像Thymeleaf。同时，按前端的开发习惯，他们是不可能先启动你一个Java进程后，再对前端页面进行调试。\n总的来说，这样的技术栈是不利于前端本地开发的。\n所以，在改进前，前后端的协作的方式是：\n前后端同时进行开发； 后端负责在Java工程中实现后端的逻辑； 前端负责在另一个独立的前端工程中开发HTML和CSS； 前端完成页面的开发后，他会把前端页面html文件名和页面的逻辑告诉后端； 后端再把html页面里内容与Java工程中的vm进行对比，然后将不同的部分转换到Java工程的vm模板页面中。 这样的协作方式下，经常出现以下问题：\n在将html转换到vm模板的过程中，后端有时会转换漏内容； 转换后，前端调试前端问题会很麻烦。前端需要占用一个后端和他一起调试。因为前端不懂如何启动复杂的后端工程； 命名上经常出现不一致，进而导致前端bug。对于同一个概念的情况下，前端命名叫item，后端叫project； 效能提升措施与效果 经分析，以上问题均由“技术之间的转换”导致，即人工的将html页面从一个前端工程转换到一个后端工程的vm模板语言。\n所以，解决以上问题的思路就是：最好能消除html和vm的转换，又或者能减少这个转换过程的失误。\n思路有了以后，解决方案有以下几个：\n更换前端技术栈，不再使用Java+vm模板； 让前端学习vm模板语言，转换过程由前端完成； 优化后端开发在本地启动的流程，方便前端也能在本地启动； 减小测试环境的部署难度。让任何人都可以部署，方便前端进行调试。 方案1短时间无法做到，而且改动巨大，收益也未知。所以，最终是同时做了2、3、4。\n虽然当时没有进行度量，但实际效果就是前文提到的所有问题都得到了减轻。\n原因就是方案2、3、4，减少了前端需要向后端传递的信息。很多事情，前端一个人就可以解决了。\n关于方案2、3，可能有人会好奇：前端去学习一门新的模板语言，成本大吗？前端在本地启动一个后端的工程去调试前端代码，成本高吗？\n首先，vm模板并不是一门难的语言，只要有类C语言的基础（比如Java、JavaScript、C#等），都不难学。无非就是if-else判断、变量定义、循环等。\n其次，在后端优化本地开发环境后，前端启动一个Java工程并不是一件难事。\n反思 整个事件下来，本质上是前端本地开发习惯与现有技术栈的冲突。这次效能的提升需要前端开发做一些工作习惯上妥协。\n而这只是表面上的问题，我们需要去思考更深层的问题：\n团队成员在改进前为什么一直没有考虑如何改进呢？又或者不知道该如何改进？ 对于这次效能改进，感观上是提升了，但是该如何度量呢？ 思考问题1是为了让团队的所有的人有意识和思路地提升效能。思考问题2是为了让实践有数据支撑。\n答案是什么呢？\n","permalink":"https://showme.codes/zh-cn/2023-10-23-a-case-of-productivity/","summary":"\u003cp\u003e前天与一个大佬交流。想起自己在6年多前在团队里做的一次小小的效能提升。\u003c/p\u003e\n\u003ch2 id=\"改进前\"\u003e改进前\u003c/h2\u003e\n\u003cp\u003e在同一个产品团队，同时有前端工程师和后端工程师。他们经常需要共同协作完成features。\u003c/p\u003e\n\u003cp\u003e前端是一个传统的多页应用。前端渲染是由后端的velocity模板引擎实现的。\u003c/p\u003e\n\u003cp\u003e打包后，最终执行就是一个jar包：\n\u003cimg loading=\"lazy\" src=\"/assets/images/hellojar.png\"\u003e\nvm文件后缀名是velocity模板文件。它们内容大概是这样的：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ehtml\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    Hello $customer.Name!\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003etable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    #foreach( $mud in $mudsOnSpecial )\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      #if ( $customer.hasPurchased($mud) )\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003etr\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003etd\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            $flogger.getPromo( $mud )\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003etd\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003etr\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      #end\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    #end\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003etable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ehtml\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e其中一些非html代码就是Velocity Template Language。\u003c/p\u003e\n\u003cp\u003e默认情况下，一个前端工程师是不懂这门模板语言的。而且，这种模板语言对浏览器不友好，不像Thymeleaf。同时，按前端的开发习惯，他们是不可能先启动你一个Java进程后，再对前端页面进行调试。\u003c/p\u003e\n\u003cp\u003e总的来说，这样的技术栈是不利于前端本地开发的。\u003c/p\u003e\n\u003cp\u003e所以，在改进前，前后端的协作的方式是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e前后端同时进行开发；\u003c/li\u003e\n\u003cli\u003e后端负责在Java工程中实现后端的逻辑；\u003c/li\u003e\n\u003cli\u003e前端负责在另一个独立的前端工程中开发HTML和CSS；\u003c/li\u003e\n\u003cli\u003e前端完成页面的开发后，他会把前端页面html文件名和页面的逻辑告诉后端；\u003c/li\u003e\n\u003cli\u003e后端再把html页面里内容与Java工程中的vm进行对比，然后将不同的部分转换到Java工程的vm模板页面中。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这样的协作方式下，经常出现以下问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在将html转换到vm模板的过程中，后端有时会转换漏内容；\u003c/li\u003e\n\u003cli\u003e转换后，前端调试前端问题会很麻烦。前端需要占用一个后端和他一起调试。因为前端不懂如何启动复杂的后端工程；\u003c/li\u003e\n\u003cli\u003e命名上经常出现不一致，进而导致前端bug。对于同一个概念的情况下，前端命名叫item，后端叫project；\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"效能提升措施与效果\"\u003e效能提升措施与效果\u003c/h2\u003e\n\u003cp\u003e经分析，以上问题均由“技术之间的转换”导致，即人工的将html页面从一个前端工程转换到一个后端工程的vm模板语言。\u003c/p\u003e\n\u003cp\u003e所以，解决以上问题的思路就是：最好能消除html和vm的转换，又或者能减少这个转换过程的失误。\u003c/p\u003e\n\u003cp\u003e思路有了以后，解决方案有以下几个：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e更换前端技术栈，不再使用Java+vm模板；\u003c/li\u003e\n\u003cli\u003e让前端学习vm模板语言，转换过程由前端完成；\u003c/li\u003e\n\u003cli\u003e优化后端开发在本地启动的流程，方便前端也能在本地启动；\u003c/li\u003e\n\u003cli\u003e减小测试环境的部署难度。让任何人都可以部署，方便前端进行调试。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e方案1短时间无法做到，而且改动巨大，收益也未知。所以，最终是同时做了2、3、4。\u003c/p\u003e\n\u003cp\u003e虽然当时没有进行度量，但实际效果就是前文提到的所有问题都得到了减轻。\u003c/p\u003e\n\u003cp\u003e原因就是方案2、3、4，减少了前端需要向后端传递的信息。很多事情，前端一个人就可以解决了。\u003c/p\u003e\n\u003cp\u003e关于方案2、3，可能有人会好奇：前端去学习一门新的模板语言，成本大吗？前端在本地启动一个后端的工程去调试前端代码，成本高吗？\u003c/p\u003e\n\u003cp\u003e首先，vm模板并不是一门难的语言，只要有类C语言的基础（比如Java、JavaScript、C#等），都不难学。无非就是if-else判断、变量定义、循环等。\u003c/p\u003e\n\u003cp\u003e其次，在后端优化本地开发环境后，前端启动一个Java工程并不是一件难事。\u003c/p\u003e\n\u003ch2 id=\"反思\"\u003e反思\u003c/h2\u003e\n\u003cp\u003e整个事件下来，本质上是前端本地开发习惯与现有技术栈的冲突。这次效能的提升需要前端开发做一些工作习惯上妥协。\u003c/p\u003e\n\u003cp\u003e而这只是表面上的问题，我们需要去思考更深层的问题：\u003c/p\u003e","title":"反思一次效能提升"},{"content":" Bash能力对于一个运维重要吗？\n这个问题本身该不该问？我觉得是有必要的思考的。这关乎团队的能力建设。\n对于这个问题的答案，我的答案是看情况。\n先说一个现实中真实发生的案例。\n多年前，某部门所维护的系统经常出现一些“异常”的流量。部长让架构师对20多台机器上的系统日志进行分析，以确认是否真的有“异常IP”在做坏事。\n这下可难倒这位架构师了，三天时间没有搞定这个需求。\n最后，过了没有多久，这位架构师就被毕业了。\n纵然这位架构师被毕业的原因很多，但是他是真不会做这个事情，也是事实。\n在手工运维情况下，使用Bash对日志进行分析，在运维行业里一件很常见的事情，是必备技能。\n所以，这位架构师在部长面前就是“没有能力”。\n这时，你觉得Shell/Bash的能力重要吗？\n再说我做DevOps这几年的感受。\n我经历过将手工运维升级至自动化运维的过程。这整个过程，从手工，到使用Ansible自动化部署到虚拟机，再到使用Helm实现自动化部署应用到Kubernetes。\n以上的那位架构师遇到问题，在手工运维阶段，我们也遇到过。\n而且这个问题，是有一个专门的“熟手”负责。他可以熟练的同时登录上多台服务器，然后在多个窗口中娴熟地敲打命令。因为只有他懂grep哪些关键字能快速找到问题，所以，团队里的成员经常找他grep排查问题。\n然而，在我们将日志进行结构化后，所有有权限的人（不论开发还是运维），只要简单的学习一个sql就可以轻松统计分析日志了。\n结果就是不仅这位“熟手”的生产力被释放了，团队里其他经常排队等他帮忙的人的生产力也被释放了。\n在这样的场景下，Shell/Bash的能力重要吗？\nShell/Bash的能力，除了被应用于日志分析，还有一个很大应用：部署。\n比如以下类似的Bash脚本（示例代码是从网上获取的）：\ngroup1_deploy(){ # 代码解压部署函数 writelog \u0026#34;group1_code_deploy\u0026#34; for node in ${GROUP1_LIST};do # 循环生产服务器节点列表 cluster_node_remove $node echo \u0026#34;group1, cluster_node_remove $node\u0026#34; ssh ${node} \u0026#34;cd /opt/webroot \u0026amp;\u0026amp; tar zxf ${PKG_NAME}.tar.gz\u0026#34; # 分别到各web服务器节点执行压缩包解压命令 ssh ${node} \u0026#34;rm -f /webroot/web-demo \u0026amp;\u0026amp; ln -s /opt/webroot/${PKG_NAME} /webroot/web-demo\u0026#34; # 整个自动化的核心，创建软连接 done scp ${CONFIG_DIR}/other/192.168.3.13.server.xml 192.168.3.13:/webroot/web-demo/server.xml # 将差异项目的配置文件scp到此web服务器并以项目结尾 } 当你所在部门还在使用这样的脚本进行自动化部署时，你就必须要有Shell/Bash的能力。但是，当你使用Ansible来实现wordpress的部署，几乎不需要写任何Bash脚本。\n因此，团队里，你不需要招Bash高手，你只需要招一个刚毕业没多久的运维或者开发，就可以维护此Ansible脚本。代码如下：\n- hosts: all become: true # 省略部分代码 tasks: - name: ==\u0026gt; 0 - add host info lineinfile: dest=/etc/hosts line=\u0026#34;10.0.0.10 {{ hostname }}\u0026#34; state=present - name: ==\u0026gt; 1 - add PPA of php7 (community) apt_repository: repo=\u0026#34;ppa:ondrej/php\u0026#34; - name: add Nginx stable repository (deb) apt_repository: \u0026gt; repo=\u0026#39;deb http://nginx.org/packages/ubuntu/ trusty nginx\u0026#39; state=present # 省略部分代码 - name: ==\u0026gt; 4 - install nginx, php-fpm, and php-mysql apt: name={{ item }} state=present with_items: - nginx - php7.0-fpm - php7.0-mysql - name: download wordpress tarball get_url: url: \u0026#34;https://tw.wordpress.org/wordpress-{{ wordpress_version }}-zh_TW.tar.gz\u0026#34; dest: /tmp/ - name: extract wordpress tarball unarchive: src: \u0026#34;/tmp/wordpress-{{ wordpress_version }}-zh_TW.tar.gz\u0026#34; dest: \u0026#34;{{ wordpress_parent_path }}\u0026#34; owner: \u0026#34;{{ wordpress_owner }}\u0026#34; group: \u0026#34;{{ wordpress_group }}\u0026#34; copy: no # 省略部分代码 - name: copy wordpress site conf for nginx template: src: ./templates/nginx-wordpress.conf.j2 dest: /etc/nginx/conf.d/nginx-wordpress.conf - name: fix listen.owner for php-fpm lineinfile: dest: /etc/php/7.0/fpm/pool.d/www.conf regexp: \u0026#39;^listen.owner\\s*=.*$\u0026#39; line: \u0026#34;listen.owner=nginx\u0026#34; state: present - name: restart php-fpm service: name=php7.0-fpm state=restarted 在这样的场景下，Shell/Bash的能力重要吗？\n小结 在手工运维或者基于Bash的运维的场景下，Shell/Bash的能力很重要，他关乎你的饭碗。\n在基于声明式的自动化运维场景，Shell/Bash的能力就没有那么重要了。你能看懂Bash脚本就可以了。即使要写，你让GPT给你写个模板出来即可。\n当然，即使真要写非声明式的自动化脚本，写Python脚本会不会更好？\n","permalink":"https://showme.codes/zh-cn/2023-10-22-shell-bash-non-important/","summary":"\u003cblockquote\u003e\n\u003cp\u003eBash能力对于一个运维重要吗？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这个问题本身该不该问？我觉得是有必要的思考的。这关乎团队的能力建设。\u003c/p\u003e\n\u003cp\u003e对于这个问题的答案，我的答案是看情况。\u003c/p\u003e\n\u003cp\u003e先说一个现实中真实发生的案例。\u003c/p\u003e\n\u003cp\u003e多年前，某部门所维护的系统经常出现一些“异常”的流量。部长让架构师对20多台机器上的系统日志进行分析，以确认是否真的有“异常IP”在做坏事。\u003c/p\u003e\n\u003cp\u003e这下可难倒这位架构师了，三天时间没有搞定这个需求。\u003c/p\u003e\n\u003cp\u003e最后，过了没有多久，这位架构师就被毕业了。\u003c/p\u003e\n\u003cp\u003e纵然这位架构师被毕业的原因很多，但是他是真不会做这个事情，也是事实。\u003c/p\u003e\n\u003cp\u003e在手工运维情况下，使用Bash对日志进行分析，在运维行业里一件很常见的事情，是必备技能。\u003c/p\u003e\n\u003cp\u003e所以，这位架构师在部长面前就是“没有能力”。\u003c/p\u003e\n\u003cp\u003e这时，你觉得Shell/Bash的能力重要吗？\u003c/p\u003e\n\u003cp\u003e再说我做DevOps这几年的感受。\u003c/p\u003e\n\u003cp\u003e我经历过将手工运维升级至自动化运维的过程。这整个过程，从手工，到使用Ansible自动化部署到虚拟机，再到使用Helm实现自动化部署应用到Kubernetes。\u003c/p\u003e\n\u003cp\u003e以上的那位架构师遇到问题，在手工运维阶段，我们也遇到过。\u003c/p\u003e\n\u003cp\u003e而且这个问题，是有一个专门的“熟手”负责。他可以熟练的同时登录上多台服务器，然后在多个窗口中娴熟地敲打命令。因为只有他懂grep哪些关键字能快速找到问题，所以，团队里的成员经常找他grep排查问题。\u003c/p\u003e\n\u003cp\u003e然而，在我们将日志进行结构化后，所有有权限的人（不论开发还是运维），只要简单的学习一个sql就可以轻松统计分析日志了。\u003c/p\u003e\n\u003cp\u003e结果就是不仅这位“熟手”的生产力被释放了，团队里其他经常排队等他帮忙的人的生产力也被释放了。\u003c/p\u003e\n\u003cp\u003e在这样的场景下，Shell/Bash的能力重要吗？\u003c/p\u003e\n\u003cp\u003eShell/Bash的能力，除了被应用于日志分析，还有一个很大应用：部署。\u003c/p\u003e\n\u003cp\u003e比如以下类似的Bash脚本（示例代码是从网上获取的）：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egroup1_deploy\u003cspan class=\"o\"\u003e(){\u003c/span\u003e \u003cspan class=\"c1\"\u003e# 代码解压部署函数\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    writelog \u003cspan class=\"s2\"\u003e\u0026#34;group1_code_deploy\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e node in \u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eGROUP1_LIST\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"k\"\u003edo\u003c/span\u003e \u003cspan class=\"c1\"\u003e# 循环生产服务器节点列表\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        cluster_node_remove \u003cspan class=\"nv\"\u003e$node\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;group1, cluster_node_remove \u003c/span\u003e\u003cspan class=\"nv\"\u003e$node\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        ssh \u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003enode\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;cd /opt/webroot \u0026amp;\u0026amp; tar zxf \u003c/span\u003e\u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003ePKG_NAME\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e.tar.gz\u0026#34;\u003c/span\u003e \u003cspan class=\"c1\"\u003e# 分别到各web服务器节点执行压缩包解压命令\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        ssh \u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003enode\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;rm -f /webroot/web-demo \u0026amp;\u0026amp; ln -s /opt/webroot/\u003c/span\u003e\u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003ePKG_NAME\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e /webroot/web-demo\u0026#34;\u003c/span\u003e \u003cspan class=\"c1\"\u003e# 整个自动化的核心，创建软连接\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    scp \u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eCONFIG_DIR\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e/other/192.168.3.13.server.xml 192.168.3.13:/webroot/web-demo/server.xml  \u003cspan class=\"c1\"\u003e# 将差异项目的配置文件scp到此web服务器并以项目结尾\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e当你所在部门还在使用这样的脚本进行自动化部署时，你就必须要有Shell/Bash的能力。但是，当你使用Ansible来实现wordpress的部署，几乎不需要写任何Bash脚本。\u003c/p\u003e\n\u003cp\u003e因此，团队里，你不需要招Bash高手，你只需要招一个刚毕业没多久的运维或者开发，就可以维护此Ansible脚本。代码如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ehosts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eall\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ebecome\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 省略部分代码\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003etasks\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e==\u0026gt; 0 - add host info\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003elineinfile\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edest=/etc/hosts line=\u0026#34;10.0.0.10  {{ hostname }}\u0026#34; state=present\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e==\u0026gt; 1 - add PPA of php7 (community)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eapt_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003erepo=\u0026#34;ppa:ondrej/php\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eadd Nginx stable repository (deb)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eapt_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e        repo=\u0026#39;deb http://nginx.org/packages/ubuntu/ trusty nginx\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e        state=present\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 省略部分代码\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e==\u0026gt; 4 - install nginx, php-fpm, and php-mysql\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eapt\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ename={{ item }} state=present\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith_items\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"l\"\u003enginx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"l\"\u003ephp7.0-fpm\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"l\"\u003ephp7.0-mysql\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edownload wordpress tarball\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eget_url\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;https://tw.wordpress.org/wordpress-{{ wordpress_version }}-zh_TW.tar.gz\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/tmp/\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eextract wordpress tarball\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eunarchive\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/tmp/wordpress-{{ wordpress_version }}-zh_TW.tar.gz\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{ wordpress_parent_path }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eowner\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{ wordpress_owner }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003egroup\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{ wordpress_group }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003ecopy\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003eno\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 省略部分代码\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ecopy wordpress site conf for nginx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e./templates/nginx-wordpress.conf.j2\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/etc/nginx/conf.d/nginx-wordpress.conf\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003efix listen.owner for php-fpm\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003elineinfile\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/etc/php/7.0/fpm/pool.d/www.conf\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eregexp\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;^listen.owner\\s*=.*$\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eline\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;listen.owner=nginx\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003epresent\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003erestart php-fpm\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eservice\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ename=php7.0-fpm state=restarted\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e在这样的场景下，Shell/Bash的能力重要吗？\u003c/p\u003e","title":"Shell Bash能力对于运维很重要吗？"},{"content":"微服务被滥用是不争的事实。被滥用的同时，很少人留意到它所带来的配置治理的问题。本文我们介绍两种常见的治理模式。\n基于common的配置治理模式 当微服务数量多时，开发人员倾向于创建这样的配置文件：\ncommon-redis.json common-mysql.json common-mq.json 甚至还有会有common.json这种从名字上就不知道它的作用的配置。但是，几乎所有的微服务都会引用common.json这个配置。原因如下：\n在common.json可以无脑增加配置项，不需要改业务代码； 配置项可能是被n个微服务引用，为了这一个配置项，又新增一个配置文件，不值得。common.json看起来是最合适的。反正每个微服务都已经引用了common.json。 ![](/assets/images/Pasted image 20221220161521.png)\n基于common的配置，在写入配置项的时候是爽了，但是，也带来了问题：\n改了common.json文件中的配置后，很难确认这个变更会影响到哪里，因为每个微服务都引用了common.json； common.json会变得越来越大； 并不是每次发布，都发布所有的微服务。所以，微服务A可能采用的是common.json的v1版本，而微服务B可能采用的是common.json的v2版本。 随着时间迁移，谁也不敢动common.json中的配置，即使有些配置项已经很久没有被使用了。 基于服务级别粒度的配置治理模式 基于服务级别粒度的配置方式，很容易理解，如下图： ![](/assets/images/Pasted image 20221220161557.png) 每个服务只引用一个配置文件。此模式完全避免了基于common的治理模式所带来的问题。但是，又带来了新的问题，即不同的微服务配置之间出现大量的重复配置。修改大量重复配置容易出错，且痛苦。\n大量配置重复的问题，可以通过类似Jsonnet或者CUE这样的配置编程语言解决。如下所示：\n当修改metrics.libsonnet时，我们很容易就知道这个变更将直接影响：microservice-c.jsonnet和microservice-a.jsonnet。进而，我们也就可以知道了它将间接影响microservice-c和microservice-b两个服务。\n不存在没有缺点的解决方案。使用Jsonnet和CUE这样的语言，意味着一定的学习成本和现在有的工程的改造成本（引入新的构建工具和对现在有的配置的转换）。\n不论哪种模式，你都必须要做到 不论哪种配置治理模式，都必须要做到：配置应该尽量小。\n至于小到什么程度？这个问题回答了，作用也不大。就像菜谱上写的是10克的香精，也没有几个人在放香精时进行称重，而是凭感觉。\n如果真要我回答，我的回答是：作为一个逻辑单元，多一行代码是多余，少一行代码则不行。\n成本 当我提出采用一种新的配置编程语言来统一所有的配置时，团队里的人都反对。行业里其他的人，也啧啧着这样做所带来的成本。\n所以，以下是我们团队经验，供大家参考：\nJsonnet的学习成本：像Jsonnet这样专为配置而生的配置编程语言，语法也只有一张A4纸，非常值得投入。我们团队里两个不懂编程的刚毕业没有多久的运维小年轻，很快就上手了。多快？半天左右吧。 构建工具Bazel的学习成本：Jsonnet本身的构建命令就和Java的javac一样低级，所以，需要借助其它构建工具。我们选择Bazel。它支持Jsonnet的单元测试。我们顺便实现了配置的自动化测试。Bazel的学习只需要团队里的几个人会就可以了。这个工具本身其实不难，但是因为中文学习教程太少了，导致了学习成本高。 其它配置格式转换成Jsonnet格式的成本：这个应该是我们成本最高的，也是风险最高的。因为一个配置错，可能带来线上事故。这个过程也是一个还债的过程。以前不合理的配置，在这个过程中会被发现。我们转换的过程是 通过自动转换工具将旧配置转成json格式，json格式与jsonnet格式是兼容的，所以就相当于自动得到了jsonnet格式的配置； 将公共配置抽离出来，比如redis的配置。并对敏感配置进行加密处理。这个过程是重建配置的过程； 将所有的配置转换完成后，再与原来的配置的json格式进行内容级别的对比。如果没有区别，就代表转换成功。 因为我们很早之前就对配置进行了标准化，所以对我们来说这个转换成本和配置的量对比起来，也不算太高。而这个成本绝大多数都是由基础设施团队完成，而不是业务开发团队。\n","permalink":"https://showme.codes/zh-cn/2023-06-12-patterns-of-configuration-under-microservice/","summary":"\u003cp\u003e微服务被滥用是不争的事实。被滥用的同时，很少人留意到它所带来的配置治理的问题。本文我们介绍两种常见的治理模式。\u003c/p\u003e\n\u003ch2 id=\"基于common的配置治理模式\"\u003e基于common的配置治理模式\u003c/h2\u003e\n\u003cp\u003e当微服务数量多时，开发人员倾向于创建这样的配置文件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ecommon-redis.json\u003c/li\u003e\n\u003cli\u003ecommon-mysql.json\u003c/li\u003e\n\u003cli\u003ecommon-mq.json\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e甚至还有会有common.json这种从名字上就不知道它的作用的配置。但是，几乎所有的微服务都会引用common.json这个配置。原因如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在common.json可以无脑增加配置项，不需要改业务代码；\u003c/li\u003e\n\u003cli\u003e配置项可能是被n个微服务引用，为了这一个配置项，又新增一个配置文件，不值得。common.json看起来是最合适的。反正每个微服务都已经引用了common.json。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e![](/assets/images/Pasted image 20221220161521.png)\u003c/p\u003e\n\u003cp\u003e基于common的配置，在写入配置项的时候是爽了，但是，也带来了问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e改了common.json文件中的配置后，很难确认这个变更会影响到哪里，因为每个微服务都引用了common.json；\u003c/li\u003e\n\u003cli\u003ecommon.json会变得越来越大；\u003c/li\u003e\n\u003cli\u003e并不是每次发布，都发布所有的微服务。所以，微服务A可能采用的是common.json的v1版本，而微服务B可能采用的是common.json的v2版本。\u003c/li\u003e\n\u003cli\u003e随着时间迁移，谁也不敢动common.json中的配置，即使有些配置项已经很久没有被使用了。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"基于服务级别粒度的配置治理模式\"\u003e基于服务级别粒度的配置治理模式\u003c/h2\u003e\n\u003cp\u003e基于服务级别粒度的配置方式，很容易理解，如下图：\n![](/assets/images/Pasted image 20221220161557.png)\n每个服务只引用一个配置文件。此模式完全避免了基于common的治理模式所带来的问题。但是，又带来了新的问题，即不同的微服务配置之间出现大量的重复配置。修改大量重复配置容易出错，且痛苦。\u003c/p\u003e\n\u003cp\u003e大量配置重复的问题，可以通过类似Jsonnet或者CUE这样的配置编程语言解决。如下所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/microservice-common-config.png\"\u003e\u003c/p\u003e\n\u003cp\u003e当修改metrics.libsonnet时，我们很容易就知道这个变更将直接影响：microservice-c.jsonnet和microservice-a.jsonnet。进而，我们也就可以知道了它将间接影响microservice-c和microservice-b两个服务。\u003c/p\u003e\n\u003cp\u003e不存在没有缺点的解决方案。使用Jsonnet和CUE这样的语言，意味着一定的学习成本和现在有的工程的改造成本（引入新的构建工具和对现在有的配置的转换）。\u003c/p\u003e\n\u003ch2 id=\"不论哪种模式你都必须要做到\"\u003e不论哪种模式，你都必须要做到\u003c/h2\u003e\n\u003cp\u003e不论哪种配置治理模式，都必须要做到：配置应该尽量小。\u003c/p\u003e\n\u003cp\u003e至于小到什么程度？这个问题回答了，作用也不大。就像菜谱上写的是10克的香精，也没有几个人在放香精时进行称重，而是凭感觉。\u003c/p\u003e\n\u003cp\u003e如果真要我回答，我的回答是：作为一个逻辑单元，多一行代码是多余，少一行代码则不行。\u003c/p\u003e\n\u003ch2 id=\"成本\"\u003e成本\u003c/h2\u003e\n\u003cp\u003e当我提出采用一种新的配置编程语言来统一所有的配置时，团队里的人都反对。行业里其他的人，也啧啧着这样做所带来的成本。\u003c/p\u003e\n\u003cp\u003e所以，以下是我们团队经验，供大家参考：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eJsonnet的学习成本：像Jsonnet这样专为配置而生的配置编程语言，语法也只有一张A4纸，非常值得投入。我们团队里两个不懂编程的刚毕业没有多久的运维小年轻，很快就上手了。多快？半天左右吧。\u003c/li\u003e\n\u003cli\u003e构建工具Bazel的学习成本：Jsonnet本身的构建命令就和Java的javac一样低级，所以，需要借助其它构建工具。我们选择Bazel。它支持Jsonnet的单元测试。我们顺便实现了配置的自动化测试。Bazel的学习只需要团队里的几个人会就可以了。这个工具本身其实不难，但是因为中文学习教程太少了，导致了学习成本高。\u003c/li\u003e\n\u003cli\u003e其它配置格式转换成Jsonnet格式的成本：这个应该是我们成本最高的，也是风险最高的。因为一个配置错，可能带来线上事故。这个过程也是一个还债的过程。以前不合理的配置，在这个过程中会被发现。我们转换的过程是\n\u003col\u003e\n\u003cli\u003e通过自动转换工具将旧配置转成json格式，json格式与jsonnet格式是兼容的，所以就相当于自动得到了jsonnet格式的配置；\u003c/li\u003e\n\u003cli\u003e将公共配置抽离出来，比如redis的配置。并对敏感配置进行加密处理。这个过程是重建配置的过程；\u003c/li\u003e\n\u003cli\u003e将所有的配置转换完成后，再与原来的配置的json格式进行内容级别的对比。如果没有区别，就代表转换成功。\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e因为我们很早之前就对配置进行了标准化，所以对我们来说这个转换成本和配置的量对比起来，也不算太高。而这个成本绝大多数都是由基础设施团队完成，而不是业务开发团队。\u003c/p\u003e","title":"微服务架构下的配置治理模式"},{"content":"最近行业里流行降本增效。本文就一个现实中经常发生的日志成本的案例进行讨论，讨论该如何降本。\n背景 假如存在一家IoT公司，它拥有1亿的在线设备（长连接着云端）。这些设备每21秒会向云端发送心跳，以进行连接的保活。假如1次连接的保活，在云端产生1条日志，那么1台设备1天产生的日志量是：24 * 60 * 60 / 21 = 4114条，假设1条日志100字节，所以：\n1台设备1天保活日志数：24 * 60 * 60 / 21 = 4114条 1条日志占用100字节，1台设备1天产生保活日志：4114 * 100 = 411400byte 1天内1亿的设备产生日志：411400 * 100000000 ≈ 37.4T 我们来看下该保活日志内容： 2021-02-12:12:32:12 application-abc-prod INFO com.platform.cloud.impl keep alive, deviceId:1a2b3b3p8\n从日志内容来看，没有什么“营养”，纯粹是为方便程序员debug。\n这是问题吗？ 一行没有营养的日志每天产生37.4T的日志，是个问题吗？对谁是个问题？\n对于写那行代码的人来说，并不是问题，因为日志量大小本身并不是他的KPI。\n对于运维可能是个问题，因为他们可能因此需要增加存储服务扩容的工作。但是从另一个方面来看不是个问题，因为他们有借口招更多的人。\n对于大数据团队可能是个问题，因为他们发现数据清洗pipeline比之前更慢了，进而导致每日报表都被延迟了。但是从另一个角度来看不是个问题，因为这是一个表现的机会——在人员不变的情况下优化清洗速度的机会。\n这对于关心成本的人来说是个问题。谁关心成本呢？这不是本文讨论的话题。\n如何面对问题？ 事后面对 事后面对指的是事情已经发生了，即大量无用配置已经产生了。只能采取补救止血措施。\n作为运维，需要对于日志量进行多维度监控与告警。可以包括以下维度：\n应用维度 实例维度 设备维度：部分设备可能有Bug，会死循环会不停进行连接请求 告警内容可以包括：\n进行环比对比，出现突增 进行环比对比，突降 在收到告警后，找到相应的应用的owner，让其检查日志是否合理，不合理则需要制定改进措施。\n同时，每周都向相应的owner发送日志量报告，让其了解当前生产环境的日志量情况。\nCode Review时面对 Code Review时，有经验的人会看出日志问题，但是这种方式并不可靠。\n写代码时，就避免 指的是在程序员写在log.info(\u0026quot;无营养日志\u0026quot;)的那一时刻，IDE就提醒程序员：这行日志每日将产生4114条日志，将占所有日志总量的80%，请注意！\n当然，除了在IDE时提醒，我们可以在Code Review时，review机器人自动对该行代码进行评论。\n这个实现起来难度会比较大。\n但是并不是没有思路。笔者的思路如下，仅供参考，欢迎交流：\n我们要知道，只有被请求到API，API执行时，被执行到的代码才会打日志。所以，只要能知道打日志的代码在过去一段时间内，在生产环境的相应的API被请求了多少次，我们就可以预测到增加1行日志会增加多少日志了。进而在IDE里提示，或者Code Review进行评论。\n而对于那些完全新增的API，就没有办法预测了，因为没有历史纪录。\n以上只是思路，具体实现方法今后有机会实现再说。\n小结 日志成本通常不会被人留意。它通常就一个人身上的慢性病，时间长了，谁知道会发展什么样呢？\n进行日志降本的最简单的办法就是增加对于日志量的监控与告警。个人认为在写下那行打日志的代码进行干预才是终极方案。\n","permalink":"https://showme.codes/zh-cn/2023-09-22-logs-finops/","summary":"\u003cp\u003e最近行业里流行降本增效。本文就一个现实中经常发生的日志成本的案例进行讨论，讨论该如何降本。\u003c/p\u003e\n\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e假如存在一家IoT公司，它拥有1亿的在线设备（长连接着云端）。这些设备每21秒会向云端发送心跳，以进行连接的保活。假如1次连接的保活，在云端产生1条日志，那么1台设备1天产生的日志量是：\u003ccode\u003e24 * 60 * 60 / 21 = 4114条\u003c/code\u003e，假设1条日志100字节，所以：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e1台设备1天保活日志数：\u003ccode\u003e24 * 60 * 60 / 21 = 4114\u003c/code\u003e条\u003c/li\u003e\n\u003cli\u003e1条日志占用100字节，1台设备1天产生保活日志：\u003ccode\u003e4114 * 100 = 411400\u003c/code\u003ebyte\u003c/li\u003e\n\u003cli\u003e1天内1亿的设备产生日志：\u003ccode\u003e411400 * 100000000\u003c/code\u003e ≈ 37.4T\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e我们来看下该保活日志内容：\n\u003ccode\u003e2021-02-12:12:32:12 application-abc-prod INFO com.platform.cloud.impl keep alive, deviceId:1a2b3b3p8\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e从日志内容来看，没有什么“营养”，纯粹是为方便程序员debug。\u003c/p\u003e\n\u003ch2 id=\"这是问题吗\"\u003e这是问题吗？\u003c/h2\u003e\n\u003cp\u003e一行没有营养的日志每天产生37.4T的日志，是个问题吗？对谁是个问题？\u003c/p\u003e\n\u003cp\u003e对于写那行代码的人来说，并不是问题，因为日志量大小本身并不是他的KPI。\u003c/p\u003e\n\u003cp\u003e对于运维可能是个问题，因为他们可能因此需要增加存储服务扩容的工作。但是从另一个方面来看不是个问题，因为他们有借口招更多的人。\u003c/p\u003e\n\u003cp\u003e对于大数据团队可能是个问题，因为他们发现数据清洗pipeline比之前更慢了，进而导致每日报表都被延迟了。但是从另一个角度来看不是个问题，因为这是一个表现的机会——在人员不变的情况下优化清洗速度的机会。\u003c/p\u003e\n\u003cp\u003e这对于关心成本的人来说是个问题。谁关心成本呢？这不是本文讨论的话题。\u003c/p\u003e\n\u003ch2 id=\"如何面对问题\"\u003e如何面对问题？\u003c/h2\u003e\n\u003ch3 id=\"事后面对\"\u003e事后面对\u003c/h3\u003e\n\u003cp\u003e事后面对指的是事情已经发生了，即大量无用配置已经产生了。只能采取补救止血措施。\u003c/p\u003e\n\u003cp\u003e作为运维，需要对于日志量进行多维度监控与告警。可以包括以下维度：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e应用维度\u003c/li\u003e\n\u003cli\u003e实例维度\u003c/li\u003e\n\u003cli\u003e设备维度：部分设备可能有Bug，会死循环会不停进行连接请求\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e告警内容可以包括：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e进行环比对比，出现突增\u003c/li\u003e\n\u003cli\u003e进行环比对比，突降\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在收到告警后，找到相应的应用的owner，让其检查日志是否合理，不合理则需要制定改进措施。\u003c/p\u003e\n\u003cp\u003e同时，每周都向相应的owner发送日志量报告，让其了解当前生产环境的日志量情况。\u003c/p\u003e\n\u003ch3 id=\"code-review时面对\"\u003eCode Review时面对\u003c/h3\u003e\n\u003cp\u003eCode Review时，有经验的人会看出日志问题，但是这种方式并不可靠。\u003c/p\u003e\n\u003ch3 id=\"写代码时就避免\"\u003e写代码时，就避免\u003c/h3\u003e\n\u003cp\u003e指的是在程序员写在\u003ccode\u003elog.info(\u0026quot;无营养日志\u0026quot;)\u003c/code\u003e的那一时刻，IDE就提醒程序员：这行日志每日将产生4114条日志，将占所有日志总量的80%，请注意！\u003c/p\u003e\n\u003cp\u003e当然，除了在IDE时提醒，我们可以在Code Review时，review机器人自动对该行代码进行评论。\u003c/p\u003e\n\u003cp\u003e这个实现起来难度会比较大。\u003c/p\u003e\n\u003cp\u003e但是并不是没有思路。笔者的思路如下，仅供参考，欢迎交流：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我们要知道，只有被请求到API，API执行时，被执行到的代码才会打日志。所以，只要能知道打日志的代码在过去一段时间内，在生产环境的相应的API被请求了多少次，我们就可以预测到增加1行日志会增加多少日志了。进而在IDE里提示，或者Code Review进行评论。\u003c/p\u003e\n\u003cp\u003e而对于那些完全新增的API，就没有办法预测了，因为没有历史纪录。\u003c/p\u003e","title":"我是如何进行日志降本的"},{"content":"前文中，我们给了“精准测试”定义：\n它是一种能力，能只针对变更进行测试，而不是每次变更都进行全量测试。\n同时，介绍了当前行业里的主流实现方法。个人并不看好该实现方法。\n本文介绍的另一种实现精准测试的方法。在真正介绍前，我们就必须先说增量构建和Bazel。\n全量构建与增量构建 在软件构建领域，存在两种构建类型：全量构建和增量构建。\n全量构建指的是针对代码仓库中所有的代码进行构建； 增量构建是指只针对有变动的代码及受变动影响的相关代码进行重新构建。 从定义看，增量构建要做的事情和精准测试要做的事情几乎是一样的。只不过，把build命令，换成test命令罢了。\n这也就是为什么我觉得应该把“精准测试”叫做“增量测试”才对。\n目前行业里，在增量构建领域，Bazel可谓是佼佼者。\nBazel介绍 Bazel是Google 2015年开源的一款构建工具。采用声明式的方式定义所有的构建任务。Bazel叫target。\n每个target声明包含了：构建类型、输入、构建方式、输出、依赖等。以下代码展示了两个构建任务：\n# 声明打成jar包，作为library被其他任务使用 java_library( name = \u0026#34;greeter\u0026#34;, srcs = [\u0026#34;src/main/java/com/example/Greeting.java\u0026#34;], ) # 声明打包成一个可执行Jar java_binary( name = \u0026#34;ProjectRunner\u0026#34;, srcs = [\u0026#34;src/main/java/com/example/ProjectRunner.java\u0026#34;], main_class = \u0026#34;com.example.ProjectRunner\u0026#34;, # 依赖之前打好的library，这是实现增量构建的关键 deps = [\u0026#34;:greeter\u0026#34;], ) Bazel在运行时，就会根据target声明，在内部维护一个有向依赖图，如下： 有了这个有向依赖图，Bazel就可以实现增量构建。\n当用户修改了 Greeting.java 文件时，Bazel知道 //:greeter target 依赖它，所以Bazel知道要执行 //:greeter 时。同时Bazel又知道 //:ProjectRunner 依赖 //:greeter ，所以Bazel知道还要执行 //:ProjectRunner target。\n以上是在同一个语言下的增量构建的案例，让我们看下多语言场景下，Bazel是如何实现增量构建的。\n多语言场景下Bazel是如何实现增量构建的 如下图，在一个软件工程下，同时使用到了：Docker、Python、YAML、C++等技术。\n这个依赖关系是在开发和运维在写代码的时候就定义好了的。所以，Bazel从一开始就有了这个依赖图。\nBazel允许声明不同语言的target之间的依赖，所以，很自然的，一个软件工程的完成的依赖图就有了。你不需要花额外的精力去收集。\n当执行Bazel进行构建build时，Bazel发现配置文件config.yaml是被修改了，这时它就计算出接下来要执行的构建，如下图标为橙色的路径。即所有的依赖于//:config.yaml 的直接依赖和间接依赖。\n但是，因为执行的是build，所以，Bazel只会build路径上的所有源代码，并不会去执行 *_test 测试任务。\n这就是精准构建，不，叫增量构建：只构建需要构建的。\n也许你会好奇Bazel是怎么做到的？请关注我的公众号，将来我还会分享更多Bazel的内容。\nBazel是如何精准测试 Bazel有很多子命令，有两个常用的子命令，一个是build，一个是test。这两个子命令是用于区分构建的类型的。因为有时，你可能只是想build，不想test。\n接着上面的例子，同样修改了config.yaml文件，当我们执行的是test子命令，Bazel会计算出要执行的路径——和上文一样的路径，因为//:config.yaml所影响的依赖范围是一样的。\n区别是，这次它除了执行build，还会运行*_test类的任务。Bazel并不会关心它是单元测试，还是集成测试，只关心该测试的大小。如下图中的标为黑色的部分： ![](/assets/images/Screen Shot 2023-11-02 at 9.30.45 PM.png)\n这就是精准测试了。同时，Bazel还会发现//:x_test、//:main_test、//:docker_image_test是完全独立的测试，那么Bazel就可以进行并行测试，进而提升测试的速度。\n精准测试是增量构建的副产品 说回我们之前总结的精准测试的实现思路：\n找到变更； 根据变更找到相关联的测试用例； 只执行相关联的测试用例。 以上所有步骤都可由Bazel完成，而且可以在本地完成。\n所以，使用Bazel后，精准测试的实现，你不需要自己投入研发以支持多语言，更不需要另外开发一堆平台。\n但是，以上的好处并不是没有代价的。\n增量构建（精准测试）的代价 通过以上例子，有读者应该已经注意到了，以上案例是一个单仓库项目，也就是所有的工程（前后端、运维、手机端）的代码都放在同一个仓库下。\n这是要实现增量构建的一个前提。\n第二个前提是：你必须使用类似Bazel这样的支持增量构建的工具，这意味着过去的项目都可能需要进行构建工具的迁移。而且，类似Bazel的工具，在整个行业的使用率还很低，在公司里推行，需要一定的成本。\n最后，你会选择基于Bazel来实现精准测试吗？\n","permalink":"https://showme.codes/zh-cn/2023-04-22-accuration-testing-is-wrong-2/","summary":"\u003cp\u003e前文中，我们给了“精准测试”定义：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e它是一种能力，能只针对变更进行测试，而不是每次变更都进行全量测试。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e同时，介绍了当前行业里的主流实现方法。个人并不看好该实现方法。\u003c/p\u003e\n\u003cp\u003e本文介绍的另一种实现精准测试的方法。在真正介绍前，我们就必须先说增量构建和Bazel。\u003c/p\u003e\n\u003ch2 id=\"全量构建与增量构建\"\u003e全量构建与增量构建\u003c/h2\u003e\n\u003cp\u003e在软件构建领域，存在两种构建类型：全量构建和增量构建。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e全量构建指的是针对代码仓库中所有的代码进行构建；\u003c/li\u003e\n\u003cli\u003e增量构建是指只针对有变动的代码及受变动影响的相关代码进行重新构建。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e从定义看，增量构建要做的事情和精准测试要做的事情几乎是一样的。只不过，把build命令，换成test命令罢了。\u003c/p\u003e\n\u003cp\u003e这也就是为什么我觉得应该把“精准测试”叫做“增量测试”才对。\u003c/p\u003e\n\u003cp\u003e目前行业里，在增量构建领域，Bazel可谓是佼佼者。\u003c/p\u003e\n\u003ch2 id=\"bazel介绍\"\u003eBazel介绍\u003c/h2\u003e\n\u003cp\u003eBazel是Google 2015年开源的一款构建工具。采用声明式的方式定义所有的构建任务。Bazel叫target。\u003c/p\u003e\n\u003cp\u003e每个target声明包含了：构建类型、输入、构建方式、输出、依赖等。以下代码展示了两个构建任务：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 声明打成jar包，作为library被其他任务使用\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ejava_library\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;greeter\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esrcs\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;src/main/java/com/example/Greeting.java\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 声明打包成一个可执行Jar\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ejava_binary\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;ProjectRunner\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esrcs\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;src/main/java/com/example/ProjectRunner.java\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emain_class\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;com.example.ProjectRunner\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# 依赖之前打好的library，这是实现增量构建的关键\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003edeps\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;:greeter\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBazel在运行时，就会根据target声明，在内部维护一个有向依赖图，如下：\n\u003cimg loading=\"lazy\" src=\"/assets/images/dag-bazel2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e有了这个有向依赖图，Bazel就可以实现增量构建。\u003c/p\u003e\n\u003cp\u003e当用户修改了 Greeting.java 文件时，Bazel知道 //:greeter target 依赖它，所以Bazel知道要执行 //:greeter 时。同时Bazel又知道 //:ProjectRunner 依赖 //:greeter ，所以Bazel知道还要执行 //:ProjectRunner target。\u003c/p\u003e\n\u003cp\u003e以上是在同一个语言下的增量构建的案例，让我们看下多语言场景下，Bazel是如何实现增量构建的。\u003c/p\u003e\n\u003ch2 id=\"多语言场景下bazel是如何实现增量构建的\"\u003e多语言场景下Bazel是如何实现增量构建的\u003c/h2\u003e\n\u003cp\u003e如下图，在一个软件工程下，同时使用到了：Docker、Python、YAML、C++等技术。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/bazel-test2.png\"\u003e\n这个依赖关系是在开发和运维在写代码的时候就定义好了的。所以，Bazel从一开始就有了这个依赖图。\u003c/p\u003e\n\u003cp\u003eBazel允许声明不同语言的target之间的依赖，所以，很自然的，一个软件工程的完成的依赖图就有了。你不需要花额外的精力去收集。\u003c/p\u003e\n\u003cp\u003e当执行Bazel进行构建build时，Bazel发现配置文件config.yaml是被修改了，这时它就计算出接下来要执行的构建，如下图标为橙色的路径。即所有的依赖于//:config.yaml 的直接依赖和间接依赖。\u003c/p\u003e\n\u003cp\u003e但是，因为执行的是build，所以，Bazel只会build路径上的所有源代码，并不会去执行 \u003ccode\u003e*_test\u003c/code\u003e 测试任务。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/bazel-test.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这就是精准构建，不，叫增量构建：只构建需要构建的。\u003c/p\u003e\n\u003cp\u003e也许你会好奇Bazel是怎么做到的？请关注我的公众号，将来我还会分享更多Bazel的内容。\u003c/p\u003e\n\u003ch2 id=\"bazel是如何精准测试\"\u003eBazel是如何精准测试\u003c/h2\u003e\n\u003cp\u003eBazel有很多子命令，有两个常用的子命令，一个是\u003ccode\u003ebuild\u003c/code\u003e，一个是\u003ccode\u003etest\u003c/code\u003e。这两个子命令是用于区分构建的类型的。因为有时，你可能只是想build，不想test。\u003c/p\u003e\n\u003cp\u003e接着上面的例子，同样修改了config.yaml文件，当我们执行的是test子命令，Bazel会计算出要执行的路径——和上文一样的路径，因为//:config.yaml所影响的依赖范围是一样的。\u003c/p\u003e\n\u003cp\u003e区别是，这次它除了执行build，还会运行\u003ccode\u003e*_test\u003c/code\u003e类的任务。Bazel并不会关心它是单元测试，还是集成测试，只关心该测试的大小。如下图中的标为黑色的部分：\n![](/assets/images/Screen Shot 2023-11-02 at 9.30.45 PM.png)\u003c/p\u003e","title":"精准测试不过是增量构建的副产品"},{"content":"如果你已经了解了精准测试在行业的主流做法，你可以跳过相关内容。\n行业里对于精准测试的定义 在网上流传着一些精准测试的定义（如果你对这些定义不感冒，可直接跳到我个人的定义）：\n自网易陈逸青（2020）的定义：\n借助一定的技术手段、通过辅助算法对传统软件测试过程进行可视化、分析及优化的过程，使得测试过程更加可视化、智能、可信和精准。 原文：https://www.infoq.cn/article/xuu91crqa4hcjz8uomjs\n来自HSBC的测试咨询专家齐磊（2021年）：\n通俗点讲：核心基于源代码变更分析，结合分析算法，确定影响范围，提升测试效率。 原文：https://www.infoq.cn/article/2feiv8a5kogaqlbzwosh\n来自星云测试（2022年）：\n精准测试一句话概括就是：测试用例和代码之间的追溯，这是它最本质的东西。精准测试的本质决定了它抓住了测试的一个核心要点。 原文： https://testerhome.com/topics/34557\n来自得物技术（2023年）：\n精准测试是基于源代码变更分析，结合一些分析算法，从而确定改动代码影响的范围，设计测试用例进行针对性测试，一方面可以提升测试效率，另一方面精准测试还可以将测试用例与程序代码之间的逻辑映射关系建立起来， 而这个过程则是通过工具去采集测试过程执行的代码逻辑及测试数据。这两个点也正是精准测试的核心：正向追溯和逆向追溯。原文： https://tech.dewu.com/article?id=43\n以下是来自网易严选的架构图：\n![](/assets/images/网易严选的架构图1.png]]\n我个人的定义 在笔者看来，精准测试的定义应该是这样的：它是一种能力，能只针对变更进行测试，而不是每次变更都进行全量测试。注意，我指的是“变更”，而不只是“代码变更”，也就是说所有类型的变更，包括手动变更。\n精准测试的思路并不复杂，分成三个步骤：\n找到变更； 根据变更找到相关联的测试用例； 只执行相关联的测试用例。 其实，把这种方法叫增量测试（Incremental Testing） 更准确，更合适。毕竟你是针对增量的代码变更进行测试。\n如果不是针对增量变更进行测试，你也能只执行一个你想测试的测试。难道这样不算精准测试吗？\n国内行业主流的实现精准测试的方法 步骤一：找到代码变更 通过commit之间进行差异对比​。​\n步骤二：根据代码变更找到相关联的测试用例 要做到“根据代码变更找到相关联的测试用例”，我们就必须知道代码与测试用例之间的关系。\n获取这个关系的做法是在执行测试的同时，做以下事情：\n将流量记录下来； 将因流量而执行地代码的调用链记录下来； 将测试用例的元数据与代码调用链的关系记录下来； 这个过程就完成了对被调用代码与测试用例之间的映射关系的建立。\n另，现实往往存在很多未被测试用例覆盖到的代码，这时，通过静态代码分析和测试覆盖率计算技术结合，生成未被测试到的代码的报告。\n可以看出，通过以上方式“找出代码与测试用例之间的关系”的成本是极高的。所以，在这个领域会有：引流平台、测试用例管理平台、精准测试平台等等平台。这也给大家一个感觉，我们要先有一个平台才能做到精准测试。\n说到底就是通过插桩技术，构建代码的执行路径，并找到​对应的测试用例之间关系。\n目前在网上目前看到大多还只是针对Java语言或者C++来实现精准测试，其它的语言目前没有见到。\n步骤三：只执行相关联的测试用例 当有了代码与测试用例之间的关系，只执行相关联的测试用例就简单很多了。\n主流方法的坑 以下是齐磊总结的精准测试存在的问题：\n基于手工测试的精准测试建立映射关系繁杂，如果需求改变频繁，用例维护以及之间的关系维护需要耗费大量时间精力。 精准测试需要一定的自动化测试的覆盖，这样做起来更有意义，例如 api 自动化测试，如果本身用例过少，与代码之间关联关系不多时，变更代码后可能不会得出什么结果。 最好有对应的用例管理系统，能够方便的帮助我们建立与代码之间的关系。 需要投入开发能力强的 QA 或者测试开发建立整套系统环境，但长远考虑，将精准测试嵌入整个公司的质量平台中，不管对于新项目还说维护项目来说都是一种提升。 项目生命周期需要较长，短期项目花费巨大精力开发和维护整套精准测试系统得不偿失。短期项目可以利用精准测试以 api 测试覆盖率作为衡量标准。不去建立繁杂的关系，只监控 UI API 测试覆盖率迭代时的变更来达到目的。 但是，个人认为齐磊总结的内容没有问题，的确都是坑。但是那些不是精准测试的坑，而是国内行业主流的实现方式的坑。直白地说就是喝水时，喝水的角度错了。\n为什么主流实现方法从方向上就是错的 为什么我认为以上地坑是由实现方法导致的？以下是我的论点，欢迎讨论指正：\n该方法只局限于单一语言 准确来说，精准测试不应该只针对代码的变更，而是所有的变更。更不应该只针对单一语言的变更，而是可以针对所有的语言。\n因为精准测试的定义本身不局限于某种语言的代码变更，而是对一个软件工程中所有的变更而言。一次SQL的变更，你是否需要精准的知道要执行哪些测试？一个前端的CSS代码的变更，你是否需要精准的知道要执行哪些测试？\n目前行业里主流的方法，只是针对单一语言下的场景而设计的。按同样的思路是无法做到多语言的。我说的多语言指提同一工程下的多语言，不是指相互独立的单语言工程。\n只能在平台上做精准测试 即，我们首先需要一个平台，才能做到精准测试。\n但我们希望在开发者本地开发环境就可以做到精准测试。\n最后 文章标题并不是说“精准测试”本身是一个错误，是想说上述的实现方法是一个错误方法。\n那，什么样的方向是正确的呢？\n","permalink":"https://showme.codes/zh-cn/2023-04-21-accuration-testing-is-wrong/","summary":"\u003cp\u003e如果你已经了解了精准测试在行业的主流做法，你可以跳过相关内容。\u003c/p\u003e\n\u003ch2 id=\"行业里对于精准测试的定义\"\u003e行业里对于精准测试的定义\u003c/h2\u003e\n\u003cp\u003e在网上流传着一些精准测试的定义（如果你对这些定义不感冒，可直接跳到我个人的定义）：\u003c/p\u003e\n\u003cp\u003e自网易陈逸青（2020）的定义：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e借助一定的技术手段、通过辅助算法对传统软件测试过程进行可视化、分析及优化的过程，使得测试过程更加可视化、智能、可信和精准。 原文：https://www.infoq.cn/article/xuu91crqa4hcjz8uomjs\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e来自HSBC的测试咨询专家齐磊（2021年）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e通俗点讲：核心基于源代码变更分析，结合分析算法，确定影响范围，提升测试效率。 原文：https://www.infoq.cn/article/2feiv8a5kogaqlbzwosh\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e来自星云测试（2022年）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e精准测试一句话概括就是：测试用例和代码之间的追溯，这是它最本质的东西。精准测试的本质决定了它抓住了测试的一个核心要点。 原文： \u003ca href=\"https://testerhome.com/topics/34557\"\u003ehttps://testerhome.com/topics/34557\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e来自得物技术（2023年）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e精准测试是基于源代码变更分析，结合一些分析算法，从而确定改动代码影响的范围，设计测试用例进行针对性测试，一方面可以提升测试效率，另一方面精准测试还可以将测试用例与程序代码之间的逻辑映射关系建立起来， 而这个过程则是通过工具去采集测试过程执行的代码逻辑及测试数据。这两个点也正是精准测试的核心：正向追溯和逆向追溯。原文： \u003ca href=\"https://tech.dewu.com/article?id=43\"\u003ehttps://tech.dewu.com/article?id=43\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e以下是来自网易严选的架构图：\u003c/p\u003e\n\u003cp\u003e![](/assets/images/网易严选的架构图1.png]]\u003c/p\u003e\n\u003ch2 id=\"我个人的定义\"\u003e我个人的定义\u003c/h2\u003e\n\u003cp\u003e在笔者看来，精准测试的定义应该是这样的：它是一种能力，能只针对变更进行测试，而不是每次变更都进行全量测试。注意，我指的是“变更”，而不只是“代码变更”，也就是说所有类型的变更，包括手动变更。\u003c/p\u003e\n\u003cp\u003e精准测试的思路并不复杂，分成三个步骤：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e找到变更；\u003c/li\u003e\n\u003cli\u003e根据变更找到相关联的测试用例；\u003c/li\u003e\n\u003cli\u003e只执行相关联的测试用例。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e其实，把这种方法叫\u003cstrong\u003e增量测试（Incremental Testing）\u003c/strong\u003e 更准确，更合适。毕竟你是针对增量的代码变更进行测试。\u003c/p\u003e\n\u003cp\u003e如果不是针对增量变更进行测试，你也能只执行一个你想测试的测试。难道这样不算精准测试吗？\u003c/p\u003e\n\u003ch2 id=\"国内行业主流的实现精准测试的方法\"\u003e国内行业主流的实现精准测试的方法\u003c/h2\u003e\n\u003ch3 id=\"步骤一找到代码变更\"\u003e步骤一：找到代码变更\u003c/h3\u003e\n\u003cp\u003e通过commit之间进行差异对比​。​\u003c/p\u003e\n\u003ch3 id=\"步骤二根据代码变更找到相关联的测试用例\"\u003e步骤二：根据代码变更找到相关联的测试用例\u003c/h3\u003e\n\u003cp\u003e要做到“根据代码变更找到相关联的测试用例”，我们就必须知道代码与测试用例之间的关系。\u003c/p\u003e\n\u003cp\u003e获取这个关系的做法是在执行测试的同时，做以下事情：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e将流量记录下来；\u003c/li\u003e\n\u003cli\u003e将因流量而执行地代码的调用链记录下来；\u003c/li\u003e\n\u003cli\u003e将测试用例的元数据与代码调用链的关系记录下来；\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这个过程就完成了对被调用代码与测试用例之间的映射关系的建立。\u003c/p\u003e\n\u003cp\u003e另，现实往往存在很多未被测试用例覆盖到的代码，这时，通过静态代码分析和测试覆盖率计算技术结合，生成未被测试到的代码的报告。\u003c/p\u003e\n\u003cp\u003e可以看出，通过以上方式“找出代码与测试用例之间的关系”的成本是极高的。所以，在这个领域会有：引流平台、测试用例管理平台、精准测试平台等等平台。这也给大家一个感觉，我们要先有一个平台才能做到精准测试。\u003c/p\u003e\n\u003cp\u003e说到底就是通过插桩技术，构建代码的执行路径，并找到​对应的测试用例之间关系。\u003c/p\u003e\n\u003cp\u003e目前在网上目前看到大多还只是针对Java语言或者C++来实现精准测试，其它的语言目前没有见到。\u003c/p\u003e\n\u003ch3 id=\"步骤三只执行相关联的测试用例\"\u003e步骤三：只执行相关联的测试用例\u003c/h3\u003e\n\u003cp\u003e当有了代码与测试用例之间的关系，只执行相关联的测试用例就简单很多了。\u003c/p\u003e\n\u003ch2 id=\"主流方法的坑\"\u003e主流方法的坑\u003c/h2\u003e\n\u003cp\u003e以下是齐磊总结的精准测试存在的问题：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003col\u003e\n\u003cli\u003e基于手工测试的精准测试建立映射关系繁杂，如果需求改变频繁，用例维护以及之间的关系维护需要耗费大量时间精力。\u003c/li\u003e\n\u003cli\u003e精准测试需要一定的自动化测试的覆盖，这样做起来更有意义，例如 api 自动化测试，如果本身用例过少，与代码之间关联关系不多时，变更代码后可能不会得出什么结果。\u003c/li\u003e\n\u003cli\u003e最好有对应的用例管理系统，能够方便的帮助我们建立与代码之间的关系。\u003c/li\u003e\n\u003cli\u003e需要投入开发能力强的 QA 或者测试开发建立整套系统环境，但长远考虑，将精准测试嵌入整个公司的质量平台中，不管对于新项目还说维护项目来说都是一种提升。\u003c/li\u003e\n\u003cli\u003e项目生命周期需要较长，短期项目花费巨大精力开发和维护整套精准测试系统得不偿失。短期项目可以利用精准测试以 api 测试覆盖率作为衡量标准。不去建立繁杂的关系，只监控 UI API 测试覆盖率迭代时的变更来达到目的。\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e但是，个人认为齐磊总结的内容没有问题，的确都是坑。但是那些不是精准测试的坑，而是国内行业主流的实现方式的坑。直白地说就是喝水时，喝水的角度错了。\u003c/p\u003e\n\u003ch2 id=\"为什么主流实现方法从方向上就是错的\"\u003e为什么主流实现方法从方向上就是错的\u003c/h2\u003e\n\u003cp\u003e为什么我认为以上地坑是由实现方法导致的？以下是我的论点，欢迎讨论指正：\u003c/p\u003e\n\u003ch3 id=\"该方法只局限于单一语言\"\u003e该方法只局限于单一语言\u003c/h3\u003e\n\u003cp\u003e准确来说，精准测试不应该只针对代码的变更，而是所有的变更。更不应该只针对单一语言的变更，而是可以针对所有的语言。\u003c/p\u003e\n\u003cp\u003e因为精准测试的定义本身不局限于某种语言的代码变更，而是对一个软件工程中所有的变更而言。一次SQL的变更，你是否需要精准的知道要执行哪些测试？一个前端的CSS代码的变更，你是否需要精准的知道要执行哪些测试？\u003c/p\u003e\n\u003cp\u003e目前行业里主流的方法，只是针对单一语言下的场景而设计的。按同样的思路是无法做到多语言的。我说的多语言指提同一工程下的多语言，不是指相互独立的单语言工程。\u003c/p\u003e\n\u003ch3 id=\"只能在平台上做精准测试\"\u003e只能在平台上做精准测试\u003c/h3\u003e\n\u003cp\u003e即，我们首先需要一个平台，才能做到精准测试。\u003c/p\u003e\n\u003cp\u003e但我们希望在开发者本地开发环境就可以做到精准测试。\u003c/p\u003e\n\u003ch2 id=\"最后\"\u003e最后\u003c/h2\u003e\n\u003cp\u003e文章标题并不是说“精准测试”本身是一个错误，是想说上述的实现方法是一个错误方法。\u003c/p\u003e","title":"精准测试是个错误"},{"content":"背景 团队基于Jsonnet（配置语言）和Bazel（构建工具）实现Everything as Code。但是，将近一年的时间里，团队也只有少数人深入参与Everything as Code的代码的编写（其实，大多数人是不需要深入参与）。\n最近，组内的一个开发同学——他之前对Everything as Code完全零基础——在运维同事的辅助下，为网关配置增加了金丝雀的能力。\n在这种能力下，开发人员可以只需要改几行配置，并部署配置，就可以实现金丝雀发布。这种能力的根本就是：通过Jsonnet动态生成网关“根据Header进行流量路由”的配置的能力。\n如果没有这种能力，需要手写大量的重复的YAML配置，即麻烦，又容易出错。\n同事使用Everything as Code的工具后有感 在他与Everything as Code“亲密”接触一个多月后，他说出了他的感受：\n直接写yaml的话，所见即所得，非常直观。但是有个致命缺陷，没法动态化生成配置，所以必须要寻找一个脚本语言。 jsonnet其实就是一门简单化的脚本语言，通过自定义方法，通过for循环，通过读取自定义的变量，更灵活生成我们想要的json。但是写代码都有一个共性，代码可以写的很复杂，现在用jsonnet了，你想看一个key的value是什么，需要看懂别人的代码才知道value是什么甚至可能理解错别人的代码，违背了配置所见即所得，这对新手来说非常痛苦。很多时候，我们不关心jsonnet的过程，我们只想看到生成最终的配置文件是什么。 如果能把云端编译好的jsonnet展示出来，就可以看到生成的最终json是啥了，但是我们没有。所以需要本地搭建bazel，但是bazel对win非常不友好，搞起来非常困难，但是本地如果不能把monorepo buil通过，那么可以宣告你无法彻底搞清楚monorepo，更不可能自定义更多自己想要的东西了。 既然是通过代码来生成json，用jsonnet和python和js甚至是用java都可以，现在我觉得jsonnet的设计者对灵活性做了一个取舍，有一些我想要的操作jsonnet没法实现。 然后他又补充到：\n只要自己本地能build monorepo，那么就既能享受jsonnet的灵活性，又能通过编译好的文件做到所见即所得。可惜bazel对win不友好，新手能在win 编译通过mainrepo太难了。\n总结下来，这位同事：\n对Jsonnet的理解还不够：Jsonnet并不是脚本语言，而是一门可编程的配置语言，一种为配置而生的DSL（领域特定语言）； 在Windows系统上使用Bazel遇到了非常大的阻力。 以上是同事针对Jsonnet和构建工具Bazel的感受。\n我认为不是Bazel对Windows不友好，而是Bazel在Windows下的生态太弱了。谁叫后端服务是Linux的天下呢？\n同事对Everything as Code本身的看法 接着，他说出对Everything as Code的看法：\n我觉得everything as code太理想，只能面向开发人员。你不能保证运维的能力，绝对不开放给第三方使用。指不定某天某个需求就需要你的运维的某个能力，让不懂代码的人也要操作。\n假如某个运维能力，要开放给产品或者测试来用，要让别人给你学个jsonnet，那要我是产品或者测试只能口吐芬芳了。所以我认为，面向开发者的部分，使用every thing as code是趋势，比较好溯源跟踪，迭代。但是架构设计上，还需要有支持面向第三方的部分，可以做到让不懂代码的人也能用到一些运维的能力。\n这位同事实现的是金丝雀配置生成，是需要修改Jsonnet代码的。当产品经理需要将某用户的流量路由到后端服务的某个版本上时，这位同事就认为产品经理就需要去改Jsonnet代码了。\n同事认为这就是Everything as Code的问题：不方便非开发人使用。\n同事的另一层意思没有说出来：并不是所有的事情都适合as Code。\n其实，有这样的误解很正常，“Everything as Code”中的“Everything”字面含义上就是所有的，一切的意思。而Code是代码的意思。\n所以，“Everything as Code”字面上完整的意思是：一切皆代码。\n从字面上理解“Everything as Code”，只看到了它的表面\nEverything as Code的本质 “Everything as Code”中的”Code“，其实指的是配置，即：一切皆配置。\n这句话本身是没有错的，如同“道生一，一生二，二生三，三生万物“这句话。\n“Everything as Code”这句话并没说什么是配置，以及哪些配置该放哪。这才是我们实践时真正要考虑的问题。\n配置的本质是软件的灵活（soft）部分。通过这个”灵活部分“，我们可以根据自己的需求，有限地改变软件的行为。\n在上面的案例中，我们需要控制网关的路由行为，需要用到两个配置：\n网关的行为逻辑的配置。这是技术部分，由程序员或者DevOps决定是否改动； 用户列表配置。这是业务部分，由产品经理或测试决定是否改动。 业务部分的配置由业务方——产品经理——决定，我们认为通过界面来实现更合理。\n而技术部分的配置放在配置代码中更合适。\n通过案例，我想说明的是配置该放哪里，是由以下维度决定的：\n配置的属性：是技术配置，还是业务配置； 配置的改动来源：是技术人员，还是非技术人员； 配置的改动频率：是经常改动，还是不经常。”经常“如何定义，我无法给出，要看情况。 接着刚才的案例，在还没有开发出界面让产品经理对用户进行配置前，我们可以通过代码配置用户列表。如果产品经理需要配置，由让开发人员配置一下即可。\n所以，配置即代码的实践也是要看具体情况的。我们可以通过以上几个维度判断是否进行as Code。\n（完）\n","permalink":"https://showme.codes/zh-cn/2023-04-20-when-coworker-use-everything-as-code/","summary":"\u003ch3 id=\"背景\"\u003e背景\u003c/h3\u003e\n\u003cp\u003e团队基于Jsonnet（配置语言）和Bazel（构建工具）实现Everything as Code。但是，将近一年的时间里，团队也只有少数人深入参与Everything as Code的代码的编写（其实，大多数人是不需要深入参与）。\u003c/p\u003e\n\u003cp\u003e最近，组内的一个开发同学——他之前对Everything as Code完全零基础——在运维同事的辅助下，为网关配置增加了金丝雀的能力。\u003c/p\u003e\n\u003cp\u003e在这种能力下，开发人员可以只需要改几行配置，并部署配置，就可以实现金丝雀发布。这种能力的根本就是：通过Jsonnet动态生成网关“根据Header进行流量路由”的配置的能力。\u003c/p\u003e\n\u003cp\u003e如果没有这种能力，需要手写大量的重复的YAML配置，即麻烦，又容易出错。\u003c/p\u003e\n\u003ch3 id=\"同事使用everything-as-code的工具后有感\"\u003e同事使用Everything as Code的工具后有感\u003c/h3\u003e\n\u003cp\u003e在他与Everything as Code“亲密”接触一个多月后，他说出了他的感受：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003col\u003e\n\u003cli\u003e直接写yaml的话，所见即所得，非常直观。但是有个致命缺陷，没法动态化生成配置，所以必须要寻找一个脚本语言。\u003c/li\u003e\n\u003cli\u003ejsonnet其实就是一门简单化的脚本语言，通过自定义方法，通过for循环，通过读取自定义的变量，更灵活生成我们想要的json。但是写代码都有一个共性，代码可以写的很复杂，现在用jsonnet了，你想看一个key的value是什么，需要看懂别人的代码才知道value是什么甚至可能理解错别人的代码，违背了配置所见即所得，这对新手来说非常痛苦。很多时候，我们不关心jsonnet的过程，我们只想看到生成最终的配置文件是什么。\u003c/li\u003e\n\u003cli\u003e如果能把云端编译好的jsonnet展示出来，就可以看到生成的最终json是啥了，但是我们没有。所以需要本地搭建bazel，但是bazel对win非常不友好，搞起来非常困难，但是本地如果不能把monorepo buil通过，那么可以宣告你无法彻底搞清楚monorepo，更不可能自定义更多自己想要的东西了。\u003c/li\u003e\n\u003cli\u003e既然是通过代码来生成json，用jsonnet和python和js甚至是用java都可以，现在我觉得jsonnet的设计者对灵活性做了一个取舍，有一些我想要的操作jsonnet没法实现。\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e然后他又补充到：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e只要自己本地能build monorepo，那么就既能享受jsonnet的灵活性，又能通过编译好的文件做到所见即所得。可惜bazel对win不友好，新手能在win 编译通过mainrepo太难了。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e总结下来，这位同事：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e对Jsonnet的理解还不够：Jsonnet并不是脚本语言，而是一门可编程的配置语言，一种为配置而生的DSL（领域特定语言）；\u003c/li\u003e\n\u003cli\u003e在Windows系统上使用Bazel遇到了非常大的阻力。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e以上是同事针对Jsonnet和构建工具Bazel的感受。\u003c/p\u003e\n\u003cp\u003e我认为不是Bazel对Windows不友好，而是Bazel在Windows下的生态太弱了。谁叫后端服务是Linux的天下呢？\u003c/p\u003e\n\u003ch3 id=\"同事对everything-as-code本身的看法\"\u003e同事对Everything as Code本身的看法\u003c/h3\u003e\n\u003cp\u003e接着，他说出对Everything as Code的看法：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我觉得everything as code太理想，只能面向开发人员。你不能保证运维的能力，绝对不开放给第三方使用。指不定某天某个需求就需要你的运维的某个能力，让不懂代码的人也要操作。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e假如某个运维能力，要开放给产品或者测试来用，要让别人给你学个jsonnet，那要我是产品或者测试只能口吐芬芳了。所以我认为，面向开发者的部分，使用every thing as code是趋势，比较好溯源跟踪，迭代。但是架构设计上，还需要有支持面向第三方的部分，可以做到让不懂代码的人也能用到一些运维的能力。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这位同事实现的是金丝雀配置生成，是需要修改Jsonnet代码的。当产品经理需要将某用户的流量路由到后端服务的某个版本上时，这位同事就认为产品经理就需要去改Jsonnet代码了。\u003c/p\u003e\n\u003cp\u003e同事认为这就是Everything as Code的问题：不方便非开发人使用。\u003c/p\u003e\n\u003cp\u003e同事的另一层意思没有说出来：并不是所有的事情都适合as Code。\u003c/p\u003e\n\u003cp\u003e其实，有这样的误解很正常，“Everything as Code”中的“Everything”字面含义上就是所有的，一切的意思。而Code是代码的意思。\u003c/p\u003e\n\u003cp\u003e所以，“Everything as Code”字面上完整的意思是：一切皆代码。\u003c/p\u003e\n\u003cp\u003e从字面上理解“Everything as Code”，只看到了它的表面\u003c/p\u003e\n\u003ch3 id=\"everything-as-code的本质\"\u003eEverything as Code的本质\u003c/h3\u003e\n\u003cp\u003e“Everything as Code”中的”Code“，其实指的是配置，即：一切皆配置。\u003c/p\u003e\n\u003cp\u003e这句话本身是没有错的，如同“道生一，一生二，二生三，三生万物“这句话。\u003c/p\u003e\n\u003cp\u003e“Everything as Code”这句话并没说什么是配置，以及哪些配置该放哪。这才是我们实践时真正要考虑的问题。\u003c/p\u003e\n\u003cp\u003e配置的本质是软件的灵活（soft）部分。通过这个”灵活部分“，我们可以根据自己的需求，有限地改变软件的行为。\u003c/p\u003e","title":"当同事实践Everything as Code之后"},{"content":"背景 SQL进行版本化控制后，我们希望为SQL加入lint步骤。这样做的好处是我们可以在真正执行SQL前发现问题。\n本文中，我们通过Bazel执行SQLFluff以实现SQL的lint。\nSQLFluff是一款使用Python语言使用的，支持SQL多方言的SQL lint工具。\n它的特点是：\n支持多方言。如：Snowflake、PostgreSQL、ClickHouse。所有支持的方言列表：https://docs.sqlfluff.com/en/stable/dialects.html； 可以输出正确的SQL，减少了我们手工修正SQL的工作； 同时支持命令行方式使用和API调用方式。 集成到CI/CD流水线中 在我看来，在CICD流水线中实现SQL lint有两种方式：\n方式一：在流水线中增加一个SQL lint步骤； 方式二：将SQL lint的逻辑写在测试代码，执行测试步骤，就自动执行了SQL lint。 方式二是我最爱，我会在本文最后讲原因。\n工程结构 . ├── BUILD.bazel ├── WORKSPACE ├── repository-hibernate-impl │ ├── BUILD.bazel │ └── src │ ├── main │ │ └── sql │ │ └── V1__runbook_table.sql │ └── test │ └── python │ ├── BUILD.bazel │ ├── requirements_lock.txt │ └── sql_test.py 步骤1: 在WORKSPACE中增加Python外部依赖 本文中我们使用的是Bazel 5.4.0，所以还在使用WORKSPACE定义外部依赖\nhttp_archive( name = \u0026#34;rules_python\u0026#34;, sha256 = \u0026#34;a644da969b6824cc87f8fe7b18101a8a6c57da5db39caa6566ec6109f37d2141\u0026#34;, strip_prefix = \u0026#34;rules_python-0.20.0\u0026#34;, url = \u0026#34;https://github.com/bazelbuild/rules_python/releases/download/0.20.0/rules_python-0.20.0.tar.gz\u0026#34;, ) load(\u0026#34;@rules_python//python:repositories.bzl\u0026#34;, \u0026#34;py_repositories\u0026#34;) py_repositories() load(\u0026#34;@rules_python//python:repositories.bzl\u0026#34;, \u0026#34;python_register_toolchains\u0026#34;) python_register_toolchains( name = \u0026#34;python3_11\u0026#34;, python_version = \u0026#34;3.11\u0026#34;, ) load(\u0026#34;@python3_11//:defs.bzl\u0026#34;, interpreter_3_11 = \u0026#34;interpreter\u0026#34;) load(\u0026#34;@rules_python//python:pip.bzl\u0026#34;, \u0026#34;pip_parse\u0026#34;) # Create a central repo that knows about the dependencies needed from # requirements_lock.txt. pip_parse( name = \u0026#34;pip_deps\u0026#34;, python_interpreter_target = interpreter_3_11, requirements_lock = \u0026#34;//repository-hibernate-impl/src/test/python:requirements_lock.txt\u0026#34;, ) # Load the starlark macro which will define your dependencies. load(\u0026#34;@pip_deps//:requirements.bzl\u0026#34;, \u0026#34;install_deps\u0026#34;) # Call it to define repos for your requirements. install_deps() 步骤2: 定义SQLFluff依赖 requirements_lock.txt的内容如下：\nsqlfluff==2.0.5 Jinja2==3.1.2 MarkupSafe==2.1.2 Pygments==2.15.0 appdirs==1.4.4 chardet==5.1.0 click==8.1.3 colorama==0.4.6 diff_cover==7.5.0 iniconfig==2.0.0 packaging==23.1.0 pathspec==0.11.1 pluggy==1.0.0 pytest==7.3.1 tomli==2.0.1 toml==0.10.2 exceptiongroup==1.1.1 pyyaml==6.0 regex===2023.3.23 tblib==1.7.0 tqdm==4.65.0 typing_extensions==4.5.0 步骤3: 定义BUILD目标 load(\u0026#34;@pip_deps//:requirements.bzl\u0026#34;, \u0026#34;requirement\u0026#34;) load(\u0026#34;@rules_python//python:defs.bzl\u0026#34;, \u0026#34;py_test\u0026#34;) py_test( name = \u0026#34;sql_test\u0026#34;, srcs = [\u0026#34;sql_test.py\u0026#34;], # data传入是sql的label data = [ \u0026#34;//repository-hibernate-impl:sqlTest\u0026#34;,], deps = [ requirement(\u0026#34;sqlfluff\u0026#34;), requirement(\u0026#34;Jinja2\u0026#34;), requirement(\u0026#34;MarkupSafe\u0026#34;), requirement(\u0026#34;Pygments\u0026#34;), requirement(\u0026#34;appdirs\u0026#34;), requirement(\u0026#34;chardet\u0026#34;), requirement(\u0026#34;click\u0026#34;), requirement(\u0026#34;colorama\u0026#34;), requirement(\u0026#34;diff_cover\u0026#34;), requirement(\u0026#34;iniconfig\u0026#34;), requirement(\u0026#34;packaging\u0026#34;), requirement(\u0026#34;pathspec\u0026#34;), requirement(\u0026#34;pluggy\u0026#34;), requirement(\u0026#34;pytest\u0026#34;), requirement(\u0026#34;tomli\u0026#34;), requirement(\u0026#34;toml\u0026#34;), requirement(\u0026#34;exceptiongroup\u0026#34;), requirement(\u0026#34;pyyaml\u0026#34;), requirement(\u0026#34;regex\u0026#34;), requirement(\u0026#34;tblib\u0026#34;), requirement(\u0026#34;tqdm\u0026#34;), requirement(\u0026#34;typing_extensions\u0026#34;), ], ) 注：sql的BUILD目标(repository-hibernate-impl/BUILD.bazel)为：\nfilegroup( name = \u0026#34;sqlTest\u0026#34;, testonly = 1, srcs = glob([\u0026#34;src/main/sql/*.sql\u0026#34;]), visibility = [\u0026#34;//visibility:public\u0026#34;], ) 步骤4: 调用SQLFluff实现SQL lint import unittest import sqlfluff import os import codecs sqls_path = os.path.join(os.getcwd(), \u0026#34;repository-hibernate-impl/src/main/sql/\u0026#34;) dialect = \u0026#34;postgres\u0026#34; class TestSum(unittest.TestCase): def test_lint_sql(self): sql_dir_files = os.listdir(sqls_path) # 确保目录中有sql文件 self.assertTrue(len(sql_dir_files) \u0026gt; 0) for sql_filename in sql_dir_files: if sql_filename.endswith(\u0026#34;.sql\u0026#34;): f = codecs.open(os.path.join(sqls_path, sql_filename), \u0026#34;r\u0026#34;, \u0026#34;utf-8\u0026#34;) sql_content = f.read() lint_result = sqlfluff.lint(sql_content, dialect=dialect) # 如果存在lint问题 if len(lint_result) \u0026gt; 0: # 通过sqlfluff修复sql的问题，并返回正确的写法。 fix_result = sqlfluff.fix(sql_content, dialect=dialect) # 将正确的sql写法打印出来方便查看 print(\u0026#34;correct sql should be: \\n\u0026#34; + fix_result) self.assertEqual(len(lint_result), 0) if __name__ == \u0026#34;__main__\u0026#34;: unittest.main() 执行 我们只需要在工程根目录执行bazel test //...命令，就可以对SQL进行lint了。\n为什么我选择方式二 选择方式二（通过Bazel实现SQL lint）原因有二：\n方式一需要开发人员将代码提交后，才可以解决流水线的执行，而方式二，在本地就可以执行，有利于开发人员在本地就可以实现SQL lint。 方式二可以实现构建缓存（Bazel天然支持），可以节约大量的构建成本。 ","permalink":"https://showme.codes/zh-cn/2023-04-17-sql-lint-by-bazel-sqlfluff/","summary":"\u003ch3 id=\"背景\"\u003e背景\u003c/h3\u003e\n\u003cp\u003eSQL进行版本化控制后，我们希望为SQL加入lint步骤。这样做的好处是我们可以在真正执行SQL前发现问题。\u003c/p\u003e\n\u003cp\u003e本文中，我们通过Bazel执行\u003ca href=\"https://github.com/sqlfluff/sqlfluff\"\u003eSQLFluff\u003c/a\u003e以实现SQL的lint。\u003c/p\u003e\n\u003cp\u003eSQLFluff是一款使用Python语言使用的，支持SQL多方言的SQL lint工具。\u003c/p\u003e\n\u003cp\u003e它的特点是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e支持多方言。如：Snowflake、PostgreSQL、ClickHouse。所有支持的方言列表：https://docs.sqlfluff.com/en/stable/dialects.html；\u003c/li\u003e\n\u003cli\u003e可以输出正确的SQL，减少了我们手工修正SQL的工作；\u003c/li\u003e\n\u003cli\u003e同时支持命令行方式使用和API调用方式。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"集成到cicd流水线中\"\u003e集成到CI/CD流水线中\u003c/h3\u003e\n\u003cp\u003e在我看来，在CICD流水线中实现SQL lint有两种方式：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e方式一：在流水线中增加一个SQL lint步骤；\u003c/li\u003e\n\u003cli\u003e方式二：将SQL lint的逻辑写在测试代码，执行测试步骤，就自动执行了SQL lint。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e方式二是我最爱，我会在本文最后讲原因。\u003c/p\u003e\n\u003ch3 id=\"工程结构\"\u003e工程结构\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── BUILD.bazel\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── WORKSPACE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── repository-hibernate-impl\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│   ├── BUILD.bazel\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│   └── src\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│       ├── main\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│       │   └── sql\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│       │       └── V1__runbook_table.sql\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│       └── \u003cspan class=\"nb\"\u003etest\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│           └── python\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│               ├── BUILD.bazel\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│               ├── requirements_lock.txt\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│               └── sql_test.py\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"步骤1-在workspace中增加python外部依赖\"\u003e步骤1: 在WORKSPACE中增加Python外部依赖\u003c/h3\u003e\n\u003cp\u003e本文中我们使用的是Bazel 5.4.0，所以还在使用WORKSPACE定义外部依赖\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ehttp_archive\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;rules_python\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esha256\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;a644da969b6824cc87f8fe7b18101a8a6c57da5db39caa6566ec6109f37d2141\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estrip_prefix\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;rules_python-0.20.0\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eurl\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://github.com/bazelbuild/rules_python/releases/download/0.20.0/rules_python-0.20.0.tar.gz\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@rules_python//python:repositories.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;py_repositories\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epy_repositories\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@rules_python//python:repositories.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;python_register_toolchains\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epython_register_toolchains\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;python3_11\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003epython_version\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;3.11\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@python3_11//:defs.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einterpreter_3_11\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;interpreter\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@rules_python//python:pip.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;pip_parse\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Create a central repo that knows about the dependencies needed from  \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# requirements_lock.txt.  \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epip_parse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;pip_deps\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   \u003cspan class=\"n\"\u003epython_interpreter_target\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003einterpreter_3_11\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   \u003cspan class=\"n\"\u003erequirements_lock\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;//repository-hibernate-impl/src/test/python:requirements_lock.txt\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Load the starlark macro which will define your dependencies.  \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eload\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@pip_deps//:requirements.bzl\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;install_deps\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Call it to define repos for your requirements.  \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003einstall_deps\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"步骤2-定义sqlfluff依赖\"\u003e步骤2: 定义SQLFluff依赖\u003c/h3\u003e\n\u003cp\u003erequirements_lock.txt的内容如下：\u003c/p\u003e","title":"基于Bazel + SQLFluff实现SQL lint"},{"content":"\n前段时间和不同的人交流了DevOps平台后，大概了解了行业里DevOps平台是怎么回事。\nDevOps平台最大的问题 DevOps的定义是什么？面对这样的问题，我想说行业里，10个人可能有11个答案。既然DevOps的定义在行业都没有一个权威的定义，那么，对于DevOps平台是什么这样一一个问题，自然也就没有权威的定义了。这就是行业里DevOps平台最大的问题。\nP.S. 对于DevOps Master证书又应该如何定义呢？\n如果DevOps平台没有定义，那么，我们就无法以下类似的问题：\n它解决了谁的什么问题？ 行业里，它的趋势是什么？ DevOps平台发展到后期，是不是AIOps？ 因为，你再怎么回答这些问题，只要你与提问人对于DevOps的认知不一样，你都会与提问人内心的答案不一样。\n这让我想起马斯克的名言：我现在不和人争吵了，因为我开始意识到，每个人只能在他的认知水准基础上去思考，以后有人告诉我2+2等于10，我会说，你真厉害！\n谈DevOps平台的设计 我看到的是它们有很多功能：项目管理、需求管理、测试管理、流水线、制品管理、自动化部署……所有人都类似地美其名为：一站式研发云平台。\n可是，当你在使用时，你会发现，它们不过是在以前的项目管理平台、测试管理平台、流水线平台、制品管理平台、部署平台等多个平台的功能的重新堆砌。而这些功能之间有有联系吗？很少。\n行业里不少客户会要求DevOps平台的Dashboard，不同的角色进入要显示不一样的东西。其实就是开发者进入Dashboard应该只显示开发者关心的功能，测试人员进入Dashboard应该只显示测试用例相关的功能……这是不是可以成为康威推论的一个佐证？\n总的一句，DevOps平台不过是过去多个不相关的烟囱系统，通过一个Dashboard，让它们显得像是一个平台。\n研发效率有最优解吗？ 和这么多人交流后，发现大家还是有一些共识的。那就是：DevOps平台应该是可以提高研发（or 运维）效率的（如果我只说研发效率，估计有人会以为我认为DevOps平台只是为研发侧服务的）。\n行业里，To C产品的设计，最优解通常是未知的，是需要不断寻找的。所以，发展出各种方法论寻找方法论。这对于To C产品的设计是有效的。因为它的前提是最优解真的在于用户。\n抛开产品设计方法论，对于研发效率的提升这个领域， DevOps平台的设计者应该比用户知道最优解是什么？\n那我们使用To B的产品设计方法论去设计如何？个人觉得，即对，也不对。对的地方是DevOps产品毕竟是卖给企业的，所以，在设计DevOps时不得不考虑谁给它买单这个问题。不对的还是我上文提的最优解问题。\nDevOps平台与Srcum、敏捷、迭代开发之间的关系 如果你的DevOps平台不与这些听起来牛x、高大上的名词关系起来，在这个行业里，你的产品估计有点难打开市场。\n这就要求，我们在设计DevOps平台时，不得不考虑行业里的人对于Scrum、敏捷、迭代开发的理解。\n个人觉得难点在于：在DevOps平台的设计上，无论用户希望使用何种方法论，都能在平台内实现DevOps平台本身的目标，同时平台还能保持自身的简洁。\n用户使用那些方法论背后的目的才是关键。\n后记 感谢这些同学花时间与我交流。本文作为一篇交流文章，目的不是为了贬低DevOps平台。\n","permalink":"https://showme.codes/zh-cn/2020-11-02-devops-platform-interview/","summary":"\u003cp\u003e\u003cimg alt=\"sunset-815270_640.jpg\" loading=\"lazy\" src=\"/assets/images/292372-dc1ef40bb713c6f5.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e前段时间和不同的人交流了DevOps平台后，大概了解了行业里DevOps平台是怎么回事。\u003c/p\u003e\n\u003ch3 id=\"devops平台最大的问题\"\u003eDevOps平台最大的问题\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eDevOps\u003c/strong\u003e的定义是什么？面对这样的问题，我想说行业里，10个人可能有11个答案。既然\u003cstrong\u003eDevOps\u003c/strong\u003e的定义在行业都没有一个权威的定义，那么，对于\u003cstrong\u003eDevOps平台\u003c/strong\u003e是什么这样一一个问题，自然也就没有权威的定义了。这就是行业里DevOps平台最大的问题。\u003c/p\u003e\n\u003cp\u003eP.S. 对于DevOps Master证书又应该如何定义呢？\u003c/p\u003e\n\u003cp\u003e如果DevOps平台没有定义，那么，我们就无法以下类似的问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e它解决了谁的什么问题？\u003c/li\u003e\n\u003cli\u003e行业里，它的趋势是什么？\u003c/li\u003e\n\u003cli\u003eDevOps平台发展到后期，是不是AIOps？\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e因为，你再怎么回答这些问题，只要你与提问人对于DevOps的认知不一样，你都会与提问人内心的答案不一样。\u003c/p\u003e\n\u003cp\u003e这让我想起马斯克的名言：我现在不和人争吵了，因为我开始意识到，每个人只能在他的认知水准基础上去思考，以后有人告诉我2+2等于10，我会说，你真厉害！\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-8006bbebe81e5cc9.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"谈devops平台的设计\"\u003e谈DevOps平台的设计\u003c/h3\u003e\n\u003cp\u003e我看到的是它们有很多功能：项目管理、需求管理、测试管理、流水线、制品管理、自动化部署……所有人都类似地美其名为：一站式研发云平台。\u003c/p\u003e\n\u003cp\u003e可是，当你在使用时，你会发现，它们不过是在以前的项目管理平台、测试管理平台、流水线平台、制品管理平台、部署平台等多个平台的功能的重新堆砌。而这些功能之间有有联系吗？很少。\u003c/p\u003e\n\u003cp\u003e行业里不少客户会要求DevOps平台的Dashboard，不同的角色进入要显示不一样的东西。其实就是开发者进入Dashboard应该只显示开发者关心的功能，测试人员进入Dashboard应该只显示测试用例相关的功能……这是不是可以成为康威推论的一个佐证？\u003c/p\u003e\n\u003cp\u003e总的一句，DevOps平台不过是过去多个不相关的烟囱系统，通过一个Dashboard，让它们显得像是一个平台。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-b6ecbc50818d9413.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"研发效率有最优解吗\"\u003e研发效率有最优解吗？\u003c/h3\u003e\n\u003cp\u003e和这么多人交流后，发现大家还是有一些共识的。那就是：DevOps平台应该是可以提高研发（or 运维）效率的（如果我只说研发效率，估计有人会以为我认为DevOps平台只是为研发侧服务的）。\u003c/p\u003e\n\u003cp\u003e行业里，To C产品的设计，最优解通常是未知的，是需要不断寻找的。所以，发展出各种方法论寻找方法论。这对于To C产品的设计是有效的。因为它的前提是最优解真的在于用户。\u003c/p\u003e\n\u003cp\u003e抛开产品设计方法论，对于研发效率的提升这个领域， DevOps平台的设计者应该比用户知道最优解是什么？\u003c/p\u003e\n\u003cp\u003e那我们使用To B的产品设计方法论去设计如何？个人觉得，即对，也不对。对的地方是DevOps产品毕竟是卖给企业的，所以，在设计DevOps时不得不考虑谁给它买单这个问题。不对的还是我上文提的最优解问题。\u003c/p\u003e\n\u003ch3 id=\"devops平台与srcum敏捷迭代开发之间的关系\"\u003eDevOps平台与Srcum、敏捷、迭代开发之间的关系\u003c/h3\u003e\n\u003cp\u003e如果你的DevOps平台不与这些听起来牛x、高大上的名词关系起来，在这个行业里，你的产品估计有点难打开市场。\u003c/p\u003e\n\u003cp\u003e这就要求，我们在设计DevOps平台时，不得不考虑行业里的人对于Scrum、敏捷、迭代开发的理解。\u003c/p\u003e\n\u003cp\u003e个人觉得难点在于：在DevOps平台的设计上，无论用户希望使用何种方法论，都能在平台内实现DevOps平台本身的目标，同时平台还能保持自身的简洁。\u003c/p\u003e\n\u003cp\u003e用户使用那些方法论背后的目的才是关键。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e感谢这些同学花时间与我交流。本文作为一篇交流文章，目的不是为了贬低DevOps平台。\u003c/p\u003e","title":"和行业里多家DevOps平台的同学交流后，我发现……"},{"content":"\n虽然，读者朋友可能觉得自己已经理解这些概念了，但是，还是希望读者读完。笔者从权威的书上将这些概念的定义摘抄下来，最后给出笔者对于“持续”的理解。\n构建（Build）：\n一次构建不止是一次编译（或者动态语言中的某种称谓）。一次构建可能包含编译、测试、审查和部署以及其他一些事情。一次构建是将源代码放在一起，并验证软件可以作为一个一致的单元运行的过程。摘自《持续集成》\n其实构建过程中还可以包括测试、部署。这点可能和很多人的理解有出入。这里就会有疑问了，既然构建中包括了部署，那么持续构建与持续部署又有什么关系？笔者是这样理解的，因为软件系统是需要部署了，才能测试的，所以，为了在构建过程加入测试，就必须引入部署。\n部署（Deployment）：\n部署是一种技术领域的操作，也就是说从某处获取软件包，并按照预先设计的方案将其安装在计算节点上，并确保系统可以正常启动，但它并不定意味着“必须包含业务功能的发布或交付”。摘自《持续交付2.0》\n交付（Delivery，也被称为发布）：\n是一个业务决策活动，通常也被称为“发布”，也就是说，如果将新的构建的特性交到客户（用户）手中，用户就可以看到并使用它们。摘自《持续交付2.0》\n我们可以将代码部署上生产环境，但是我们可以通过某种技术手段，让用户看不到，也不能使用它。这就是只部署，但不交付。部署与交付的差异在于部署是技术端操作、交付是业务端决策。\n这里，读者可能又有疑问了：未完成的功能，可以部署上生产环境吗？笔者的回答：是的。前提是你能控制该功能是否对用户可见。这称为功能开关。\n持续集成（CI）：\n它是一种软件开发实践，即团队的成员经常集成他们的工作。通常每个成员每天至少集成一次——这导致每天发生多次集成。每次集成都通过自动化的构建（包括测试）来验证，从而尽快地检测出集成错误。摘自《持续集成——软件质量改进和风险降低之道》\n注意，持续集成中包括了构建与测试。所以，我们在行业里经常听到的“持续测试（CT）”又是什么呢？这是笔者的疑问。\n持续交付1.0（CD1.0）：\n持续交付是一种能力，也就是说，能够以持续方式，安全快速地把代码变更（包括特性、配置、缺陷和试验）部署到生产环境上，让用户使用。摘自《持续交付2.0——业务引领的DevOps精要》\n持续交付2.0（CD2.0）：\n“持续交付2.0”建立在“持续交付1.0”的“可持续地快速发布软件服务”及精益创业的“最小化可行产品”两种理念基础之上，强调要以业务为导向，从一开始就业务问题进行分解，并通过不断的科学探索与快速验证，减少浪费的同时，快速找到正确的业务前进方向，简称为“双环模型”。摘自《持续交付2.0》\n持续集成、持续部署、持续交付之间是什么关系呢？笔者认为是：持续交付的过程会包含持续部署，持续部署的前提是持续集成。把集成与部署组合到一起，并完全自动化，这个自动化的过程，称为部署流水线。持续交付1.0强调的是交付的效率，持续交付2.0则除了强调交付的效率，还强调交付的效果。\n最后，我们来谈谈“持续”，笔者是这样理解的：\n所谓的“持续”，就是指经常地做。而“经常”是一个相对的概念。对于每年交付一次的软件系统，优化成每个月一次，也算是“持续”了。另，“持续”代表的是一种能力。有能力持续交付，但是业务不一定允许。要实现“持续”的能力，自动化就成为了必然的选择。\n说到了“持续”就不得不说“持续改进”。上文说过，持续指的是经常做。持续改进的意思是经常做改进。持续改进的极限是無時不刻地在改进。那么，如何让一个团队无时不刻地进行改进呢？这是一个非常大的话题。关注笔者的公众号，将来会讲到。\n","permalink":"https://showme.codes/zh-cn/2020-12-01-ci-cd-ct/","summary":"\u003cp\u003e\u003cimg alt=\"dawn-190055_640.jpg\" loading=\"lazy\" src=\"/assets/images/292372-3086e60b5982cac6.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e虽然，读者朋友可能觉得自己已经理解这些概念了，但是，还是希望读者读完。笔者从\u003cstrong\u003e权威\u003c/strong\u003e的书上将这些概念的定义摘抄下来，最后给出笔者对于“持续”的理解。\u003c/p\u003e\n\u003cp\u003e构建（Build）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e一次构建不止是一次编译（或者动态语言中的某种称谓）。一次构建可能包含编译、测试、审查和部署以及其他一些事情。一次构建是将源代码放在一起，并验证软件可以作为一个一致的单元运行的过程。摘自《持续集成》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e其实构建过程中还可以包括测试、部署。这点可能和很多人的理解有出入。这里就会有疑问了，既然构建中包括了部署，那么持续构建与持续部署又有什么关系？笔者是这样理解的，因为软件系统是需要部署了，才能测试的，所以，为了在构建过程加入测试，就必须引入部署。\u003c/p\u003e\n\u003cp\u003e部署（Deployment）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e部署是一种技术领域的操作，也就是说从某处获取软件包，并按照预先设计的方案将其安装在计算节点上，并确保系统可以正常启动，但它并不定意味着“必须包含业务功能的发布或交付”。摘自《持续交付2.0》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e交付（Delivery，也被称为发布）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e是一个业务决策活动，通常也被称为“发布”，也就是说，如果将新的构建的特性交到客户（用户）手中，用户就可以看到并使用它们。摘自《持续交付2.0》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我们可以将代码部署上生产环境，但是我们可以通过某种技术手段，让用户看不到，也不能使用它。这就是只部署，但不交付。部署与交付的差异在于部署是技术端操作、交付是业务端决策。\u003c/p\u003e\n\u003cp\u003e这里，读者可能又有疑问了：未完成的功能，可以部署上生产环境吗？笔者的回答：是的。前提是你能控制该功能是否对用户可见。这称为功能开关。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"IMG_20200916_054551__01.jpg\" loading=\"lazy\" src=\"/assets/images/292372-73c0527482838a56.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e持续集成（CI）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e它是一种软件开发实践，即团队的成员经常集成他们的工作。通常每个成员每天至少集成一次——这导致每天发生多次集成。每次集成都通过自动化的构建（包括测试）来验证，从而尽快地检测出集成错误。摘自《持续集成——软件质量改进和风险降低之道》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e注意，持续集成中包括了构建与测试。所以，我们在行业里经常听到的“持续测试（CT）”又是什么呢？这是笔者的疑问。\u003c/p\u003e\n\u003cp\u003e持续交付1.0（CD1.0）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e持续交付是一种能力，也就是说，能够以持续方式，安全快速地把代码变更（包括特性、配置、缺陷和试验）部署到生产环境上，让用户使用。摘自《持续交付2.0——业务引领的DevOps精要》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e持续交付2.0（CD2.0）：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“持续交付2.0”建立在“持续交付1.0”的“可持续地快速发布软件服务”及精益创业的“最小化可行产品”两种理念基础之上，强调要以业务为导向，从一开始就业务问题进行分解，并通过不断的科学探索与快速验证，减少浪费的同时，快速找到正确的业务前进方向，简称为“双环模型”。摘自《持续交付2.0》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e持续集成、持续部署、持续交付之间是什么关系呢？笔者认为是：持续交付的过程会包含持续部署，持续部署的前提是持续集成。把集成与部署组合到一起，并完全自动化，这个自动化的过程，称为部署流水线。持续交付1.0强调的是交付的效率，持续交付2.0则除了强调交付的效率，还强调交付的效果。\u003c/p\u003e\n\u003cp\u003e最后，我们来谈谈“持续”，笔者是这样理解的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e所谓的“持续”，就是指经常地做。而“经常”是一个相对的概念。对于每年交付一次的软件系统，优化成每个月一次，也算是“持续”了。另，“持续”代表的是一种能力。有能力持续交付，但是业务不一定允许。要实现“持续”的能力，自动化就成为了必然的选择。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e说到了“持续”就不得不说“持续改进”。上文说过，持续指的是经常做。持续改进的意思是经常做改进。持续改进的极限是無時不刻地在改进。那么，如何让一个团队无时不刻地进行改进呢？这是一个非常大的话题。关注笔者的公众号，将来会讲到。\u003c/p\u003e","title":"持续构建、持续测试、持续集成、持续部署、持续交付、持续.....“持续”到底是什么意思？"},{"content":"分析团队的问题 我是2020年3月份加入该部门。刚加入时发现问题还挺多。而这些问题在行业里都很典型，比如：\n分支管理不统一：虽说大部分人还是在master上开发，但是还有部分人自己拉feature分支开发。 没有统一的制品打包：Docker镜像的打包基本都是在开发人员的电脑上进行的。 对制品仓库的push权限没有管理：每个人都有push权限，而且使用的是同一个账号。 没有版本管理：在交给测试人员测试（俗称提测）时，开发在本地打包后，push上制品库的包的版本号为uat20200302（UAT是测试环境的简称）。测试通过后，再使用此版本号，部署到生产环境。结果就是你会看到生产环境运行的包的版本：uat20200302，是不是很奇怪？ 没有监控：这也是很多团队的通病了。 没有单元测试：开发人员有在main方法写单元测试的，有写出来的测试是无法自动化的。 开发团队没有自己的自动化测试。 多个应用部署在同一台机器。 手工部署：每次部署都是人工登录到服务器执行部署。 数据库没有版本化。 没有代码审查。 问题还有很多，就不一一列举。在笔者看来以上问题的最终表现都是软件系统的质量低下。笔者希望通过实践持续交付以提升软件系统的质量。\n但是问题是该如何实践呢？笔者认为只要掌握了它的基本原则，剩下的就是根据实际情况结合基本原则来解决问题了。\n持续交付的原则 幸福的家庭都是相似的，不幸的家庭各有各的不幸——《安娜卡列尼娜》\n从《持续交付》书中，基本原则有：\n为软件的发布创建一个可重复且可靠的过程 将几乎所有的事情自动化 把所有的东西都纳入版本控制 提前并频繁地做让你感到痛苦的事 内建质量 “DONE”意味着“已发布” 交付过程是每个成员的责任 持续改进 我们可以这么理解这些原则：基于所有东西都要进行版本化的原则，所有的东西都要代码化。\n因为代码化以后就可以放到类似Git这类版本化工具中。而代码化以后的东西就可以很容易地实现自动化。在实现自动化以后就可以为软件的发布创建一个可重复且可靠的过程。\n实现自动化的过程是需要每一位成员持续参与的，因为交付过程是每个人责任。\n“DONE意味着已发布”是团队每个成员都要达成的共识。达成共识后，才能更好的参与持续改进。在持续改进过程中，我们的软件系统就获得了内建质量。\n笔者认为，在持续交付中，代码化与版本化是基础。\n实践持续交付 在理解原则后，我们就可以开始实践了。可是该如何下手呢？笔者通常遵循以下指导思想：\n先CI，后CD。 无监控，无安心觉。 先配置项版本化，后标准化，最后才有自动化。 根据指导思想，再结合团队的实际情况，笔者做出以下计划：\n打包自动化。 实现基础监控（机器级别监控、中间件监控）。 实现所有的配置版本化。 实现自动化部署应用。 实现应用监控。 实现数据库版本化。 实现业务监控。 研发数据收集 \u0026hellip;. 由于团队原来已经有日志收集机制，所以，暂且不需要实现。以上步骤只代表一个优先级。如果团队人力充足，可以同时一起做。\n虽说有了计划，但是团队不具备相应的能力，什么计划都白搭。\n1. 打包自动化 之所以使用“打包”这个听起来不怎么“高端”的词，而不是使用“构建”。是因为“构建”这个词，太容易引起歧义。而且打包这个词很形象，就是把源代码编译后，链接，最后打包成一个可执行包。当然不同的编程语言，打包过程可能不同。\n因为大多数团队都没有写自动化测试的习惯（我们团队也不例外），让他们写自动化测试，他们只会觉得自己的工作量增加了。所以，我在团队中导入持续交付实践时，一开始就不要求自动化测试。团队意识的转变需要很长的过程。这是使用“打包”的第二个原因：它不包括自动化测试。\n要实现自动化打包，其实并不难。基本步骤就是：\n搭建制品库：Nexus。 搭建自动化服务：Jenkins。 在Jenkins中创建pipeline任务。 在业务代码仓库中加入Jenkinsfile，将打包逻辑写到Jenkinsfile中。 所谓打包逻辑就是你在本地开发时，利用IDE或命令将源代码编译成可执行文件的过程。打包自动化的过程，就是将你在本地执行的打包过程“搬”到自动化系统上执行，再加上一些优化。\n在这个阶段中，我们需要实现：\n统一制品库：收回所有人上传制品的账号。只能由Jenkins打包上传。 统一版本号：比如使用格式年-月-日-commitId-构建号来定义所有的后端应用。注意，不管使用哪种方式，你必须很容易的根据版本号找回相应的源码。 统一分支管理：使用主干开发，分支发布的模式。我们根据团队情况有稍微做了一些改变。发布并没有切分支出来，而在发布后发现某版本有Bug，我们就会从该版本的代码切一个分支出来改，打包，部署。最后再将该分支的commit cherry pick回master分支。 2. 实现基础监控 没有监控，在我们这个行业太常见了。所以，在我加入团队后，发现几乎没有任何监控，也就没有什么好惊讶的了。所以，在解决打包问题之后 ，紧接着就是给所有的机器加上监控。至少机器的CPU、硬盘、内存等要监控起来。\n上一阶段，我们已经把Jenkins搭建起来，所以，Prometheus就开始自动化部署了。\n使用Prometheus的原因很多，但是关键是它的配置是代码化的，非常容易版本化。持续交付的原则：将几乎所有的事情自动化、把所有的东西都纳入版本控制。像Zabbix，使用需要使用界面进行操作的，就被我排除了。\n在这个阶段中，我们需要实现：\n尽量不要动老机器的配置。要做的就是在该机器上安装node-exporter方便监控。 新机器使用Hashcorp Packer进行标准化操作系统 。新机器统一使用新的标准化的操作系统 。 使用Prometheus监控所有的机器。同时，需要对告警进行分级。我们根据紧急程度，将告警发往不同的钉钉群。 中间件的监控，找到相应的exporter即可。 3. 实现所有的配置项版本化 “配置”是一个非常容易产生分歧的词。它可以是一个名词，也可以是一个动词。作为名词，我们把它称为配置项，以区别于动词。另一个使用“配置项”的原因是，当人们讨论配置时，大脑里通常是properties、yaml等这类文件格式，又或者是像consul、ctripcorp/apollo等这类分布式配置中心。而笔者所说的配置项是独立于任何文件格式及其被使用的方式的。\n笔者认为这样理解“配置”更合理：\n虽然配置项从概念上是独立的，但是它总归要被人增加、删除、修改吧？笔者使用的是Ansible的配置管理方式管理团队所有的配置项。目录结构如下：\n├── dev --\u0026gt; dev环境。一个环境一个git仓库。 │ ├── group_vars --\u0026gt; 组配置 │ │ ├── all │ │ │ └── global.yaml --\u0026gt; 全局配置 │ │ ├── nginx.yaml --\u0026gt; nginx组配置 │ │ └── springboot.yaml --\u0026gt; springboot应用组配置 │ ├── hosts --\u0026gt; 定义机器IP与应用组之间的关系 │ └── host_vars\t--\u0026gt; 主机级配置 │ ├── 192.168.52.10 --\u0026gt; 192.168.52.10的配置 │ └── 192.168.52.11\t--\u0026gt; 192.168.52.11的配置 通常我们认为这是针对使用Ansible进行传统部署时的管理方式。但是我们的实践结果证明，它也适用于K8s的应用部署。\n在这个阶段中，我们需要实现：\n不同环境的配置项放在不同的Git仓库。 将敏感配置项进行加密处理，使用ansible-vault。 与现有的配置中心集成，即将配置项部署到配置中心。比如，使用Nacos时，部署配置时，通过Nacos的API将配置发布到Nacos中。 4. 实现自动化部署应用 实际上，我们的基础监控、配置项版本化、自动化部署是同时进行的。因为搭建基础监控，也需要配置项版本化和自动化。\n自动化部署又分成两种：传统部署和K8s应用部署。初期团队还没有迁移至K8s，所以，初期还是得使用Ansible部署。\n这个阶段，我们需要实现：\n标准化所有的应用的部署逻辑，争取所有的应用使用同一套部署代码，只要修改几个配置就能部署不同的应用。 实现部署清单。所谓部署清单就是包含了所有应用的版本的列表。部署流水线会读取部署清单，并进行部署。这点有点抽象，后面会讲。 对所有应用进行自动化部署的策略：部署到新机器，采取一台机器只部署一个应用的原则。使用这种策略逐渐淘汰旧机器。 搭建堡垒机。之前因为是人工部署，开发从自己的办公电脑SSH到远程服务进行部署。现在自动化部署了，需要将SSH登录机器的权限收回。 部署清单其实就是一个Yaml文件，如下：\ndeployments: - name: foo version: 2020-09-02-75c23d40-1 deploy: true - name: config version: 2020-08-02-15q6hd41-111 deploy: true - name: prometheus version: 2020-09-02-75c23d40-1 deploy: true .... 其他需要部署应用的版本列表 我们在Jenkins pipeline中读取此文件，然后根据文件内容执行相应的部署脚本。因为我们已经把应用的部署方式标准化了，所以，真正部署时，就只需要将部署清单中的name和version等配置值以参数的方式传给标准的部署方式就好。\n为什么要这样做呢？\n团队中的任何人，只要有版本号，通过修改文件中的版本，就可以实现部署。以实现“交付过程是每个成员的责任”。 在一个地方就能看到软件系统当前的所有的组件的版本，一目了然。 与具体的流水线技术解耦。即使不通过Jenkins pipeline，通过其它pipeline方式也可以实现。 5. 实现应用监控 实现应用监控，有三种方式：日志、指标、链路跟踪。通过指标的方式的成本是最低的。因为通过日志，需要搭建EFK；通过链路跟踪，需要对应用进行改动。所以，我选择了使用Prometheus监控各个应用。再者，前面我们已经搭建好了Prometheus。\n我们后端基本是Java应用。所以，只需要在Java工程中加入Micrometer的依赖，接着加入配置就可以。不需要对业务代码做任何的修改。\n这个阶段，我们需要实现：\n为所有的应用加入Micrometer监控依赖。 加入告警规则。 6. 实现数据库版本化 实现数据库版化，是一项需要非常谨慎实施的工作。我们使用的是Flyway实现的版本化。注意，网上不少文章写的是将Flyway集成到Java应用中实现的。这种方式不适合工程化。更工程化的方式是，单独创建一个数据库版本化的Git仓库，然后通过执行Flyway的命令行工具进行版本化数据库。\n需要注意的是，生产环境的DB与测试环境的数据量没有可比性。在测试环境能直接运行的SQL，放在生产环境执行可能会发现事故。所以，数据库版本化，我们也只是在测试环境做。\n对于数据库版本化，我们是这样实现的：\n创建一个Git仓库，仓库的目录按照Flyway的目录结构要求创建。 团队成员根据Flyway的命名方式提交自己要执行sql文件。 Git的push触发Jenkins pipeline执行flyway命令，以进行数据库版本化。 生产环境还是由人工执行。 Flyway目录结构：\n├── config │ ├── flyway.conf │ └── flyway.template.conf ├── Jenkinsfile.groovy ├── README.md └── sql └── V1__Create_person_table.sql └── V2__Add_person_column.sql 7. 实现业务监控 软件开发的目的是为了实现业务价值。如何进行业务监控其实应该是每个软件功能开发前就要思考了。可惜，我们做软件行业的人做着做着就忘记了初心，业务监控往往等功能上线后才想到。\n实现业务监控可通过日志数据取得。这个阶段需要实现：\n标准化日志，方便收集日志。 实现日志处理逻辑。 8. 研发数据收集与分析 在我们实现自动化、版本化一切后，收集研发数据就变得可行了。目前行业里还没有好的开源的研发数据模型。我们也还在摸索中。\n迁移到K8s 我们一开始也是传统部署。而传统的部署方式要实现像K8s那样多的功能（比如：自动化扩缩容、滚动更新等），非常的困难。所以，也决定迁移到K8s。\n迁移的思路是先迁移无状态的应用上K8s，然后再逐渐迁移其它应用。而pipeline的具体做法：\n实现新打包方式\n应用Docker化。将Docker镜像放至制品库。 使用Helm3管理应用的k8s yaml部署文件。在业务代码仓库中加入相应的chart包的代码。 所以，一个应用将会有3种制品：Jar，Docker镜像，Chart包。每次部署它们的版本号都应该是确定的，可查的。 实现新的部署方式。优化部署清单，让其既可以支持传统部署，又可以支持K8s部署方式。 因为我们之前实现了配置项是与具体的使用方式无关的，所以，切换到K8s部署时，我们的配置项的管理方式不需要变。这样就节约了很多成本。\n关于度量 “你如果无法度量它，就无法管理它”。——彼得.德鲁克\n我相信一定会有人问，经过这一整套实践，你们团队的软件质量提升了多少？\n因为在本文开发，我们也讲了，希望持续交付的实践能提高我们的软件系统的质量。\n在讨论软件质量之前，我们都应该先讨论软件质量的定义。在整个行业中，笔者认同温伯格在其《质量.软件.管理》的第一卷中，给出的定义：\n质量就是对某个（些）人而言的价值。 需求并非是最终的目的，而只是达到目的的手段——我们的目的在于，要为某个（某些）人提供相应的价值。\n基于此定义。如果我们没有对用户价值的定义，那么也就无法定义软件质量。不同类型的软件，用户价值具体表现不同，我们不在此文展开。\n当实现业务监控，我们才能了解用户价值。因为每一次的交付之后的，监控数据就会告诉我们，这次交付，对于某个（些）人到底有没有价值。基于此，我们谈软件质量才是更合适的。\n回到我们的问题。我们团队的软件质量提升了多少？如果使用政治套话，肯定是提升的，你看，持续交付所有的实践，我们都实践了；如果从实际出发，没有业务监控，我们无法回答软件质量提升了多少。我们依然在路上。\n后记 最后，我对于持续交付的实践，个人总结如下：\n难的不是背诵持续交付的原则和定义，而是能发现团队问题，并根据团队的情况、能力制定持续交付改进计划的能力。所以，以上经验并不适合所有的团队，只是提供一种实践持续交付的一个案例，以供大家参考。\n","permalink":"https://showme.codes/zh-cn/2020-09-09-experience-devops-cd/","summary":"\u003ch3 id=\"分析团队的问题\"\u003e分析团队的问题\u003c/h3\u003e\n\u003cp\u003e我是2020年3月份加入该部门。刚加入时发现问题还挺多。而这些问题在行业里都很典型，比如：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e分支管理不统一：虽说大部分人还是在master上开发，但是还有部分人自己拉feature分支开发。\u003c/li\u003e\n\u003cli\u003e没有统一的制品打包：Docker镜像的打包基本都是在开发人员的电脑上进行的。\u003c/li\u003e\n\u003cli\u003e对制品仓库的push权限没有管理：每个人都有push权限，而且使用的是同一个账号。\u003c/li\u003e\n\u003cli\u003e没有版本管理：在交给测试人员测试（俗称提测）时，开发在本地打包后，push上制品库的包的版本号为\u003ccode\u003euat20200302\u003c/code\u003e（UAT是测试环境的简称）。测试通过后，再使用此版本号，部署到生产环境。结果就是你会看到生产环境运行的包的版本：\u003ccode\u003euat20200302\u003c/code\u003e，是不是很奇怪？\u003c/li\u003e\n\u003cli\u003e没有监控：这也是很多团队的通病了。\u003c/li\u003e\n\u003cli\u003e没有单元测试：开发人员有在main方法写单元测试的，有写出来的测试是无法自动化的。\u003c/li\u003e\n\u003cli\u003e开发团队没有自己的自动化测试。\u003c/li\u003e\n\u003cli\u003e多个应用部署在同一台机器。\u003c/li\u003e\n\u003cli\u003e手工部署：每次部署都是人工登录到服务器执行部署。\u003c/li\u003e\n\u003cli\u003e数据库没有版本化。\u003c/li\u003e\n\u003cli\u003e没有代码审查。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e问题还有很多，就不一一列举。在笔者看来以上问题的最终表现都是软件系统的质量低下。笔者希望通过实践持续交付以提升软件系统的质量。\u003c/p\u003e\n\u003cp\u003e但是问题是该如何实践呢？笔者认为只要掌握了它的基本原则，剩下的就是根据实际情况结合基本原则来解决问题了。\u003c/p\u003e\n\u003ch3 id=\"持续交付的原则\"\u003e持续交付的原则\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e幸福的家庭都是相似的，不幸的家庭各有各的不幸——《安娜卡列尼娜》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e从《持续交付》书中，基本原则有：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e为软件的发布创建一个可重复且可靠的过程\u003c/li\u003e\n\u003cli\u003e将几乎所有的事情自动化\u003c/li\u003e\n\u003cli\u003e把所有的东西都纳入版本控制\u003c/li\u003e\n\u003cli\u003e提前并频繁地做让你感到痛苦的事\u003c/li\u003e\n\u003cli\u003e内建质量\u003c/li\u003e\n\u003cli\u003e“DONE”意味着“已发布”\u003c/li\u003e\n\u003cli\u003e交付过程是每个成员的责任\u003c/li\u003e\n\u003cli\u003e持续改进\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我们可以这么理解这些原则：基于所有东西都要进行版本化的原则，所有的东西都要代码化。\u003c/p\u003e\n\u003cp\u003e因为代码化以后就可以放到类似Git这类版本化工具中。而代码化以后的东西就可以很容易地实现自动化。在实现自动化以后就可以为软件的发布创建一个可重复且可靠的过程。\u003c/p\u003e\n\u003cp\u003e实现自动化的过程是需要每一位成员持续参与的，因为交付过程是每个人责任。\u003c/p\u003e\n\u003cp\u003e“DONE意味着已发布”是团队每个成员都要达成的共识。达成共识后，才能更好的参与持续改进。在持续改进过程中，我们的软件系统就获得了内建质量。\u003c/p\u003e\n\u003cp\u003e笔者认为，在持续交付中，代码化与版本化是基础。\u003c/p\u003e\n\u003ch3 id=\"实践持续交付\"\u003e实践持续交付\u003c/h3\u003e\n\u003cp\u003e在理解原则后，我们就可以开始实践了。可是该如何下手呢？笔者通常遵循以下指导思想：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e先CI，后CD。\u003c/li\u003e\n\u003cli\u003e无监控，无安心觉。\u003c/li\u003e\n\u003cli\u003e先配置项版本化，后标准化，最后才有自动化。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e根据指导思想，再结合团队的实际情况，笔者做出以下计划：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e打包自动化。\u003c/li\u003e\n\u003cli\u003e实现基础监控（机器级别监控、中间件监控）。\u003c/li\u003e\n\u003cli\u003e实现所有的配置版本化。\u003c/li\u003e\n\u003cli\u003e实现自动化部署应用。\u003c/li\u003e\n\u003cli\u003e实现应用监控。\u003c/li\u003e\n\u003cli\u003e实现数据库版本化。\u003c/li\u003e\n\u003cli\u003e实现业务监控。\u003c/li\u003e\n\u003cli\u003e研发数据收集\u003c/li\u003e\n\u003cli\u003e\u0026hellip;.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e由于团队原来已经有日志收集机制，所以，暂且不需要实现。以上步骤只代表一个优先级。如果团队人力充足，可以同时一起做。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e虽说有了计划，但是团队不具备相应的能力，什么计划都白搭。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"1-打包自动化\"\u003e1. 打包自动化\u003c/h3\u003e\n\u003cp\u003e之所以使用“打包”这个听起来不怎么“高端”的词，而不是使用“构建”。是因为“构建”这个词，太容易引起歧义。而且打包这个词很形象，就是把源代码编译后，链接，最后打包成一个可执行包。当然不同的编程语言，打包过程可能不同。\u003c/p\u003e\n\u003cp\u003e因为大多数团队都没有写自动化测试的习惯（我们团队也不例外），让他们写自动化测试，他们只会觉得自己的工作量增加了。所以，我在团队中导入持续交付实践时，一开始就不要求自动化测试。团队意识的转变需要很长的过程。这是使用“打包”的第二个原因：它不包括自动化测试。\u003c/p\u003e\n\u003cp\u003e要实现自动化打包，其实并不难。基本步骤就是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e搭建制品库：Nexus。\u003c/li\u003e\n\u003cli\u003e搭建自动化服务：Jenkins。\u003c/li\u003e\n\u003cli\u003e在Jenkins中创建pipeline任务。\u003c/li\u003e\n\u003cli\u003e在业务代码仓库中加入Jenkinsfile，将打包逻辑写到Jenkinsfile中。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e所谓打包逻辑就是你在本地开发时，利用IDE或命令将源代码编译成可执行文件的过程。打包自动化的过程，就是将你在本地执行的打包过程“搬”到自动化系统上执行，再加上一些优化。\u003c/p\u003e\n\u003cp\u003e在这个阶段中，我们需要实现：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e统一制品库：收回所有人上传制品的账号。只能由Jenkins打包上传。\u003c/li\u003e\n\u003cli\u003e统一版本号：比如使用格式\u003ccode\u003e年-月-日-commitId-构建号\u003c/code\u003e来定义所有的后端应用。注意，不管使用哪种方式，你必须很容易的根据版本号找回相应的源码。\u003c/li\u003e\n\u003cli\u003e统一分支管理：使用主干开发，分支发布的模式。我们根据团队情况有稍微做了一些改变。发布并没有切分支出来，而在发布后发现某版本有Bug，我们就会从该版本的代码切一个分支出来改，打包，部署。最后再将该分支的commit cherry pick回master分支。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"2-实现基础监控\"\u003e2. 实现基础监控\u003c/h3\u003e\n\u003cp\u003e没有监控，在我们这个行业太常见了。所以，在我加入团队后，发现几乎没有任何监控，也就没有什么好惊讶的了。所以，在解决打包问题之后 ，紧接着就是给所有的机器加上监控。至少机器的CPU、硬盘、内存等要监控起来。\u003c/p\u003e\n\u003cp\u003e上一阶段，我们已经把Jenkins搭建起来，所以，Prometheus就开始自动化部署了。\u003c/p\u003e\n\u003cp\u003e使用Prometheus的原因很多，但是关键是它的配置是代码化的，非常容易版本化。持续交付的原则：将几乎所有的事情自动化、把所有的东西都纳入版本控制。像Zabbix，使用需要使用界面进行操作的，就被我排除了。\u003c/p\u003e","title":"一些持续交付的实践经验"},{"content":"摘要 Flyway是一款数据库版本化工具。网上不少文章写的是将Flyway集成到Java应用中实现的。这种方式不适合工程化。本文介绍如何工程化的使用flyway进行数据库版本化。\n如何理解Flyway Flyway进行版本化的逻辑非常简单。\n在目标数据库中创建一个flyway_schema_history的表，用于记录数据库当前的版本。 当执行flyway migrate执行，根据config/flyway.conf配置中的连接信息连接到数据库。 检查sql目录的sql文件。sql文件名遵从flyway的命名约定。如果sql目录的版本比实际数据库中flyway_schema_history表里记录的版本要低，则执行升级版本的sql文件。 如果执行升级sql文件成功，则更新flyway_schema_history表中记录。 以上是个人理解flyway原理后，用大白话阐述出来的。大家可以看下官方介绍：https://flywaydb.org/getstarted/how\nsql文件的命名约定 执行样例 在安装完成flyway命令（下载地址）后，执行命令：\nflyway -configFiles=config/flyway.conf migrate 执行结果：\nFlyway Community Edition 6.5.5 by Redgate Database: jdbc:h2:file:./foobardb (H2 1.4) Successfully validated 0 migrations (execution time 00:00.009s) WARNING: No migrations found. Are your locations set up correctly? Creating Schema History table \u0026#34;PUBLIC\u0026#34;.\u0026#34;flyway_schema_history\u0026#34; ... Current version of schema \u0026#34;PUBLIC\u0026#34;: \u0026lt;\u0026lt; Empty Schema \u0026gt;\u0026gt; Schema \u0026#34;PUBLIC\u0026#34; is up to date. No migration necessary. 与CI/CD集成 使用1个Git仓库对数据库工程进行版化。目录结构如下：\n├── config │ ├── flyway.conf │ └── flyway.template.conf ├── Jenkinsfile.groovy ├── README.md └── sql └── V1__Create_person_table.sql 当Git仓库准备好后，我们就需要和类似Jenkins这样的CI/CD集成了。集成的思路很简单，就是把本地执行的命令照搬到CI/CD平台上就行。思路：\n准备Flyway的执行环境。推荐在Docker容器中运行。 执行flyway命令。 安全问题 flyway.conf文件会有数据库的连接信息，这是敏感信息。我们不应该直接放在Git仓库中。那怎么办？\n笔者的办法是config目录中只放flyway.conf的模板文件，比如config/flyway.template.conf，在CI/CD中执行flyway migrate执行前， 通过比较安全的方式将flyway.template.conf中的占位符换成真正值。\n提醒 需要注意的是，生产环境的DB与测试环境的数据量没有可比性。在测试环境能直接运行的SQL，放在生产环境执行可能会发现事故（这也是我们需要引入code review的原因之一）。所以，数据库版本化，如果基础设施能力或团队能力没跟上，不建议在生产环境上进行。\n附：工程样例链接 https://github.com/cd-in-practice/flywaydb-example\n","permalink":"https://showme.codes/zh-cn/2020-09-06-db-versioning/","summary":"\u003ch3 id=\"摘要\"\u003e摘要\u003c/h3\u003e\n\u003cp\u003eFlyway是一款数据库版本化工具。网上不少文章写的是将Flyway集成到Java应用中实现的。这种方式不适合工程化。本文介绍如何工程化的使用flyway进行数据库版本化。\u003c/p\u003e\n\u003ch3 id=\"如何理解flyway\"\u003e如何理解Flyway\u003c/h3\u003e\n\u003cp\u003eFlyway进行版本化的逻辑非常简单。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在目标数据库中创建一个\u003ccode\u003eflyway_schema_history\u003c/code\u003e的表，用于记录数据库当前的版本。\u003c/li\u003e\n\u003cli\u003e当执行\u003ccode\u003eflyway migrate\u003c/code\u003e执行，根据config/flyway.conf配置中的连接信息连接到数据库。\u003c/li\u003e\n\u003cli\u003e检查\u003ccode\u003esql\u003c/code\u003e目录的sql文件。sql文件名遵从flyway的命名约定。如果\u003ccode\u003esql\u003c/code\u003e目录的版本比实际数据库中\u003ccode\u003eflyway_schema_history\u003c/code\u003e表里记录的版本要低，则执行升级版本的sql文件。\u003c/li\u003e\n\u003cli\u003e如果执行升级sql文件成功，则更新\u003ccode\u003eflyway_schema_history\u003c/code\u003e表中记录。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e以上是个人理解flyway原理后，用大白话阐述出来的。大家可以看下官方介绍：https://flywaydb.org/getstarted/how\u003c/p\u003e\n\u003ch3 id=\"sql文件的命名约定\"\u003esql文件的命名约定\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/flywaydb.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"执行样例\"\u003e执行样例\u003c/h3\u003e\n\u003cp\u003e在安装完成flyway命令（\u003ca href=\"https://flywaydb.org/documentation/commandline/\"\u003e下载地址\u003c/a\u003e）后，执行命令：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eflyway -configFiles\u003cspan class=\"o\"\u003e=\u003c/span\u003econfig/flyway.conf migrate\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e执行结果：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-build\" data-lang=\"build\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eFlyway Community Edition 6.5.5 by Redgate\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eDatabase: jdbc:h2:file:./foobardb (H2 1.4)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eSuccessfully validated 0 migrations (execution time 00:00.009s)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eWARNING: No migrations found. Are your locations set up correctly?\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eCreating Schema History table \u0026#34;PUBLIC\u0026#34;.\u0026#34;flyway_schema_history\u0026#34; ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eCurrent version of schema \u0026#34;PUBLIC\u0026#34;: \u0026lt;\u0026lt; Empty Schema \u0026gt;\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eSchema \u0026#34;PUBLIC\u0026#34; is up to date. No migration necessary.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"与cicd集成\"\u003e与CI/CD集成\u003c/h3\u003e\n\u003cp\u003e使用1个Git仓库对数据库工程进行版化。目录结构如下：\u003c/p\u003e","title":"工程化实践：使用flyway进行数据库版本化"},{"content":"\n三年多前，一位国外的老哥在 stackexchange.com（国外的技术问答社区）上发表了一个问题。\n问题大概内容就是远程工作的他是一名程序员，每个月他只需要花大约10分钟就完成整个月的工作了。这样的状态，他维持了6个月。公司也从来没有表示过对我的表现不满意，事实上，公司聘用他，并从他那得到了想要的。他的疑问是这样继续下去道德吗。\n本文不想谈道德，而是从公司经营和团队管理角度开始谈。\n公司经营角度 如果放他在公司里坐班，他能不能用10分钟完成一个月的工作呢？我们不知道。但是，如果他能做到，他会告诉上级领导吗？\n我们也不知道。这里，我想问问坐班的读者，你会告诉你的上级吗？\n站在公司经营的角度，公司当然期望10分钟做完以前1个月的工作，节约下来的人力成本可以做其他的事情。\n这时，如果你是公司的经营者，你如何达到自己的期望呢？\n国内某些公司的做法，似乎能避免国外老哥这种情况的发生。做法简单粗暴：设立KPI等级，同时每半年淘汰KPI倒数10%的人。\n国外老哥想得到高的KPI等级，会主动告诉领导他的功劳。但是，现实会是这样吗？\n这位老哥，并不一定要一次把1个月缩短到10分钟。他可以在评KPI前的一个星期，主动告诉领导他缩短几天的工作量就可以了。\n职场里的老油条，应该能懂我说的话。在评KPI前告诉领导是为了让领导在评KPI时，能快速想起你的成绩（KPI真的很主观）。本来可以缩短1个月的，而你故意只缩短几天，是为了给自己下次评KPI留有余地。\n现实可能更复杂。如果你是公司经营者，你会如何做呢？\n团队管理角度 如果你是这位老哥的直接领导，你觉得是什么原因，一项工作本来只需要10分钟完成，你的团队却需要1个月？\n你可能会怪这位老哥不“老实”。可是老哥一辈子不告诉你事实，你连“怪”的机会都没有。\n你可能觉得这不是问题。因为手上人越多，你在公司里的份量就越大。\n你可能根本就不知道自己的团队效率能提高那么多。\n还有很多可能性。留给读者朋友自己体会。\n问题到底是什么 说了这么多，关于这位老哥的案例，问题到底是什么？\n不知道各位读者心里有没有问：他的工作内容是什么，为什么他的工作需要每个月重复。笔者认为这才是本案例的关键问题。\n如果你深入问了，团队效率提升是自然而然发生的事情。让程序员每天做重复的事情，TA会很难受。否则你招的可能是一个假的程序员。\n笔者认为：作为团队管理，通过发现“重复工作”来提高效率是一种常识。所以，如果有了这种常识，根本就不会发生本案例了。\n如何让每位团队成员拥有这种常识，这是另一个议题。而如何让整家企业的人都有这样的常识，这又是另一个议题。\n后记 过去到现在，笔者经常听到的一句话：“过程我不管，我只看结果”（也被人称为：结果导向）。这句话本身是正确的，但是，我们如果把团队管理者看作篮球队的教练，比赛中，你和你的队员一直盯着计分牌（结果），对于比赛最后得分是没有任何益处的。\n是时候重新审视“以结果导向”在企业中带来的负作用了。\n","permalink":"https://showme.codes/zh-cn/2020-08-02-about-result-orientation2/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-dd447744b3fa2ed2.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e三年多前，一位国外的老哥在 stackexchange.com（国外的技术问答社区）上发表了一个问题。\u003c/p\u003e\n\u003cp\u003e问题大概内容就是远程工作的他是一名程序员，每个月他只需要花大约10分钟就完成整个月的工作了。这样的状态，他维持了6个月。公司也从来没有表示过对我的表现不满意，事实上，公司聘用他，并从他那得到了想要的。他的疑问是这样继续下去道德吗。\u003c/p\u003e\n\u003cp\u003e本文不想谈道德，而是从公司经营和团队管理角度开始谈。\u003c/p\u003e\n\u003ch3 id=\"公司经营角度\"\u003e公司经营角度\u003c/h3\u003e\n\u003cp\u003e如果放他在公司里坐班，他能不能用10分钟完成一个月的工作呢？我们不知道。但是，如果他能做到，他会告诉上级领导吗？\u003c/p\u003e\n\u003cp\u003e我们也不知道。这里，我想问问坐班的读者，你会告诉你的上级吗？\u003c/p\u003e\n\u003cp\u003e站在公司经营的角度，公司当然期望10分钟做完以前1个月的工作，节约下来的人力成本可以做其他的事情。\u003c/p\u003e\n\u003cp\u003e这时，如果你是公司的经营者，你如何达到自己的期望呢？\u003c/p\u003e\n\u003cp\u003e国内某些公司的做法，似乎能避免国外老哥这种情况的发生。做法简单粗暴：设立KPI等级，同时每半年淘汰KPI倒数10%的人。\u003c/p\u003e\n\u003cp\u003e国外老哥想得到高的KPI等级，会主动告诉领导他的功劳。但是，现实会是这样吗？\u003c/p\u003e\n\u003cp\u003e这位老哥，并不一定要一次把1个月缩短到10分钟。他可以在评KPI前的一个星期，主动告诉领导他缩短几天的工作量就可以了。\u003c/p\u003e\n\u003cp\u003e职场里的老油条，应该能懂我说的话。在评KPI前告诉领导是为了让领导在评KPI时，能快速想起你的成绩（KPI真的很主观）。本来可以缩短1个月的，而你故意只缩短几天，是为了给自己下次评KPI留有余地。\u003c/p\u003e\n\u003cp\u003e现实可能更复杂。如果你是公司经营者，你会如何做呢？\u003c/p\u003e\n\u003ch3 id=\"团队管理角度\"\u003e团队管理角度\u003c/h3\u003e\n\u003cp\u003e如果你是这位老哥的直接领导，你觉得是什么原因，一项工作本来只需要10分钟完成，你的团队却需要1个月？\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e你可能会怪这位老哥不“老实”。可是老哥一辈子不告诉你事实，你连“怪”的机会都没有。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e你可能觉得这不是问题。因为手上人越多，你在公司里的份量就越大。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e你可能根本就不知道自己的团队效率能提高那么多。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e还有很多可能性。留给读者朋友自己体会。\u003c/p\u003e\n\u003ch3 id=\"问题到底是什么\"\u003e问题到底是什么\u003c/h3\u003e\n\u003cp\u003e说了这么多，关于这位老哥的案例，问题到底是什么？\u003c/p\u003e\n\u003cp\u003e不知道各位读者心里有没有问：他的工作内容是什么，为什么他的工作需要每个月重复。笔者认为这才是本案例的关键问题。\u003c/p\u003e\n\u003cp\u003e如果你深入问了，团队效率提升是自然而然发生的事情。让程序员每天做重复的事情，TA会很难受。否则你招的可能是一个假的程序员。\u003c/p\u003e\n\u003cp\u003e笔者认为：作为团队管理，通过发现“重复工作”来提高效率是一种常识。所以，如果有了这种常识，根本就不会发生本案例了。\u003c/p\u003e\n\u003cp\u003e如何让每位团队成员拥有这种常识，这是另一个议题。而如何让整家企业的人都有这样的常识，这又是另一个议题。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e过去到现在，笔者经常听到的一句话：“过程我不管，我只看结果”（也被人称为：结果导向）。这句话本身是正确的，但是，我们如果把团队管理者看作篮球队的教练，比赛中，你和你的队员一直盯着计分牌（结果），对于比赛最后得分是没有任何益处的。\u003c/p\u003e\n\u003cp\u003e是时候重新审视“以结果导向”在企业中带来的负作用了。\u003c/p\u003e","title":"远程办公十分钟，干一个月的活，剩下的时间……"},{"content":"昨天各种朋友、群，广泛传播以下信息：\n重磅消息!!!Terraform、Consul、Vagrant等禁止中国使用！\n我不清楚上面“Terms of Evaluation for HashiCorp Software”这个页面截图是什么时候的。HashiCorp旗下这么多软件，如上图。为什么他只圈Terraform、Consul、Vagrant？其它几款软件怎么不提？难道当时“Terms of Evaluation for HashiCorp Software”页面下文只提了Terraform、Consul、Vagrant？\n以下是我的最新截图（2020-5-30 06:34 中国时间）： 使用机器翻译如下： 请注意，“Terms of Evaluation for HashiCorp Software”最新版说的是Vault企业版\n整件事情，我们其实更应该问HashiCorp的人，他们为什么做这样的决定。 以下是2020-5-3 6:43 北京时间截图： 原文链接：https://news.ycombinator.com/item?id=23349635\n笔者使用机器翻译如下：\n您好，我是HashiCorp的创始人，我想解释一下。 首先，本文档仅适用于企业评估软件。这不适用于我们的OSS软件，除非在注册企业评估的上下文中，否则不应将其链接到我们的OSS附近。 最重要的是：这为什么在这里？这不是政治声明。这是法律要求。我们在保险柜中使用的加密受中国出口管制法律的约束，并且（根据中国法律）我们在中国销售是非法的。 为了能够在中国销售保险柜，我们必须将可以在保险柜中使用的加密限制为政府可接受的版本。 我们不这样做，因此在中国销售是非法的。我们必须在企业术语中包括这一行。 编辑：我们的法律团队已更详尽地更新了我们的条款。您可以在此处的第二段中阅读更新的副本：https://www.hashicorp.com/terms-of-evaluation\n最后结论，Terraform、Consul、Vagrant等可以继续在中国使用！\n本文不是为了给HashiCorp洗白，其实别人也没故意黑。\n最后，如果您觉得此文章说的是事实，请转发给更多的朋友，让他们看到事实。\n","permalink":"https://showme.codes/zh-cn/2020-5-30-hashicorp/","summary":"\u003cp\u003e昨天各种朋友、群，广泛传播以下信息：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e重磅消息!!!Terraform、Consul、Vagrant等禁止中国使用！\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg alt=\"WechatIMG169.jpeg\" loading=\"lazy\" src=\"/assets/images/292372-d392a13ead288926.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e我不清楚上面“Terms of Evaluation for HashiCorp Software”这个页面截图是什么时候的。HashiCorp旗下这么多软件，如上图。为什么他只圈Terraform、Consul、Vagrant？其它几款软件怎么不提？难道当时“Terms of Evaluation for HashiCorp Software”页面下文只提了Terraform、Consul、Vagrant？\u003c/p\u003e\n\u003cp\u003e以下是我的最新截图（2020-5-30 06:34 中国时间）：\n\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-80beb80388997476.png\"\u003e\u003c/p\u003e\n\u003cp\u003e使用机器翻译如下：\n\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-0def52638593cbce.png\"\u003e\u003c/p\u003e\n\u003cp\u003e请注意，“Terms of Evaluation for HashiCorp Software”最新版说的是\u003cstrong\u003eVault企业版\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e整件事情，我们其实更应该问HashiCorp的人，他们为什么做这样的决定。\n以下是2020-5-3 6:43 北京时间截图：\n\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-fe46f33a640ffd8f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e原文链接：\u003ca href=\"https://news.ycombinator.com/item?id=23349635\"\u003ehttps://news.ycombinator.com/item?id=23349635\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e笔者使用机器翻译如下：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e您好，我是HashiCorp的创始人，我想解释一下。\n首先，本文档仅适用于企业评估软件。这不适用于我们的OSS软件，除非在注册企业评估的上下文中，否则不应将其链接到我们的OSS附近。\n最重要的是：这为什么在这里？这不是政治声明。这是法律要求。我们在保险柜中使用的加密受中国出口管制法律的约束，并且（根据中国法律）我们在中国销售是非法的。\n为了能够在中国销售保险柜，我们必须将可以在保险柜中使用的加密限制为政府可接受的版本。\n我们不这样做，因此在中国销售是非法的。我们必须在企业术语中包括这一行。\n编辑：我们的法律团队已更详尽地更新了我们的条款。您可以在此处的第二段中阅读更新的副本：https://www.hashicorp.com/terms-of-evaluation\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e最后结论，Terraform、Consul、Vagrant等可以继续在中国使用！\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e本文不是为了给HashiCorp洗白，其实别人也没故意黑。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e最后，如果您觉得此文章说的是事实，请转发给更多的朋友，让他们看到事实。\u003c/strong\u003e\u003c/p\u003e","title":"突发！！！Terraform、Consul、Vagrant等可以继续在中国使用！"},{"content":"\n如何使用 使用Kubernetes插件时，我们需要做三件事情：\n根据官方文档，在Jenkins上加入kubernetes配置。 在Jenkinsfile中加入kubernetes agent的申明。 指定容器执行你的业务脚本。 关于第2点，kubernetes agent的申明又有两种方式。一种是脚本式的，代码样例如下：\npodTemplate(containers: […]) { node(POD_LABEL) { stage(\u0026#39;Run shell\u0026#39;) { container(\u0026#39;mycontainer\u0026#39;) { sh \u0026#39;echo hello world\u0026#39; }}}} 一种是申明式，代码样例如下：\npipeline { stages { stage(\u0026#39;Run maven\u0026#39;) { agent { kubernetes { yaml \u0026#34;\u0026#34;\u0026#34; apiVersion: v1 kind: Pod metadata: labels: app: jenkins-agent spec: containers: - name: maven image: maven:alpine command: - cat tty: true - name: busybox image: busybox command: - cat tty: true \u0026#34;\u0026#34;\u0026#34; }} steps { container(\u0026#39;maven\u0026#39;) { sh \u0026#39;mvn -version\u0026#39; }}}}} 笔者推荐使用申明式。yaml配置部分看起来并不优雅，这是另一个话题。咱们今后再讲。\n原理 我们都知道Jenkins是master/agent的架构。而master与agent之间通信方法有两种：\n通过JNLP协议：需要启动JNLP客户端主动连接master。这是Kubernetes插件使用的方式。 通过SSH协议：master使用SSH主动连接agent机器。 Kubernetes插件的具体的做法就是连接到Kubernetes集群，然后启动一个Pod。Pod中包含一个JNLP客户端，容器名约定为：jnlp。jnlp 会主动连接Jenkins master。\n所以，当你发现Jenkins任务的日志中，一直在等待jnlp连接时，我们可以这样查问题：\n查看相应的Pod是否存活。 jnlp 容器连接不上master：大概率是配置不对。 可是，我们看到上面的示例代码中，都没有叫jnlp的容器呢。这是因为Jenkins kubernates插件在真正创建pod前，为我们混入了默认的jnlp的容器定义。也就是，最终执行的yaml其实是：\napiVersion: v1 kind: Pod metadata: labels: some-label: some-label-value spec: containers: - name: jnlp image: jenkins/jnlp-slave:alpine args: [\u0026#39;\\$(JENKINS_SECRET)\u0026#39;, \u0026#39;\\$(JENKINS_NAME)\u0026#39;] - name: maven image: maven:alpine command: - cat tty: true ...省略其它 最后，pod启动后，pod中的jnlp容器会连上Jenkins master。当pipeline运行到以下代码：\ncontainer(\u0026#39;maven\u0026#39;) { sh \u0026#39;mvn -version\u0026#39; } kubernates插件会找到名为maven的容器，然后将闭包内的代码发给它执行。\n以上基本就是kubernates插件全部。\n更换jnlp实现 当我们知道它的原理后，我们也就可以更换jnlp的实现镜像了。比如有些同学是在arm架构的机器上执行Kubernetes的，那么，他可以创建一个基于arm架构的jnlp镜像，然后，加入到yaml中。比如：\ncontainers: - name: jnlp image: supercom.com/jnlp-arm-agent:1.0 args: [\u0026#39;\\$(JENKINS_SECRET)\u0026#39;, \u0026#39;\\$(JENKINS_NAME)\u0026#39;] 小结 总的来说，它的原理无非就是创建pod，pod中的jnlp容器连接到Jenkins master，然后Jenkins master根据需要，将需要执行的命令发送给相应的容器执行。\n附录 Jenkins kubernetes源码：https://github.com/jenkinsci/kubernetes-plugin 混入jnlp容器的代码位置：org.csanchez.jenkins.plugins.kubernetes.PodTemplateBuilder#build() 创建pod的代码位置：org.csanchez.jenkins.plugins.kubernetes.KubernetesLauncher#launch ","permalink":"https://showme.codes/zh-cn/2020-5-4-jenkins-kubernates-plugin/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-8163b7fa93e5d1fb.jpg\"\u003e\u003c/p\u003e\n\u003ch2 id=\"如何使用\"\u003e如何使用\u003c/h2\u003e\n\u003cp\u003e使用Kubernetes插件时，我们需要做三件事情：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e根据官方文档，在Jenkins上加入kubernetes配置。\u003c/li\u003e\n\u003cli\u003e在Jenkinsfile中加入kubernetes agent的申明。\u003c/li\u003e\n\u003cli\u003e指定容器执行你的业务脚本。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e关于第2点，kubernetes agent的申明又有两种方式。一种是脚本式的，代码样例如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-groovy\" data-lang=\"groovy\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epodTemplate\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003econtainers:\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"err\"\u003e…\u003c/span\u003e\u003cspan class=\"o\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ePOD_LABEL\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Run shell\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003econtainer\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;mycontainer\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;echo hello world\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}}}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e一种是申明式，代码样例如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-groovy\" data-lang=\"groovy\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epipeline\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003estages\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Run maven\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003eagent\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ekubernetes\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"n\"\u003eyaml\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                apiVersion: v1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                kind: Pod\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                metadata:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                  labels:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    app: jenkins-agent\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                spec:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                  containers:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                  - name: maven\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    image: maven:alpine\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    command:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    - cat\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    tty: true\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                  - name: busybox\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    image: busybox\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    command:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    - cat\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                    tty: true\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e                \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003esteps\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtainer\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;maven\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;mvn -version\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}}}}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e笔者推荐使用申明式。yaml配置部分看起来并不优雅，这是另一个话题。咱们今后再讲。\u003c/p\u003e","title":"Jenkins kubernates原理"},{"content":"\n本文继续前两篇 Jenkins + Ansible 的文章（见附录）的例子。代码仓库结构与 《使用 Jenkins + Ansible 实现 Spring Boot 自动化部署101》 介绍的相似。\n但是以下改进：\n增加了展示跨应用配置管理的样例（本文重点） 实现了二进制包与配置分离 跨应用配置是什么 《持续交付》的2.4.4节介绍了“跨应用配置管理”。但是书中没有明确给出它的定义。以下是笔者所理解的“跨应用配置”：\n所谓跨应用配置指的是在同一个配置项同时被多个应用引用。\n比如现实中同一个 Redis 的配置项（如地址、端口）就可能同时被多个业务系统引用。如下图所示。 为什么要进行跨应用的配置管理 如果没有跨应用配置的管理，我们就必须在应用1和应用2的配置文件中写死 redis 的配置项（在没有配置中心的情况下）。这样一看是没有问题的。但是笔者认为应用在到达10个以上的时候会（经常）遇到以下问题：\n无法实现快速重建一整套新的环境。新的环境意味着新的 redis 地址。也意味着所有引用了 redis 地址的应用的配置都要改。手工修改很容易出错。 当你希望对现有的 redis 进行调整时，你无法评估影响面，因为你不知道哪些应用使用了这个 redis。进而，导致团队对架构优化的信心不足。 这两个问题会随着系统数量增加而加重。\n那么如何实现跨应用的配置管理以解决上述问题呢？\n如何实现跨应用的配置管理 如果使用如 Ansible、Puppet、Chef 这类自动化工具，跨应用的配置管理就很容易实现。因为它们的变量系统，天生就支持一处定义配置项，其它地方到处引用。对 Ansible 变量不熟悉的同学可以在文末找到学习链接。\n在我们的 Nginx + Spring Boot 的例子中，对配置代码仓库（2-env-conf）进行了调整，结构如下：\n├── Jenkinsfile ├── README.MD └── dev ├── group_vars │ ├── all # Ansible 默认的 all 组变量目录 │ │ └── global.yaml │ └── nginx.yaml # nginx 组变量 ├── host_vars │ ├── 192.168.52.10 │ └── 192.168.52.11 └── hosts 因为 Spring Boot 应用的端口会被 Nginx 的配置引用，所以，我们将端口的配置项放到 global.yaml 中，代码如下所示。\napp_springboot_config: port: 7896 nginx.yaml 文件在 group_vars 目录中代表它是 nginx 这个组的变量文件。以下是它的部分配置项。\nservers: backend_server_1: address: \u0026#34;{{groups[\u0026#39;springboot\u0026#39;][0]}}\u0026#34; port: \u0026#34;{{ app_springboot_config.port }}\u0026#34; weight: 1 health_check: max_fails=3 fail_timeout=5s 这里需要简单介绍一下。Ansible 使用了 Jinja2 模板系统。{{ }} 是它的占位符。占位符中，可以使用 . 号访问该配置的属性。port: \u0026quot;{{ app_springboot_config.port }}\u0026quot; 最终会变成：port: \u0026quot;7896\u0026quot;。\nAnsible 在执行过程，默认会提供一些默认变量，比如 groups。\ngroups 是 inventory（就是那个 hosts 文件）中所有群组（主机）的列表。可用于枚举群组中的所有主机。除了使用 . 号访问配置的属性，还可以使用 配置['属性名'] 的方式。而 [0] 代表取数组中的第0个。\n因为目前后端只有一个 Spring Boot 应用。所以，取第0个配置到 Nginx 中就可以。\n同时，Spring Boot 应用本身的 application.yml 配置文件也使用了 app_springboot_config 配置项。这样就实现真正的一处定义，多处引用了。\n最后，本文代码样例都放在 https://github.com/cd-in-practice下 2- 开头的工程：\ntree -L 1 ├── 2-cd-platform ├── 2-env-conf ├── 2-nginx-deploy └── 2-springboot 附： Jinja2 模板系统：http://jinja.pocoo.org/docs/2.10/ 使用 Jenkins + Ansible 实现 Spring Boot 自动化部署101：https://jenkins-zh.cn/wechat/articles/2019/05/2019-05-20-jenkins-ansible-springboot/ 使用 Jenkins + Ansible 实现自动化部署 Nginx ：https://jenkins-zh.cn/wechat/articles/2019/04/2019-04-25-jenkins-ansible-nginx/ Ansible 变量：https://ansible-tran.readthedocs.io/en/latest/docs/playbooks_variables.html ","permalink":"https://showme.codes/zh-cn/2020-3-12-jenkins-ansible-cross-conf/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-29bb768786bc0467.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文继续前两篇 Jenkins + Ansible 的文章（见附录）的例子。代码仓库结构与 《使用 Jenkins + Ansible 实现 Spring Boot 自动化部署101》 介绍的相似。\u003c/p\u003e\n\u003cp\u003e但是以下改进：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e增加了展示跨应用配置管理的样例（本文重点）\u003c/li\u003e\n\u003cli\u003e实现了二进制包与配置分离\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"跨应用配置是什么\"\u003e跨应用配置是什么\u003c/h3\u003e\n\u003cp\u003e《持续交付》的2.4.4节介绍了“跨应用配置管理”。但是书中没有明确给出它的定义。以下是笔者所理解的“跨应用配置”：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e所谓跨应用配置指的是在同一个配置项同时被多个应用引用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e比如现实中同一个 Redis 的配置项（如地址、端口）就可能同时被多个业务系统引用。如下图所示。\n\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-fc2abb960d0575b5.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"为什么要进行跨应用的配置管理\"\u003e为什么要进行跨应用的配置管理\u003c/h3\u003e\n\u003cp\u003e如果没有跨应用配置的管理，我们就必须在应用1和应用2的配置文件中写死 redis 的配置项（在没有配置中心的情况下）。这样一看是没有问题的。但是笔者认为应用在到达10个以上的时候会（经常）遇到以下问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e无法实现快速重建一整套新的环境。新的环境意味着新的 redis 地址。也意味着所有引用了 redis 地址的应用的配置都要改。手工修改很容易出错。\u003c/li\u003e\n\u003cli\u003e当你希望对现有的 redis 进行调整时，你无法评估影响面，因为你不知道哪些应用使用了这个 redis。进而，导致团队对架构优化的信心不足。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这两个问题会随着系统数量增加而加重。\u003c/p\u003e\n\u003cp\u003e那么如何实现跨应用的配置管理以解决上述问题呢？\u003c/p\u003e\n\u003ch3 id=\"如何实现跨应用的配置管理\"\u003e如何实现跨应用的配置管理\u003c/h3\u003e\n\u003cp\u003e如果使用如 Ansible、Puppet、Chef 这类自动化工具，跨应用的配置管理就很容易实现。因为它们的变量系统，天生就支持一处定义配置项，其它地方到处引用。对 Ansible 变量不熟悉的同学可以在文末找到学习链接。\u003c/p\u003e\n\u003cp\u003e在我们的 Nginx + Spring Boot 的例子中，对配置代码仓库（2-env-conf）进行了调整，结构如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003e├── Jenkinsfile\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003e├── README.MD\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003e└── dev\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003e├── group_vars\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003e│   ├── all \u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c\"\u003e# Ansible 默认的 all 组变量目录\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003e│   │   └── global.yaml  \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003e│   └── nginx.yaml\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c\"\u003e# nginx 组变量\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003e├── host_vars\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003e│   ├── 192.168.52.10\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003e│   └── 192.168.52.11\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003e└── hosts\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e因为 Spring Boot 应用的端口会被 Nginx 的配置引用，所以，我们将端口的配置项放到 global.yaml 中，代码如下所示。\u003c/p\u003e","title":"使用 Jenkins + Ansible 实现跨应用配置管理"},{"content":"\n设计原则 张小龙谈微信：\n我们没办法让10亿人来投票决定什么是好的，也投不出来。那怎么才能通过改变寻求设计的优化，让它变得更好呢？这个决策必须遵循好的设计原则。\n张小龙谈 DevOps 平台：\n我们没办法让所有研发团队来投票决定什么样的 DevOps 平台是好的，也投不出来。那怎么才能通过改变寻求设计的优化，让它变得更好呢？这个决策必须遵循好的设计原则。 我把这几个原则念给大家听下，大家可以对照 DevOps 平台来思考一下，会很有意思。\n为软件的发布创建一个可重复且可靠的过程 将几乎所有的事情自动化 把所有的东西都纳入版本控制 提前并频繁地做让你感到痛苦的事 内建质量 \u0026ldquo;DONE\u0026rdquo; 意味着“已发布” 交付过程是每个成员的责任 持续改进 老翟插话：以上设计原则，是《持续交付》中1.6章节中写的。\n做最好的工具与 996 张小龙谈微信：\n一个用户每天的时间是有限的，这是次要的。最主要的是，技术的使命应该是帮助人类提高效率。\n张小龙谈 DevOps 平台：\n一个程序员每天的时间是有限的，这是次要的。最主要的是 DevOps 平台的使命应该是帮助研发团队提高软件发布的效率。\n老翟插话：我的真实经历，当我问一个 DevOps 平台的设计人员为什么要把部署阶段设计得这么难用（效率低下）。得到的答案是怕用户部署错。这是使用老的思路来设计 DevOps 平台：如果一件事情容易出错，那我们就尽量少做。而让用户难用，就可以自然实现目的。\n关于社交，关于 DevOps 本源 张小龙谈微信：\n其实我们人的社交是没有发生改变的，或者说社交的需求并没有发生改变。我们在线上的社交只是线下的社交的一个映射而已。\n张小谈 DevOps 平台：\n其实我们研发团队的软件发布是没有发生改变的，或者说软件发布的需求并没有发生改变。我们在 DevOps 平台上的软件发布只是线下的软件发布的一个映射而已。\n老翟插话：为什么开源类的 CI/CD 平台，Jenkins 占有率那么高？很大一部分原因是人们从原来的手工发布迁移到 Jenkins 上，非常的平滑，自然。纵观现在很多 DevOps 平台，把基本的构建编译的命令都隐藏起来，不允许用户轻松地看到或者修改。这是那些 DevOps 平台“难用”的原因之一。\n什么是好的 DevOps 平台 张小龙谈微信：\n我觉得一个好的产品不需要费口舌解释，我解释了这么多，说明我们做得不够好。\n张小龙谈 DevOps 平台：\n我觉得一个好的 DevOps 平台不需要费口舌解释，我解释了这么多，说明我们做得不够好。\n关于阅读，关于研发流程 张小龙谈微信：\n我们希望卷入几亿用户，通过社交推荐这种模式，将阅读变成一个日常的事情。\n张小龙谈 DevOps 平台：\n我们希望卷入研发流程过所有的角色，通过 DevOps 平台，将持续交付变成一个日常的事情。\n老翟插话：从演讲原文看“关于阅读”这部分，张小龙一系列的思考，包括丰富大家眼界、打破信息茧房等，以达到“将阅读变成一个日常的事情”。而且这个过程要做到润物细无声。我们的 DevOps平台是不是也能润物细无声地帮助团队达到“将持续交付变成一个日常的事情”的目的？\n关于 AI 编程，关于 AIOps，关于AI…… 张小龙谈微信：\n好的技术是为产品服务的，AI应该默默躲在后面帮助用户来做一些事情，就像语音识别一样。\n张小龙谈 DevOps 平台：\n好的 DevOps 产品是为研发团队服务的，AI 应该默默躲在后面帮助用户来做一些事情，就比如它会自动发现依赖包有漏洞，还能自动提出合理的解决方案，甚至能自动提 PR 或 MR。\n后记 最近几天又重听张小龙在2019年的演讲，发现学习到了不少东西。关键是学习他的思维方式。我写这篇文章是为了验证我学习到的，能否用在 DevOps 平台的设计上。有没有真的学到，是另一回事。\n看看查理·芒格说过的话，你就明白我的意思了：\n长久以来，我坚信存在某个系统——几乎所有聪明人都能掌握的系统，它比绝大多数人用的系统管用。你需要做的是在你的头脑里形成一种思维模型的复式框架。有了那个系统之后，你就能逐渐提高对事物的认识。\n现实中，至于 DevOps 平台要怎么设计，留给读者朋友思考了。\n附录 2019微信公开课PRO版张小龙演讲全文（官方完整版）： https://zhuanlan.zhihu.com/p/54490834\n查理·芒格：让我受用一生的思维方式：https://36kr.com/p/5081596\n","permalink":"https://showme.codes/zh-cn/2020-1-3-devops-zhangxiaolong-wechat/","summary":"\u003cp\u003e\u003cimg alt=\"zhangxiaolong.jpg\" loading=\"lazy\" src=\"/assets/images/292372-2967a495fcbf69d0.jpg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"设计原则\"\u003e设计原则\u003c/h3\u003e\n\u003cp\u003e张小龙谈微信：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我们没办法让10亿人来投票决定什么是好的，也投不出来。那怎么才能通过改变寻求设计的优化，让它变得更好呢？这个决策必须遵循好的设计原则。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e张小龙谈 DevOps 平台：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我们没办法让所有研发团队来投票决定什么样的 DevOps 平台是好的，也投不出来。那怎么才能通过改变寻求设计的优化，让它变得更好呢？这个决策必须遵循好的设计原则。\n我把这几个原则念给大家听下，大家可以对照 DevOps 平台来思考一下，会很有意思。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e为软件的发布创建一个可重复且可靠的过程\u003c/li\u003e\n\u003cli\u003e将几乎所有的事情自动化\u003c/li\u003e\n\u003cli\u003e把所有的东西都纳入版本控制\u003c/li\u003e\n\u003cli\u003e提前并频繁地做让你感到痛苦的事\u003c/li\u003e\n\u003cli\u003e内建质量\u003c/li\u003e\n\u003cli\u003e\u0026ldquo;DONE\u0026rdquo; 意味着“已发布”\u003c/li\u003e\n\u003cli\u003e交付过程是每个成员的责任\u003c/li\u003e\n\u003cli\u003e持续改进\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e老翟插话：以上设计原则，是《持续交付》中1.6章节中写的。\u003c/p\u003e\n\u003ch3 id=\"做最好的工具与-996\"\u003e做最好的工具与 996\u003c/h3\u003e\n\u003cp\u003e张小龙谈微信：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e一个用户每天的时间是有限的，这是次要的。最主要的是，技术的使命应该是帮助人类提高效率。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e张小龙谈 DevOps 平台：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e一个程序员每天的时间是有限的，这是次要的。最主要的是 DevOps 平台的使命应该是帮助研发团队提高软件发布的效率。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e老翟插话：我的真实经历，当我问一个 DevOps 平台的设计人员为什么要把部署阶段设计得这么难用（效率低下）。得到的答案是怕用户部署错。这是使用老的思路来设计 DevOps 平台：如果一件事情容易出错，那我们就尽量少做。而让用户难用，就可以自然实现目的。\u003c/p\u003e\n\u003ch3 id=\"关于社交关于-devops-本源\"\u003e关于社交，关于 DevOps 本源\u003c/h3\u003e\n\u003cp\u003e张小龙谈微信：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e其实我们人的社交是没有发生改变的，或者说社交的需求并没有发生改变。我们在线上的社交只是线下的社交的一个映射而已。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e张小谈 DevOps 平台：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e其实我们研发团队的软件发布是没有发生改变的，或者说软件发布的需求并没有发生改变。我们在 DevOps 平台上的软件发布只是线下的软件发布的一个映射而已。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e老翟插话：为什么开源类的 CI/CD 平台，Jenkins 占有率那么高？很大一部分原因是人们从原来的手工发布迁移到 Jenkins 上，非常的平滑，自然。纵观现在很多 DevOps 平台，把基本的构建编译的命令都隐藏起来，不允许用户轻松地看到或者修改。这是那些 DevOps 平台“难用”的原因之一。\u003c/p\u003e\n\u003ch3 id=\"什么是好的-devops-平台\"\u003e什么是好的 DevOps 平台\u003c/h3\u003e\n\u003cp\u003e张小龙谈微信：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我觉得一个好的产品不需要费口舌解释，我解释了这么多，说明我们做得不够好。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e张小龙谈 DevOps 平台：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我觉得一个好的 DevOps 平台不需要费口舌解释，我解释了这么多，说明我们做得不够好。\u003c/p\u003e","title":"如果张小龙谈 DevOps 平台"},{"content":"\n要的是确切结果，不要忽悠我 前段时间，乔帮主（乔梁，《持续交付2.0》的作者）在持续交付2.0的群里发出这一句话：\n业务老大问：“原来100工程师做的一个产品，用半年时间做 devops 改进。半年之后会得到什么确切的结果？”\n以上是原话。\n乔帮主在群发这话的意思是：如果你的业务老大问这样的问题，你该如何回答？\n请注意，业务老大要的是确切结果，不要拿“虚”的东西来忽悠人。\n请读者朋友思考一会。 。\n。\n。 客官不要急，请再思考一会鸭。 。\n。\n面对老大这样的提问，技术人可能觉得好笑，接着可能装作一本正经回答老大：\n如果DevOps改进半年后，会使单元测试覆盖率提高到 80%。 如果DevOps改进半年后，会使A系统的部署耗时缩短到 1 分钟。 这回答真的很“确切”，但是，还是没有办法说服业务老大。注意，业务老大是不懂技术的。老大听到你的回答，估计一头雾水：什么是单元测试覆盖率？提高到 80% 后，对我的业务 KPI 又有什么关系？\n别笑，这在 IT 行业里是常态。很多时候是不懂技术的老大，却又领导着一批技术人员。\n面对这样的“常态”，作为技术人员，我们有必要，也有责任让不懂技术的业务老大理解 IT 行业里必要的“常识”。\n回到业务老大的问题，如果你仔细思考，还真不好回答。比如，单元测试覆盖率提高到 80% 后，对我的业务 KPI 又有什么关系？能让我的业务 KPI 提高 80% 吗？\n老翟的做法 当笔者看到这个问题，第一感觉就是：我们必须找到 DevOps 改进措施和业务老大关心的 KPI 之间的关系。换句话说，就是如果在100名工程师做一个产品的团队的情况下，进行 DevOps 改进半年后，会给我的业务 KPI 带来什么确切的结果？\n笔者认为业务老大的问题，可以拆分成两个小问题：\n有效性问题：如何证明 DevOps 改进是对业务 KPI 提升是有效的？ 进度问题：怎么评估 DevOps 改进对业务 KPI 提升了多少？ 如何证明 DevOps 改进是对业务 KPI 提升是有效的？ 这就是我说的：我们必须找到 DevOps 改进措施和业务 KPI 之间的关系。\n“DevOps 改进措施和业务 KPI 之间的关系”指的是什么？这需要针对不同的业务场景进行举例说明。\n另外，面对类似这种“改进”类型的问题，我们首先，要了解当前情况。否则就是隔山估大猪——瞎猜。所以，当别人给你信誓旦旦说改进了多少时，你需要提问：你知道当前业务 KPI 是多少吗。这样就可以判断他是不是瞎猜了。\n接下来，我们就拿秒杀业务系统来举例，找到 DevOps 改进措施和业务 KPI 之间的关系。\n笔者从网上找到了秒杀系统的 KPI：\n((带有秒杀商品的购物车成功支付金额 + 秒杀单品成功支付金额) - 秒杀商品成本价) / (带有秒杀商品的购物车支付金额 + 秒杀单品支付金额)\n从公式来看，就是利润率。为了提高这个利润率，单位时间内我们必须：\n提高带有秒杀商品的成功支付率。 降低秒杀商品的成本价。 关于第1点。很明显，我们 DevOps 改进措施必须能提高“带有秒杀商品的成功支付率”指标。\n关于第2点。秒杀的成本价。笔者认为可能包括：商品本身进货价、物流费用、IT 方面的硬件成本等。\n拿 IT 硬件方面的成本来举例。比如秒杀的 1 个小时，在没有 DevOps 改进前，我们的带宽需要从5MB提升到20MB，机器数量从3台增加到12台等。但是，这样的技术决定是根据经验猜的。秒杀活动过于火爆的话，如果机器不够，增加机器不及时，影响了用户支付，会严重影响业务KPI。秒杀活动无人理睬时，带宽和机器都浪费了。\n所以，DevOps 改进后，就能实现动态伸缩，机器和带宽都可以根据实际情况增减。这时，IT 成本指标就降低了。\n实现动态伸缩，需要实现应用的自动化部署。接着，要实现自动化部署，又要提高单元测试覆盖率以保证软件的正确性……就这样从业务层到技术层，慢慢推导，证明给业务老大：看，从逻辑上，DevOps 的改进对业务 KPI 的提升是有效的。\n进度问题：怎么评估 DevOps 改进对业务 KPI 提升了多少？ 关于进度问题，第一件事情，要做的就是了解当前的业务 KPI 值，并进行监控。这里并不是一开始就要建一个完整的全面的监控系统。\n第二件事情：根据 DevOps 改进措施和业务 KPI 之间的关系，还有当前的业务 KPI 的情况，计算改进措施对业务 KPI 的提升值。\n关键问题来了，怎么计算？\n笔者认为可以这样做。\n找到当前业务 KPI 受技术影响的点，并提问，如果改进（优化）掉了这些影响点，业务 KPI 会提高多少？注意，需要从全局考虑这些点的优先级。并不是要一次性改进所有的点，也不是必须要改进。通过这一步，我们可以得到理想状态下，业务 KPI 能提高到什么程度。 评估到底需要哪些改进措施，并找到这些改进措施的限制点。通过这步，我们可以有目的的安排工作去改进。 了解当前团队的能力，就可以评估半年后的确切结果了。 第2步要做得好，我们必须了解整个团队（甚至整个公司）的协作方式，包括代码管理、发布模式；了解团队的自动化程度；了解各方的利益关系，包括测试、开发和运维等。\n小记 以上的例子是笔者臆造的。思考过程才是关键。从这个过程，你可以看出，其实提高业务 KPI 的方式，其实很多很多。而使用 DevOps 方法，只是具体实现中的一种。我们不应该使用“DevOps”这个框，把自己的思维限制了。\n还有一点需要提一下。在跟业务老大讨论 DevOps 改进时，我们需要区分当前业务中，软件是处于成本中心（比如HR系统），还是处于利润中心（比如淘宝网）。\n处于成本中心的情况，我们重点放在帮助业务节约成本。比如你可以说自动化程度越高，需要招的人越少。\n处于利润中心的情况，我们重点放在帮助业务提高盈利能力。比如你可以说发布越多，正确性越高，盈利越强。\n最后，整篇文章一个很大的漏洞，不知道读者有没有注意到。从头到尾，笔者都没有谈“DevOps改进”是什么。因为整个行业对 DevOps 的定义本身，没有共识，所以，讨论“DevOps 改进”的定义，笔者觉得明智。不过，笔者十分赞同乔帮主说的：可以将 DevOps 理解为一场运动。\n附录 年度回顾：百度乔梁谈持续交付与 DevOps: https://www.infoq.cn/article/2012/02/baidu-salon-review-qiaoliang 时间、商品、价格、数量，四个要素做好一场“秒杀”:http://www.woshipm.com/operate/584869.html ","permalink":"https://showme.codes/zh-cn/2019-12-31-devops-kpi/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-a70cbe852bb13759.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"要的是确切结果不要忽悠我\"\u003e要的是确切结果，不要忽悠我\u003c/h3\u003e\n\u003cp\u003e前段时间，乔帮主（乔梁，《持续交付2.0》的作者）在持续交付2.0的群里发出这一句话：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e业务老大问：“原来100工程师做的一个产品，用半年时间做 devops 改进。半年之后会得到什么确切的结果？”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e以上是原话。\u003c/p\u003e\n\u003cp\u003e乔帮主在群发这话的意思是：如果你的业务老大问这样的问题，你该如何回答？\u003c/p\u003e\n\u003cp\u003e请注意，业务老大要的是确切结果，不要拿“虚”的东西来忽悠人。\u003c/p\u003e\n\u003cp\u003e请读者朋友思考一会。\n。\u003c/p\u003e\n\u003cp\u003e。\u003c/p\u003e\n\u003cp\u003e。\n客官不要急，请再思考一会鸭。\n。\u003c/p\u003e\n\u003cp\u003e。\u003c/p\u003e\n\u003cp\u003e面对老大这样的提问，技术人可能觉得好笑，接着可能装作一本正经回答老大：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e如果DevOps改进半年后，会使单元测试覆盖率提高到 80%。\u003c/li\u003e\n\u003cli\u003e如果DevOps改进半年后，会使A系统的部署耗时缩短到 1 分钟。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这回答真的很“确切”，但是，还是没有办法说服业务老大。注意，业务老大是不懂技术的。老大听到你的回答，估计一头雾水：什么是单元测试覆盖率？提高到 80% 后，对我的业务 KPI 又有什么关系？\u003c/p\u003e\n\u003cp\u003e别笑，这在 IT 行业里是常态。很多时候是不懂技术的老大，却又领导着一批技术人员。\u003c/p\u003e\n\u003cp\u003e面对这样的“常态”，作为技术人员，我们有必要，也有责任让不懂技术的业务老大理解 IT 行业里必要的“常识”。\u003c/p\u003e\n\u003cp\u003e回到业务老大的问题，如果你仔细思考，还真不好回答。比如，单元测试覆盖率提高到 80% 后，对我的业务 KPI 又有什么关系？能让我的业务 KPI 提高 80% 吗？\u003c/p\u003e\n\u003ch3 id=\"老翟的做法\"\u003e老翟的做法\u003c/h3\u003e\n\u003cp\u003e当笔者看到这个问题，第一感觉就是：我们必须找到 DevOps 改进措施和业务老大关心的 KPI 之间的关系。换句话说，就是如果在100名工程师做一个产品的团队的情况下，进行 DevOps 改进半年后，会给我的业务 KPI 带来什么\u003cstrong\u003e确切\u003c/strong\u003e的结果？\u003c/p\u003e\n\u003cp\u003e笔者认为业务老大的问题，可以拆分成两个小问题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e有效性问题：如何证明 DevOps 改进是对业务 KPI 提升是有效的？\u003c/li\u003e\n\u003cli\u003e进度问题：怎么评估 DevOps 改进对业务  KPI 提升了多少？\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"如何证明-devops-改进是对业务-kpi-提升是有效的\"\u003e如何证明 DevOps 改进是对业务 KPI 提升是有效的？\u003c/h4\u003e\n\u003cp\u003e这就是我说的：\u003cstrong\u003e我们必须找到 DevOps 改进措施和业务 KPI 之间的关系\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e“DevOps 改进措施和业务 KPI 之间的关系”指的是什么？这需要针对不同的业务场景进行举例说明。\u003c/p\u003e","title":"业务老大问 DevOps 改进半年后，会得什么确切结果？"},{"content":"\n餐饮行业里，有些饭店的服务员应聘是需要试工的。就是让应聘者穿上酒店的工作服，然后在工作繁忙时间，工作一段时间。\n这段时间里，面试官可以观察到他在点菜时会不会与客人互动，上菜过程中的专业程度等。进而判断要不要录入他。\n试工后，基础能回答这人能否胜任当前工作。\n当然，“试工”对于招聘起到效果也取决于面试官。这是另一个问题了。\n而 IT 行业，如果面试官不问个偏门算法，问个万万亿级流量的处理解决方案，似乎显得自己比面试者差。所以，IT 行业有个笑话：面试造火箭，入职拧螺丝。\n我们是不是应该换个方式来招聘程序员？也用“试工”来遴选人才。\n据我所知，ThoughtWorks 很多年前就已经采用“试工”的方式招人了。以下是笔者当时的面试经历：\n一面是与 HR 简单聊聊。\n二面，你必须在规定时间内完成一个家庭作业（需要写代码）。当你把作业交上去，HR 会找到公司内部的程序员帮忙看题（这样有助于缓和HR与程序员的关系，因为HR有求于程序员啊。）。\n三面，他们会邀请你到办公室，然后HR找两个程序员和你结对编程（注意：这里是真实的上机写代码），内容就是在你交的作业的基础上加需求。过程中，他们会观察你，会提问你。\n其实，整个三面的过程，就是试工的过程。虽然不能拿真实代码来改，但是也尽量模拟真实的工作场景：结对编程、别人对你代码的质疑等。\n像 ThoughtWorks 这样试工的，在我们行业里，真的太少了。\n回到阿里面试王垠（暂不说是不是受邀面试）这件事。\n从赵海平的回复来看：\n整个面试最关键的过程恰好是对简历上具体工作的详细了解，这个王垠在博客里完全没有提到，实际上我问了将近二十到三十分钟，我希望王垠能够意识到这部分才是面试真正考核的部分，应该尽量把自己最拿手最出彩的工作分享给面试官，详细解释为什么难，为什么有意义，为什么对公司有着深远的影响，而不是直接问面试官是做什么的，到底懂不懂，很遗憾，我恰好是做编译器的，在Facebook做了PHP编译器，在阿里巴巴领导了团队在Java里加入了透明的协程\n从这一段话来看，赵海平花了很长时间问王垠的过去。\n赵海平是不是可以让王垠试试解决一下自己所在团队当前遇到的技术问题，又或者让王垠试着重新实现一遍自己骄傲的“透明的协程”？这个过程，我相信是非常兴奋的。\n以上两种尝试其实也算是一种试工。能解决团队遇到的问题，能做面试官能做的，应该算是能胜任他所面试的岗位了吧？\n毕竟，是想招这个人来解决问题的。而不是抓住他的过去不放。\n再说，有些应聘者可能真的不知道要怎么回答面试中的——真正考核的部分。所以，回答不上来，个人也觉得很正常。因为我也是那样的人。\n最后，我疑问，在赵海平的这次面试里，“真正考核的部分”真的比“这个人能否真正解决问题”重要吗？HR 面，我可以理解。\n后记 我们都是外人。所以，真正的背后的动机，上下文只有当事人知道。我不想评价他们个人和公司。只想讨论一下“试工”在IT行业的可能性。让更多人知道，招聘程序员，还有另一种姿势。\n笔者是从这文章了解到事件的：https://www.ithome.com/0/464/417.htm\n笔者的阿里三面经历：https://showme.codes/2018-06-24/alibaba-interview/\n","permalink":"https://showme.codes/zh-cn/2019-12-25-yinwang-alibaba/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-fa34b7044dfbb1bc.png\"\u003e\u003c/p\u003e\n\u003cp\u003e餐饮行业里，有些饭店的服务员应聘是需要试工的。就是让应聘者穿上酒店的工作服，然后在工作繁忙时间，工作一段时间。\u003c/p\u003e\n\u003cp\u003e这段时间里，面试官可以观察到他在点菜时会不会与客人互动，上菜过程中的专业程度等。进而判断要不要录入他。\u003c/p\u003e\n\u003cp\u003e试工后，基础能回答这人能否胜任当前工作。\u003c/p\u003e\n\u003cp\u003e当然，“试工”对于招聘起到效果也取决于面试官。这是另一个问题了。\u003c/p\u003e\n\u003cp\u003e而 IT 行业，如果面试官不问个偏门算法，问个万万亿级流量的处理解决方案，似乎显得自己比面试者差。所以，IT 行业有个笑话：面试造火箭，入职拧螺丝。\u003c/p\u003e\n\u003cp\u003e我们是不是应该换个方式来招聘程序员？也用“试工”来遴选人才。\u003c/p\u003e\n\u003cp\u003e据我所知，ThoughtWorks 很多年前就已经采用“试工”的方式招人了。以下是笔者当时的面试经历：\u003c/p\u003e\n\u003cp\u003e一面是与 HR 简单聊聊。\u003c/p\u003e\n\u003cp\u003e二面，你必须在规定时间内完成一个家庭作业（需要写代码）。当你把作业交上去，HR 会找到公司内部的程序员帮忙看题（这样有助于缓和HR与程序员的关系，因为HR有求于程序员啊。）。\u003c/p\u003e\n\u003cp\u003e三面，他们会邀请你到办公室，然后HR找两个程序员和你结对编程（注意：这里是真实的上机写代码），内容就是在你交的作业的基础上加需求。过程中，他们会观察你，会提问你。\u003c/p\u003e\n\u003cp\u003e其实，整个三面的过程，就是试工的过程。虽然不能拿真实代码来改，但是也尽量模拟真实的工作场景：结对编程、别人对你代码的质疑等。\u003c/p\u003e\n\u003cp\u003e像 ThoughtWorks 这样试工的，在我们行业里，真的太少了。\u003c/p\u003e\n\u003cp\u003e回到阿里面试王垠（暂不说是不是受邀面试）这件事。\u003c/p\u003e\n\u003cp\u003e从赵海平的回复来看：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e整个面试最关键的过程恰好是对简历上具体工作的详细了解，这个王垠在博客里完全没有提到，实际上我问了将近二十到三十分钟，我希望王垠能够意识到这部分才是面试真正考核的部分，应该尽量把自己最拿手最出彩的工作分享给面试官，详细解释为什么难，为什么有意义，为什么对公司有着深远的影响，而不是直接问面试官是做什么的，到底懂不懂，很遗憾，我恰好是做编译器的，在Facebook做了PHP编译器，在阿里巴巴领导了团队在Java里加入了透明的协程\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e从这一段话来看，赵海平花了很长时间问王垠的过去。\u003c/p\u003e\n\u003cp\u003e赵海平是不是可以让王垠试试解决一下自己所在团队当前遇到的技术问题，又或者让王垠试着重新实现一遍自己骄傲的“透明的协程”？这个过程，我相信是非常兴奋的。\u003c/p\u003e\n\u003cp\u003e以上两种尝试其实也算是一种试工。能解决团队遇到的问题，能做面试官能做的，应该算是能胜任他所面试的岗位了吧？\u003c/p\u003e\n\u003cp\u003e毕竟，是想招这个人来解决问题的。而不是抓住他的过去不放。\u003c/p\u003e\n\u003cp\u003e再说，有些应聘者可能真的不知道要怎么回答面试中的——真正考核的部分。所以，回答不上来，个人也觉得很正常。因为我也是那样的人。\u003c/p\u003e\n\u003cp\u003e最后，我疑问，在赵海平的这次面试里，“真正考核的部分”真的比“这个人能否真正解决问题”重要吗？HR 面，我可以理解。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e我们都是外人。所以，真正的背后的动机，上下文只有当事人知道。我不想评价他们个人和公司。只想讨论一下“试工”在IT行业的可能性。让更多人知道，招聘程序员，还有另一种姿势。\u003c/p\u003e\n\u003cp\u003e笔者是从这文章了解到事件的：\u003ca href=\"https://www.ithome.com/0/464/417.htm\"\u003ehttps://www.ithome.com/0/464/417.htm\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e笔者的阿里三面经历：\u003ca href=\"https://showme.codes/2018-06-24/alibaba-interview/\"\u003ehttps://showme.codes/2018-06-24/alibaba-interview/\u003c/a\u003e\u003c/p\u003e","title":"从王垠面试阿里的事件看程序员招聘"},{"content":"\n最近发现用户都喜欢构建命令最后加上一行：ls -a .。为什么呢？\n是因为他们想知道执行 npm run build 后的目录的结果是什么样的。目录里到底有没有出现期望的文件。\n这个功能在 Jenkins 中叫做“工作空间”。其实就是源码在 Jenkins 上下载后的目录。Jenkins 中，用户是可以直接像在查看本地文件夹一样查看这个工作空间的内容。如下图如示。\n为什么用户想要看工作空间中的目录结构呢？说到底就是用户本地打包环境和平台打包环境还是有区别的。当出现构建结果不符合预期时，用户需要根据工作空间的信息来找到失败原因。\n另，笔者看了行业内的几个平台，没有找到“工作空间”的功能。为什么没有这个功能呢？不知道。\n后记 “工作空间”这个特性本身提高了用户在平台上的自检能力。因为平台上的信息对于用户更透明了。那么实现这个功能的成本呢？这是我们在实现前要考虑的问题。\n","permalink":"https://showme.codes/zh-cn/2019-11-28-devops-platform-workspace/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-fcd261b506ae3d41.png\"\u003e\u003c/p\u003e\n\u003cp\u003e最近发现用户都喜欢构建命令最后加上一行：\u003ccode\u003els -a .\u003c/code\u003e。为什么呢？\u003c/p\u003e\n\u003cp\u003e是因为他们想知道执行 \u003ccode\u003enpm run build\u003c/code\u003e 后的目录的结果是什么样的。目录里到底有没有出现期望的文件。\u003c/p\u003e\n\u003cp\u003e这个功能在 Jenkins 中叫做“工作空间”。其实就是源码在 Jenkins 上下载后的目录。Jenkins 中，用户是可以直接像在查看本地文件夹一样查看这个工作空间的内容。如下图如示。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-43a761d9a9f32200.png\"\u003e\u003c/p\u003e\n\u003cp\u003e为什么用户想要看工作空间中的目录结构呢？说到底就是用户本地打包环境和平台打包环境还是有区别的。当出现构建结果不符合预期时，用户需要根据工作空间的信息来找到失败原因。\u003c/p\u003e\n\u003cp\u003e另，笔者看了行业内的几个平台，没有找到“工作空间”的功能。为什么没有这个功能呢？不知道。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e“工作空间”这个特性本身提高了用户在平台上的自检能力。因为平台上的信息对于用户更透明了。那么实现这个功能的成本呢？这是我们在实现前要考虑的问题。\u003c/p\u003e","title":"谈 DevOps 平台设计：为什么用户喜欢在构建后加一句 ls -al ."},{"content":"\n为什么部署后的包还是旧的包？\n有位同学说他在 DevOps 平台部署后，代码还是旧的。\n在我跟他确认了构建时拉取的代码是版本是对的，部署时，使用的制品包也是正确的情况下 ，我也是摸不着头脑了。\n最后，这位同学在 DevOps 平台的界面上看到了部署的目标机器的IP。\n这才找到了真正的原因：他们自己部署的目标机器搞错了。也就是本来想部署到 A 机器，但是部署任务填写的是 B 机器的 IP。然后自己一直在 A 机器上检查部署结果。\n接着，这位同学就感叹自己对 DevOps 不熟。\n然而，笔者认为，那根本不是用户对 DevOps 熟不熟的问题。而是 DevOps 平台的设计问题。\n为什么用户连自己部署错了主机都不知道？笔者认为那是因为目标机器的 IP 信息在界面上不够明显，其他所有的信息就只能通过分析 DevOps 平台提供的日志才能知道了。\n所以，我们的平台需要提供这两个功能：\n部署清单：列出本次部署的具体内容，可以包括：制品版本，执行人，目标机器列表，部署策略，回滚策略等。 部署结果报告：制品版本，目标机器的部署结果，与上一次部署的差异（这是关键）、多次部署报告的差异性比较功能等。 部署结果报告中必须要强调此次部署与上次部署之间的差异，毕竟大多数应用不会那么频繁的更换部署机器。\n后记 “DevOps” 这个概念本身已经把很多人搞得云里雾里。在网上搜索一下 DevOps 的定义就知道了。所以，用户在使用 DevOps 平台时，达不到预期值，就习惯性地认为是自己的问题。我们作为平台设计方，用户达不到预期，那就是平台设计的问题。\n","permalink":"https://showme.codes/zh-cn/2019-11-27-devops-platform-deploy-inventory/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-44c2901e907f8a74.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为什么部署后的包还是旧的包？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e有位同学说他在 DevOps 平台部署后，代码还是旧的。\u003c/p\u003e\n\u003cp\u003e在我跟他确认了构建时拉取的代码是版本是对的，部署时，使用的制品包也是正确的情况下 ，我也是摸不着头脑了。\u003c/p\u003e\n\u003cp\u003e最后，这位同学在 DevOps 平台的界面上看到了部署的目标机器的IP。\u003c/p\u003e\n\u003cp\u003e这才找到了真正的原因：他们自己部署的目标机器搞错了。也就是本来想部署到 A 机器，但是部署任务填写的是 B 机器的 IP。然后自己一直在 A 机器上检查部署结果。\u003c/p\u003e\n\u003cp\u003e接着，这位同学就感叹自己对 DevOps 不熟。\u003c/p\u003e\n\u003cp\u003e然而，笔者认为，那根本不是用户对 DevOps 熟不熟的问题。而是 DevOps 平台的设计问题。\u003c/p\u003e\n\u003cp\u003e为什么用户连自己部署错了主机都不知道？笔者认为那是因为目标机器的 IP 信息在界面上不够明显，其他所有的信息就只能通过分析 DevOps 平台提供的日志才能知道了。\u003c/p\u003e\n\u003cp\u003e所以，我们的平台需要提供这两个功能：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e部署清单\u003c/strong\u003e：列出本次部署的具体内容，可以包括：制品版本，执行人，目标机器列表，部署策略，回滚策略等。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e部署结果报告\u003c/strong\u003e：制品版本，目标机器的部署结果，与上一次部署的差异（这是关键）、多次部署报告的差异性比较功能等。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e部署结果报告中必须要强调此次部署与上次部署之间的差异，毕竟大多数应用不会那么频繁的更换部署机器。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e“DevOps” 这个概念本身已经把很多人搞得云里雾里。在网上搜索一下 DevOps 的定义就知道了。所以，用户在使用 DevOps 平台时，达不到预期值，就习惯性地认为是自己的问题。我们作为平台设计方，用户达不到预期，那就是平台设计的问题。\u003c/p\u003e","title":"谈 DevOps 平台设计：为什么部署后的包还是旧的包？"},{"content":"\n一位同学的疑问 有一位同学问我：\n你们一个流水线从编译到部署成功需要多少分钟啊。我们快的2分钟，Java 普遍10分钟，开发同学总是觉得慢，我不知道业界是什么水平。\n我的回答是： 没有行业标准的，有些项目10万行代码，有些100万行代码，没法比。\n后来，我又问他：你们的流水线都包括了哪些阶段？\n“编译，带数据库的测序，sonar，docker build，ansible deploy，mvn release”，他回答。\n后来的沟通中，我得知，他们流水线时间长发生在两个阶段：带数据库的测试和下载依赖。\n下载依赖慢是因为他们每次构建都重新下载依赖，这样做又是因为总是遇到缓存问题，所以，干脆就每次重新下载了。\n带数据库的测试通常会慢，因为要启动应用，然后操作数据库，这个数据不确定他是类似 MySQL 这样的真实数据库，还是使用 H2 这样的内存数据库。\n出乎我意料的是，他们的 SonarQube 扫描倒是很快。\n所以，他们的优化点就是那两个慢的阶段。具体解决办法还要看具体情况，比如带数据库的测试是不是可以通过并行解决、构建时依赖缓存遇到的问题是不是可以通修改构建工具配置来解决。\n写到这里，我想表达的是，优化流水线的速度的思路，差不多就是这样：先找到最慢的阶段，然后根据具体情况来优化。\n从 DevOps 平台设计角度解决 那么，作为 DevOps 平台，我们能通过什么办法帮助用户提高流水线的速度呢？\n笔者认为，只要将流水线中的每个阶段中的每个步骤的耗时都记录下来，然后显示给用户，用户自然会注意到每次流水线的执行速度差异。当然，管理层也可以对这部分内容进行考核。\n同时，要将耗时进行分类，一类是用户步骤的耗时，比如执行mvn package，执行单元测试等，一类是 DevOps 平台本身的耗时，比如初始化构建环境耗时，上传制品耗时等。\n为什么要进行这样的分类？是因为 DevOps 平台使用过程中，用户遇到问题，往往是区分不了，是平台的问题，还是自己的问题。这时，我们将平台的信息显示给用户，用户就可以自行判断，自行处理了。这会大大节约平台维护者的时间。而且，平台维护者也可以根据平台运行耗时统计来对平台进行有依有据的优化。这是一箭双雕。\n我把这个功能叫做：流水线耗时统计。\n那么这个功能，到底应该如何实现？不同的平台有不同的实现，比如基于 Jenkins 的话，在每个步骤后加上一个回调请求就可以了；基于 GitLab 的话，就不了解了。 图来自：https://wiki.jenkins.io/display/JENKINS/Pipeline+Stage+View+Plugin\n后记 流水线的速度是一个很重要的指标，它直接显示了一个软件开发团队在工程方面的效率（正确性问题是另一个问题）。而流水线耗时统计功能可以有效地帮助用户提高自己的流水线速度。\n","permalink":"https://showme.codes/zh-cn/2019-11-21-devops-pipeline-speed/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-7716aa968a03ce80.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"一位同学的疑问\"\u003e一位同学的疑问\u003c/h3\u003e\n\u003cp\u003e有一位同学问我：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e你们一个流水线从编译到部署成功需要多少分钟啊。我们快的2分钟，Java 普遍10分钟，开发同学总是觉得慢，我不知道业界是什么水平。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我的回答是： 没有行业标准的，有些项目10万行代码，有些100万行代码，没法比。\u003c/p\u003e\n\u003cp\u003e后来，我又问他：你们的流水线都包括了哪些阶段？\u003c/p\u003e\n\u003cp\u003e“编译，带数据库的测序，sonar，docker build，ansible deploy，mvn release”，他回答。\u003c/p\u003e\n\u003cp\u003e后来的沟通中，我得知，他们流水线时间长发生在两个阶段：带数据库的测试和下载依赖。\u003c/p\u003e\n\u003cp\u003e下载依赖慢是因为他们每次构建都重新下载依赖，这样做又是因为总是遇到缓存问题，所以，干脆就每次重新下载了。\u003c/p\u003e\n\u003cp\u003e带数据库的测试通常会慢，因为要启动应用，然后操作数据库，这个数据不确定他是类似 MySQL 这样的真实数据库，还是使用 H2 这样的内存数据库。\u003c/p\u003e\n\u003cp\u003e出乎我意料的是，他们的 SonarQube 扫描倒是很快。\u003c/p\u003e\n\u003cp\u003e所以，他们的优化点就是那两个慢的阶段。具体解决办法还要看具体情况，比如带数据库的测试是不是可以通过并行解决、构建时依赖缓存遇到的问题是不是可以通修改构建工具配置来解决。\u003c/p\u003e\n\u003cp\u003e写到这里，我想表达的是，优化流水线的速度的思路，差不多就是这样：先找到最慢的阶段，然后根据具体情况来优化。\u003c/p\u003e\n\u003ch3 id=\"从-devops-平台设计角度解决\"\u003e从 DevOps 平台设计角度解决\u003c/h3\u003e\n\u003cp\u003e那么，作为 DevOps 平台，我们能通过什么办法帮助用户提高流水线的速度呢？\u003c/p\u003e\n\u003cp\u003e笔者认为，只要将流水线中的每个阶段中的每个步骤的耗时都记录下来，然后显示给用户，用户自然会注意到每次流水线的执行速度差异。当然，管理层也可以对这部分内容进行考核。\u003c/p\u003e\n\u003cp\u003e同时，要将耗时进行分类，一类是用户步骤的耗时，比如执行mvn package，执行单元测试等，一类是 DevOps 平台本身的耗时，比如初始化构建环境耗时，上传制品耗时等。\u003c/p\u003e\n\u003cp\u003e为什么要进行这样的分类？是因为 DevOps 平台使用过程中，用户遇到问题，往往是区分不了，是平台的问题，还是自己的问题。这时，我们将平台的信息显示给用户，用户就可以自行判断，自行处理了。这会大大节约平台维护者的时间。而且，平台维护者也可以根据平台运行耗时统计来对平台进行有依有据的优化。这是一箭双雕。\u003c/p\u003e\n\u003cp\u003e我把这个功能叫做：\u003cstrong\u003e流水线耗时统计\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e那么这个功能，到底应该如何实现？不同的平台有不同的实现，比如基于 Jenkins 的话，在每个步骤后加上一个回调请求就可以了；基于 GitLab 的话，就不了解了。\n\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-4a7380dad7d90eee.png\"\u003e\n图来自：\u003ca href=\"https://wiki.jenkins.io/display/JENKINS/Pipeline+Stage+View+Plugin\"\u003ehttps://wiki.jenkins.io/display/JENKINS/Pipeline+Stage+View+Plugin\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e流水线的速度是一个很重要的指标，它直接显示了一个软件开发团队在工程方面的效率（正确性问题是另一个问题）。而\u003cstrong\u003e流水线耗时统计\u003c/strong\u003e功能可以有效地帮助用户提高自己的流水线速度。\u003c/p\u003e","title":"谈 DevOps 平台落地：你们流水线从编译到部署需要多少分钟啊？"},{"content":"\n同事发了一个前端构建失败的链接过来，接着就是那句：任务执行失败了，麻烦帮忙看看。\nDevOps 平台的“老手”了，所以，在找我们解决问题时，都知道附上平台任务的链接。\n我们打开链接，第一件事情就是看日志。是的，DevOps 平台的使用者很多都认为：在本地执行构建成功，那么在平台上构建失败就是平台的问题。所以部分人连构建日志都不看，直接把链接发给我们这些平台维护者看。\n不出意外，这次又是依赖管理问题。只不过，这次是发生在前端项目上。错误截图下如下：\n日志里（画红线部分）已经说得很清楚了。虽然我不清楚“Tristan”是什么，但是可以猜到是他的业务代码报这样的错。但是他本地执行没报错，那通常就是依赖的版本的问题了。\n他的前端的依赖定义(package.json)类似以下这样：\n{ ..... \u0026#34;dependencies\u0026#34;: { \u0026#34;cookie-parser\u0026#34;: \u0026#34;^1.4.3\u0026#34;, \u0026#34;debug\u0026#34;: \u0026#34;~2.6.9\u0026#34;, \u0026#34;express\u0026#34;: \u0026#34;^4.16.0\u0026#34;, \u0026#34;http-errors\u0026#34;: \u0026#34;^1.6.2\u0026#34;, \u0026#34;morgan\u0026#34;: \u0026#34;~1.9.0\u0026#34;, \u0026#34;pug\u0026#34;: \u0026#34;2.0.0-beta11\u0026#34; }, ... } 我们看到依赖的版本号的前缀有 ~，有也 ^。这是什么意思呢？\n~: 前缀表示，安装大于指定的这个版本，并且匹配到 x.y.z 中 z 最新的版本。 ^: 前缀在 ^0.y.z 时的表现和 ~0.y.z 是一样的，然而 ^1.y.z 的时候，就会 匹配到 y 和 z 都是最新的版本。 也就是说，每次执行 npm install ，该项目所依赖的内容，都是有可能变的。\n这对我来说是不可思议的。为什么？\n因为依赖的版本代表着一个软件的基础。依赖的版本在你不知道的情况下发生变更，就好比建房子，建第一层时，地基是100个平方，建第二层时，地基突然就变成了90个平方。而前端项目中大量这种情况。\n你可能会说开源前端node项目都会遵循语义化的版本号，小版本升级不会出问题的。我想说，那只是约定，还是要看那个人遵守不遵守。如果你真相信每个人都遵守，本质上是把软件开发风险的控制权交给了开源软件作者的个人习惯。开发出来的软件注定是不稳定的。\n但是，为什么前端项目的依赖的版本号前普遍会加上 ~ 和 ^ 。在我亲自执行 npm install express 命令时，我知道了原因。因为在执行命令后， package.json 文件中就出现了：\u0026quot;express\u0026quot;: \u0026quot;^4.16.0\u0026quot;。也是 npm 在安装依赖时，默认就给版本号加上 ^ 前缀。而很多人可能改都不会去改。这就导致了文章开头所说的那位同事的问题。\n后记 真心希望大家固定下 package.json 中的依赖的版本号。这样的前端项目构建起来才有稳定的基础。\n同时，我们的 DevOps 平台在设计时，是不是可以考虑增加这么一个功能：自动检测项目有没有固定依赖的版本号，如果没有固定，就告警。\n我把这个功能叫做：依赖版本号不稳定预警。\n","permalink":"https://showme.codes/zh-cn/2019-11-20-devops-platform-front-dependency-manage-error/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-29f9b65037c41ed7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e同事发了一个前端构建失败的链接过来，接着就是那句：任务执行失败了，麻烦帮忙看看。\u003c/p\u003e\n\u003cp\u003eDevOps 平台的“老手”了，所以，在找我们解决问题时，都知道附上平台任务的链接。\u003c/p\u003e\n\u003cp\u003e我们打开链接，第一件事情就是看日志。是的，DevOps 平台的使用者很多都认为：在本地执行构建成功，那么在平台上构建失败就是平台的问题。所以部分人连构建日志都不看，直接把链接发给我们这些平台维护者看。\u003c/p\u003e\n\u003cp\u003e不出意外，这次又是依赖管理问题。只不过，这次是发生在前端项目上。错误截图下如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-2a1aace1705f4f15.png\"\u003e\u003c/p\u003e\n\u003cp\u003e日志里（画红线部分）已经说得很清楚了。虽然我不清楚“Tristan”是什么，但是可以猜到是他的业务代码报这样的错。但是他本地执行没报错，那通常就是依赖的版本的问题了。\u003c/p\u003e\n\u003cp\u003e他的前端的依赖定义(package.json)类似以下这样：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e.....\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;dependencies\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;cookie-parser\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;^1.4.3\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;debug\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;~2.6.9\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;express\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;^4.16.0\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;http-errors\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;^1.6.2\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;morgan\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;~1.9.0\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;pug\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;2.0.0-beta11\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e我们看到依赖的版本号的前缀有 \u003ccode\u003e~\u003c/code\u003e，有也 \u003ccode\u003e^\u003c/code\u003e。这是什么意思呢？\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e~\u003c/code\u003e: 前缀表示，安装大于指定的这个版本，并且匹配到 x.y.z 中 z 最新的版本。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e^\u003c/code\u003e: 前缀在 ^0.y.z 时的表现和 ~0.y.z 是一样的，然而 ^1.y.z 的时候，就会 匹配到 y 和 z 都是最新的版本。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e也就是说，每次执行 npm install ，该项目所依赖的内容，都是有可能变的。\u003c/p\u003e\n\u003cp\u003e这对我来说是不可思议的。为什么？\u003c/p\u003e\n\u003cp\u003e因为依赖的版本代表着一个软件的基础。依赖的版本在你不知道的情况下发生变更，就好比建房子，建第一层时，地基是100个平方，建第二层时，地基突然就变成了90个平方。而前端项目中大量这种情况。\u003c/p\u003e\n\u003cp\u003e你可能会说开源前端node项目都会遵循语义化的版本号，小版本升级不会出问题的。我想说，那只是约定，还是要看那个人遵守不遵守。如果你真相信每个人都遵守，本质上是把软件开发风险的控制权交给了开源软件作者的个人习惯。开发出来的软件注定是不稳定的。\u003c/p\u003e\n\u003cp\u003e但是，为什么前端项目的依赖的版本号前普遍会加上 \u003ccode\u003e~\u003c/code\u003e 和 \u003ccode\u003e^\u003c/code\u003e 。在我亲自执行 \u003ccode\u003enpm install express\u003c/code\u003e 命令时，我知道了原因。因为在执行命令后， package.json 文件中就出现了：\u003ccode\u003e\u0026quot;express\u0026quot;: \u0026quot;^4.16.0\u0026quot;\u003c/code\u003e。也是 npm 在安装依赖时，默认就给版本号加上 \u003ccode\u003e^\u003c/code\u003e 前缀。而很多人可能改都不会去改。这就导致了文章开头所说的那位同事的问题。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e真心希望大家固定下 package.json 中的依赖的版本号。这样的前端项目构建起来才有稳定的基础。\u003c/p\u003e","title":"谈 DevOps 平台落地：前端项目构建又失败了"},{"content":"\n题记：DevOps 平台通常搭建于内网环境，不能直接外网，所以，如果你也要在内网环境构建前端，就一定会遇到本文所说的问题。\n我们发现在 DevOps 平台构建前端项目时，会报这以下这样的错误：\nnode scripts/install.js Downloading binary from https://github.com/sass/node-sass/releases/download/v4.9.0/linux-x64-57_binding.node Cannot download \u0026ldquo;https://github.com/sass/node-sass/releases/download/v4.9.0/linux-x64-57_binding.node\u0026rdquo;: tunneling socket could not be established, statusCode=500 Hint: If github.com is not accessible in your location try setting a proxy via HTTP_PROXY, e.g. export HTTP_PROXY=http://example.com:1234 or configure npm proxy via npm config set proxy http://example.com:8080 node-sass@4.9.0 postinstall 以上的错误日志的意思是node在安装 node-sass 时，要去 github.com/sass/node-sass 下载一个名为 linux-x64-57_binding.node 的二进制包。然后它无法下载（其实是因为DevOps平台搭建在企业的内网，是无法直接连接外网的），就建议你设置一下系统的HTTP代理，让它能连接到 github.com。\n除此之外，错误日志中，还发现了，node-sass 依赖本身的构建，还需要 Python2 环境：\ngyp verb check python checking for Python executable \u0026#34;python2\u0026#34; in the PATH gyp verb `which` failed Error: not found: python2 对于一个 Java 后端开发人员，看到这样的错误就懵了。心里在想：\n我不是已经设置了代理了吗？为什么还要从 GitHub 下载依赖？一个 node 项目，为什么还需要 python2 ？\n该 node 项目的构建命令是这样写的：\nnpm install --registry=https://registry.npm.abc.org npm run build 是的，命令中明明写清楚了依赖的下载地址https://registry.npm.abc.org，它为什么还要从 GitHub 下载。\n后来，我们上网查，发现很多人遇到了同样的问题：\n这解决方案无非就以下三种：\n设置网络代理，让构建环境能连上 GitHub。 设置环境变量SASS_BINARY_PATH=/test-sass/binding.node指定从本地目录读取该二进制文件的路径。 设置环境变量SASS_BINARY_SITE=http://npm.taobao.org/mirrors/node-sass 指定从哪里下载这个二进制文件。 这三种方案在开发者自己的电脑上是能解决问题的。但是对于 DevOps 平台是无法解决的。\n方案1：有些 node 包是从非 GitHub 下载的，比如cypress 库要从 https://cdn.cypress.io/desktop 下载。而且，构建环境处于企业内网不能直接连外网。设置代理也不合适。 方案2：不可能遇到一个依赖就自己手工下载，然后再放到编译环境中。不仅工作量大，用户体验还很差。 方案3：不可能设置一个外网的镜像。 那怎么办呢？笔者最终采用了方案3的变种：在内网搭建一个 npm/mirrors 的服务。\n笔者研究发现，http://npm.taobao.org/mirrors 是使用 https://github.com/cnpm/mirrors来搭建的，那我们在内网也搭建一个。前端构建时就可以直接从内网下载了。\n最后笔者就是在内网搭建这么一个 cnpm/mirros 服务，解决了前端构建时的二进制依赖的问题。而用户只需要在自己的构建命令前加一句环境变量的设置：\nSASS_BINARY_SITE=http://npm.abc.org/mirrors/node-sass 慢着，我们可是 DevOps 平台，能不能让用户用得更爽，无感知的解决前端二进制依赖的问题呢？\n其实，DevOps 平台可以直接构建环境中提前设置好相应的环境变量，比如：\nELECTRON_MIRROR=http://npm.abc.org/mirrors/electron/ SASS_BINARY_SITE=http://npm.abc.org/mirrors/node-sass SQLITE3_BINARY_SITE=http://npm.abc.org/mirrors/sqlite3 这样，用户就不需要修改构建命令就解决了问题，用户体验得到提升。\n小结 本文标题是有些标题党。但是，使用过 Java 构建工具的后端开发人员，遇到的前端构建的这类问题的人都会这样疑问。因为使用 Maven 或 Gradle 从来不需要从两个地方下载依赖，而且，node 下载依赖的位置，还要看写那个 node 库的人的“脾气”。\n笔者在此并不是想挑起前端和后端的战争，更不是在说明 Maven 和 Gradle 的优越。只是疑问，node 社区是不是s可以规范一下二进制的下载位置呢？这样，可以节约很多开发者的时间。不过，说回来，node 社区说不定正在讨论着怎么建立相应的规范。\n最后，感谢淘宝为 cnpm/mirros 做出的贡献。让我们能快速的解决前端依赖的问题。\n","permalink":"https://showme.codes/zh-cn/2019-11-12-devops-platform-front-dependency-manage/","summary":"\u003cp\u003e\u003cimg alt=\"tibet-4538357_640.jpg\" loading=\"lazy\" src=\"/assets/images/292372-cdd2f3d31e821aad.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e题记：DevOps 平台通常搭建于内网环境，不能直接外网，所以，如果你也要在内网环境构建前端，就一定会遇到本文所说的问题。\u003c/p\u003e\n\u003cp\u003e我们发现在 DevOps 平台构建前端项目时，会报这以下这样的错误：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003enode scripts/install.js\nDownloading binary from \u003ca href=\"https://github.com/sass/node-sass/releases/download/v4.9.0/linux-x64-57_binding.node\"\u003ehttps://github.com/sass/node-sass/releases/download/v4.9.0/linux-x64-57_binding.node\u003c/a\u003e\nCannot download \u0026ldquo;\u003ca href=\"https://github.com/sass/node-sass/releases/download/v4.9.0/linux-x64-57_binding.node\"\u003ehttps://github.com/sass/node-sass/releases/download/v4.9.0/linux-x64-57_binding.node\u003c/a\u003e\u0026rdquo;:\ntunneling socket could not be established, statusCode=500\nHint: If github.com is not accessible in your location\ntry setting a proxy via HTTP_PROXY, e.g.\nexport HTTP_PROXY=\u003ca href=\"http://example.com:1234/\"\u003ehttp://example.com:1234\u003c/a\u003e\nor configure npm proxy via\nnpm config set proxy \u003ca href=\"http://example.com:8080/\"\u003ehttp://example.com:8080\u003c/a\u003e\n\u003ca href=\"mailto:node-sass@4.9.0\"\u003enode-sass@4.9.0\u003c/a\u003e postinstall \u003c/pre\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e以上的错误日志的意思是node在安装 node-sass 时，要去 github.com/sass/node-sass 下载一个名为 \u003ccode\u003elinux-x64-57_binding.node\u003c/code\u003e 的二进制包。然后它无法下载（其实是因为DevOps平台搭建在企业的内网，是无法直接连接外网的），就建议你设置一下系统的HTTP代理，让它能连接到 github.com。\u003c/p\u003e\n\u003cp\u003e除此之外，错误日志中，还发现了，node-sass 依赖本身的构建，还需要 Python2 环境：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egyp verb check python checking for Python executable \u0026#34;python2\u0026#34; in the PATH\ngyp verb `which` failed Error: not found: python2\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e对于一个 Java 后端开发人员，看到这样的错误就懵了。心里在想：\u003c/p\u003e","title":"谈DevOps平台落地：前端构建怎么这么变态"},{"content":" 我在本地跑明明成功的，为什么在你平台跑就报错？\n用户在 Jenkins 上跑构建时，失败了，把日志截图给我看，如下图：\n在过去几个月，每个星期都会有一两个 Jenkins 用户就会给我发送类似的错误日志。\n这样的日志，我通常回：请检查你们的依赖，是不是有依赖没有上传到咱们的 Nexus 仓库。验证方法是先在本地删除你的 .m2 目录，然后再执行一次构建。\n当用户业务开发比较急的时候，他们还会说本文标题中的那句话。有些抱怨的意思。我都已经习惯了。\n出现这样的情况，我总结大概会有以下原因：\n用户对于 Maven 这类构建工具不熟悉。 用户对于依赖管理不重视，或者没有依赖管理的意识。 用户根本不看日志。 面对这三个原因，我就在思考：我们 DevOps 平台能做些什么呢？\n我觉得 DevOps 平台是不是可以直截了当地告诉用户：\nxxx 依赖在 Nexus 仓库（maven.abc.com）中没有找到，请您先 deploy 该依赖到 Nexus 仓库后，再执行此任务。\n如果能检测到缺少的依赖放在哪个代码仓库就更好了。因为这样，就可以提示用户直接到该代码仓库的 deploy 了。\n这样的技术，我称为依赖AI管理技术（笑）。当然，这样的技术，应该可以应用于所有的语言。\n同时，我们将这些数据（依赖管理失误）统计起来，就可以看出一个团队在依赖管理方面的能力表现了，进而可以有效的对团队进行培训，以提高相应的能力。\n回到本文主题，当用户自行检查依赖后，大多数时候，用户就不会来找我了，因为问题已经解决了。可是有一次，用户还是说不行，他已经把 .m2 删除，并把依赖包上传到 Nexus 仓库了。\n我检查了他的 pom.xml 文件，发现版本号的定义也是正确的。可是，放在 Jenkins 上执行时，使用的还是旧版本的类的定义。\n这就奇怪了。这种情况还是头一回遇到。来来回回检查了好几次，查了好久才知道，是因为用户 deploy 依赖到 Nexus 时，deploy 的是相同的版本号，就是覆盖了原来的版本的包，但是版本没有升级。而 Maven 检测到本地就该版本的依赖，就不会重新下载了。最后，就是大家看到的，本地可以，但是 Jenkins 上就是不行。\n最后的解决方式是：\n用户 deploy 一个新的版本到 Nexus 仓库，并在 pom.xml 中使用新的版本。 我们将 Nexus 设置为不允许重复 deploy。 小结 经过这次事件，我们可以看出，依赖管理对于工程质量的重要性。因为，依赖管理不当，很有可能在连开发人员都不知情的情况下引入Bug。\n而 DevOps 平台能实现依赖AI管理技术将有效的提升工程质量。\n","permalink":"https://showme.codes/zh-cn/2019-11-11-devops-platform-dependency-manage/","summary":"\u003cblockquote\u003e\n\u003cp\u003e我在本地跑明明成功的，为什么在你平台跑就报错？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e用户在 Jenkins 上跑构建时，失败了，把日志截图给我看，如下图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-61d91a89ff7c3df4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在过去几个月，每个星期都会有一两个 Jenkins 用户就会给我发送类似的错误日志。\u003c/p\u003e\n\u003cp\u003e这样的日志，我通常回：请检查你们的依赖，是不是有依赖没有上传到咱们的 Nexus 仓库。验证方法是先在本地删除你的 .m2  目录，然后再执行一次构建。\u003c/p\u003e\n\u003cp\u003e当用户业务开发比较急的时候，他们还会说本文标题中的那句话。有些抱怨的意思。我都已经习惯了。\u003c/p\u003e\n\u003cp\u003e出现这样的情况，我总结大概会有以下原因：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e用户对于 Maven 这类构建工具不熟悉。\u003c/li\u003e\n\u003cli\u003e用户对于依赖管理不重视，或者没有依赖管理的意识。\u003c/li\u003e\n\u003cli\u003e用户根本不看日志。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e面对这三个原因，我就在思考：我们 DevOps 平台能做些什么呢？\u003c/p\u003e\n\u003cp\u003e我觉得 DevOps 平台是不是可以直截了当地告诉用户：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003exxx 依赖在 Nexus 仓库（maven.abc.com）中没有找到，请您先 deploy 该依赖到 Nexus 仓库后，再执行此任务。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e如果能检测到缺少的依赖放在哪个代码仓库就更好了。因为这样，就可以提示用户直接到该代码仓库的 deploy 了。\u003c/p\u003e\n\u003cp\u003e这样的技术，我称为\u003cstrong\u003e依赖AI管理技术\u003c/strong\u003e（笑）。当然，这样的技术，应该可以应用于所有的语言。\u003c/p\u003e\n\u003cp\u003e同时，我们将这些数据（依赖管理失误）统计起来，就可以看出一个团队在依赖管理方面的能力表现了，进而可以有效的对团队进行培训，以提高相应的能力。\u003c/p\u003e\n\u003cp\u003e回到本文主题，当用户自行检查依赖后，大多数时候，用户就不会来找我了，因为问题已经解决了。可是有一次，用户还是说不行，他已经把 .m2 删除，并把依赖包上传到 Nexus 仓库了。\u003c/p\u003e\n\u003cp\u003e我检查了他的 pom.xml 文件，发现版本号的定义也是正确的。可是，放在 Jenkins 上执行时，使用的还是旧版本的类的定义。\u003c/p\u003e\n\u003cp\u003e这就奇怪了。这种情况还是头一回遇到。来来回回检查了好几次，查了好久才知道，是因为用户 deploy 依赖到 Nexus 时，deploy 的是相同的版本号，就是覆盖了原来的版本的包，但是版本没有升级。而 Maven 检测到本地就该版本的依赖，就不会重新下载了。最后，就是大家看到的，本地可以，但是 Jenkins 上就是不行。\u003c/p\u003e\n\u003cp\u003e最后的解决方式是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e用户 deploy 一个新的版本到 Nexus 仓库，并在 pom.xml 中使用新的版本。\u003c/li\u003e\n\u003cli\u003e我们将 Nexus 设置为不允许重复 deploy。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e经过这次事件，我们可以看出，依赖管理对于工程质量的重要性。因为，依赖管理不当，很有可能在连开发人员都不知情的情况下引入Bug。\u003c/p\u003e","title":"谈 DevOps 平台实施：我在本地跑明明成功的，为什么在你平台跑就报错？"},{"content":"通常我不喜欢写开发环境搭建类文章的，但是见到不少同学在 Ansible 的开发环境花了很多时间。所以，就想写这么一篇文章。希望能帮助到有需要的同学。\n在介绍开发环境搭建之前，需要介绍 Ansible 脚本的开发流程。\n不像普通的业务系统的开发，只需要打开 IDE 就可以写代码，然后调试了。当然 Ansible 脚本也可以进行单元测试，但是 Ansible 脚本还是需要真实运行并部署，才能验证脚本的正确性。\n所以，Ansible 脚本的开发过程通常是这样的：\n启动一台虚拟机。 在开发机上编辑 Ansible 脚本。 在开发机上执行 ansible-playbook -i hosts playbook.yml 命令。 通过 Ansible 脚本的开发过程了解到，开发环境的搭建可以分成3部分：\n测试机器的准备。 文本编辑器的准备。 Ansible 的安装。 1. 测试机器的准备 见到不少同学使用 Vmware 或 VirtualBox 手工创建虚拟机。这种方式是可以达到搭建测试机器的目的。但是笔者认为这样不够好。因为验证 Ansible 脚本，我们需要频繁创建新的虚拟机。手工创建虚拟机的效率太低。而且不利于版本控制。\n所以，测试机器的准备，笔者使用的是 Vagrant。通过它，可以自动化创建和配置虚拟机。当然，整个过程还是版本控制的。\n同时需要注意，Vagrant 本身并不是一个虚拟机的实现，它是基于 VirtualBox 和 Vmware 的。换句说就是我们可以通过 Vagrant 去控制 Vmware 和 VirtualBox。所以，在安装 Vagrant 的同时，也需要安装 VirtualBox 或 Vmware。本文使用 VirtualBox。\nVagrant 和 VirtualBox 的具体安装在本文末有官方教程。\n2. Vagrant 介绍 Vagrant 本身只是一个软件，提供了 vagrant 命令。我们通过一个名为 Vagrantfile 的文件声明启动什么配置的虚拟机。\nVagrantfile 文件的编写，不需要从零开始，可以使用 vagrant 命令生成：\nvagrant init ubuntu/trusty64 vagrant up ubuntu/trusty64 是在 Vagrant 官网搜索到的虚拟机的镜像： https://app.vagrantup.com/boxes/search： vagrant init 会在本地生成一个 Vagrantfile 文件。接着执行 vagrant up 时，Vagrant 读取当前目录下的 Vagrantfile 文件。然后会检查本地缓存是有 ubuntu/trusty64 镜像，如果没有则从 Vagrant 网站下载。\n最终，你就可以得到一个装有 ubuntu 的虚拟机了。\n接下来介绍 Vagrantfile。生成的 Vagrantfile 内容如下：\n# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure(\u0026#34;2\u0026#34;) do |config| config.vm.box = \u0026#34;ubuntu/trusty64\u0026#34; end 如果想自定义宿主机与虚拟机的网络、虚拟机的内存及 CPU 个数，Vagrantfile 可以修改成：\n# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure(\u0026#34;2\u0026#34;) do |config| config.vm.define \u0026#34;local-env-1\u0026#34; do |machine| machine.vm.box = \u0026#34;ubuntu/trusty64\u0026#34; machine.vm.hostname = \u0026#34;local-env-1\u0026#34; machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.33.11\u0026#34; machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node| node.name = \u0026#34;local-env-1\u0026#34; node.memory = 2048 node.cpus = 2 end end end 现实中，往往还会需要同时虚拟化出多台机器，以测试分布式。Vagrant 也是支持的，只要将以上单台的配置复制出并修改：\n# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure(\u0026#34;2\u0026#34;) do |config| machine_box = \u0026#34;ubuntu/trusty64\u0026#34; config.vm.define \u0026#34;local-env-1\u0026#34; do |machine| machine.vm.box = machine_box machine.vm.hostname = \u0026#34;local-env-1\u0026#34; machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.33.11\u0026#34; machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node| node.name = \u0026#34;local-env-1\u0026#34; node.memory = 2048 node.cpus = 2 end end config.vm.define \u0026#34;local-env-2\u0026#34; do |machine| machine.vm.box = machine_box machine.vm.hostname = \u0026#34;local-env-2\u0026#34; machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.33.12\u0026#34; machine.vm.network \u0026#34;forwarded_port\u0026#34;, guest: 26379, host: 26380 machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node| node.name = \u0026#34;local-env-2\u0026#34; node.memory = 2048 node.cpus = 2 end end end 在 local-env-2 机器中，我们设置机器的 IP，内存大小，CPU 个数，与宿主机器端口的映射。\nVagrantfile 使用的是 Ruby 语言写的。但是写 Vagrantfile 基本不需要 Ruby 知识。不懂的地方谷歌查一下就可以解决。\n另外，如果你真实环境的机器的操作系统比较特殊，这种情况下你就需要制作一个自定义的镜像了。这不在本文讨论范围。\nVagrant 常用命令有：\nvagrant up：启动虚拟机。 vagrant destroy：销毁虚拟机。 vagrant status：查看当前虚拟机的状态。 vagrant reload：修改 Vagrantfile 后重新启动虚拟机。 vagrant restart：重启虚拟机。 vagrant box list：列出当前机器已经缓存了的镜像。 最后需要提醒的是：目前 Vagrant 只能适配部分 VirtualBox 版本：4.0.x, 4.1.x, 4.2.x, 4.3.x, 5.0.x, 5.1.x, 5.2.x, and 6.0.x。建议整个开发团队统一一个版本。\n3. Ansible 的安装 Ansible 的安装根据你的开发机的操作系统分成两种情况：\n类 Linux 系统 Windows 系统 类 Linux 系统下安装 Ansible 如果你的开发机是一台 Linux 或 Mac 电脑，那么恭喜。你可以直接在开发机上安装并使用 Ansible。\nAnsible 是使用 Python 开发的。所以，开发机上必须安装 Python。Python 版本必须在 2.7 以上。P.S. 在命令行中输入 python --version 可了解当前版本。\n接着安装 Ansible：\ncurl https://bootstrap.pypa.io/get-pip.py -o get-pip.py sudo python get-pip.py sudo pip install --trusted-host mirrors.aliyun.com \\ --index-url= http://mirrors.aliyun.com/pypi/simple/ ansible==2.7.1 为安装指定版本的 Ansible，我们决定通过 pip 安装 Ansible。这样就可以保持生产环境与实验环境的 Ansible 的版本一致。\nWindows 系统下如何使用 Ansible 如果开发机是 Windows 机器，那么，笔者建议你放弃安装 Ansible。笔者尝试了 N 种在 Windows 上安装 Ansible 的办法，都以失败告终。\n但是，如果不使用图形界面的方式编辑 Ansible 脚本。我们生产力会受到影响。我们不可能在开发机编辑 Ansible 脚本，然后，再手工将脚本上传到测试机器，最后在测试机器上执行 ansible-playbook 命令进行调试。\n这个生产力问题，我们后文会解决。\n4. 文本编辑器准备 因为 Ansible 脚本的开发需要同时在多个文件夹及文件之间进行来回切换，所以文本编辑器至少需要以下3个基本功能：\n文件夹的树状视图。 快速搜索文件的。 语法高度。 笔者强烈推荐使用 VS Code，效果如下图： 了解 Java 开发的同学都知道。IDEA 和 Eclipse 都非常强大。能做到变量定义快速跳转、重构、自动补全等等。但是对于 Ansible 脚本开发，目前没有哪个编辑器有这些功能（反正笔者没有找到）。\n所以，笔者常常需要将屏幕分成多个块，以实现多个文件之间的对比，拷贝。VS Code 有分屏功能：\n另，Vagrantfile 是基于 Ruby 语言的。所以编辑 Vagrantfile 时，注意将文本语法高亮设置为 Ruby。在 VS Code 的右下角可以轻松设置： 使用 SFTP 扩展解决 Windows 下的生产力问题 笔者想到一个办法提升开发 Ansible 脚本的效率：那就是我们在本地编辑，然后内容自动同步到测试机器上。接着，我们就可以在测试机器上执行 ansible-playbook 命令。\n推荐 VS Code 编辑器有一个原因是它的扩展非常丰富。VS Code SFTP 扩展就可以实现我们的需求，如下图：\n安装扩展后，根据VS Code SFTP 扩展的使用说明配置即可使用。\n后记 在考虑企业效能提升时，也需要考虑开发人员在开发环境搭建方面的耗时。所以，以上所说的也应该做成自动化。当做到自动初始化开发环境后，你会发现它还会带来另一个好处：统一开发环境。\n相信不少人觉得统一开发环境没有必要。笔者觉得有必要，原因如下：\n从根本上避免了由于开发环境不同引起的没有必要沟通效率损耗 统一了所有的基本软件的版本（比如 Ansible 的版本）。有没有想过，某个同学使用了在本地安装 Ansible 的最新版本，谁知道生产环境使用的却是老版本的 Ansible。进而导致这们同学写的 Ansible 在生产环境下无法使用。 附录 VS Code：https://code.visualstudio.com/ Vagrant：https://www.vagrantup.com/ VirtualBox：https://www.virtualbox.org/ Vagrant 支持的 VirtualBox 的版本：https://www.vagrantup.com/docs/virtualbox/ ","permalink":"https://showme.codes/zh-cn/2019-11-7-ansible-dev-setup/","summary":"\u003cp\u003e通常我不喜欢写开发环境搭建类文章的，但是见到不少同学在 Ansible 的开发环境花了很多时间。所以，就想写这么一篇文章。希望能帮助到有需要的同学。\u003c/p\u003e\n\u003cp\u003e在介绍开发环境搭建之前，需要介绍 Ansible 脚本的开发流程。\u003c/p\u003e\n\u003cp\u003e不像普通的业务系统的开发，只需要打开 IDE 就可以写代码，然后调试了。当然 Ansible 脚本也可以进行单元测试，但是 Ansible 脚本还是需要真实运行并部署，才能验证脚本的正确性。\u003c/p\u003e\n\u003cp\u003e所以，Ansible 脚本的开发过程通常是这样的：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e启动一台虚拟机。\u003c/li\u003e\n\u003cli\u003e在开发机上编辑 Ansible 脚本。\u003c/li\u003e\n\u003cli\u003e在开发机上执行 \u003ccode\u003eansible-playbook -i hosts playbook.yml\u003c/code\u003e 命令。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-d7b06e6bd82792df.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\"\u003e\u003c/p\u003e\n\u003cp\u003e通过 Ansible 脚本的开发过程了解到，开发环境的搭建可以分成3部分：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e测试机器的准备。\u003c/li\u003e\n\u003cli\u003e文本编辑器的准备。\u003c/li\u003e\n\u003cli\u003eAnsible 的安装。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"1-测试机器的准备\"\u003e1. 测试机器的准备\u003c/h3\u003e\n\u003cp\u003e见到不少同学使用 Vmware 或 VirtualBox 手工创建虚拟机。这种方式是可以达到搭建测试机器的目的。但是笔者认为这样不够好。因为验证 Ansible 脚本，我们需要频繁创建新的虚拟机。手工创建虚拟机的效率太低。而且不利于版本控制。\u003c/p\u003e\n\u003cp\u003e所以，测试机器的准备，笔者使用的是 Vagrant。通过它，可以自动化创建和配置虚拟机。当然，整个过程还是版本控制的。\u003c/p\u003e\n\u003cp\u003e同时需要注意，Vagrant 本身并不是一个虚拟机的实现，它是基于 VirtualBox 和 Vmware 的。换句说就是我们可以通过 Vagrant 去控制 Vmware 和 VirtualBox。所以，在安装 Vagrant 的同时，也需要安装 VirtualBox 或 Vmware。本文使用 VirtualBox。\u003c/p\u003e\n\u003cp\u003eVagrant 和 VirtualBox 的具体安装在本文末有官方教程。\u003c/p\u003e\n\u003ch3 id=\"2-vagrant-介绍\"\u003e2. Vagrant 介绍\u003c/h3\u003e\n\u003cp\u003eVagrant 本身只是一个软件，提供了 \u003ccode\u003evagrant\u003c/code\u003e 命令。我们通过一个名为 Vagrantfile 的文件声明启动什么配置的虚拟机。\u003c/p\u003e","title":"Ansible 开发环境的搭建"},{"content":"在设计 DevOps 平台时，笔者认为版本号的管理是一个绕不开的课题。可是，行业里似乎很少人提这个事，笔者觉得要谈一谈，所以就有了这篇文章。\n一万个人的眼里有一万个“版本号” 笔者这三年在同一家公司里，换岗换了四个团队。团队的成员组成各异，有的团队都是在大型跨国企业跳槽过来的，有的团队大部人都是刚毕业的。\n每到一个团队，团队运行一段时间，都会做一件事情：讨论该怎么定义这个版本号。版本号的制定，有些只有开发人员参与，有时会有产品经理参与，有时还有 PMO 参与。\n经过这些讨论，我发现：一万人的眼里有一万个“版本号”。讨论的最后，基本上就是谁的嗓子大，听谁的。\n所以，在讨论“版本号”之前，一定要搞清楚讨论各方对于“版本号”的理解，再深入讨论，否则，大家谈的都会是牛头不对马嘴的东西。浪费时间。\n为什么对于“版本号”，各方的理解，差异会如此大。笔者认为，主要是因为他们关心的面不同。\nAPP产品经理关心的是该APP在用户界面上显示的版本号，比如当前爱彼迎的APP的版本号是：1.9.44.china。\n对于后端开发工程师，关心的是网关服务的版本是1.2.1、客服服务的版本是4.11.1。\n对于前端开发工程师，关心的是通用组件的版本是2.1.1、首页组件的版本是3.1.1。\n而对于 PMO，他们可能只关心在 Staging 环境的最后一个版本是否为一个稳定的版本（这写在他们的管理规范里），保证不影响测试人员的工作，根本不关心具体的“版本号”是多少。\n重新认识版本号 各方的关注点不同，不是问题，但是我们作为一个平台的设计必须对“版本号”有更深入的理解。\n笔者分析各方的关注点，他们所说的“版本号”分布在以下两个层面：\n技术层面：程序员关心线上跑的是哪份代码（对应的是Git\\SVN中的Commit ID）、运维关心线上跑是哪个版本（对应的就是具体哪个包）。 业务层面：方便终端用户识别的版本号，产品经理也属于这一层面。 认识到这点，我们设计DevOps平台，就会对两种版本号进行区别对待，进而设计出对团队非常有用的功能，最终帮助团队更好的实现交付。\n为方便沟通，技术层面的版本号，如 Commit ID 我们称为技术版本号，业务层面的版本号，称为业务版本号。\n版本号相关功能设计 但是版本号有什么用？仔细想想，除了产品经理发布时要定个版，后端服务的版本用于保证服务之间的相互引用或调用不出问题，就没有什么别的用处了。\n也许是因为大家都不了解版本号的用处，也或者是认为它根本就不值得讨论，所以，笔者在国内的几个大的平台都没有看到版本号的相关功能的设计。唯一使用到版本号的地方就是在制品库，部署时需要指定制品的版本号。而业务版本号与技术号之间的关系被隐藏得很深，用户很难查到。\n笔者不想一开始就谈它的好处。我直接上功能，下图是笔者臆想出来的。\n笔者认为，DevOps 平台应该有的功能之一：能输出这么一幅图，暂定名为版本关系图。图中的方块下，同时标有业务版本号和技术版本号。而图中的系统之间的连接线是应用系统的调用链，读者可忽略。\n版本关系图应该能提供以下信息：\n系统应用之间的版本依赖。 系统内部所依赖的组件的版本。 能根据某系统的版本查到目前直接依赖于或间接依赖于它的其他系统。 各系统的版本变迁信息。 这些信息能给用户带来的价值如下：\n团队内信息更透明，沟通效率更高，可以有效避免某个员工成为单点。你不必等其他成员，自己也可以得到整个系统的版本信息。 可以提高团队成员的排错能力，因为当A发布新版本后，APP 首页打开变慢，有了版本关系，我们可以首根据整个平台的“版本事件”来排查问题。同时，团队也很快可以找到相应的代码变更，然后进行 review 及修复。 上图中，当 A 服务是一个集群时，我们还可以将部署的目标机器与版本号关联起来了。这样，团队就可以轻松的知道，哪台机器部署了哪个版本。 上图只是整个业务系统的某个时间点的“快照”。事实上，我们还可以在版本号上做更大的文章。比如让技术版本号与代码质量、构建速度等过程指标关联起来，这样我们可以在不同的版本之间进行对比。再比如计算两个业务版本号之间，代码质量的差异，长期积累下来这些数据后，我们就有能力计算出代码质量与业务指标之间的关系。\n总的来说，版本号就是整个研发流程中的各项指标数据的枢纽。\n后记 版本号和其它数据的关系的价值，笔者认为被大大低估了。希望本文能给 DevOps 平台设计者带来不一样的想法。\n","permalink":"https://showme.codes/zh-cn/2019-11-6-devops-version-feature/","summary":"\u003cp\u003e在设计 DevOps 平台时，笔者认为版本号的管理是一个绕不开的课题。可是，行业里似乎很少人提这个事，笔者觉得要谈一谈，所以就有了这篇文章。\u003c/p\u003e\n\u003ch3 id=\"一万个人的眼里有一万个版本号\"\u003e一万个人的眼里有一万个“版本号”\u003c/h3\u003e\n\u003cp\u003e笔者这三年在同一家公司里，换岗换了四个团队。团队的成员组成各异，有的团队都是在大型跨国企业跳槽过来的，有的团队大部人都是刚毕业的。\u003c/p\u003e\n\u003cp\u003e每到一个团队，团队运行一段时间，都会做一件事情：讨论该怎么定义这个版本号。版本号的制定，有些只有开发人员参与，有时会有产品经理参与，有时还有 PMO 参与。\u003c/p\u003e\n\u003cp\u003e经过这些讨论，我发现：一万人的眼里有一万个“版本号”。讨论的最后，基本上就是谁的嗓子大，听谁的。\u003c/p\u003e\n\u003cp\u003e所以，在讨论“版本号”之前，一定要搞清楚讨论各方对于“版本号”的理解，再深入讨论，否则，大家谈的都会是牛头不对马嘴的东西。浪费时间。\u003c/p\u003e\n\u003cp\u003e为什么对于“版本号”，各方的理解，差异会如此大。笔者认为，主要是因为他们关心的面不同。\u003c/p\u003e\n\u003cp\u003eAPP产品经理关心的是该APP在用户界面上显示的版本号，比如当前爱彼迎的APP的版本号是：1.9.44.china。\u003c/p\u003e\n\u003cp\u003e对于后端开发工程师，关心的是网关服务的版本是1.2.1、客服服务的版本是4.11.1。\u003c/p\u003e\n\u003cp\u003e对于前端开发工程师，关心的是通用组件的版本是2.1.1、首页组件的版本是3.1.1。\u003c/p\u003e\n\u003cp\u003e而对于 PMO，他们可能只关心在 Staging 环境的最后一个版本是否为一个稳定的版本（这写在他们的管理规范里），保证不影响测试人员的工作，根本不关心具体的“版本号”是多少。\u003c/p\u003e\n\u003ch3 id=\"重新认识版本号\"\u003e重新认识版本号\u003c/h3\u003e\n\u003cp\u003e各方的关注点不同，不是问题，但是我们作为一个平台的设计必须对“版本号”有更深入的理解。\u003c/p\u003e\n\u003cp\u003e笔者分析各方的关注点，他们所说的“版本号”分布在以下两个层面：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e技术层面：程序员关心线上跑的是哪份代码（对应的是Git\\SVN中的Commit ID）、运维关心线上跑是哪个版本（对应的就是具体哪个包）。\u003c/li\u003e\n\u003cli\u003e业务层面：方便终端用户识别的版本号，产品经理也属于这一层面。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e认识到这点，我们设计DevOps平台，就会对两种版本号进行区别对待，进而设计出对团队非常有用的功能，最终帮助团队更好的实现交付。\u003c/p\u003e\n\u003cp\u003e为方便沟通，技术层面的版本号，如 Commit ID 我们称为\u003cstrong\u003e技术版本号\u003c/strong\u003e，业务层面的版本号，称为\u003cstrong\u003e业务版本号\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 id=\"版本号相关功能设计\"\u003e版本号相关功能设计\u003c/h3\u003e\n\u003cp\u003e但是版本号有什么用？仔细想想，除了产品经理发布时要定个版，后端服务的版本用于保证服务之间的相互引用或调用不出问题，就没有什么别的用处了。\u003c/p\u003e\n\u003cp\u003e也许是因为大家都不了解版本号的用处，也或者是认为它根本就不值得讨论，所以，笔者在国内的几个大的平台都没有看到版本号的相关功能的设计。唯一使用到版本号的地方就是在制品库，部署时需要指定制品的版本号。而业务版本号与技术号之间的关系被隐藏得很深，用户很难查到。\u003c/p\u003e\n\u003cp\u003e笔者不想一开始就谈它的好处。我直接上功能，下图是笔者臆想出来的。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-5ee21298d18bf722.png\"\u003e\u003c/p\u003e\n\u003cp\u003e笔者认为，DevOps 平台应该有的功能之一：能输出这么一幅图，暂定名为版本关系图。图中的方块下，同时标有业务版本号和技术版本号。而图中的系统之间的连接线是应用系统的调用链，读者可忽略。\u003c/p\u003e\n\u003cp\u003e版本关系图应该能提供以下信息：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e系统应用之间的版本依赖。\u003c/li\u003e\n\u003cli\u003e系统内部所依赖的组件的版本。\u003c/li\u003e\n\u003cli\u003e能根据某系统的版本查到目前直接依赖于或间接依赖于它的其他系统。\u003c/li\u003e\n\u003cli\u003e各系统的版本变迁信息。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这些信息能给用户带来的价值如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e团队内信息更透明，沟通效率更高，可以有效避免某个员工成为单点。你不必等其他成员，自己也可以得到整个系统的版本信息。\u003c/li\u003e\n\u003cli\u003e可以提高团队成员的排错能力，因为当A发布新版本后，APP 首页打开变慢，有了版本关系，我们可以首根据整个平台的“版本事件”来排查问题。同时，团队也很快可以找到相应的代码变更，然后进行 review 及修复。\u003c/li\u003e\n\u003cli\u003e上图中，当 A 服务是一个集群时，我们还可以将部署的目标机器与版本号关联起来了。这样，团队就可以轻松的知道，哪台机器部署了哪个版本。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e上图只是整个业务系统的某个时间点的“快照”。事实上，我们还可以在版本号上做更大的文章。比如让技术版本号与代码质量、构建速度等过程指标关联起来，这样我们可以在不同的版本之间进行对比。再比如计算两个业务版本号之间，代码质量的差异，长期积累下来这些数据后，我们就有能力计算出代码质量与业务指标之间的关系。\u003c/p\u003e\n\u003cp\u003e总的来说，版本号就是整个研发流程中的各项指标数据的枢纽。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e版本号和其它数据的关系的价值，笔者认为被大大低估了。希望本文能给 DevOps 平台设计者带来不一样的想法。\u003c/p\u003e","title":"谈 DevOps 平台设计：版本号相关功能的设计"},{"content":"背景 在大型企业内部，网络通常会被划分成多个不能直接访问的区域。比如本例中，网络被分成了内网和DMZ两个区域。出于安全的考虑，内网的机器不能直接访问外网。内网访问 DMZ 的机器、DMZ的机器要访问外网都需要单独提流程。\n但是，我们的应用能部署到 DMZ 区域中吗？答案是技术上不是问题，但是管理上不允许这样做。\n所以，在这样的大型企业内部，应用都会部署到内网中（本例中的A、B、C、D）。\n可是，总会有一些应用需要发 HTTP 请求到外网。比如实施DevOps平时，我们的应用需要从外网拉取依赖。\n这时，怎么办呢？本文就是为解决此问题而写。\n解决方案 最后的解决方案如下：\nPrivoxy 是一个HTTP 协议过滤代理。 Squid 是HTTP代理服务器软件。Squid用途广泛，可以作为缓存服务器，可以过滤流量帮助网络安全，也可以作为代理服务器链中的一环，向上级代理转发数据或直接连接互联网。 说实话，光看介绍，笔者一开始也一头雾水。不过，看完本文就应该知道它们的作用了。\n以下是方案具体实施步骤：\n在应用机器上设置全局环境变量： export http_proxy=http://192.168.1.100:3126 export https_proxy=http://192.168.1.100:3126 这一步的作用是将本机的 http 流量都代理到 192.168.1.100 的 3126 端口\n在 192.168.1.100 上安装 Privoxy。它的作用是根据配置，决定流量走哪个网络。本例中，它的作用是我们指定的http请求，走到 dmz。而其它的则和原来一样。它的配置如下： cat /etc/privoxy/config listen-address 192.168.1.100:3126 forward .abc.com/ 192.168.42.12:3127 listen-address 指 Privoxy 监听的IP和端口 forward 指接收到符合域名规则（.abc.com）的请求，将转发给 192.168.42.12 的 3127 端口。 到此，得到的效果就是当在应用机器访问 abc.com,admin.abc.com 等时，这些流量都会被 Privoxy 转发到 192.168.42.12 的 3127 端口。其它 HTTP 请求则不会。\n而 192.168.42.12 则是安装了 Squid 实现 HTTP 代理的机器的 IP。\n在 DMZ 区的机器上安装并配置 Squid。它的作用才是真正地将请求代理到外网。Squid 的配置样例如下： cat /etc/squid/squid.conf http_port 192.168.42.12:3127 cache_mem 64 MB maximum_object_size 4 MB cache_dir ufs /var/spool/squid 100 16 256 access_log /var/log/squid/access.log http_access allow all visible_hostname squid.demo 以上步骤，还要注意机器本身的防火墙策略。\n后记 在大型企业内部实施 DevOps 平台，还会遇到另一个网络问题，就是内部网络区域之间，也会有不通的情况，这种情况如何解决呢？留给下篇写吧。\n","permalink":"https://showme.codes/zh-cn/2019-11-4-devops-network/","summary":"\u003ch3 id=\"背景\"\u003e背景\u003c/h3\u003e\n\u003cp\u003e在大型企业内部，网络通常会被划分成多个不能直接访问的区域。比如本例中，网络被分成了内网和DMZ两个区域。出于安全的考虑，内网的机器不能直接访问外网。内网访问 DMZ 的机器、DMZ的机器要访问外网都需要单独提流程。\u003c/p\u003e\n\u003cp\u003e但是，我们的应用能部署到 DMZ 区域中吗？答案是技术上不是问题，但是管理上不允许这样做。\u003c/p\u003e\n\u003cp\u003e所以，在这样的大型企业内部，应用都会部署到内网中（本例中的A、B、C、D）。\u003c/p\u003e\n\u003cp\u003e可是，总会有一些应用需要发 HTTP 请求到外网。比如实施DevOps平时，我们的应用需要从外网拉取依赖。\u003c/p\u003e\n\u003cp\u003e这时，怎么办呢？本文就是为解决此问题而写。\u003c/p\u003e\n\u003ch3 id=\"解决方案\"\u003e解决方案\u003c/h3\u003e\n\u003cp\u003e最后的解决方案如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-062b26be43710a4e.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePrivoxy 是一个HTTP 协议过滤代理。\u003c/li\u003e\n\u003cli\u003eSquid 是HTTP代理服务器软件。Squid用途广泛，可以作为缓存服务器，可以过滤流量帮助网络安全，也可以作为代理服务器链中的一环，向上级代理转发数据或直接连接互联网。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e说实话，光看介绍，笔者一开始也一头雾水。不过，看完本文就应该知道它们的作用了。\u003c/p\u003e\n\u003cp\u003e以下是方案具体实施步骤：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在应用机器上设置全局环境变量：\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eexport\u003c/span\u003e \u003cspan class=\"nv\"\u003ehttp_proxy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003ehttp://192.168.1.100:3126\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eexport\u003c/span\u003e \u003cspan class=\"nv\"\u003ehttps_proxy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003ehttp://192.168.1.100:3126\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这一步的作用是将本机的 http 流量都代理到 192.168.1.100 的 3126 端口\u003c/p\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003e在 192.168.1.100 上安装 Privoxy。它的作用是根据配置，决定流量走哪个网络。本例中，它的作用是我们指定的http请求，走到 dmz。而其它的则和原来一样。它的配置如下：\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecat /etc/privoxy/config\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003elisten-address  192.168.1.100:3126\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eforward  .abc.com/  192.168.42.12:3127\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003elisten-address 指 Privoxy 监听的IP和端口\u003c/li\u003e\n\u003cli\u003eforward 指接收到符合域名规则（.abc.com）的请求，将转发给 192.168.42.12 的 3127 端口。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e到此，得到的效果就是当在应用机器访问 abc.com,admin.abc.com 等时，这些流量都会被 Privoxy 转发到 192.168.42.12 的 3127 端口。其它 HTTP 请求则不会。\u003c/p\u003e\n\u003cp\u003e而 192.168.42.12 则是安装了 Squid 实现 HTTP 代理的机器的 IP。\u003c/p\u003e","title":"谈DevOps平台实施：实现从内网拉取外网依赖的一种方案"},{"content":" 滚滚长江东逝水，浪花淘尽英雄。是非成败转头空。青山依旧在，几度夕阳红。—— 《临江仙》\n电脑店 从前，有一家电脑店。原来你即是老板，又是店员时，拿到清单，你就必须亲自动手采购，然后一个个零件组装。每天都做着即重复又辛苦的活。\n虽说你的组装技术已经很娴熟了，但是偶尔还发生装错的情况（大概是那天和老板娘吵架了），把一个客人要求的 CPU i5 装成了 CPU i7。结果是你亏本或者赚得少了。\n后来，你采购了一个自动组装电脑的机器人。你只要告诉它电脑的配置，并把零件放到指定的箱子中。接着启动这台机器，它就自动帮你组装好电脑了。它每天都干重复的活也不会叫辛苦。\n最重要的是准确，它不会因为心情不好，而装错。因为它根本不会闹情绪。\n这样，老板就可以从重复的工作解放出来。然后将多出来的时间花在与人的沟通上，为有不同需求的人设计更合适的电脑配置清单。毕竟游戏发烧友和办公小白领的需求是不一样的。\n在运维领域，不少运维人都干着即是老板又是店员的工作。如果在运维领域也能有这样的“机器人”该多好。事实上，Ansible、Puppet、Check 就是这样的机器人。\n为什么要从零设计一个运维机器人 本文并不想生硬地罗列 Ansible 的各个知识点。因为那样，大家不如直接看 Ansible 官方文档就好。\n笔者采用从零设计一个运维机器人的方式来告诉你，为什么 Ansible 会是现在这个样子。当然，现实中的 Ansible 不会像本文所写的那样一步步设计。\n为什么要这样呢？因为笔者觉得只有知道一个工具背后的设计原理，真正用这个工具才会得心应手。\n运维机器人的最终模样 首先，需要确定一下实现这个运维机器人的目的是什么。我们并不是希望所有的运维工作都交给运维机器人，而是希望运维工作中重复的那部分尽可能的交给机器人，把创造性的工作全部交给人。如下图所示。\n以终为始是一种非常有效的实现目标的思考模型。根据此思考模型，我们首先必须探讨运维机器人的最终模样。然后，再讨论可能的解决方案。\n那么，什么样的运维机器人能帮助我们实现上述的自动化运维目标呢？想像一下。是不是只要我们对着运维机器人说一句：“我要部署一个 Nginx 到 192.168.12.11”。它就可以帮我们完成了？\n但是它怎么知道如何连接到 192.168.12.11 呢？是使用用户名密码的方式，还是使用私钥？它又怎么知道 Nginx 需要什么样的配置呢？一问下来，其实，语音运维只适用于启动一些预定义的动作。就像汽车的一键启动。你不可能使用语音来对 Nginx 进行大量的配置。\n而纯文本才是进行大量配置的最好媒介。\n所以，运维机器人的最终模样是：我们将部署的主机 IP、登录方式、Nginx 的配置放在一个文本文件中，然后运维机器人读取这个文本文件，然后根据配置进行部署。如果部署的是业务系统，我们还需要准备该业务系统的二进制包。如下图所示。\n那么，我们在文本文件中使用何种语言描述我们的配置需求呢？可以分成两种。一种是利于人类学习的自然语言（如英语）。另一种是利于机器读取的结构化数据（如YAML、JSON）。\n按当前的技术实现的可能性，不论是运维机器人，还是交给其它程序，都需要将自然语言转到结构化的数据。就像程序员，需要将业务知识翻译成编程语言；像编译器将编程语言翻译成机器真正能识别的二进制代码。\n运维机器人的真正核心不是将自然语言转成结构化的数据。所以，文本文件中，我们直接写结构化数据。同时，我们决定使用 YAML 格式作为结构化数据的载体。因为它是非常流行的配置文件格式。降低人们写结构化数据的难度。\n当然，好的设计不应该与具体配置文件格式耦合。\n实现运维机器人要解决哪些问题 以上只是确定了运维机器人的最终模样及使用方式，解决的是用户的问题。但是因为我们是运维机器人的设计者，我们必须考虑如何实现它。\n回想一下，平时我们的运维人员是如何实现自动化的？是不是写好了 bash 脚本后，然后将脚本上传到目标机器，最后在目标机器上执行该脚本。\n这个 bash 脚本其实也可以算是一种结构化的数据格式，而且是一种不需要再做编译，目标机器能直接运行的格式。\n在平时的自动化方式基础上进行抽象。我们觉得要实现运维机器人要解决的关键问题有：\n需要将 YAML 转成目标机器可执行的程序（或脚本）。 需要将可执行的程序上传到目标机器，并执行。 为什么一定要将 YAML 转成目标机器可执行的程序呢？直接写 bash 不就可以了？\n因为运维机器人要解决的不止是 Linux 系统的自动化运维，还有 Windows，甚至路由器的。所以，我们需要使用一种独立于目标机器的语言来描述我们的运维需求。\n问题 2 我们先放一放，因为上传代码到目标不是运维机器人的关键问题。\n实现 YAML 转成 ？ 虽然咱们希望运维机器人能运维所有类型的机器，但是本文重点不是要在一篇文章内实现所有类型机器的自动化运维。接下我们只针对 Linux 机器进行讲解。\n现在我们遇到的问题是要将 YAML 转成什么程序以实现在目标机器上执行？\n如果我们将 YAML 转成 Java 程序，那么目标机器就必须装有 JDK，这是不现实的。你不可能让所有的 Linux 机器都安装 JDK。所以，YAML 最好是能转成所有 Linux 都支持程序。目前笔者能想到的就是：bash 和 Python。\nP.S. 现实中，很多运维工具要求所有的目标机器必须安装特定的客户端的。而 Ansible 却不需要。如果在使用自动化运维工具前，你要为所有的机器安装特定的客户端，那么，你怎么自动化为所有的机器自动安装客户端呢？留着读者思考。\n从 bash 和 Python 之间做选择，没有什么好讨论的，选择 Python。\n那么，怎么将 YAML 转成 Python 代码呢？这要看我们怎么设计 YAML 内的描述语言了。其实就是设计自动化运维机器人的领域特定语言（DSL）。为方便讨论，我们称之为：OPL 语言。\nOPL 语言1.0：基本要素 现在咱们在为一台机器部署一个 Nginx 作为切入点，来设计我们的 OPL 语言。以下就是1.0版本：\n--- - host: \u0026#34;192.168.35.10\u0026#34; ansible_ssh_user: vagrant ansible_ssh_pass: vagrant tasks: - name: install nginx yum: name: nginx action: install host：代表部署的目标机器。 ansible_ssh_*：开头的 key 是指定 ssh 连接的用户名和密码。 tasks：是一个数组，包含一系列任务。 name：任务名称，方便人阅读。 yum：要执行的任务的类型，也就是要执行 yum 操作。yum 任务下的 name 属性代表要安装的软件名称，action 属性代表执行的是安装操作。 1.0版的 OPL 语言包含了运维领域最基本要素：\n目标主机的描述 连接目标主机的必要信息 任务描述 为方便沟通，我们暂将些 YAML 文件命名为 playbook.yml。\nOPL 语言2.0：采用声明式的任务描述方式 注意到 OPL 语言1.0中的任务描述方式很像现实中执行shell：yum install nginx。我们称这种方式为脚本式的。脚本式的描述方式与传统的运维方式更像。\n但是声明式的描述方式更适合 OPL 语言所要解决的问题。我们期望的是描述我们期望的结果，而不是换一种方式写脚本。\n采用声明式的描述方式，还有一个很重要的原因：幂等的。在概念上，声明式的描述被执行了多少次，结果都应该是我们所声明的。而第二次执行脚本式的代码时，你会问，结果还会和我第一次执行的一样吗？\n所以，2.0中，任务描述方式改成声明式：\n- name: Ensure nginx present yum: name: nginx state: present state 属性的值为 present 形容词。\nP.S. 识别声明式与脚本式的简单办法是看它在描述目标状态时，是用动词，还是用形容词。\n除了 yum 任务，接下来要实现的所有任务也将采用声明式。\nOPL 语言3.0：主机清单 在 2.0 版本中，我们只实现了一台目标机器的部署。但是现实中，我们常常要针对多主机进行部署。我们需要一种更好的方式去描述目标主机。\n主机清单的内容，也需要一个文件来存放。关于文件的格式，我们采用一种类 INI （INI-like）的格式。文件名暂命名为：inventory。以下是 inventory 文件的内容：\n[nginx] 192.168.35.10 192.168.35.11 192.168.35.12 [springboot] 192.168.35.20 192.168.35.21 192.168.35.22 [] 括号中是主机分组的名称，接下来是就是这个组内目标机器的 IP 列表。\n而上一版本中的 playbook.yml 中的 host key 是单数，所以不合适了。我们改成 hosts 复数，同时值变成了在主机清单中的组名，而不是具体某台主机的 IP。\nplaybook.yml 文件内容如下：\n- hosts: \u0026#34;nginx\u0026#34; ansible_ssh_user: vagrant ansible_ssh_pass: vagrant tasks: - name: install nginx yum: name: nginx state: present 细心的读者朋友应该发现了，目标机器的连接方式是各不相同的，有些用用户密码的方式，有些用密钥的方式。所以，我们再将 ansible_ssh_* 写在 playbook.yml 中已经不合适了。\n笔者只能告诉你，这是剧情需要。OPL 语言的后续版本会解决此问题。\n现在咱们为运维机器人准备的内容已经不再只是一个 playbook.yml 文件，它应该是一个目录了，目录内容如下：\n├── inventory └── playbook.yml OPL 语言4.0：include 任务 在 OPL 1.0 版本，已经考虑到了一次部署将会包括很多子任务。所以使用了 tasks 这个复数词。\n而现实中一次部署任务往往包含几十个子任务，playbook.yml 的文件内容一定会膨胀。这样的源代码非常难维护。所以，在 4.0 版本，我们决定为 OPL 增加一个 include_tasks 任务类型。用户可以通过 include_tasks 任务类型将另一个包含任务描述的文件（这里我们称为子任务集）引入到当前的 playbook.yml。\n- hosts: \u0026#34;nginx\u0026#34; tasks: - name: config firewalld include_tasks: firewalld.yml - name: install and config nginx include_tasks: nginx.yml 这时的目录结构如下：\n├── firewalld.yml ├── inventory ├── nginx.yml └── playbook.yml 所有的子任务文件都与 playbook.yml 同级，会显得很乱，不利于区分哪个文件是 OPL 的执行入口。我们是不是可以建了一个 tasks 目录来专门存放呢？事实上就应该这么做。\n所以，经过重构，得到了4.1 版本。目录结构调整如下：\n├── inventory ├── playbook.yml └── tasks ├── firewalld.yml └── nginx.yml playbook.yml 中 include_tasks 任务的文件路径做相应的调整，改成：\n- name: install and config nginx include_tasks: tasks/nginx.yml OPL 语言5.0：丰富任务类型 5.0 之前的版本，已经实现了一个基本框架。5.0 版本中我们希望加入更多的任务类型，以满足不同的运维需求。\ncopy 任务 部署过程中，常常需要将一些文件从本地 copy 到目标机器。copy 任务的代码样例如下：\n- name: \u0026#34;ensure nginx package exists\u0026#34; copy: src: \u0026#34;./files/nginx.tar.gz\u0026#34; dest: \u0026#34;/tmp\u0026#34; 为方便管理，我们将所有 copy 任务用到的文件放在 files 目录中。此时目录结构调整如下：\n├── inventory ├── playbook.yml └── tasks ├── files │ └── nginx.tar.gz ├── firewalld.yml └── nginx.yml file 任务 设置文件夹的权限是非常常见的操作，所以就有了 file 任务。\n- name: \u0026#34;ensure folder /app/nginx is created\u0026#34; file: path: \u0026#34;/app/nginx\u0026#34; owner: \u0026#34;nginx\u0026#34; group: \u0026#34;nginx\u0026#34; mode: \u0026#34;0700\u0026#34; state: \u0026#34;directory\u0026#34; owner：指定文件的所属用户。 group：指定文件的所属用户组。 state 属性的值可以为： absent：不存在。可以理解为删除该文件或文件夹。 directory：文件夹。如果该文件夹不存在，则创建。 file：文件。如果不存在，则创建。 touch：与 linux 的 touch 实现相同的效果。 service 任务 在服务安装完成后，最常用的操作就是启动服务了。同时，它会根据不同的操作决定使用何种 service 实现。支持：BSD init, OpenRC, SysV, Solaris SMF, systemd, upstart。这就是封装的强大。用户只需要描述他的期望，剩下的机器能解决的，都由机器解决。\n- name: ensure svn service started service: name: svnserver state: started enabled: true enabled 属性值为 true 代表开机自动启动。state 属性值可以为：\nreloaded：服务是被重新加载过的。 restarted：服务是被重启过的。 started：服务是启动的。 stopped：服务是停止的。 小结 如果 OPL 语言设计得足够好，它应该可以轻松地进行扩展。此处举的几个例子已经达到目的，就不再举更多的例子。\nP.S. OPL 语言的子任务在 Ansible 中称为模块（module）。\nOPL 语言6.0：模块化任务 在 5.0 版本中，我们为 OPL 增加了一些的任务类型。在写了一段时间 OPL 语言后，发现采用 include_tasks 对大规模 playbook.yml 进行拆分的方式，设计上的存在不足：不够内聚。具体表现如下：\ninclude_tasks 任务不利于分享给其他人使用。 nginx.yml 中的 copy 任务中，我们约定从 files 目录中读取文件。但是其它子任务中的 copy 又从哪里读取文件呢？这就是子任务之间会相互影响。 那么如何让子任务更内聚呢？将“子任务”的集合进行封装，并命名为 role。这样每个 role 都被看作成一个模块。\n目录结构调整为：\n├── inventory ├── playbook.yml └── roles ## 存放 playbook 使用到的 role ├── firewalld │ ├── files │ └── tasks │ └── main.yml └── nginx ├── files │ └── nginx.tar.gz └── tasks └── main.yml playbook.yml 内容调整如下：\n- hosts: \u0026#34;nginx\u0026#34; ansible_ssh_user: vagrant ansible_ssh_pass: vagrant roles: - firewalld - nginx 每个 role 只需要管理自己内部的逻辑，比如，每个 role 都会有一个 files 目录。copy 任务默认从所在 role 目录中的 files 目录中读取。上文介绍的 copy 任务（注意 src 属性值）改为：\n- name: \u0026#34;ensure nginx package exists\u0026#34; copy: src: \u0026#34;nginx.tar.gz\u0026#34; dest: \u0026#34;/tmp\u0026#34; 今后设计的所有任务类型默认都从 role 自身目录开始。\n每个 role 的执行入口约定为 tasks/main.yml。include_tasks 任务仍然可以使用，只不过，默认从 main.yml 同级目录获取子任务集合。比如 nginx/tasks/main.yml 包含任务：\n- name: \u0026#34;Config nginx\u0026#34; includes_tasks: config.yml nginx role 的 tasks 目录内容如下：\n└── nginx ├── files │ └── nginx.tar.gz └── tasks ├── config.yml └── main.yml OPL 语言7.0：支持变量 6.0 版本中，我们如何将 role 分享给其他人使用呢？目前能想到的成本最低的方式是直接将 role 目录拷贝一份，并 push 到 GitHub 上。其他人将 role 下载到自己的 roles 目录即可。\n可是，其他人下载 role 后，也需要查看 role 的内部逻辑，然后修改其中的逻辑，才能为自己所用。因为并不是每个人的 nginx 配置都是一样的。这说明咱们当的 OPL 的设计违反了程序设计的开闭原则：\n开闭原则规定“软件中的对象（类，模块，函数等等）应该对于扩展是开放的，但是对于修改是封闭的”，这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。 —— 维基百科\n那如何设计才能符合开闭原则呢？方法是将经常变化的与基本不用变化的逻辑分离。\n以上文中的 nginx role 为例讲解。nginx 的安装部署，整个过程对于所有人来说都是大体相同的。不相同的是 nginx 的配置。\nOPL 语言7.0版本将这些“配置”抽象出来，也就是变量。role 根据自身需要在 role 内部定义变量，用户在 role 外部可重新设置变量的值，即可定义 role 中的变化的部分。比如重新定义 nginx 的配置。\n那么，具体如何定义及使用变量呢？\n区分默认变量和用户定义变量 对于“具体如何定义及使用变量呢”的问题，我们第一步是要将默认变量和用户定义变量区分开。\n以上文的 copy 子任务为例：\n- name: \u0026#34;ensure nginx package exists\u0026#34; copy: src: \u0026#34;nginx.tar.gz\u0026#34; dest: \u0026#34;/tmp\u0026#34; copy 的目的地的属性为 dest，它的值是“写死”的。但是并不是所有人都希望 copy 到 /tmp 目录。/tmp 目录是 role 本身的默认值，如果用户不满意这个默认值，可以在使用 role 时，修改 dest 的值。\n这又引出另一个问题：role 内部如何定义默认值？\n在 role 目录下，我们新建一个 defaults 目录，并放一个 main.yml 。defaults/main.yml 文件内容定义变量的默认值。role 的目录结构调整为：\n├── defaults │ └── main.yml ├── files │ └── nginx.tar.gz └── tasks ├── config.yml └── main.yml nginx/defaults/main.yml 的内容如下：\n{% raw %}\n--- nginx_package_tmp_dir: \u0026#34;/tmp\u0026#34; # 其它变量，此处省略 copy 子任务的描述改成：\n- name: \u0026#34;ensure nginx package exists\u0026#34; copy: src: \u0026#34;nginx.tar.gz\u0026#34; dest: \u0026#34;{{ nginx_package_tmp_dir }}\u0026#34; {{ }} 是变量的占位符。nginx_package_tmp_dir 会被实际值所替换。 {% endraw %}\ntemplate 子任务 nginx 的配置是一个 nginx.conf 文件。nginx role 中的 nginx.conf 应该是一个模板，模板内的变量占位符会被变量实际值替换。模板引擎我们选择：Jinja2，一个 Python 的模板引擎。\n这样想来，我们需要一个新的子任务类型 template：\n- name: ensure nginx config template: src: \u0026#34;nginx.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf/nginx.conf\u0026#34; # 目标路径 与 copy 任务类似，从哪里拿 nginx.conf 模板文件呢？为区分模板文件与一般文件，我们在 role 目录下创建一个 templates 目录。最终 nginx role 的目录结构看起来是这样的：\n├── defaults │ └── main.yml ├── files │ └── nginx.tar.gz ├── tasks │ ├── config.yml │ └── main.yml └── templates └── nginx.conf nginx.conf 部分内容如下：\n## 省略 server { location / { proxy_pass http://127.0.0.1:{{apps_port}}/; } } {% raw %} 文件中 {{apps_port}} 最终会被实际变量的值替换。 {% endraw %}\n在哪里定义变量？ 目前所有的默认变量值都定义在 defaults/main.yml 中，那么，role 的使用者如何定义自己的变量值呢？\n我们支持多种方式：\nplaybook.yml 文件中 playbook.yml 文件中，又分为两种情况：1. 在 roles 级别；2. 在 playbook 级别的。代码样例如下：\n--- - hosts: \u0026#34;nginx\u0026#34; ## 1. playbook 级别变量 vars: apps_port: 9090 roles: - firewalld ## 2. roles 级别变量 - { role: nginx, apps_port: 9091 } inventory 文件中 针对每台机器，同一变量可能需要设置成不同的值。\n[springboot] 192.168.35.20 apps_port=9091 ansible_ssh_user=vagrant ansible_ssh_pass=vagrant 192.168.35.21 apps_port=9092 ansible_ssh_user=vagrant1 ansible_ssh_pass=vagrant1 192.168.35.22 apps_port=9093 ansible_ssh_user=vagrant2 ansible_ssh_pass=vagrant2 在 OPL3.0 版本中的遗留问题，终于解决了。ansible_ssh_* 类的变量可以关联到具体某台机器了。也就是每台主机的 ansible_ssh_* 的值都可以是不一样的。\n命令行转入 在命令行中加入参数：--extra-vars 即可。\nansible-playbook release.yml --extra-vars \u0026#34;version=1.23.45 other_variable=foo\u0026#34; 当变量可以在多处定义时，随之而来的就是变量的优先级问题。变量的优先级应该如何设计呢？问题留给读者思考。\nOPL 语言8.0：支持条件判断 8.0 版本决定加入条件判断：when。用法如下：\n- name: Ensure nginx exists yum: name: nginx state: present when: ansible_os_family == \u0026#34;CentOS\u0026#34; 当目标机器不是 CentOS 时，执行 yum 操作一定是失败的。所以，只有 ansible_os_family == \u0026quot;CentOS\u0026quot; 为 true 时才执行该子任务。\n上例中，只是单条件。如果多条件呢？如何实现“与”和“或”呢？\n“与”的示例如下：\n- name: \u0026#34;shut down CentOS 6 and Debian 7 systems\u0026#34; command: /sbin/shutdown -t now when: ansible_distribution == \u0026#34;CentOS\u0026#34; and ansible_distribution_major_version == \u0026#34;6\u0026#34; 注：command 是一个执行命令的子任务类型。\n“与”还有另一种写法：\n- name: \u0026#34;shut down CentOS 6 systems\u0026#34; command: /sbin/shutdown -t now when: - ansible_distribution == \u0026#34;CentOS\u0026#34; - ansible_distribution_major_version == \u0026#34;6\u0026#34; “或”的示例如下：\n- name: \u0026#34;shut down CentOS 6 and Debian 7 systems\u0026#34; command: /sbin/shutdown -t now when: ansible_distribution == \u0026#34;CentOS\u0026#34; or ansible_distribution == \u0026#34;Debian\u0026#34; ) 当然，你可以结合“与”和“或”来使用：\n- name: \u0026#34;shut down CentOS 6 and Debian 7 systems\u0026#34; command: /sbin/shutdown -t now when: (ansible_distribution == \u0026#34;CentOS\u0026#34; and ansible_distribution_major_version == \u0026#34;6\u0026#34;) or (ansible_distribution == \u0026#34;Debian\u0026#34; and ansible_distribution_major_version == \u0026#34;7\u0026#34;) 你可以把 when 想像成编程语言中的 if语句。它与具体的子任务的类型无关，任务子类型下都可以使用 when。\nOPL 语言8.1：支持遍历 当某个任务需要根据数组中的数据进行重复执行时，OPL 语言就要考虑支持遍历了。 {% raw %}\n- name: Ensure soft exists yum: name: \u0026#34;{{ item }}\u0026#34; state: present when: ansible_os_family == \u0026#34;CentOS\u0026#34; with_items: - gcc - gcc-c++ 可以将 with_items 理解成编程语言中的 for语句。{{ item }} 中的 item 约定代表遍历的元素。 {% endraw %}\nOPL 语言9.0：子任务支持返回值 用户在使用 OPL 语言时，写出以下代码：\n- name: ensure nginx config template: src: \u0026#34;nginx.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf\u0026#34; - name: restart nginx service: name: nginx state: restarted 代码问题出现在哪里呢？它不是幂等的。不论 nginx.conf 文件是否有更新，每执行一次 OPL 它都会重启一次 nginx 服务。\n我们期望的是当 nginx.conf 有更新时才执行启动 nginx 服务。\n我们应该如何设计 OPL 以规避此类问题呢？\n我们从问题本身开始。当子任务设计完成后，我们需要根据子任务的执行结果去执行另一个子任务。用通用编程语言来表达，很简单：\nboolean changed = template(\u0026#34;nginx.conf\u0026#34;, \u0026#34;/usr/local/nginx/conf\u0026#34;) if(changed){ service(\u0026#34;nginx\u0026#34;,\u0026#34;restarted\u0026#34;) } 在通用编程语言中，我们很容易就实现的功能，在 YAML 中如何实现呢？其实类似的，让所有的子任务支持返回值，然后在后续子任务做判断就好了。代码如下：\n- name: ensure nginx config template: src: \u0026#34;nginx.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf\u0026#34; register: nginx_config_result - name: restart nginx service: name: nginx state: restarted when: nginx_config_result.changed register 是新的语句，用于定义子任务返回结果名称。nginx_config_result 是一个对象，其中 changed 就是它的属性。changed 为 true 时，代表 ensure nginx config 子任务有变化。\nOPL 语言9.1：支持延迟处理 在使用 9.0 版本一段时间后，用户开始报怨以下代码写起来太哆嗦，而且容易出错：\n- name: ensure nginx config template: src: \u0026#34;nginx.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf\u0026#34; register: nginx_conf_result - name: ensure nginx upstream config template: src: \u0026#34;upstream.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf/upstream.conf\u0026#34; register: nginx_upstream_result - name: ensure nginx vhosts config template: src: \u0026#34;vhosts.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf/vhosts.conf\u0026#34; register: nginx_vhosts_result - name: restarted nginx service service: name: nginx state: restarted when: nginx_conf_result.changed or nginx_upstream_result.changed or nginx_vhosts_result.changed 用户写成这么哆嗦的语句，并不是用户的问题。而是我们的设计有没考虑到的地方。也就是有些任务是需要被另外一些任务触发执行的。我们当前不支持此种场景。\n为实现此类场景，需要做两件事情：\n主动触发子任务，使用 notify 语句指定要触发的另一个任务的任务名。 集中保存被动触发的任务，以区分主动执行的任务和被动执行的任务。约定被动触发的任务放在 role 目录下的 handlers/main.yml 文件。 接下来，我们具体看下如何重构以上哆嗦的写法。\n第一步，对 nginx role 目录进行调整：\n├── defaults │ └── main.yml ├── files │ └── nginx.tar.gz ├── handlers │ └── main.yml ├── tasks │ ├── config.yml │ └── main.yml └── templates └── nginx.conf 第二步，重构 tasks/config.yml：\n- name: ensure nginx config template: src: \u0026#34;nginx.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf\u0026#34; notify: - restart nginx - name: ensure nginx upstream config template: src: \u0026#34;upstream.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf/upstream.conf\u0026#34; notify: - restart nginx - name: ensure nginx vhosts config template: src: \u0026#34;vhosts.conf\u0026#34; dest: \u0026#34;/usr/local/nginx/conf/vhosts.conf\u0026#34; notify: - restart nginx 第三步，向 handlers/main.yml 文件加入被动执行的任务：\n--- - name: restart nginx service: name: nginx state: restarted OPL 语言之后 以上只是 OPL 语言的雏形，我们还需要根据现实的情况不断的扩展和完善。OPL 语言的设计只是实现运维机器人的一部分工作。我们还需要做的工作包括：实现一个程序，它会对 OPL 语言进行编译。并将编译后的 python 脚本上传到目标服务器。python 脚本运行后，这个程序将运行结果反馈给用户。\n这个程序就是我们在命令行中敲入的 ansible-playbook 了。\n至于 ansible-playbook 的更多细节，已不属于本文的内容，不作讨论。\n总结 本文首先从电脑店引出现代自动化运维工具的基本模型。接着讨论应该如何设计一款自动化运维工具（上文称为运维机器人）。从而得知需要解决两个关键问题：1. 需要将 YAML 转成目标机器可执行的程序（或脚本）；2. 需要将可执行的程序上传到目标机器，并执行。然后我们花了大篇幅介绍 OPL 语言的设计（实际上是介绍 Ansible 的 YAML 为什么要像现在这样写）。最后简单介绍 OPL 语言后要做的事情。\n老实说，以上并不是真正意义上的从零设计 Ansible。也不存在什么 OPL 语言。笔者只是希望通过 OPL 语言的设计过程，尝试让读者了解 Ansible 的设计思路。期望读者因为本文，学习 Ansible 变得更轻松。\n最后，文末有 OPL 语言各版本的样例。\n附 从零设计 Ansible 代码样例：https://github.com/zacker330/design-ansible Jinja2 模板语言：http://docs.jinkan.org/docs/jinja2/ ","permalink":"https://showme.codes/zh-cn/2019-09-19-understand-ansible/","summary":"\u003cblockquote\u003e\n\u003cp\u003e滚滚长江东逝水，浪花淘尽英雄。是非成败转头空。青山依旧在，几度夕阳红。—— 《临江仙》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"电脑店\"\u003e电脑店\u003c/h3\u003e\n\u003cp\u003e从前，有一家电脑店。原来你即是老板，又是店员时，拿到清单，你就必须亲自动手采购，然后一个个零件组装。每天都做着即重复又辛苦的活。\u003c/p\u003e\n\u003cp\u003e虽说你的组装技术已经很娴熟了，但是偶尔还发生装错的情况（大概是那天和老板娘吵架了），把一个客人要求的 CPU i5 装成了 CPU i7。结果是你亏本或者赚得少了。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-68214cb605b8f715.png\"\u003e\u003c/p\u003e\n\u003cp\u003e后来，你采购了一个自动组装电脑的机器人。你只要告诉它电脑的配置，并把零件放到指定的箱子中。接着启动这台机器，它就自动帮你组装好电脑了。它每天都干重复的活也不会叫辛苦。\u003c/p\u003e\n\u003cp\u003e最重要的是准确，它不会因为心情不好，而装错。因为它根本不会闹情绪。\u003c/p\u003e\n\u003cp\u003e这样，老板就可以从重复的工作解放出来。然后将多出来的时间花在与人的沟通上，为有不同需求的人设计更合适的电脑配置清单。毕竟游戏发烧友和办公小白领的需求是不一样的。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-6e0381758051d19d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在运维领域，不少运维人都干着即是老板又是店员的工作。如果在运维领域也能有这样的“机器人”该多好。事实上，Ansible、Puppet、Check 就是这样的机器人。\u003c/p\u003e\n\u003ch2 id=\"为什么要从零设计一个运维机器人\"\u003e为什么要从零设计一个运维机器人\u003c/h2\u003e\n\u003cp\u003e本文并不想生硬地罗列 Ansible 的各个知识点。因为那样，大家不如直接看 Ansible 官方文档就好。\u003c/p\u003e\n\u003cp\u003e笔者采用从零设计一个运维机器人的方式来告诉你，为什么 Ansible 会是现在这个样子。当然，现实中的 Ansible 不会像本文所写的那样一步步设计。\u003c/p\u003e\n\u003cp\u003e为什么要这样呢？因为笔者觉得只有知道一个工具背后的设计原理，真正用这个工具才会得心应手。\u003c/p\u003e\n\u003ch2 id=\"运维机器人的最终模样\"\u003e运维机器人的最终模样\u003c/h2\u003e\n\u003cp\u003e首先，需要确定一下实现这个运维机器人的目的是什么。我们并不是希望所有的运维工作都交给运维机器人，而是希望运维工作中重复的那部分尽可能的交给机器人，把创造性的工作全部交给人。如下图所示。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-00c4ed1733233e40.png\"\u003e\u003c/p\u003e\n\u003cp\u003e以终为始是一种非常有效的实现目标的思考模型。根据此思考模型，我们首先必须探讨运维机器人的最终模样。然后，再讨论可能的解决方案。\u003c/p\u003e\n\u003cp\u003e那么，什么样的运维机器人能帮助我们实现上述的自动化运维目标呢？想像一下。是不是只要我们对着运维机器人说一句：“我要部署一个 Nginx 到 192.168.12.11”。它就可以帮我们完成了？\u003c/p\u003e\n\u003cp\u003e但是它怎么知道如何连接到 192.168.12.11 呢？是使用用户名密码的方式，还是使用私钥？它又怎么知道 Nginx 需要什么样的配置呢？一问下来，其实，语音运维只适用于启动一些预定义的动作。就像汽车的一键启动。你不可能使用语音来对 Nginx 进行大量的配置。\u003c/p\u003e\n\u003cp\u003e而纯文本才是进行大量配置的最好媒介。\u003c/p\u003e\n\u003cp\u003e所以，运维机器人的最终模样是：我们将部署的主机 IP、登录方式、Nginx 的配置放在一个文本文件中，然后运维机器人读取这个文本文件，然后根据配置进行部署。如果部署的是业务系统，我们还需要准备该业务系统的二进制包。如下图所示。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-726fa3270e9fba14.png\"\u003e\u003c/p\u003e\n\u003cp\u003e那么，我们在文本文件中使用何种语言描述我们的配置需求呢？可以分成两种。一种是利于人类学习的自然语言（如英语）。另一种是利于机器读取的结构化数据（如YAML、JSON）。\u003c/p\u003e\n\u003cp\u003e按当前的技术实现的可能性，不论是运维机器人，还是交给其它程序，都需要将自然语言转到结构化的数据。就像程序员，需要将业务知识翻译成编程语言；像编译器将编程语言翻译成机器真正能识别的二进制代码。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-6e902b854db65914.png\"\u003e\u003c/p\u003e\n\u003cp\u003e运维机器人的真正核心不是将自然语言转成结构化的数据。所以，文本文件中，我们直接写结构化数据。同时，我们决定使用 YAML 格式作为结构化数据的载体。因为它是非常流行的配置文件格式。降低人们写结构化数据的难度。\u003c/p\u003e\n\u003cp\u003e当然，好的设计不应该与具体配置文件格式耦合。\u003c/p\u003e\n\u003ch2 id=\"实现运维机器人要解决哪些问题\"\u003e实现运维机器人要解决哪些问题\u003c/h2\u003e\n\u003cp\u003e以上只是确定了运维机器人的最终模样及使用方式，解决的是用户的问题。但是因为我们是运维机器人的设计者，我们必须考虑如何实现它。\u003c/p\u003e\n\u003cp\u003e回想一下，平时我们的运维人员是如何实现自动化的？是不是写好了 bash 脚本后，然后将脚本上传到目标机器，最后在目标机器上执行该脚本。\u003c/p\u003e\n\u003cp\u003e这个 bash 脚本其实也可以算是一种结构化的数据格式，而且是一种不需要再做编译，目标机器能直接运行的格式。\u003c/p\u003e\n\u003cp\u003e在平时的自动化方式基础上进行抽象。我们觉得要实现运维机器人要解决的关键问题有：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e需要将 YAML 转成目标机器可执行的程序（或脚本）。\u003c/li\u003e\n\u003cli\u003e需要将可执行的程序上传到目标机器，并执行。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e为什么一定要将 YAML 转成目标机器可执行的程序呢？直接写 bash 不就可以了？\u003c/p\u003e","title":"这样理解Ansible更容易"},{"content":" 注：Dao 在不同语言中的叫法可能不一样。Dao 可以理解为对数据进行持久化的具体实现。\n关于实体的保存，笔者知道行业内有两种方式：\ndogDao.save(dog); dog.save(); 相信不少同学，现实中，通常使用第一种，很少见到第二种写法。\n为了让大家站在同一个讨论上下文，笔者决定贴出更详细的代码。\n注：以下代码会省略很多本文不相关的代码，比如数据校验。读者朋友不必太纠结。\n第一种写法：save 方法写在 Dao 上 Dog dog = new Dog() dog.setName(\u0026#39;didi\u0026#39;); dog.setColor(\u0026#39;white\u0026#39;); dogDao.save(dog); 第二种写法：save 方法写在实体上 // DogService.java Dog dog = new Dog() dog.setName(\u0026#39;didi\u0026#39;); dog.setColor(\u0026#39;white\u0026#39;); dog.save(); 这两种写法，有什么区别吗？事实上，从字面上看，没有什么区别。因为 Dog 类的 save() 方法是这样实现：\n// Dog.java public void save(){ dogDao.save(this); } 但是从抽象的角度来看，就不一样了。\n假如你的项目中存在这样的抽象： 如果采用第一种写法，意味着，每多出多一种 Animal，我们就必须多写一套 Service。Service 中会很多这样的方法：\n// DogService.java void save(Dog dog){ dogDao.save(dog); } // FishService.java void save(Fish fish){ fishDao.save(fish); } // BirdService.java void save(Bird bird){ birdDao.save(bird); } 相信大家对于以上代码并不陌生。\n但是如果采用第二种方法就不一样了，不论 Animal 新增多少种子类，只需要一个 Service，并只需要一个方法：\n// AnimalService.java void saveAnimal(Animal animal){ animal.save(); } 相对第一种方法，第二种方法更内聚，更简洁。\n如何更优雅将前端传过来的数据结构转成抽象类？ 采用第一种方法，我们还必须在 Controller 上，类似于 Service 也要针对不同的子类写不同的方法。所以，会出现 N 个接口：\n// AnnimalController.java @PostMapping(\u0026#34;/dog\u0026#34;) public String saveDog(@RequestBody Dog dog){ dogService.save(dog); } @PostMapping(\u0026#34;/fish\u0026#34;) public String saveFish(@RequestBody Fish fish){ fishService.save(fish); } @PostMapping(\u0026#34;/bird\u0026#34;) public String saveBird(@RequestBody Bird bird){ birdService.save(bird); } 这不是我们想要的。我们更希望类似这样的：\n// AnnimalController.java @PostMapping(\u0026#34;/animal\u0026#34;) public String saveDog(@RequestBody Animal animal){ animal.save(); } 也就是说，每次 Animal 新增子类，我都不用动 Controller 。但是 Controller 并不会自动将前端传入的 JSON 结构转换成 Animal 抽象类。\n这个问题的关键点在于序列化过程，我们是否可以定制。换句个说法，就是定制 JSON 结构转成对象的过程，我们就可以将前端的 JSON 数据转成子类，再赋值给 Animal 抽象类了。\n笔者使用的是 Spring Boot 的默认 JSON 序列化库，定制某个类的反序列化，只需要在该类上使用注解就可以，代码样例如下：\n@JsonDeserialize(using = AnimalDeSerializer.class) public abstract class Animal{ } 而 AnimalDeSerializer 的实现如下：\npublic static class AnimalDeSerializer extends StdDeserializer\u0026lt;Animal\u0026gt; { @Override public Animal deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser); TreeNode animalTypeNode = treeNode.get(\u0026#34;animalType\u0026#34;); if(animalTypeNode == null){ return Animal.emptyImplement(); } String animalType = ((TextNode) animalTypeNode).asText(); switch (animalType){ case \u0026#34;dog\u0026#34;: return JSON.parseObject(treeNode.toString(), Dog.class); case \u0026#34;fish\u0026#34;: return JSON.parseObject(treeNode.toString(), Fish.class); .... } } } 前提是JSON数据中必须有能区分不同子类的字段，这样才可以使用。例如示例中，我们使用 animalType 来进行区分。\n嗅觉灵敏的同学会觉得这个 switch 味道不好。但是如何重构呢？笔者常说：要学会使用枚举消除 switch。点到为止。懂的同学就就懂了。不懂的，就算是留给大家的思考题。\n后记 现实中并不一定要完全按第一种写法或第二种写法，还要视具体情况而定。本文的主要目的是想让更多人知道，save 方法放哪里这个问题，还有另一种答案。\n","permalink":"https://showme.codes/zh-cn/2019-07-30-entity-repository/","summary":"\u003cblockquote\u003e\n\u003cp\u003e注：Dao 在不同语言中的叫法可能不一样。Dao 可以理解为对数据进行持久化的具体实现。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e关于实体的保存，笔者知道行业内有两种方式：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003edogDao.save(dog);\u003c/li\u003e\n\u003cli\u003edog.save();\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e相信不少同学，现实中，通常使用第一种，很少见到第二种写法。\u003c/p\u003e\n\u003cp\u003e为了让大家站在同一个讨论上下文，笔者决定贴出更详细的代码。\u003c/p\u003e\n\u003cp\u003e注：以下代码会省略很多本文不相关的代码，比如数据校验。读者朋友不必太纠结。\u003c/p\u003e\n\u003ch4 id=\"第一种写法save-方法写在-dao-上\"\u003e第一种写法：save 方法写在 Dao 上\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eDog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eDog\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetName\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003edidi\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetColor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003ewhite\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edogDao\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"第二种写法save-方法写在实体上\"\u003e第二种写法：save 方法写在实体上\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// DogService.java\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eDog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eDog\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetName\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003edidi\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetColor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003ewhite\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这两种写法，有什么区别吗？事实上，从字面上看，没有什么区别。因为 Dog 类的 save() 方法是这样实现：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Dog.java\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003edogDao\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e但是从抽象的角度来看，就不一样了。\u003c/p\u003e\n\u003cp\u003e假如你的项目中存在这样的抽象：\n\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-a0fbd312da0a8264.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如果采用第一种写法，意味着，每多出多一种 Animal，我们就必须多写一套 Service。Service 中会很多这样的方法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// DogService.java\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003edogDao\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edog\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// FishService.java\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eFish\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003efish\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003efishDao\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efish\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// BirdService.java\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eBird\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ebird\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003ebirdDao\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebird\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e相信大家对于以上代码并不陌生。\u003c/p\u003e","title":"你们的 save 方法是写在实体上，还是写 Dao 上？"},{"content":"\n当电梯到的时候，一只大土狗也跟着我进了电梯。我按了7楼。大狗依然张开嘴吐着舌头，站在原地。电梯直达7楼，我不知道大土狗要去几楼，所以没帮它按。走出电梯后，电梯理所当然的关门，然后向下了。它还在电梯里。\n一天，我们一家人挤进电梯下楼。电梯有一股恶臭。儿子叫了一声：大黄。这时，我才注意到拥挤的电梯里，大土狗在角落里。看样子，它也是要下楼。\n后来，从我老婆那里了解到。她经常看到这只大土狗，所以和儿子给它起了一个名字：大黄。因为它的毛是黄色的。\n而狗的主人就住在11楼。只不过，现在主人不要它了。它现在每天就睡在前主人的11楼的门外。饭点的时候，就会看到它端端正正的坐在餐桌不远处，目不转睛地看着吃饭的人。偶尔会有好心丢给吃的给它。\n当听到这些时，就酸了鼻子，两眼湿润。\n再后来，当我和大黄坐电梯时，都会帮它按下11楼。\n某一天，在楼下的操场边上，看到大黄兴奋地围在一年轻人的身边疯跑。看得出来，年轻人是它的前主人。只是当年的主人已经不是它的主人。年轻人直视前方冷漠地自走自的，仿佛大黄不存在一样。\n而我身为旁观者，本来想说点什么，最后，也只能路过。\n","permalink":"https://showme.codes/zh-cn/2019-07-26-human-and-dog/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-7b45268f64e22511.png\"\u003e\u003c/p\u003e\n\u003cp\u003e当电梯到的时候，一只大土狗也跟着我进了电梯。我按了7楼。大狗依然张开嘴吐着舌头，站在原地。电梯直达7楼，我不知道大土狗要去几楼，所以没帮它按。走出电梯后，电梯理所当然的关门，然后向下了。它还在电梯里。\u003c/p\u003e\n\u003cp\u003e一天，我们一家人挤进电梯下楼。电梯有一股恶臭。儿子叫了一声：大黄。这时，我才注意到拥挤的电梯里，大土狗在角落里。看样子，它也是要下楼。\u003c/p\u003e\n\u003cp\u003e后来，从我老婆那里了解到。她经常看到这只大土狗，所以和儿子给它起了一个名字：大黄。因为它的毛是黄色的。\u003c/p\u003e\n\u003cp\u003e而狗的主人就住在11楼。只不过，现在主人不要它了。它现在每天就睡在前主人的11楼的门外。饭点的时候，就会看到它端端正正的坐在餐桌不远处，目不转睛地看着吃饭的人。偶尔会有好心丢给吃的给它。\u003c/p\u003e\n\u003cp\u003e当听到这些时，就酸了鼻子，两眼湿润。\u003c/p\u003e\n\u003cp\u003e再后来，当我和大黄坐电梯时，都会帮它按下11楼。\u003c/p\u003e\n\u003cp\u003e某一天，在楼下的操场边上，看到大黄兴奋地围在一年轻人的身边疯跑。看得出来，年轻人是它的前主人。只是当年的主人已经不是它的主人。年轻人直视前方冷漠地自走自的，仿佛大黄不存在一样。\u003c/p\u003e\n\u003cp\u003e而我身为旁观者，本来想说点什么，最后，也只能路过。\u003c/p\u003e","title":"那只住在我们楼上的大黄狗"},{"content":"\n前段时间，项目实施人员告诉我，我写的 Ansible 脚本中有一处写死了版本号。并把代码截图给我看。我一看，这代码是老版本了。他代码应该没有更新。我这么跟他说。\n然后他说出了一句出乎我意料的话：我们不是研发，不会天天去关注代码。\n最后，我回了一句：要想自动化，就必须关注代码。屏幕背后的我，露出无奈与惋惜的表情。\n惋惜的不是他有没有关注代码这件事情。而是他使用职位来限制住自己的能力。\n不要笑话上面的同事，工作中不少这样的事情：\n我是测试，那是研发的事情。 我是研发，那是运维人员的事情。 我是设计人员，不是研发。 前段时间，听另一个项目组的同事说：两周一迭代，前一周测试闲死，后一周开发闲死。\n当时，我问了两个问题：\n在后一周的时候，开发在干嘛？ 在后一周的时候，产品经理在干嘛？ 开发与产品经理都不是很忙的情况，为什么他们不可以参与测试呢？如果问他们，得到的回答可能是：因为那是测试的事情。\n后来，我仔细想，那是XXX的事情，其实也不能完全怪他们。因为现实中，如果线上出现测试不到位的Bug，测试人员很可能会被 KPI。\n最后，我才恍然大悟：那是XXX的事情的思维方式并不是员工原本的思维方式，而是这个管理制度下的结果。\n","permalink":"https://showme.codes/zh-cn/2019-07-22-code/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-7a048e5ce07282ad.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e前段时间，项目实施人员告诉我，我写的 Ansible 脚本中有一处写死了版本号。并把代码截图给我看。我一看，这代码是老版本了。他代码应该没有更新。我这么跟他说。\u003c/p\u003e\n\u003cp\u003e然后他说出了一句出乎我意料的话：我们不是研发，不会天天去关注代码。\u003c/p\u003e\n\u003cp\u003e最后，我回了一句：要想自动化，就必须关注代码。屏幕背后的我，露出无奈与惋惜的表情。\u003c/p\u003e\n\u003cp\u003e惋惜的不是他有没有关注代码这件事情。而是他使用职位来限制住自己的能力。\u003c/p\u003e\n\u003cp\u003e不要笑话上面的同事，工作中不少这样的事情：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e我是测试，那是研发的事情。\u003c/li\u003e\n\u003cli\u003e我是研发，那是运维人员的事情。\u003c/li\u003e\n\u003cli\u003e我是设计人员，不是研发。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e前段时间，听另一个项目组的同事说：两周一迭代，前一周测试闲死，后一周开发闲死。\u003c/p\u003e\n\u003cp\u003e当时，我问了两个问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在后一周的时候，开发在干嘛？\u003c/li\u003e\n\u003cli\u003e在后一周的时候，产品经理在干嘛？\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e开发与产品经理都不是很忙的情况，为什么他们不可以参与测试呢？如果问他们，得到的回答可能是：因为那是测试的事情。\u003c/p\u003e\n\u003cp\u003e后来，我仔细想，那是XXX的事情，其实也不能完全怪他们。因为现实中，如果线上出现测试不到位的Bug，测试人员很可能会被 KPI。\u003c/p\u003e\n\u003cp\u003e最后，我才恍然大悟：那是XXX的事情的思维方式并不是员工原本的思维方式，而是这个管理制度下的结果。\u003c/p\u003e","title":"我们不是研发，不会天天去关注代码"},{"content":"\n本月在公司内部做了一次 Ansible 的入门工作坊。本文即对这次工作坊的设计过程进行一次总结。其他技术类的工作坊也可以参考。\n设计过程大概过程如下文所述。\n首先，我们需要确定参加本次工作坊的受众。他们是否具有最基本的前提。本次工作坊的受众有开发、测试、运维，还有毕业生。但是他们都会使用 shell。这已经满足最基本的前提。同时，了解受众后了，也就可以因材施教。\n第二，分析工作坊的内容。Ansible 是一款上手非常容易的自动化运维工具。它的特点就是实操性非常强，不需要理解 Ansible 背后的概念就可以使用的工具。\n笔者根据受众和教学内容的特点，得出本次工作坊的目标（教学目标）：\n知道 Ansible 是什么，并知道它的作用。 了解如何查文档。 能部署一个 Spring Boot 应用。 是不是很简单？其实不然。整个工作坊没有一个人能完成所有的任务。同时发现有运维和开发基础的同学会做得更快。\n那接下来怎么实现这个目标呢？笔者使用的是任务驱动的方法。也就是受众通过做一个个任务，在任务中完成学习。同时，教师可以任务过程穿插讲相关的知识点。\n以下为任务列表：\n执行 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 文件 多环境部署 任务的设计并不是随意的，而是有意的。比如：\n任务1：受众拿到练习代码后，执行命令，一定会报错。这时，教师可以讲解 Ansible 部署时需要确定“部署位置”和“部署逻辑”。顺便扩展一下：其它的自动化部署工具，也需要确定这两部分。 任务2：受众在创建用户时，一定会失败。因为用户组还没有创建。 任务3：重复创建多个文件夹，由于新手不懂with_items可以遍历创建文件夹，所以，新手写出来的代码会很多重复的。有悟性的同学，会想办法减少这种重复。 任务5： 由于 app.service 模板中使用了未定义的变量，所以，此任务用户也没有办法一次运行成功，而是需要学习在 playbook.yml 中定义变量，才能运行成功。 可以看到这些任务中充满了“陷阱”。本文就不一一列出所有的陷阱。这些陷阱能达到以下效果：\n在多次出现错误时，受众会学会自己看日志，查文档，找原因。 受众可以在这个不断遇到问题，解决问题的过程中， 体会到真实的开发是怎样的。 激发受众的自主思考（最重要）。 采用任务驱动的方式，还能规避受众能力参差不齐的问题，因为能力好的同学可以帮助能力差的同学。\n后记 很久没有做老师了，稍微找回了当年做老师的感觉。\n本次工作坊的练习代码：https://github.com/zacker330/ansible-workshop\n","permalink":"https://showme.codes/zh-cn/2019-07-19-ansible-workshop/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-28f3aed45a46631e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本月在公司内部做了一次 Ansible 的入门工作坊。本文即对这次工作坊的设计过程进行一次总结。其他技术类的工作坊也可以参考。\u003c/p\u003e\n\u003cp\u003e设计过程大概过程如下文所述。\u003c/p\u003e\n\u003cp\u003e首先，我们需要确定参加本次工作坊的受众。他们是否具有最基本的前提。本次工作坊的受众有开发、测试、运维，还有毕业生。但是他们都会使用 shell。这已经满足最基本的前提。同时，了解受众后了，也就可以因材施教。\u003c/p\u003e\n\u003cp\u003e第二，分析工作坊的内容。Ansible 是一款上手非常容易的自动化运维工具。它的特点就是实操性非常强，不需要理解 Ansible 背后的概念就可以使用的工具。\u003c/p\u003e\n\u003cp\u003e笔者根据受众和教学内容的特点，得出本次工作坊的目标（教学目标）：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e知道 Ansible 是什么，并知道它的作用。\u003c/li\u003e\n\u003cli\u003e了解如何查文档。\u003c/li\u003e\n\u003cli\u003e能部署一个 Spring Boot 应用。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e是不是很简单？其实不然。整个工作坊没有一个人能完成所有的任务。同时发现有运维和开发基础的同学会做得更快。\u003c/p\u003e\n\u003cp\u003e那接下来怎么实现这个目标呢？笔者使用的是任务驱动的方法。也就是受众通过做一个个任务，在任务中完成学习。同时，教师可以任务过程穿插讲相关的知识点。\u003c/p\u003e\n\u003cp\u003e以下为任务列表：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e执行 \u003ccode\u003eansible-playbook -i hosts playbook.yml\u003c/code\u003e 成功\u003c/li\u003e\n\u003cli\u003e创建用户 apps 及用户组 apps：\n\u003cul\u003e\n\u003cli\u003euser 模块: \u003ca href=\"https://docs.ansible.com/ansible/latest/modules/user_module.html\"\u003ehttps://docs.ansible.com/ansible/latest/modules/user_module.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003egroup 模块: \u003ca href=\"https://docs.ansible.com/ansible/latest/modules/group_module.html\"\u003ehttps://docs.ansible.com/ansible/latest/modules/group_module.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e创建以下文件夹，并设置文件夹的用户和组为 apps：\n/apps，/apps/hello，/apps/hello/bin，/apps/hello/logs\n\u003cul\u003e\n\u003cli\u003efile 模块: \u003ca href=\"https://docs.ansible.com/ansible/latest/modules/file_module.html\"\u003ehttps://docs.ansible.com/ansible/latest/modules/file_module.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e将 helloworld-0.0.2.jar copy 到 /apps/hello/bin 目录下，设置该 jar 文件的用户和用户组为 apps\n\u003cul\u003e\n\u003cli\u003ecopy 模块: \u003ca href=\"https://docs.ansible.com/ansible/latest/modules/copy_module.html\"\u003ehttps://docs.ansible.com/ansible/latest/modules/copy_module.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e使用 template 模块将 app.service copy 到目标服务器的 /etc/systemd/system 中，并重命名 hello.service :\n\u003cul\u003e\n\u003cli\u003etemplate 模块: \u003ca href=\"https://docs.ansible.com/ansible/latest/modules/template_module.html\"\u003ehttps://docs.ansible.com/ansible/latest/modules/template_module.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e启动 hello 服务\n\u003cul\u003e\n\u003cli\u003eservice 模块: \u003ca href=\"https://docs.ansible.com/ansible/latest/modules/service_module.html\"\u003ehttps://docs.ansible.com/ansible/latest/modules/service_module.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e监听 hello 服务是否启动成功\n\u003cul\u003e\n\u003cli\u003ewait_for 模块: \u003ca href=\"https://docs.ansible.com/ansible/latest/modules/wait_for_module.html\"\u003ehttps://docs.ansible.com/ansible/latest/modules/wait_for_module.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e为目标机器安装 JDK 1.8:\n\u003col\u003e\n\u003cli\u003e在本地仓库中创建 roles 目录\u003c/li\u003e\n\u003cli\u003eclone 代码：https://github.com/geerlingguy/ansible-role-java 到 roles 目录中\u003c/li\u003e\n\u003cli\u003e在 playbook.yml 文件中加入 ansible-role-java 的role\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e创建自定义 role: hello role\n\u003col\u003e\n\u003cli\u003e进入 roles 目录：cd roles\u003c/li\u003e\n\u003cli\u003e使用命令生成 role 模板：\u003ccode\u003eansible-galaxy init hello\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e将 hello 的部署逻辑（在 playbook.yml 中）写入到 hello role 中\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e将 hello 部署到多台机器\n\u003cul\u003e\n\u003cli\u003e需要修改 hosts 文件\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e多环境部署\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e任务的设计并不是随意的，而是有意的。比如：\u003c/p\u003e","title":"如何设计 Ansible 的入门工作坊"},{"content":"小明修复了 Web 后端的一个不大不小的 Bug。只修改了一行代码。并在 UAT 环境测试通过。\n可是，当我问他为什么不发版时，他说：\n代码是测试通过了，但是修改了一行代码，等和其他人的功能一起上吧\n在我不长不短的职业生涯中，经常遇到这样的小明。我已经见怪不怪了。\n笔者的观念是：如果变更的代码上线不会死人，能上就上。如果每次发版都很痛苦，那么先把发版难这个问题解决。至少也要朝着这个方向前进。\n为什么我会这样觉得呢？因为：\n程序员写出来的代码，只有真正运行在生产环境上了，才算完成工作。测试通过的代码不上线，就是库存。这在丰田称为“库存的浪费”。也就是不发布到生产环境，你为什么要写出来？还测试通过了。 如果一个分支停留太长时间，分支之间发生冲突的可能性就越大，而解决冲突这类操作对于产品的最终用户来说，是毫无价值的。用丰田生产方式的话说来，就是“动作上的无效劳动”。再者合并冲突过程容易再次引入缺陷。所以，应该避免分支停留过长时间。这个“过长”怎么定义，需要具体问题具体分析。 后记 其实，我很理解小明说出这样的话。大多是因为对部署没有足够的信心。对于没有自动化部署的团队来说，属于正常现象，你不能将责任推到一个人身上。而解决部署难的问题，不仅在管理上下功夫，还要在技术上下功能。\n","permalink":"https://showme.codes/zh-cn/2019-07-07-no-release/","summary":"\u003cp\u003e小明修复了 Web 后端的一个不大不小的 Bug。只修改了一行代码。并在 UAT 环境测试通过。\u003c/p\u003e\n\u003cp\u003e可是，当我问他为什么不发版时，他说：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e代码是测试通过了，但是修改了一行代码，等和其他人的功能一起上吧\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在我不长不短的职业生涯中，经常遇到这样的小明。我已经见怪不怪了。\u003c/p\u003e\n\u003cp\u003e笔者的观念是：如果变更的代码上线不会死人，能上就上。如果每次发版都很痛苦，那么先把发版难这个问题解决。至少也要朝着这个方向前进。\u003c/p\u003e\n\u003cp\u003e为什么我会这样觉得呢？因为：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e程序员写出来的代码，只有真正运行在生产环境上了，才算完成工作。测试通过的代码不上线，就是库存。这在丰田称为“库存的浪费”。也就是不发布到生产环境，你为什么要写出来？还测试通过了。\u003c/li\u003e\n\u003cli\u003e如果一个分支停留太长时间，分支之间发生冲突的可能性就越大，而解决冲突这类操作对于产品的最终用户来说，是毫无价值的。用丰田生产方式的话说来，就是“动作上的无效劳动”。再者合并冲突过程容易再次引入缺陷。所以，应该避免分支停留过长时间。这个“过长”怎么定义，需要具体问题具体分析。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e其实，我很理解小明说出这样的话。大多是因为对部署没有足够的信心。对于没有自动化部署的团队来说，属于正常现象，你不能将责任推到一个人身上。而解决部署难的问题，不仅在管理上下功夫，还要在技术上下功能。\u003c/p\u003e","title":"小明说：代码是测试通过了，但是修改了一行代码，等和其他人的功能一起上吧"},{"content":"\nPhoto by **Leon Macapagal **from Pexels\n团队每天都会面对着看板开早会。过完卡后，会有一些技术问题需要进行深入讨论。大家你一句我一句，实在说不清了，我就想从桌子上拿一支笔将想法画出来。\n但是在会议室找不到可用的笔，然后跑到文印室也找不到。这样，几分钟过去了，5个人团队就干等着我找笔。实在找不到，我们只得打开投影仪，打开 Windows 系统自带的画图软件画起架构图。\n在经历过几次这样的痛苦后，我决定找办公室负责采购的小姐姐说这事。小姐姐心好，从其它地方拿笔到我们经常早会的会议室。\n最后小姐姐补充：本月的文具已购买，下月我再向经理审批再买哦。\n我当时没有反应过来，没有理解她这句话的意思。\n过了两个星期，我发现又没有笔可用了（后来发现是我在文印室没有找仔细）。我再次找到小姐姐。并跟她说：能不能多放一些笔？\n小姐姐说：笔是统一放在文印室的，因为不知道哪个地方需要用。所以，你们有需要去文印室拿哦。\n我说：文印室也没有了，没找到。\n然后我实在没有忍住，一连串说了几句文具的成本是多低，节约人的时间，提高工作效率，可以节约更多的成本。\n她似乎没有听进去：之前放了一支，你们又丢了。\n我说：就一支，是不是别人拿到别的会议室了？\n她说：不知道。我买文具是要经理审批的。\n我这才想意识到，她没有决定权。问题出在经理那里。为什么经理不允许买呢？\n从小姐姐那里了解到原来有一次小姐姐按往常一次提流程买笔和本子。谁知经理说：纸巾每月买可以理解，但是为什么笔和本子每个月都要买。\n这就是现在为什么小姐姐很少买笔和本子的原因了。\n最后，我也不提笔的事情了。因为我单独找经理谈这个事情，在当前的环境下，会被人说“跨级”。再者对小姐姐可能也不好。\n谁知，过了两个星期，事业部的副经理从会议室跑出来，问：有没有见到大头笔？\n我苦笑地说：没有。\n苦笑是因为我猜到发生在我身上的事，也会发生在别人身上。只不过没想到，发生在了经理级别上而已。\n后来，也不知道他有没有找到笔。但是那个会议室还是偶尔找不到笔。\n不知道读者朋友看出来问题没有？笔者认为关键问题出在：\n提效不一定非要花巨资买个 DevOps 平台，效率发生在每一个企业运营的细节。 软件工程是知识密集型工作，人与人之间需要高效沟通。那么笔、白板（或纸）就是即高效，成本又低的工具。说实在点，工作少出现一个沟通失误，节约下来的钱就可能买下整个公司几年的笔了。小姐姐没有认识到，经理也没有认识到。 企业文化让看到问题的人不敢提问题。 你没有想到吧，一个负责采购小姐姐的决定，也可能影响一家公司的效率？这里没有贬低小姐姐的意思。每个岗位都有它的意义。 这些问题怎么从根本上解决？\n笔者认为，关键点是没有人从效率的角度考虑公司内部的行为。小姐姐考虑的是下次提流程时，经理不会问那样的问题。经理疑问了文具为什么用得这么快，但是并没有深究（几支笔的事情，当然没有必要深究）。其他人（可能）不想惹事，也不会找相关的人提“笔”的事情。在很多企业奉行谁提问题谁解决。有时我怀疑这句话对企业是有害的。\n怎么解决呢？我只说一句：解铃还须系铃人。\n一定能解决吗？我不知道。因为我也只是觉得可以解决。所以希望和更多人交流解决之道。\n我把这些写出来，很有可能犯“政治错误”，得罪某些人。我还是要写出来。因为我相信还有不少企业发生类似的事情。不敢面对，怎么进步。\n","permalink":"https://showme.codes/zh-cn/2019-05-27-pens-team-productivity/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-bfb542fc0c82f75b.png\"\u003e\u003c/p\u003e\n\u003cp\u003ePhoto by **\u003ca href=\"https://www.pexels.com/@leon-macapagal-1234433?utm_content=attributionCopyText\u0026amp;utm_medium=referral\u0026amp;utm_source=pexels\"\u003eLeon Macapagal \u003c/a\u003e**from \u003cstrong\u003e\u003ca href=\"https://www.pexels.com/photo/aerial-photo-of-railway-lines-2346006/?utm_content=attributionCopyText\u0026amp;utm_medium=referral\u0026amp;utm_source=pexels\"\u003ePexels\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e团队每天都会面对着看板开早会。过完卡后，会有一些技术问题需要进行深入讨论。大家你一句我一句，实在说不清了，我就想从桌子上拿一支笔将想法画出来。\u003c/p\u003e\n\u003cp\u003e但是在会议室找不到可用的笔，然后跑到文印室也找不到。这样，几分钟过去了，5个人团队就干等着我找笔。实在找不到，我们只得打开投影仪，打开 Windows 系统自带的画图软件画起架构图。\u003c/p\u003e\n\u003cp\u003e在经历过几次这样的痛苦后，我决定找办公室负责采购的小姐姐说这事。小姐姐心好，从其它地方拿笔到我们经常早会的会议室。\u003c/p\u003e\n\u003cp\u003e最后小姐姐补充：本月的文具已购买，下月我再向经理审批再买哦。\u003c/p\u003e\n\u003cp\u003e我当时没有反应过来，没有理解她这句话的意思。\u003c/p\u003e\n\u003cp\u003e过了两个星期，我发现又没有笔可用了（后来发现是我在文印室没有找仔细）。我再次找到小姐姐。并跟她说：能不能多放一些笔？\u003c/p\u003e\n\u003cp\u003e小姐姐说：笔是统一放在文印室的，因为不知道哪个地方需要用。所以，你们有需要去文印室拿哦。\u003c/p\u003e\n\u003cp\u003e我说：文印室也没有了，没找到。\u003c/p\u003e\n\u003cp\u003e然后我实在没有忍住，一连串说了几句文具的成本是多低，节约人的时间，提高工作效率，可以节约更多的成本。\u003c/p\u003e\n\u003cp\u003e她似乎没有听进去：之前放了一支，你们又丢了。\u003c/p\u003e\n\u003cp\u003e我说：就一支，是不是别人拿到别的会议室了？\u003c/p\u003e\n\u003cp\u003e她说：不知道。我买文具是要经理审批的。\u003c/p\u003e\n\u003cp\u003e我这才想意识到，她没有决定权。问题出在经理那里。为什么经理不允许买呢？\u003c/p\u003e\n\u003cp\u003e从小姐姐那里了解到原来有一次小姐姐按往常一次提流程买笔和本子。谁知经理说：纸巾每月买可以理解，但是为什么笔和本子每个月都要买。\u003c/p\u003e\n\u003cp\u003e这就是现在为什么小姐姐很少买笔和本子的原因了。\u003c/p\u003e\n\u003cp\u003e最后，我也不提笔的事情了。因为我单独找经理谈这个事情，在当前的环境下，会被人说“跨级”。再者对小姐姐可能也不好。\u003c/p\u003e\n\u003cp\u003e谁知，过了两个星期，事业部的副经理从会议室跑出来，问：有没有见到大头笔？\u003c/p\u003e\n\u003cp\u003e我苦笑地说：没有。\u003c/p\u003e\n\u003cp\u003e苦笑是因为我猜到发生在我身上的事，也会发生在别人身上。只不过没想到，发生在了经理级别上而已。\u003c/p\u003e\n\u003cp\u003e后来，也不知道他有没有找到笔。但是那个会议室还是偶尔找不到笔。\u003c/p\u003e\n\u003cp\u003e不知道读者朋友看出来问题没有？笔者认为关键问题出在：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e提效不一定非要花巨资买个 DevOps 平台，效率发生在每一个企业运营的细节。\u003c/li\u003e\n\u003cli\u003e软件工程是知识密集型工作，人与人之间需要高效沟通。那么笔、白板（或纸）就是即高效，成本又低的工具。说实在点，工作少出现一个沟通失误，节约下来的钱就可能买下整个公司几年的笔了。小姐姐没有认识到，经理也没有认识到。\u003c/li\u003e\n\u003cli\u003e企业文化让看到问题的人不敢提问题。\u003c/li\u003e\n\u003cli\u003e你没有想到吧，一个负责采购小姐姐的决定，也可能影响一家公司的效率？这里没有贬低小姐姐的意思。每个岗位都有它的意义。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这些问题怎么从根本上解决？\u003c/p\u003e\n\u003cp\u003e笔者认为，关键点是没有人从效率的角度考虑公司内部的行为。小姐姐考虑的是下次提流程时，经理不会问那样的问题。经理疑问了文具为什么用得这么快，但是并没有深究（几支笔的事情，当然没有必要深究）。其他人（可能）不想惹事，也不会找相关的人提“笔”的事情。在很多企业奉行谁提问题谁解决。有时我怀疑这句话对企业是有害的。\u003c/p\u003e\n\u003cp\u003e怎么解决呢？我只说一句：解铃还须系铃人。\u003c/p\u003e\n\u003cp\u003e一定能解决吗？我不知道。因为我也只是觉得可以解决。所以希望和更多人交流解决之道。\u003c/p\u003e\n\u003cp\u003e我把这些写出来，很有可能犯“政治错误”，得罪某些人。我还是要写出来。因为我相信还有不少企业发生类似的事情。不敢面对，怎么进步。\u003c/p\u003e","title":"从文具的购买看一家企业的效率"},{"content":"\n本篇文章只是笔者看到外墙清洗工后，在管理方面思考的总结。笔者是外墙清洗行业的外行，所以，本文可能存在一些错误的假设。期望这些可能错误的假设不影响管理方面的思考。\n引发思考的起因 某天上班，仰头看到大厦清洗外墙工人悬挂在20层楼的半空中作业，除了觉得他们危险外，脑海浮现一个问题。如果作为外墙清洗工的管理者，我们如何保证大厦的外墙的每一处被清洗干净了。\n我们经常听管理者这么说：\n我只要一个结果，不管过程。\n可是这个“结果”是什么呢？\n谁来定义“结果” 回到外墙清洗的管理，作为工程队的管理者，我们要的结果是什么呢？\n面对这个问题，我们要马上回答的并不是“结果”的定义是什么。而是要问：这个问题本身应该由谁来回答。是由老板回答、管理者回答、还是由清洗工回答（我们假设管理者与老板这两种角色由不同的人担当）？\n也就是说，对于不同角色的人，对于“结果”的定义是不一样的。\n清洗外墙作为一门生意，结果当然是以最低成本赚最多钱。对于这一点似乎不用定义了。\n但是，这个结果只是“老板”希望自己得到的结果。它与管理者希望清洗工给的结果是两回事。如下图所示。\n至此，我们发现“谁来定义结果”这个问题，应该换个问法：谁来确定管理者想要的结果与老板想要的结果之间的关系？\n笔者认为应该由管理者与老板共同确定。\n如何确定不同层次结果之间的关系 管理者想要的结果与老板想要的结果处于不同的层次。那么如何确定不同层次结果之间的关系，这个问题由老板与管理者共同回答。\n如果是把清洗外墙是一次性的生意，结果很准确，就是收到甲方的款。不论外墙是否真的被清洗干净了，因为贿赂验收人员我们一样能得到想要的结果。我们甚至可以设置一个部门专门搞定验收人员。这不是本文要讨论的范围。\n如果基于企业长期发展的考虑，我们希望能把洗墙这项业务做好。最终会得到老板想要的结果。\n本文，我们假设保证大厦外墙的每一处都被清洗干净是长期正作用于老板想要的结果的。如下图所示。\n接下来，下文所说的“结果”均指管理者的结果。\n如何验证结果 绕了一圈，终于把结果定义好了。现在咱们把“结果”定义为在文章开头就提了的：\n保证大厦外墙的每一处都被清洗干净\n但是我们如何验证这个结果呢？我们的管理者不可能也把自己挂在高高的外墙眼睁睁地检查清洗工洗过的每一处。就像产品经理不可能自己打 IDE 一行行的检查程序员的代码。\n如果管理者无法做到，加一个监工不就行了？但是谁又来监督这个监工的工作呢？也就是谁来保证监工的工作做到位了呢？\n还有一种办法：让工人相互监工。这样，花两个人的工资，顶三个人的活。可是，在企业里待过的人动下脑子都知道这个方案就不可靠。他们会串通的嘛。因为“偷懒”符合他们的共同利益。（在信息透明度高的情况下，让人相互监工是可行的。比如结对编程。）\n以上都是臆造出来的解决方案。都是基于“监工”的模型。看看我们的身边，是不是也是这样？\n回到我们的问题本身：如何确保大厦外墙的每一处都被清洗干净？我们换一种方式解决。\n如果采用“激励”的方式呢？假如请了10个清洗工人，等最后清洗完成后，对这10个清洗工人的工作进行ABC评级。得A的人可以得到原来薪水的1.2倍，得B的人保持不变，得C的人是原来薪水的0.8倍。是不是感觉这套路很熟悉？\n“激励”的模型虽好，解决的是清洗工人的积极性问题，还是没有解决我们的根本问题：如何验证结果。\n定期验收 也许我们可以把“每一处”的标准降低。在所有的清洗工中，选一个最合适的人作为清洗工组长，让其工资高于一般的清洗工人。组长的其中一个重要职责是定期对玻璃进行验收。至于定期的周期设为多长，涉及到工作的反馈周期了，属于另一个问题了。暂不讨论。\n虽然这样管理者不用自己被挂在外墙，但是“保证大厦外墙的每一处都被清洗干净”的结果强依赖于组长的责任心。\n这个模型的缺点是管理者想要的结果必须强依赖于一个很不稳定的因素：组长的责任心。\n用对比法解决 突然有一天，我再看大厦的时候想到，用清洗前和清洗后的大厦照片对比不就可以了吗？\n也就是在保证环境一致的情况下，在清洗前拍一次，在清洗后再拍一次。只要照片足够高清，你想对比多细节都可以。更进一步，我们甚至可以使用软件进行自动对比。\n使用此种办法，除了解决以上方案的缺点，还使得“保证大厦外墙的每一处都被清洗干净”的结果从模糊变成准确被量化了。\n这意味着什么？\n更短的反馈周期：每天都可以当天的清洗情况。这对于能否按时完成工作非常重要。 更准确的定义“干净”：怎么样才算干净，一对比就知道。同时，清洗工之间绩效对比也有了。 成本更低的检查“每一处”：由于只需要坐在电脑前进行检查，成本会低很多。可能不需要多发一份组长的工资了。 最后，笔者想找同一建筑的两张图进行对比，但是实在找不到。读者朋友就脑补一下吧。\n使用对比法就是最终答案了吗？不是的。也许，将来成本更低的办法是使用外墙清洗机器人会代替人工。也许这样更容易得到老板想要的结果。\n更甚至于搞个 AI 外墙清洗，然后融个资？笔者调皮了。\n后记 以上内容虽不能完全重现笔者的思考过程。但是大体思路就是这样的。这个思考过程，在软件工程方面，笔者认为是相通的。\n产品经理必须思考做出来的软件功能是否正作用于老板想要的结果；必须想办法加快反馈；必须想办法更低成本的检查程序员的工作结果；必须想办法让所有人准确理解需求。\n同时，本文还有很多方面没有讨论，比如我们是否需要以及如何关心每个人想要的结果；清洗工也是人，企业中的人文关怀问题等等。\n最后，说明一下，我并不想挑战别人的专业。以上只是讨论。欢迎大家交流\n参考 玻璃外墙清洗的准备工作与注意事项：http://www.fjyybj.com/news/517.html 外墙清洗机器人现身多幢大楼，清洗前后泾渭分明！ https://juejin.im/post/5c73c3a251882562e5445024 人类真的是趋利避害的吗？：https://www.zhihu.com/question/60711385 ","permalink":"https://showme.codes/zh-cn/2019-05-26-how-to-manage-cleaning-engineering-team/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-2abe0a46591e1aae.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e本篇文章只是笔者看到外墙清洗工后，在管理方面思考的总结。笔者是外墙清洗行业的外行，所以，本文可能存在一些错误的假设。期望这些可能错误的假设不影响管理方面的思考。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"引发思考的起因\"\u003e引发思考的起因\u003c/h3\u003e\n\u003cp\u003e某天上班，仰头看到大厦清洗外墙工人悬挂在20层楼的半空中作业，除了觉得他们危险外，脑海浮现一个问题。如果作为外墙清洗工的管理者，我们如何保证大厦的外墙的每一处被清洗干净了。\u003c/p\u003e\n\u003cp\u003e我们经常听管理者这么说：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我只要一个结果，不管过程。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e可是这个“结果”是什么呢？\u003c/p\u003e\n\u003ch3 id=\"谁来定义结果\"\u003e谁来定义“结果”\u003c/h3\u003e\n\u003cp\u003e回到外墙清洗的管理，作为工程队的管理者，我们要的结果是什么呢？\u003c/p\u003e\n\u003cp\u003e面对这个问题，我们要马上回答的并不是“结果”的定义是什么。而是要问：这个问题本身应该由谁来回答。是由老板回答、管理者回答、还是由清洗工回答（我们假设管理者与老板这两种角色由不同的人担当）？\u003c/p\u003e\n\u003cp\u003e也就是说，对于不同角色的人，对于“结果”的定义是不一样的。\u003c/p\u003e\n\u003cp\u003e清洗外墙作为一门生意，结果当然是以最低成本赚最多钱。对于这一点似乎不用定义了。\u003c/p\u003e\n\u003cp\u003e但是，这个结果只是“老板”希望自己得到的结果。它与管理者希望清洗工给的结果是两回事。如下图所示。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-bad3da559ae9330b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e至此，我们发现“谁来定义结果”这个问题，应该换个问法：谁来确定管理者想要的结果与老板想要的结果之间的关系？\u003c/p\u003e\n\u003cp\u003e笔者认为应该由管理者与老板共同确定。\u003c/p\u003e\n\u003ch3 id=\"如何确定不同层次结果之间的关系\"\u003e如何确定不同层次结果之间的关系\u003c/h3\u003e\n\u003cp\u003e管理者想要的结果与老板想要的结果处于不同的层次。那么如何确定不同层次结果之间的关系，这个问题由老板与管理者共同回答。\u003c/p\u003e\n\u003cp\u003e如果是把清洗外墙是一次性的生意，结果很准确，就是收到甲方的款。不论外墙是否真的被清洗干净了，因为贿赂验收人员我们一样能得到想要的结果。我们甚至可以设置一个部门专门搞定验收人员。这不是本文要讨论的范围。\u003c/p\u003e\n\u003cp\u003e如果基于企业长期发展的考虑，我们希望能把\u003cstrong\u003e洗墙\u003c/strong\u003e这项业务做好。最终会得到老板想要的结果。\u003c/p\u003e\n\u003cp\u003e本文，我们假设保证大厦外墙的每一处都被清洗干净是长期正作用于老板想要的结果的。如下图所示。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-5ace4b542ab9cdc4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接下来，下文所说的“结果”均指管理者的结果。\u003c/p\u003e\n\u003ch3 id=\"如何验证结果\"\u003e如何验证结果\u003c/h3\u003e\n\u003cp\u003e绕了一圈，终于把结果定义好了。现在咱们把“结果”定义为在文章开头就提了的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e保证大厦外墙的每一处都被清洗干净\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e但是我们如何验证这个结果呢？我们的管理者不可能也把自己挂在高高的外墙眼睁睁地检查清洗工洗过的每一处。就像产品经理不可能自己打 IDE 一行行的检查程序员的代码。\u003c/p\u003e\n\u003cp\u003e如果管理者无法做到，加一个监工不就行了？但是谁又来监督这个监工的工作呢？也就是谁来保证监工的工作做到位了呢？\u003c/p\u003e\n\u003cp\u003e还有一种办法：让工人相互监工。这样，花两个人的工资，顶三个人的活。可是，在企业里待过的人动下脑子都知道这个方案就不可靠。他们会串通的嘛。因为“偷懒”符合他们的共同利益。（在信息透明度高的情况下，让人相互监工是可行的。比如结对编程。）\u003c/p\u003e\n\u003cp\u003e以上都是臆造出来的解决方案。都是基于“监工”的模型。看看我们的身边，是不是也是这样？\u003c/p\u003e\n\u003cp\u003e回到我们的问题本身：如何确保大厦外墙的每一处都被清洗干净？我们换一种方式解决。\u003c/p\u003e\n\u003cp\u003e如果采用“激励”的方式呢？假如请了10个清洗工人，等最后清洗完成后，对这10个清洗工人的工作进行ABC评级。得A的人可以得到原来薪水的1.2倍，得B的人保持不变，得C的人是原来薪水的0.8倍。是不是感觉这套路很熟悉？\u003c/p\u003e\n\u003cp\u003e“激励”的模型虽好，解决的是清洗工人的积极性问题，还是没有解决我们的根本问题：如何验证结果。\u003c/p\u003e\n\u003ch3 id=\"定期验收\"\u003e定期验收\u003c/h3\u003e\n\u003cp\u003e也许我们可以把“每一处”的标准降低。在所有的清洗工中，选一个最合适的人作为清洗工组长，让其工资高于一般的清洗工人。组长的其中一个重要职责是定期对玻璃进行验收。至于定期的周期设为多长，涉及到工作的反馈周期了，属于另一个问题了。暂不讨论。\u003c/p\u003e\n\u003cp\u003e虽然这样管理者不用自己被挂在外墙，但是“保证大厦外墙的每一处都被清洗干净”的结果强依赖于组长的责任心。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-89de8e47586b3e39.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这个模型的缺点是管理者想要的结果必须强依赖于一个很不稳定的因素：组长的责任心。\u003c/p\u003e\n\u003ch3 id=\"用对比法解决\"\u003e用对比法解决\u003c/h3\u003e\n\u003cp\u003e突然有一天，我再看大厦的时候想到，用清洗前和清洗后的大厦照片对比不就可以了吗？\u003c/p\u003e\n\u003cp\u003e也就是在保证环境一致的情况下，在清洗前拍一次，在清洗后再拍一次。只要照片足够高清，你想对比多细节都可以。更进一步，我们甚至可以使用软件进行自动对比。\u003c/p\u003e\n\u003cp\u003e使用此种办法，除了解决以上方案的缺点，还使得“保证大厦外墙的每一处都被清洗干净”的结果从模糊变成准确被量化了。\u003c/p\u003e\n\u003cp\u003e这意味着什么？\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e更短的反馈周期：每天都可以当天的清洗情况。这对于能否按时完成工作非常重要。\u003c/li\u003e\n\u003cli\u003e更准确的定义“干净”：怎么样才算干净，一对比就知道。同时，清洗工之间绩效对比也有了。\u003c/li\u003e\n\u003cli\u003e成本更低的检查“每一处”：由于只需要坐在电脑前进行检查，成本会低很多。可能不需要多发一份组长的工资了。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e最后，笔者想找同一建筑的两张图进行对比，但是实在找不到。读者朋友就脑补一下吧。\u003c/p\u003e\n\u003cp\u003e使用对比法就是最终答案了吗？不是的。也许，将来成本更低的办法是使用外墙清洗\u003cstrong\u003e机器人\u003c/strong\u003e会代替人工。也许这样更容易得到老板想要的结果。\u003c/p\u003e\n\u003cp\u003e更甚至于搞个 AI 外墙清洗，然后融个资？笔者调皮了。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e以上内容虽不能完全重现笔者的思考过程。但是大体思路就是这样的。这个思考过程，在软件工程方面，笔者认为是相通的。\u003c/p\u003e\n\u003cp\u003e产品经理必须思考做出来的软件功能是否正作用于老板想要的结果；必须想办法加快反馈；必须想办法更低成本的检查程序员的工作结果；必须想办法让所有人准确理解需求。\u003c/p\u003e\n\u003cp\u003e同时，本文还有很多方面没有讨论，比如我们是否需要以及如何关心每个人想要的结果；清洗工也是人，企业中的人文关怀问题等等。\u003c/p\u003e\n\u003cp\u003e最后，说明一下，我并不想挑战别人的专业。以上只是讨论。欢迎大家交流\u003c/p\u003e\n\u003ch3 id=\"参考\"\u003e参考\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e玻璃外墙清洗的准备工作与注意事项：http://www.fjyybj.com/news/517.html\u003c/li\u003e\n\u003cli\u003e外墙清洗机器人现身多幢大楼，清洗前后泾渭分明！\n\u003ca href=\"https://juejin.im/post/5c73c3a251882562e5445024\"\u003ehttps://juejin.im/post/5c73c3a251882562e5445024\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e人类真的是趋利避害的吗？：\u003ca href=\"https://www.zhihu.com/question/60711385\"\u003ehttps://www.zhihu.com/question/60711385\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"如果给你一支外墙清洗工程队，你会如何管？"},{"content":"\nJenkins 共享库是除了 Jenkins 插件外，另一种扩展 Jenkins 流水线的技术。通过它，可以定义轻松的自定义步骤，还可以对现有的流水线逻辑进行一定程度的抽象与封装。至于如何写及如何使用它，读者朋友可以移步附录中的官方文档。\n对共享库进行单元测试的原因 但是如何对它进行单元测试呢？共享库越来越大时，你不得不考虑的问题。因为如果你不在早期就开始单元测试，共享库后期可能就会发展成如下图所示的“艺术品”——能工作，但是脆弱到没有人敢动。\n[图片来自网络，侵权必删]\n这就是代码越写越慢的原因之一。后人要不断地填前人有意无意挖的坑。\n共享库单元测试搭建 共享库官方文档介绍的代码仓库结构 (root) +- src # Groovy source files | +- org | +- foo | +- Bar.groovy # for org.foo.Bar class +- vars | +- foo.groovy # for global \u0026#39;foo\u0026#39; variable | +- foo.txt # help for \u0026#39;foo\u0026#39; variable +- resources # resource files (external libraries only) | +- org | +- foo | +- bar.json # static helper data for org.foo.Bar 以上是共享库官方文档介绍的代码仓库结构。整个代码库可以分成两部分：src 目录部分和 vars 目录部分。它们的测试脚手架的搭建是不一样的。\nsrc 目录中的代码与普通的 Java 类代码本质上没有太大的区别。只不过换成了 Groovy 类。\n但是 vars 目录中代码本身是严重依赖于 Jenkins 运行时环境的脚本。\n接下来，分别介绍如何搭建它们的测试脚手架。\n测试 src 目录中的 Groovy 代码 在对 src 目录中的 Groovy 代码进行单元测试前，我们需要回答一个问题：使用何种构建工具进行构建？\n我们有两种常规选择：Maven 和 Gradle。本文选择的是前者。\n接下来的第二个问题是，共享库源代码结构并不是 Maven 官方标准结构。下例为标准结构：\n├── pom.xml └── src ├── main │ ├── java │ └── resources └── test ├── java └── resources 因为共享库使用的 Groovy 写的，所以，还必须使 Maven 能对 Groovy 代码进行编译。\n可以通过 Maven 插件：GMavenPlus 解决以上问题，插件的关键配置如下：\n\u0026lt;configuration\u0026gt; \u0026lt;sources\u0026gt; \u0026lt;source\u0026gt; \u0026lt;!-- 指定Groovy类源码所在的目录 --\u0026gt; \u0026lt;directory\u0026gt;${project.basedir}/src\u0026lt;/directory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/*.groovy\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/source\u0026gt; \u0026lt;/sources\u0026gt; \u0026lt;testSources\u0026gt; \u0026lt;testSource\u0026gt; \u0026lt;!-- 指定单元测试所在的目录 --\u0026gt; \u0026lt;directory\u0026gt;${project.basedir}/test/groovy\u0026lt;/directory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/*.groovy\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/testSource\u0026gt; \u0026lt;/testSources\u0026gt; \u0026lt;/configuration\u0026gt; 同时，我们还必须加入 Groovy 语言的依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.codehaus.groovy\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;groovy-all\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${groovy-all.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 最终目录结构如下图所示： 然后我们就可以愉快地对 src 目录中的代码进行单元测试了。\n测试 vars 目录中 Groovy 代码 对 vars 目录中的脚本的测试难点在于它强依赖于 Jenkins 的运行时环境。换句话说，你必须启动一个 Jenkins 才能正常运行它。但是这样就变成集成测试了。那么怎么实现单元测试呢？\n经 Google 发现，前人已经写了一个 Jenkins 共享库单元测试的框架。我们拿来用就好。所谓，前人载树，后人乘凉。\n这个框架叫：Jenkins Pipeline Unit testing framework。后文简称“框架”。它的使用方法如下：\n在 pom.xml 中加入依赖： \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.lesfurets\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jenkins-pipeline-unit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 写单元测试 // test/groovy/codes/showme/pipeline/lib/SayHelloTest.groovy // 必须继承 BasePipelineTest 类 class SayHelloTest extends BasePipelineTest { @Override @Before public void setUp() throws Exception { // 告诉框架，共享库脚本所在的目录 scriptRoots = [\u0026#34;vars\u0026#34;] // 初始化框架 super.setUp() } @Test void call() { // 加载脚本 def script = loadScript(\u0026#34;sayHello.groovy\u0026#34;) // 运行脚本 script.call() // 断言脚本中运行了 echo 方法 // 同时参数为\u0026#34;hello pipeline\u0026#34; assertThat( helper.callStack .findAll { c -\u0026gt; c.methodName == \u0026#39;echo\u0026#39; } .any { c -\u0026gt; c.argsToString().contains(\u0026#39;hello pipeline\u0026#39;) } ).isTrue() // 框架提供的方法，后面会介绍。 printCallStack() } } 创建单元测试时，注意选择 Groovy 语言，同时类名要以 Test 结尾。\n改进 以上代码是为了让读者对共享库脚本的单元测试有更直观的理解。实际工作中会做一些调整。我们会将 extends BasePipelineTest 和 setUp 方法抽到一个父类中，所有其它测试类继承于它。 此时，我们最简单的共享库的单元测试脚手架就搭建好了。\n但是，实际工作中遇到场景并不会这么简单。面对更复杂的场景，必须了解 Jenkins Pipeline Unit testing framework 的原理。由此可见，写单元测试也是需要成本的。至于收益，仁者见仁了。\nJenkins Pipeline Unit testing framework 原理 上文中的单元测试实际上做了三件事情：\n加载目标脚本，loadScript 方法由框架提供。 运行脚本，loadScript 方法返回加载好的脚本。 断言脚本中的方法是否有按预期执行，helper 是 BasePipelineTest 的一个字段。 从第三步的 helper.callStack 中，我们可以猜到第二步中的script.call() 并不是真正的执行，而是将脚本中方法调用被写到 helper 的 callStack 字段中。从 helper 的源码可以确认这一点：\n/** * Stack of method calls of scripts loaded by this helper */ List\u0026lt;MethodCall\u0026gt; callStack = [] 那么，script.call() 内部是如何做到将方法调用写入到 callStack 中的呢？\n一定是在 loadScript 运行过程做了什么事情，否则，script 怎么会多出这些行为。我们来看看它的底层源码：\n/** * Load the script with given binding context without running, returning the Script * @param scriptName * @param binding * @return Script object */ Script loadScript(String scriptName, Binding binding) { Objects.requireNonNull(binding, \u0026#34;Binding cannot be null.\u0026#34;) Objects.requireNonNull(gse, \u0026#34;GroovyScriptEngine is not initialized: Initialize the helper by calling init().\u0026#34;) Class scriptClass = gse.loadScriptByName(scriptName) setGlobalVars(binding) Script script = InvokerHelper.createScript(scriptClass, binding) script.metaClass.invokeMethod = getMethodInterceptor() script.metaClass.static.invokeMethod = getMethodInterceptor() script.metaClass.methodMissing = getMethodMissingInterceptor() return script } gse 是 Groovy 脚本执行引擎 GroovyScriptEngine。它在这里的作用是拿到脚本的 Class 类型，然后使用 Groovy 语言的 InvokerHelper 静态帮助类创建一个脚本对象。\n接下来做的就是核心了：\nscript.metaClass.invokeMethod = getMethodInterceptor() script.metaClass.static.invokeMethod = getMethodInterceptor() script.metaClass.methodMissing = getMethodMissingInterceptor() 它将脚本对象实例的方法调用都委托给了拦截器 methodInterceptor。Groovy 对元编程非常友好。可以直接对方法进行拦截。拦截器源码如下：\n/** * Method interceptor for any method called in executing script. * Calls are logged on the call stack. */ public methodInterceptor = { String name, Object[] args -\u0026gt; // register method call to stack int depth = Thread.currentThread().stackTrace.findAll { it.className == delegate.class.name }.size() this.registerMethodCall(delegate, depth, name, args) // check if it is to be intercepted def intercepted = this.getAllowedMethodEntry(name, args) if (intercepted != null \u0026amp;\u0026amp; intercepted.value) { intercepted.value.delegate = delegate return callClosure(intercepted.value, args) } // if not search for the method declaration MetaMethod m = delegate.metaClass.getMetaMethod(name, args) // ...and call it. If we cannot find it, delegate call to methodMissing def result = (m ? this.callMethod(m, delegate, args) : delegate.metaClass.invokeMissingMethod(delegate, name, args)) return result } 它做了三件事情：\n将调用方法名和参数写入到 callStack 中 如果被调用方法名是被注册了的方法，则执行该方法对象的 mock。下文会详细介绍。 如果被调用方法没有被注册，则真正执行它。 需要解释一个第二点。并不是所有的共享库中的方法都是需要拦截的。我们只需要对我们感兴趣的方法进行拦截，并实现 mock 的效果。\n写到这里，有些读者朋友可能头晕了。笔者在这里进行小结一下。\n因为我们不希望共享库脚本中的依赖于 Jenkins 运行时的方法（比如拉代码的步骤）真正运行。所以，我们需要对这些方法进行 mock。在 Groovy 中，我们可以通过方法级别的拦截来实现 mock 的效果。 但是我们又不应该对共享库中所有的方法进行拦截，所以就需要我们在执行单元测试前将自己需要 mock 的方法进行注册到 helper 的 allowedMethodCallbacks 字段中。methodInterceptor 拦截器会根据它来进行拦截。\n在 BasePipelineTest 的 setUp 方法中，框架注册了一些默认方法，不至于我们要手工注册太多方法。以下是部分源码：\nhelper.registerAllowedMethod(\u0026#34;sh\u0026#34;, [Map.class], null) helper.registerAllowedMethod(\u0026#34;checkout\u0026#34;, [Map.class], null) helper.registerAllowedMethod(\u0026#34;echo\u0026#34;, [String.class], null) registerAllowedMethod 各参数的作用：\n第一个参数：要注册的方法。 第二参数：该方法的参数列表。 第三参数：一个闭包。当该访问被调用时会执行此闭包。 以上就是框架的基本原理了。接下来，再介绍几种场景。\n几种应用场景 环境变量 当你的共享库脚本使用了 env 变量，可以这样测试：\nbinding.setVariable(\u0026#39;env\u0026#39;, new HashMap()) def script = loadScript(\u0026#39;setEnvStep.groovy\u0026#39;) script.invokeMethod(\u0026#34;call\u0026#34;, [k: \u0026#39;123\u0026#39;, v: \u0026#34;456\u0026#34;]) assertEquals(\u0026#34;123\u0026#34;, ((HashMap) binding.getVariable(\u0026#34;env\u0026#34;)).get(\u0026#34;k\u0026#34;)) binding 由 BasePipelineTest 的一个字段，用于绑定变量。binding 会被设置到 gse 中。\n调用其它共享库脚本 比如脚本 a 中调用到了 setEnvStep。这时可以在 a 执行前注册 setEnvStep 方法。\nhelper.registerAllowedMethod(\u0026#34;setEnvStep\u0026#34;, [LinkedHashMap.class], null) 希望被 mock 的方法能有返回值 helper.registerAllowedMethod(\u0026#34;getDevOpsMetadata\u0026#34;, [String.class, String.class], { return \u0026#34;data from cloud\u0026#34; }) 后记 不得不说 Jenkins Pipeline Unit testing framework 框架的作者非常聪明。另外，此类技术不仅可以用于单元测试。理论上还可以用于 Jenkins pipeline 的零侵入拦截，以实现一些平台级特殊的需求。\n附录 共享库官方文档：https://jenkins.io/zh/doc/book/pipeline/shared-libraries/ 本文示例代码：https://github.com/zacker330/jenkins-pipeline-shared-lib-unittest-demo JenkinsPipelineUnit：https://github.com/jenkinsci/JenkinsPipelineUnit ","permalink":"https://showme.codes/zh-cn/2019-05-25-jenkins-pipeline-shared-lib-unit-test/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-1cd01d6e3fadedae.png\"\u003e\u003c/p\u003e\n\u003cp\u003eJenkins 共享库是除了 Jenkins 插件外，另一种扩展 Jenkins 流水线的技术。通过它，可以定义轻松的自定义步骤，还可以对现有的流水线逻辑进行一定程度的抽象与封装。至于如何写及如何使用它，读者朋友可以移步附录中的官方文档。\u003c/p\u003e\n\u003ch2 id=\"对共享库进行单元测试的原因\"\u003e对共享库进行单元测试的原因\u003c/h2\u003e\n\u003cp\u003e但是如何对它进行单元测试呢？共享库越来越大时，你不得不考虑的问题。因为如果你不在早期就开始单元测试，共享库后期可能就会发展成如下图所示的“艺术品”——能工作，但是脆弱到没有人敢动。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-a904fdf36d90c714.png\"\u003e\u003c/p\u003e\n\u003cp\u003e[图片来自网络，侵权必删]\u003c/p\u003e\n\u003cp\u003e这就是代码越写越慢的原因之一。后人要不断地填前人有意无意挖的坑。\u003c/p\u003e\n\u003ch2 id=\"共享库单元测试搭建\"\u003e共享库单元测试搭建\u003c/h2\u003e\n\u003ch3 id=\"共享库官方文档介绍的代码仓库结构\"\u003e共享库官方文档介绍的代码仓库结构\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e(root)\n+- src                     # Groovy source files\n|   +- org\n|       +- foo\n|           +- Bar.groovy  # for org.foo.Bar class\n+- vars\n|   +- foo.groovy          # for global \u0026#39;foo\u0026#39; variable\n|   +- foo.txt             # help for \u0026#39;foo\u0026#39; variable\n+- resources               # resource files (external libraries only)\n|   +- org\n|       +- foo\n|           +- bar.json    # static helper data for org.foo.Bar\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e以上是共享库官方文档介绍的代码仓库结构。整个代码库可以分成两部分：src 目录部分和 vars 目录部分。它们的测试脚手架的搭建是不一样的。\u003c/p\u003e","title":"如何对 Jenkins 共享库进行单元测试"},{"content":"\n本文要点：\n设计一条 Springboot 最基本的流水线：包括构建、制品上传、部署。 使用 Docker 容器运行构建逻辑。 自动化整个实验环境：包括 Jenkins 的配置，Jenkins slave 的配置等。 1. 代码仓库安排 本次实验涉及以下多个代码仓库：\n% tree -L 1 ├── 1-cd-platform # 实验环境相关代码 ├── 1-env-conf # 环境配置代码-实现配置独立 └── 1-springboot # Springboot 应用的代码及其部署代码 1-springboot 的目录结构如下：\n% cd 1-springboot % tree -L 1 ├── Jenkinsfile # 流水线代码 ├── README.md ├── deploy # 部署代码 ├── pom.xml └── src # 业务代码 所有代码，均放在 GitHub: https://github.com/cd-in-practice\n2. 实验环境准备 笔者使用 Docker Compose + Vagrant 进行实验。环境包括以下几个系统：\nJenkins * 1 Jenkins master，全自动安装插件、默认用户名密码：admin/admin。 Jenkins agent * 2 Jenkins agent 运行在 Docker 容器中，共启动两个。 Artifactory * 1 一个商业版的制品库。笔者申请了一个 30 天的商业版。 使用 Vagrant 是为了启动虚拟机，用于部署 Springboot 应用。如果你的开发机器无法使用 Vagrant，使用 VirtualBox 也可以达到同样的效果。但是有一点需要注意，那就是网络。如果在虚拟机中要访问 Docker 容器内提供的服务，需要在 DNS 上或者 hosts 上做相应的调整。所有的虚拟机的镜像使用 Centos7。\n另，接下来笔者的所有教程都将使用 Artifactory 作为制品库。在此申明，笔者没有收 JFrog——研发 Artifactory 产品的公司——任何广告费。 笔者只是想试用商业产品，以便了解商业产品是如何应对制品管理问题的。\n启动 Artifactory 后，需要添加 “Virtual Repository” 及 “Local Repository”。具体请查看 Artifactory 的官方文档。如果你当前使用的是 Nexus，参考本教程，做一些调整，问题也不大。\n如果想使用已有制品库，可以修改 1-cd-platform 仓库中的 settings-docker.xml 文件，指向自己的制品库。\n实验环境近期的总体结构图如下：\n之所以说是“近期的”，是因为上图与本篇介绍的结构有小差异。本篇文章还没有介绍 Nginx 与 Springboot 配置共用，但是总体不影响读者理解。\n3. Springboot 应用流水线介绍 Springboot 流水线有两个阶段：\n构建并上传制品 部署应用 流水线的所有逻辑都写在 Jenkinsfile 文件。接下来，分别介绍这两个阶段。\n3.1 构建并上传制品 此阶段核心代码：\ndocker.image(\u0026#39;jenkins-docker-maven:3.6.1-jdk8\u0026#39;) .inside(\u0026#34;--network 1-cd-platform_cd-in-practice -v $HOME/.m2:/root/.m2\u0026#34;) { sh \u0026#34;\u0026#34;\u0026#34; mvn versions:set -DnewVersion=${APP_VERSION} mvn clean test install mvn deploy \u0026#34;\u0026#34;\u0026#34; } 它首先启动一个装有 Maven 的容器，然后在容器内执行编译、单元测试、发布制品的操作。\n而 mvn versions:set -DnewVersion=${APP_VERSION} 的作用是更改 pom.xml 文件中的版本。这样就可以实现每次提交对应一个版本的效果。\n3.2 部署应用 注意： 这部分需要一些 Ansible 的知识。\n首先看部署脚本的入口 1-springboot/deploy/playbook.yaml：\n--- - hosts: \u0026#34;springboot\u0026#34; become: yes roles: - {\u0026#34;role\u0026#34;: \u0026#34;ansible-role-java\u0026#34;, \u0026#34;java_home\u0026#34;: \u0026#34;{{JAVA_HOME}}\u0026#34;} - springboot 先安装 JDK，再安装 springboot。JDK 的安装，使用了现成 Ansible role: https://github.com/geerlingguy/ansible-role-java。\n重点在 springboot 部署的核心逻辑。它主要包含以下几部分：\n创建应用目录。 从制品库下载指定版本的制品。 生成 Systemd service 文件（实现服务化）。 启动服务。 以上步骤实现在 1-springboot/deploy/roles/springboot 中。\n流水线的部署阶段的核心代码如下：\ndocker.image(\u0026#39;williamyeh/ansible:centos7\u0026#39;).inside(\u0026#34;--network 1-cd-platform_cd-in-practice\u0026#34;) { checkout([$class: \u0026#39;GitSCM\u0026#39;, branches: [[name: \u0026#34;master\u0026#34;]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: \u0026#39;RelativeTargetDirectory\u0026#39;, relativeTargetDir: \u0026#34;env-conf\u0026#34;]], submoduleCfg: [], userRemoteConfigs: [[url: \u0026#34;https://github.com/cd-in-practice/1-env-conf.git\u0026#34;]]]) sh \u0026#34;ls -al\u0026#34; sh \u0026#34;\u0026#34;\u0026#34; ansible-playbook --syntax-check deploy/playbook.yaml -i env-conf/dev ansible-playbook deploy/playbook.yaml -i env-conf/dev --extra-vars \u0026#39;{\u0026#34;app_version\u0026#34;: \u0026#34;${APP_VERSION}\u0026#34;}\u0026#39; \u0026#34;\u0026#34;\u0026#34; } 它首先将配置变量仓库的代码 clone 下来，然后对 playbook 进行语法上的检查，最后执行 ansible-playbook 命令进行部署。--extra-vars 参数的 app_version 用于指定将要部署的应用的版本。\n####3.3 实现简易指定版本部署 在 1-springboot/Jenkinsfile 中实现了简易的指定版本部署。核心代码如下：\n流水线接受参数 parameters { string(name: \u0026#39;SPECIFIC_APP_VERSION\u0026#39;, defaultValue: \u0026#39;\u0026#39;, description: \u0026#39;\u0026#39;) } 如果指定了版本，则跳过构建阶段，直接执行部署阶段 stage(\u0026#34;build and upload\u0026#34;){ // 如果不指定部署版本，则执行构建 when { expression{ return params.SPECIFIC_APP_VERSION == \u0026#34;\u0026#34; } } // 构建并上传制品的逻辑 steps{...} } 之所以说是“简易”，是因为部署时只指定了制品的版本，并没有指定的部署逻辑和配置的版本。这三者的版本要同步，部署才真正做到准确。\n4. 配置管理 所有的配置项都放在 1-env-conf 仓库中。Ansible 执行部署时会读取此仓库的配置。\n将配置放在 Git 仓库中有两个好处：\n配置版本化。 任何配置的更改都可以被审查。 有好处并不代表没有成本。那就是开发人员必须开始关心软件的配置（笔者发现不少开发者忽视配置项管理的重要性。）。\n本文重点不在配置管理，后面会有文章重点介绍。\n5. 实验环境详细介绍 事实上，整个实验，工作量大的地方有两处：一是 Springboot 流水线本身的设计；二是整个实验环境的自动化。读者朋友之所以能一两条简单的命令就能启动整个实验环境，是因为笔者做了很多自动化的工作。笔者认为有必要在本篇介绍这些工作。接下来的文章将不再详细介绍。\n5.1 解决流水线中启动的 Docker 容器无法访问 http://artifactory 流水线中，我们需要将制品上传到 artifactory（settings.xml 配置的仓库地址是 http://artifactory:8081），但是发现无法解析 host。这是因为流水线中的 Docker 容器所在网络与 Docker compose 创建的网络不同。所以，解决办法就是让流水线中的 Docker 容器加入到 Docker compose 的网络。\n具体解决办法就是在启动容器时，加入参数：--network 1-cd-platform_cd-in-practice\n5.2 Jenkins 初次启动初始化 在没有做任何设置的情况启动 Jenkins，会出现一个配置向导。这个过程必须是手工的。笔者希望这一步也是自动化的。Jenkins 启动时会执行 init.groovy.d/目录下的 Groovy 脚本。\n5.3 虚拟机中如何能访问到 http://artifactory ？ http://artifactory 部署在 Docker 容器中。Springboot 应用的制品要部署到虚拟机中，需要从 http://artifactory 中拉取制品，也就是要在虚拟机里访问容器里提供的服务。虚拟机与容器之间的网络是不通的。那怎么办呢？笔者的解决方案是使用宿主机的 IP 做中转。具体做法就是在虚拟机中加一条 host 记录：\nmachine.vm.provision \u0026#34;shell\u0026#34; do |s| s.inline = \u0026#34;echo \u0026#39;192.168.52.1 artifactory\u0026#39; \u0026gt;\u0026gt; /etc/hosts\u0026#34; end 以上是使用了 Vagrant 的 provision 技术，在执行命令 vagrant up 启动虚拟机时，就自动执行那段内联 shell。192.168.52.1 是虚拟宿主机的 IP。所以，虚拟机里访问 http://artifactory:8081 时，实际上访问的是 http://192.168.52.1:8081。\n网络结构可以总结为下图：\n后记 目前遗留问题：\n部署时制品版本、配置版本、部署代码版本没有同步。 Springboot 的配置是写死在制品中的，没有实现制品与配置项的分离。 这些遗留问题在后期会逐个解决。就像现实一样，经常需要面对各种遗留项目的遗留问题。\n附录 使用 Jenkins + Ansible 实现自动化部署 Nginx：https://showme.codes/2019-04-22/jenkins-ansible-nginx/ 简单易懂Ansible系列 —— 解决了什么https://showme.codes/2017-06-12/ansible-introduce/ ","permalink":"https://showme.codes/zh-cn/2019-05-15-jenkins-ansible-springboot/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-3d64f9fe7b80ed98.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文要点：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e设计一条 Springboot 最基本的流水线：包括构建、制品上传、部署。\u003c/li\u003e\n\u003cli\u003e使用 Docker 容器运行构建逻辑。\u003c/li\u003e\n\u003cli\u003e自动化整个实验环境：包括 Jenkins 的配置，Jenkins slave 的配置等。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"1-代码仓库安排\"\u003e1. 代码仓库安排\u003c/h3\u003e\n\u003cp\u003e本次实验涉及以下多个代码仓库：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e% tree -L 1\n├── 1-cd-platform # 实验环境相关代码\n├── 1-env-conf # 环境配置代码-实现配置独立\n└── 1-springboot # Springboot 应用的代码及其部署代码\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e1-springboot 的目录结构如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e% cd 1-springboot\n% tree -L 1 \n├── Jenkinsfile # 流水线代码\n├── README.md\n├── deploy # 部署代码\n├── pom.xml \n└── src # 业务代码\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e所有代码，均放在 GitHub: \u003ca href=\"https://github.com/cd-in-practice\"\u003ehttps://github.com/cd-in-practice\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"2-实验环境准备\"\u003e2. 实验环境准备\u003c/h3\u003e\n\u003cp\u003e笔者使用 Docker Compose + Vagrant 进行实验。环境包括以下几个系统：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eJenkins * 1\nJenkins master，全自动安装插件、默认用户名密码：admin/admin。\u003c/li\u003e\n\u003cli\u003eJenkins agent * 2\nJenkins agent 运行在 Docker 容器中，共启动两个。\u003c/li\u003e\n\u003cli\u003eArtifactory * 1\n一个商业版的制品库。笔者申请了一个 30 天的商业版。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e使用 Vagrant  是为了启动虚拟机，用于部署 Springboot 应用。如果你的开发机器无法使用 Vagrant，使用 VirtualBox 也可以达到同样的效果。但是有一点需要注意，那就是网络。如果在虚拟机中要访问 Docker 容器内提供的服务，需要在 DNS 上或者 hosts 上做相应的调整。所有的虚拟机的镜像使用 Centos7。\u003c/p\u003e","title":"使用 Jenkins + Ansible 实现 Springboot 自动化部署101"},{"content":"背景 了解到行业内有些团队是基于 Jenkins 开发 DevOps 平台。而基于 Jenkins 实现的 DevOps 平台，就不得不考虑凭证的管理问题。\n本文就此问题进行讨论，尝试找出相对合理的管理凭证的方案。\n一开始我们想到的方案可能是这样的：用户在 DevOps 平台增加凭证后，DevOps 再将凭证同步到 Jenkins 上。Jenkins 任务在使用凭证时，使用的是存储在 Jenkins 上的凭证，而不是 DevOps 平台上的。\n但是，仔细想想，这样做会存在以下问题：\nJenkins 与 DevOps 平台之间的凭证数据会存在不一致问题。 存在一定的安全隐患。通过 Jenkins 脚本命令行很容易就把所有密码的明文拿到。哪天 Jenkins 被注入了，所有的凭证一下子就被扒走。 无法实现 Jenkins 高可用，因为凭证存在 Jenkins master 机器上。 那么，有没有更好的办法呢？\n期望实现的目标 先定我们觉得更合理的目标，然后讨论如何实现。以下是笔者觉得合理的目标：\n用户还是在 DevOps 管理自己的凭证。但是 DevOps 不需要将自己凭证同步到 Jenkins 上。Jenkins 任务在使用凭证时，从 DevOps 上取。\n实现方式 Jenkins 有一个 Credentials Binding Plugin 插件，在 Jenkins pipeline 中的用法如下：\nwithCredentials([usernameColonPassword(credentialsId: \u0026#39;mylogin\u0026#39;, variable: \u0026#39;USERPASS\u0026#39;)]) { sh \u0026#39;\u0026#39;\u0026#39; curl -u \u0026#34;$USERPASS\u0026#34; https://private.server/ \u0026gt; output \u0026#39;\u0026#39;\u0026#39; } withCredentials 方法做的事情就是从 Jenkins 的凭证列表中取出 id 为 mylogin 的凭证，并将值赋到变量名为 USERPASS 的变量中。接下来，你就可以在闭包中使用该变量了。\n说到这里，不知道读者朋友是否已经有思路了？\n思路就是实现一个和 Credentials Binding Plugin 插件类似功能的方法，比如叫 zWithCredentials（后文还会提到）。与 withCredentials 不同的是，zWithCredentials 根据凭证 id 获取凭证时，不是从 Jenkins 上获取，而是从 DevOps 平台获取。\n会遇到的坑 需要适配只认 Jenkins 凭证的插件 withCredentials 方法是将凭证的内容存到变量中，这可以满足一大部分场景。但是有一种场景是无法满足的。就是某些 Jenkins 插件的步骤接收参数时，参数值必须是 Jenkins 凭证管理系统中的 id。比如 git 步骤中 credentialsId 参数：\ngit branch: \u0026#39;master\u0026#39;, credentialsId: \u0026#39;12345-1234-4696-af25-123455\u0026#39;, url: \u0026#39;ssh://git@bitbucket.org:company/repo.git\u0026#39; 这种情况，我们不可能修改现有的插件。因为那样做的成本太高了。\n那怎么办呢？\n笔者想到的办法是在 zWithCredentials 中做一些 hack 操作。也就是 zWithCredentials 除了从 DevOps 平台获取凭证，还在 Jenkins 中创建一个 Jenkins 凭证。在 Jenkins 任务执行完成后，再将这个临时凭证删除。这样就可以适配那些只认 Jenkins 凭证 id 的插件了。\n对凭证本身的加密 DevOps 平台在存储凭证、传输凭证给 Jenkins 时，都需要对凭证进行加密。至于使用何种加密方式，交给读者思考了。\n小结 以上解决方案对 Jenkins 本身的改造几乎没有，我们只通过一个插件就解耦了 Jenkins 的凭证管理和 DevOps 平台的凭证管理。\n思路已经有了。具体怎么实现，由于一些原因不能开源，虽然实现起来不算难。还请读者见谅。\n最后，希望能和遇到同样问题的同学进行交流。看看是否还可以有更好的设计思路。\n","permalink":"https://showme.codes/zh-cn/2019-05-07-devops-jenkins-credential-manage/","summary":"\u003ch3 id=\"背景\"\u003e背景\u003c/h3\u003e\n\u003cp\u003e了解到行业内有些团队是基于 Jenkins 开发 DevOps 平台。而基于 Jenkins 实现的 DevOps 平台，就不得不考虑凭证的管理问题。\u003c/p\u003e\n\u003cp\u003e本文就此问题进行讨论，尝试找出相对合理的管理凭证的方案。\u003c/p\u003e\n\u003cp\u003e一开始我们想到的方案可能是这样的：用户在 DevOps 平台增加凭证后，DevOps 再将凭证同步到 Jenkins 上。Jenkins 任务在使用凭证时，使用的是存储在 Jenkins 上的凭证，而不是 DevOps 平台上的。\u003c/p\u003e\n\u003cp\u003e但是，仔细想想，这样做会存在以下问题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eJenkins 与 DevOps 平台之间的凭证数据会存在不一致问题。\u003c/li\u003e\n\u003cli\u003e存在一定的安全隐患。通过 Jenkins 脚本命令行很容易就把所有密码的明文拿到。哪天 Jenkins 被注入了，所有的凭证一下子就被扒走。\u003c/li\u003e\n\u003cli\u003e无法实现 Jenkins 高可用，因为凭证存在 Jenkins master 机器上。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e那么，有没有更好的办法呢？\u003c/p\u003e\n\u003ch3 id=\"期望实现的目标\"\u003e期望实现的目标\u003c/h3\u003e\n\u003cp\u003e先定我们觉得更合理的目标，然后讨论如何实现。以下是笔者觉得合理的目标：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e用户还是在 DevOps 管理自己的凭证。但是 DevOps 不需要将自己凭证同步到 Jenkins 上。Jenkins 任务在使用凭证时，从 DevOps 上取。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"实现方式\"\u003e实现方式\u003c/h3\u003e\n\u003cp\u003eJenkins 有一个 \u003ca href=\"%5Bhttps://jenkins.io/doc/pipeline/steps/credentials-binding/%5D(https://jenkins.io/doc/pipeline/steps/credentials-binding/)\"\u003eCredentials Binding Plugin\u003c/a\u003e 插件，在 Jenkins pipeline 中的用法如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-groovy\" data-lang=\"groovy\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ewithCredentials\u003c/span\u003e\u003cspan class=\"o\"\u003e([\u003c/span\u003e\u003cspan class=\"n\"\u003eusernameColonPassword\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003ecredentialsId:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;mylogin\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003evariable:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;USERPASS\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)])\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;\u0026#39;\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e      curl -u \u0026#34;$USERPASS\u0026#34; https://private.server/ \u0026gt; output\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e    \u0026#39;\u0026#39;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003ewithCredentials\u003c/code\u003e 方法做的事情就是从 Jenkins 的凭证列表中取出 id 为 mylogin 的凭证，并将值赋到变量名为 USERPASS 的变量中。接下来，你就可以在闭包中使用该变量了。\u003c/p\u003e","title":"基于 Jenkins 的 DevOps 平台应该如何设计凭证管理"},{"content":"手工安装 Jenkins 插件的方法 通常，我们有两种方法安装 Jenkins 插件。第一种方法是到 Jenkins 插件管理页面搜索插件，然后安装。第二种方法是上传 Jenkins 插件的 hpi 文件安装。这两种方法能满足大多数人的需求。\n第一种方法，如下图所示： 第二种方法，如下图所示： 但是对于需要保证 Jenkins 稳定或在 Jenkins 上进行二次开发的同学来说，以上方法是无法满足需求的。\n第一种方法是无法指定插件的版本。第二种方式必须自己找到该插件的依赖树，一个个依赖的安装。是的，手工上传插件的这种方法，Jenkins 是不会自动下载依赖的。\n自动安装插件的方法 那么，有什么方法能做到即指定插件的版本，又能自动下载它的依赖呢？\n幸运的是，Jenkins 的 Docker 镜像的代码仓库里的 install-plugins.sh 脚本已经实现。只不过需要我们拿过来小小修改才能使用。笔者修改后创建了相应的代码仓库：jenkins-install-plugins-shell 。链接在文章末尾。\n以下是 jenkins-install-plugins-shell 的使用方法：\n将代码 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=\u0026lt;Jenkins war文件的路径\u0026gt; chmod +x install-plugins.sh jenkins-support ./install-plugins.sh \u0026lt; plugins.txt 重启 Jenkins install-plugins 本质上做的事情就只是将插件从云端下载到 JENKINS_HOME 下的 plugins 目录中。要使安装的插件生效，还需要重启 Jenkins。 关于 Jenkins 插件的名称 Jenkins 插件有两个名称。一个叫 display name，一个叫 short name。比如 Ansible 插件的 disply name 为 Ansible plugin，short name 为 ansible。\n如何知道一个插件的 short name 呢？可以在 Jenkins 插件官网上找到，比如 Ansible 的：\n在 plugins.txt 中使用的是 short name。\n总结 笔者为什么一定要确定 Jenkins 插件的版本？是因为插件的版本会影响 Jenkins 流水线的可靠性。所以，笔者才会这么在意 Jenkins 插件的版本。\n附录 Jenkins 官方 Docker 镜像中的自动化插件安装脚本：https://github.com/jenkinsci/docker/blob/master/install-plugins.sh 笔者修改后的自动化插件安装脚本： https://github.com/zacker330/jenkins-install-plugins-shell ","permalink":"https://showme.codes/zh-cn/2019-04-27-jenkins-install-plugins-shell/","summary":"\u003ch3 id=\"手工安装-jenkins-插件的方法\"\u003e手工安装 Jenkins 插件的方法\u003c/h3\u003e\n\u003cp\u003e通常，我们有两种方法安装  Jenkins 插件。第一种方法是到 Jenkins 插件管理页面搜索插件，然后安装。第二种方法是上传 Jenkins 插件的 hpi 文件安装。这两种方法能满足大多数人的需求。\u003c/p\u003e\n\u003cp\u003e第一种方法，如下图所示：\n\u003cimg alt=\"搜索安装\" loading=\"lazy\" src=\"/assets/images/292372-12412dbb4c58b810.png\"\u003e\u003c/p\u003e\n\u003cp\u003e第二种方法，如下图所示：\n\u003cimg alt=\"上传插件\" loading=\"lazy\" src=\"/assets/images/292372-957598396b256971.png\"\u003e\u003c/p\u003e\n\u003cp\u003e但是对于需要保证 Jenkins 稳定或在 Jenkins 上进行二次开发的同学来说，以上方法是无法满足需求的。\u003c/p\u003e\n\u003cp\u003e第一种方法是无法指定插件的版本。第二种方式必须自己找到该插件的依赖树，一个个依赖的安装。是的，手工上传插件的这种方法，Jenkins 是不会自动下载依赖的。\u003c/p\u003e\n\u003ch3 id=\"自动安装插件的方法\"\u003e自动安装插件的方法\u003c/h3\u003e\n\u003cp\u003e那么，有什么方法能做到即指定插件的版本，又能自动下载它的依赖呢？\u003c/p\u003e\n\u003cp\u003e幸运的是，Jenkins 的 Docker 镜像的代码仓库里的 install-plugins.sh 脚本已经实现。只不过需要我们拿过来小小修改才能使用。笔者修改后创建了相应的代码仓库：jenkins-install-plugins-shell 。链接在文章末尾。\u003c/p\u003e\n\u003cp\u003e以下是 jenkins-install-plugins-shell 的使用方法：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e将代码 clone 到 JENKINS_HOME 目录中。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecd $JENKINS_HOME\ngit clone https://github.com/zacker330/jenkins-install-plugins-shell.git\ncd jenkins-install-plugins-shell\n\u003c/code\u003e\u003c/pre\u003e\u003col start=\"2\"\u003e\n\u003cli\u003e在 plugins.txt 中加入希望安装的插件\n在 \u003ccode\u003ejenkins-install-plugins-shell\u003c/code\u003e 目录中，有一个 plugins.txt 文件，在文件中写入希望安装的插件及版本号。例如：\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eansible:1.0\npowershell:1.3\n\u003c/code\u003e\u003c/pre\u003e\u003col start=\"3\"\u003e\n\u003cli\u003e执行安装\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \u003cspan class=\"c1\"\u003e# Jenkins War 的路径，用于分析\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eexport\u003c/span\u003e \u003cspan class=\"nv\"\u003eJENKINS_WAR_PATH\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u0026lt;Jenkins war文件的路径\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echmod +x install-plugins.sh jenkins-support\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./install-plugins.sh \u0026lt; plugins.txt\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003col start=\"4\"\u003e\n\u003cli\u003e重启 Jenkins\ninstall-plugins 本质上做的事情就只是将插件从云端下载到 JENKINS_HOME 下的 plugins 目录中。要使安装的插件生效，还需要重启 Jenkins。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"关于-jenkins-插件的名称\"\u003e关于 Jenkins 插件的名称\u003c/h3\u003e\n\u003cp\u003eJenkins 插件有两个名称。一个叫 display name，一个叫 short name。比如 Ansible 插件的 disply name 为 Ansible plugin，short  name 为 ansible。\u003c/p\u003e","title":"Jenkins 自动安装插件"},{"content":"本文介绍如何使用 Jenkins + Ansible 实现对 Nginx 的自动化部署。最终达到的效果有如下几点：\n只要你将 Nginx 的配置推送到 GitHub 中，Jenkins 就会自动执行部署，然后目标服务器的 Nginx 配置自动生效。这个过程是幂等（idempotent）的，只要代码不变，执行多少遍，最终效果不变。 如果目标机器没有安装 Nginx，则会自动安装 Nginx。 自动设置服务器防火墙规则。 1. 实验环境介绍 本次实验使用 Docker Compose 搭建 Jenkins 及 Jenkins agent。使用 Vagrant 启动一台虚拟机，用于部署 Nginx。使用 Vagrant 是可选的，读者可以使用 VirtualBox 启动一个虚拟机。使用 Vagrant 完全是为了自动化搭建实验环境。\n以下是整个实验环境的架构图： 注意，图中的 5123 \u0026lt;-\u0026gt; 80 代表将宿主机的 5123 端口请求转发到虚拟机中的 80 端口。\nVagrant：虚拟机管理工具，通过它，我们可以使用文本来定义、管理虚拟机。 Ansible：自动化运维工具 Docker Compose：它是一个用于定义和运行多容器 Docker 应用程序的工具。可以使用YAML文件来配置应用程序的服务。 2. 启动实验环境 克隆代码并进入文件夹\ngit clone https://github.com/zacker330/jenkins-ansible-nginx.git cd jenkins-ansible-nginx 构建 Jenkins agent 的镜像 需要自定义 Jenkins agent 镜像有两个原因：\n本次实验，使用 Swarm 插件实现 Jenkins master 与 Jenkins agent 之间的通信，所以 Jenkins agent 需要启动 swarm 客户端。 Jenkins agent 必须支持 Ansible。 docker build -f JenkinsSlaveAnsibleDockerfile -t jenkins-swarm-ansible . 启动 Jenkins master 及 Jenkins agent\ndocker-compose up -d 通过 http://localhost:8080 访问 Jenkins master，如果出现“解锁密码”页面，如下图，则执行命令 docker-compose logs jenkins 查看 Jenkins master 启动日志。将日志中的解锁密码输入到表单中。然后就一步步按提示安装即可。 安装 Jenkins 插件 本次实验需要安装以下插件：\nPipeline 2.6：https://plugins.jenkins.io/workflow-aggregator Swarm 3.15：https://plugins.jenkins.io/swarm 用于 实现 Jenkins master 与 Jenkins agent 自动连接 Git 3.9.3：https://plugins.jenkins.io/git 配置 Jenkins master 不执行任务 进入页面：http://localhost:8080/computer/(master)/configure，如下图所示设置： 确认 Jenkins 安全配置有打开端口，以供 Jenkins agent 连接。 我们设置 Jenkins master 开放的端口，端口可以是固定的 50000 ，也可以设置为随机。设置链接：http://localhost:8080/configureSecurity/。 启动目标机器，用于部署 Nginx 在命令行中执行以下命令：\nvagrant up 注意，Vagrantfile 文件中的 config.vm.box 值必须改成你的 vagrant box 。\n至此，实验环境已经搭建好了。接下来就可以新建 Jenkins 任务了。\n3. 在 Jenkins 上创建部署任务 新建流水线任务 配置流水线 配置 Jenkins 任务从远程仓库拉取 Jenkinsfile，如下图所示： 除此之外，不需要其它配置了，是不是很简单？ 4. 手工触发一次自动化构建 点击“立即构建”： 最终执行日志如下： 至此，部署已经完成。以后修改 Nginx 的配置，只需要修改代码，然后推送到远程仓库，就会自动化部署。不需要手工登录到目标机器手工修改了。\n最后，我们可以通过访问 http://localhost:5123，如果出现如下页面说明部署成功：\n5. 代码讲解 以上步骤并不能看出自动化部署真正做了什么。那是因为我们所有的逻辑都写在代码中。是的，可以说是 everything is code。\n接下来我们介绍代码仓库。\n% tree -L 2 ├── JenkinsSlaveAnsibleDockerfile # Jenkins agent 镜像 Dockerfile ├── Jenkinsfile # 流水线逻辑 ├── README.md ├── Vagrantfile # Vagrant 虚拟机定义文件 ├── docker-compose.yml # Jenkins 实现环境 ├── env-conf # 所有应用配置 │ └── dev # dev 环境的配置 ├── deploy # Ansible 部署脚本所在文件夹 │ ├── playbook.yaml │ └── roles └── swarm-client.sh # Jenkins swarm 插件的客户端 5.1流水线逻辑 Jenkinsfile 文件用于描述整条流水线的逻辑。代码如下：\npipeline{ // 任务执行在具有 ansible 标签的 agent 上 agent { label \u0026#34;ansible\u0026#34;} environment{ // 设置 Ansible 不检查 HOST_KEY ANSIBLE_HOST_KEY_CHECKING = false } triggers { pollSCM(\u0026#39;H/1 * * * *\u0026#39;) } stages{ stage(\u0026#34;deploy nginx\u0026#34;){ steps{ sh \u0026#34;ansible-playbook -i env-conf/dev deploy/playbook.yaml\u0026#34; } }}} environment 部分：用于定义流水线执行过程中的环境变量。 triggers 部分：用于定义流水线的触发机制。pollSCM 定义了每分钟判断一次代码是否有变化，如果有变化则自动执行流水线。 agent 部分：用于定义整条流水线的执行环境。 stages 部分：流水线的所有阶段，都被定义在这部分。 以上只是定义流水线是如何执行的，目前整条流水线只有一个 deploy nginx 阶段，并且只执行了一条 ansible-playbook 命令。但是它并没有告诉我们部署逻辑是怎么样的。\n5.2 部署逻辑 所有的部署逻辑，包括 Nginx 的安装启动、配置的更新以及加载，都放在 Ansible 脚本中。对 Ansible 不熟的同学，可以在本文末尾找到介绍 Ansible 的文章。\n整个部署逻辑的入口在 deploy/playbook.yaml，代码如下：\n--- - hosts: \u0026#34;nginx\u0026#34; become: true roles: # Nginx 的部署 - ansible-role-nginx # 对防火墙的设置 - ansible-role-firewall hosts：定义了 playbook 部署的目标主机分组名为 nginx。 roles：包含了两个执行具体部署动作的 role，至于 role 内部逻辑，不在本文讨论范围，有兴趣的同学阅读源码。 5.3 配置管理 谈到部署，就不得不谈配置管理。\n回顾前文中流水线中执行的 shell 命令：ansible-playbook -i env-conf/dev deploy/playbook.yaml 我们通过 -i 参数指定部署时所使用的环境配置。通过这种方式实现环境配置与执行脚本的分离。这样带来以下几个好处：\n新增环境时，只需要复制现有的环境，然后将里面的变量的值改成新环境的即可。比如，要对测试环境进行部署，只需要将 -i 参数值改成：env-conf/test。 对配置版本化控制。 本次实验中，各个环境的配置放在 env-conf 目录中，目前只有 dev 环境，以下是 env-conf/ 目录结构：\n% cd env-conf/ % tree └── dev ├── group_vars │ └── nginx.yaml ├── host_vars │ └── 192.168.52.10 └── hosts hosts文件：Ansible 中通过“分组”来实现对主机的管理。hosts 文件内容如下： [nginx] 192.168.52.10 host_vars 目录：用于存放主机级别的配置变量，本例中 192.168.52.10 是一个 YAML 格式文件。注意文件名是该主机的 IP。我们在文件中放主机相关的配置，比如 Ansible 连接主机时使用到的用户名和密码。 group_vars 目录：用于存放组级别的配置变量。比如 nginx.yaml 对应的就是 nginx 这个组的的配置变量。文件名与 hosts 中的组名对应。 总结 到此，我们完整的自动化部署已经讲解完成。但是还遗留下一些问题：\n本文只是安装了一个“空”的 Nginx，但是没有介绍 Nginx 真正配置。 目前主机的连接信息（SSH 密码）是明文写在 host_vars/192.168.52.10 文件中的，存在安全风险。 没有介绍如何当 Java 应用部署时，如何自动更新 Nginx 的配置。 本文属于使用 Jenkins + Ansible 实现自动化部署的入门文章，笔者将根据读者的反馈决定是否写续集。\n附录 本次实验环境代码：https://github.com/zacker330/jenkins-ansible-nginx 简单易懂Ansible系列 —— 解决了什么：https://showme.codes/2017-06-12/ansible-introduce/ Puppet，Chef，Ansible的共性：https://showme.codes/2016-01-02/the-nature-of-ansible-puppet-chef/ ","permalink":"https://showme.codes/zh-cn/2019-04-22-jenkins-ansible-nginx/","summary":"\u003cp\u003e本文介绍如何使用 Jenkins + Ansible 实现对 Nginx 的自动化部署。最终达到的效果有如下几点：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e只要你将 Nginx 的配置推送到 GitHub 中，Jenkins 就会自动执行部署，然后目标服务器的 Nginx 配置自动生效。这个过程是幂等（idempotent）的，只要代码不变，执行多少遍，最终效果不变。\u003c/li\u003e\n\u003cli\u003e如果目标机器没有安装 Nginx，则会自动安装 Nginx。\u003c/li\u003e\n\u003cli\u003e自动设置服务器防火墙规则。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"1-实验环境介绍\"\u003e1. 实验环境介绍\u003c/h2\u003e\n\u003cp\u003e本次实验使用 Docker Compose 搭建 Jenkins 及 Jenkins agent。使用 Vagrant 启动一台虚拟机，用于部署 Nginx。使用 Vagrant 是可选的，读者可以使用 VirtualBox 启动一个虚拟机。使用 Vagrant 完全是为了自动化搭建实验环境。\u003c/p\u003e\n\u003cp\u003e以下是整个实验环境的架构图：\n\u003cimg alt=\"Jenkins Ansible Nginx\" loading=\"lazy\" src=\"/assets/images/292372-ab578a7d0b27c4c6.png\"\u003e\u003c/p\u003e\n\u003cp\u003e注意，图中的 \u003ccode\u003e5123 \u0026lt;-\u0026gt; 80\u003c/code\u003e 代表将宿主机的 5123 端口请求转发到虚拟机中的 80 端口。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eVagrant：虚拟机管理工具，通过它，我们可以使用文本来定义、管理虚拟机。\u003c/li\u003e\n\u003cli\u003eAnsible：自动化运维工具\u003c/li\u003e\n\u003cli\u003eDocker Compose：它是一个用于定义和运行多容器 Docker 应用程序的工具。可以使用YAML文件来配置应用程序的服务。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"2-启动实验环境\"\u003e2. 启动实验环境\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e克隆代码并进入文件夹\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit clone https://github.com/zacker330/jenkins-ansible-nginx.git\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e jenkins-ansible-nginx\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e构建 Jenkins agent 的镜像\n需要自定义 Jenkins agent 镜像有两个原因：\u003c/p\u003e","title":"使用 Jenkins + Ansible 实现自动化部署 Nginx"},{"content":" 审校：王冬辉，linuxsuren\nJenkins master 的高可用是个老大难的问题。和很多人一样，笔者也想过两个 Jenkins master 共享同一个 JENKINS_HOME 的方案。了解 Jenkins 原理的人，都会觉得这个方案不可行。但是真的不可行吗？\n由于工作原因，笔者需要亲自验证以上猜想。\nJENKINS_HOME 介绍 Jenkins 所有状态数据都存放文件系统的目录中，这个目录被称为 JENKINS_HOME 目录。\n实验环境介绍 笔者通过 Docker compose 启动两个独立的 Jenkins master，分别为 jenkins-a 和 jenkins-b。它们共用同一个 JENKINS_HOME 目录。相应的代码仓库的链接放在文章底部。\n将代码克隆到本地后，进入仓库，执行 docker-compose up -d 即可启动实验环境。启动完成，在浏览器中输入 http://localhost:7088 可访问 jenkins-a，jenkins-b 的地址是 http://localhost:7089 。但是你会发现它们启动后的界面显示是不一样的。\njenkins-b 的界面如下图所示：\n而 jenkins-a 的界面如下图所示：\n这时，将 jenkins-a 日志中的解锁密码（Unlock password）输入到 jenkins-b 的页面中，会得到报错信息：\nERROR: The password entered is incorrect, please check the file for the correct password 这时，再次 jenkins-b 日志中的解锁密码（Unlock password）输入到表单中即可进入下一步。接下来就是按照提示一步步完成了。在 jenkins-b 安装步骤的最后一步，我们设置了管理员的用户名密码：admin/admin。然后就算完成任务了。\n然后我们再在 jenkins-a 使用 admin/admin 进行登录，登录是报错的：用户密码不正确。\n接下来，执行 docker-compose restart jenkins-a 命令重启 jenkins-a。再次使用 admin/admin 就可以登录成功了。\n当两个 Jenkins 启动完成后，接下来开始做实验。\n实验1：创建任务 在 jenkins-a 创建任务 x，刷新 jenkins-b 的页面，jenkins-b 上会不会显示出任务 x ？\n结果：jenkins-b 不会出现任务 x。重启 jenkins-b 后，任务 x 出现在任务列表中。\n实验2：任务结果可见性 jenkins-a 上任务执行，jenkins-b 上能否看到任务执行结果？\njenkins-a 执行任务 x，并且执行成功。刷新 jenkins-b 看不到任何执行记录。重启 jenkins-b 后，可看到执行记录。\n实验3：两 master 同时执行同一任务 分别在两个 Jenkins master 上（几乎）开始同一个任务 x。其中一个任务的 build number 会更新，但是另一个不会。\n其中 jenkins-a 任务 x 的 build number 会升到 2，而 jenkins-b 保持的是 1。这时，单独执行 jenkins-b 的任务 x，日志会出现错误：\njenkins-b_1 | WARNING: A new build could not be created in job x jenkins-b_1 | java.lang.IllegalStateException: JENKINS-23152: /var/jenkins_home/jobs/x/builds/2 already existed; will not overwrite with x #2 实验4：编辑任务 jenkins-a 上设置任务 x 定时执行，刷新 jenkins-b 页面，任务 x 中并没有定时执行的设置。重启 jenkins-b 后，任务 x 更新。\n实验5：定时任务的结果是什么？ 如果 jenkins-a 和 jenkins-b 两个任务均为定时任务，而且都生效了。它们运行结果是什么的呢？\n看到的现象是，两个任务都会按时执行，但是只有一个任务能将运行结果写入到磁盘中。界面如下图：\n另，从日志中，可以确认 jenkins-a 和 jenkins-b 确实按时执行了。如下图日志中，看出 jenkins-a 定时执行 #6 次构建时报错，因为 jenkins-b 已经执行过 #6 次构建了：\n小结 可以确认的是，当两个 Jenkins 进程共用同一个 JENKINS_HOME 目录时，其中一个 Jenkins 进程更新了 JENKINS_HOME 的内容，另一个是不会实时更新的。所以，同时启动两个 Jenkins master 共用同一个 JENKINS_HOME 的方案是不可行的。我们不能在 jenkins-a 挂了后，直接将流量切到 jenkins-b。因为 jenkins-b 必须重启。\n最后结论：多个 Jenkins master 共享同一个 JENKINS_HOME 的方案是无法使用 Jenkins master 的高可用。\n附录 Jenkins standby 实验环境：https://github.com/zacker330/jenkins-standby-experiment ","permalink":"https://showme.codes/zh-cn/2019-04-15-jenkins-standby/","summary":"\u003cblockquote\u003e\n\u003cp\u003e审校：\u003ca href=\"https://github.com/donhui\"\u003e王冬辉\u003c/a\u003e，\u003ca href=\"https://github.com/LinuxSuRen\"\u003elinuxsuren\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eJenkins master 的高可用是个老大难的问题。和很多人一样，笔者也想过两个 Jenkins master 共享同一个 JENKINS_HOME 的方案。了解 Jenkins 原理的人，都会觉得这个方案不可行。但是真的不可行吗？\u003c/p\u003e\n\u003cp\u003e由于工作原因，笔者需要亲自验证以上猜想。\u003c/p\u003e\n\u003ch3 id=\"jenkins_home-介绍\"\u003eJENKINS_HOME 介绍\u003c/h3\u003e\n\u003cp\u003eJenkins 所有状态数据都存放文件系统的目录中，这个目录被称为 JENKINS_HOME 目录。\u003c/p\u003e\n\u003ch3 id=\"实验环境介绍\"\u003e实验环境介绍\u003c/h3\u003e\n\u003cp\u003e笔者通过 Docker compose 启动两个独立的 Jenkins master，分别为 jenkins-a 和 jenkins-b。它们共用同一个 JENKINS_HOME 目录。相应的代码仓库的链接放在文章底部。\u003c/p\u003e\n\u003cp\u003e将代码克隆到本地后，进入仓库，执行 \u003ccode\u003edocker-compose up -d\u003c/code\u003e 即可启动实验环境。启动完成，在浏览器中输入 http://localhost:7088 可访问 jenkins-a，jenkins-b 的地址是 http://localhost:7089 。但是你会发现它们启动后的界面显示是不一样的。\u003c/p\u003e\n\u003cp\u003ejenkins-b 的界面如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/292372-fd4b85d5b9c8bdf6.png\"\u003e\u003c/p\u003e\n\u003cp\u003e而 jenkins-a 的界面如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/292372-1bfd4e033b6c25c8.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这时，将 jenkins-a 日志中的解锁密码（Unlock password）输入到 jenkins-b 的页面中，会得到报错信息：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eERROR: The password entered is incorrect, please check the file for the correct password\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这时，再次 jenkins-b 日志中的解锁密码（Unlock password）输入到表单中即可进入下一步。接下来就是按照提示一步步完成了。在 jenkins-b 安装步骤的最后一步，我们设置了管理员的用户名密码：admin/admin。然后就算完成任务了。\u003c/p\u003e","title":"你觉得能通过共享 JENKINS_HOME 目录实现 Jenkins master 的高可用吗？"},{"content":" 本文假设读者已经了解 Jenkins 基本概念及插件安装，Zabbix 基础概念。基于 Zabbix 3.4，Jenkins 2.8 做实验\n笔者最近的工作涉及到使用 Zabbix 监控 Jenkins。在谷歌上搜索到的文章非常少，能操作的就更少了。所以决定写一篇文章介绍如何使用 Zabbix 监控 Jenkins。\n下图为整体架构图：\n整体并不复杂，大体步骤如下：\n在 Jenkins 上安装 Metrics 插件，使 Jenkins 暴露 metrics api。 配置 Zabbix server 及 agent 以实现监控及告警 为方便读者实验，笔者将自己做实验的代码上传到了 GitHub，链接在文章末尾。使用的是 Docker Compose 技术（方便一次性启动所有的系统）。\n接下来，我们详细介绍 Metrics插件及如何实现 Zabbix 监控 Jenkins。\n1. 使 Jenkins 暴露 metrics api 安装 Metrics 插件，在系统配置中，会多出“Metrics”的配置，如下图： 配置项不复杂。我们需要点击“Generate\u0026hellip;”生成一个 Access Key（生成后，记得要保存）。这个 Key 用于身份校验，后面我们会用到。\n保存后，我们在浏览器中输入URL：http://localhost:8080/metrics/\u0026lt;刚生成的 Access Key\u0026gt; 验证 Jenkins 是否已经暴露 metrics。如果看到如下图，就说明可以进行下一步了。\n1.1 Metrics 插件介绍 Metrics 插件是基于 dropwizard/metrics 实现。它通过4个接口暴露指标数据：/metrics，/ping，/threads，/healthcheck。\n1.2 Metrics 插件：/metrics 接口介绍 点击上图中的metric链接（http://localhost:8080/metrics/\u0026lt;Access Key\u0026gt;/metrics），它暴露了以下指标数据：\n{ version: \u0026#34;3.0.0\u0026#34;, gauges: {...}, counters: {...}, histograms: {...}, meters: {...}, timers: {...} } 从数据结构中可以看出它将指标分成 5 种数据类型：\nGauges：某项指标的瞬时值，例如：当前 Jenkins executor 的总个数（jenkins.executor.count.value） Counters：某项指标的总数值，例如：http 请求活动连接数（http.activeRequests） Meters：一段时间内，某事件的发生概率，例如：Jenkins成功执行的任务每分钟的执行次数（jenkins.runs.success.m1_rate） Histogram：统计指标的分布情况。例如：Jenkins executor 数量的分布（jenkins.executor.count.history） Timer：某项指标的持续时间。例如：Jenkins 任务等待时间（jenkins.job.waiting.duration） 由于指标非常之多，我们就不分别介绍了。具体有哪些指标，读者朋友可以从代码仓库中的 metrics.json 文件了解。\n1.2 Metrics 插件其它接口介绍 /ping：接口返回 pong 代表 Jenkins 存活，如下图： /threads：返回 Jenkins 的线程信息\n/healthcheck：返回以下指标：\n{ disk-space: { healthy: true }, plugins: { healthy: true, message: \u0026#34;No failed plugins\u0026#34; }, temporary-space: { healthy: true }, thread-deadlock: { healthy: true } } 2. 配置 Zabbix server 与 agent 实现监控及告警 Zabbix server 通过与 Zabbix agent 进行通信实现数据的采集。而 Zabbix agent 又分为被动和主动两种模式。我们使用的是被动模式，也就是Zabbix server 向 agent 索要数据。\n所以，我们需要在 Zabbix agent 所在机器放一个获取 Jenkins 指标数据的脚本。再配置 Zabbix server 定时从该 agent 获取数据，最后配置触发器（trigger）实现告警。\n接下来的关于 Zabbix 的配置，基于我的 jenkins-zabbix 实验环境，读者朋友需要根据自己的实际情况变更。\n2.1 配置 Zabbix server 如何从 agent 获取指标数据 首先，我们需要告诉 Zabbix server 要与哪些 Zabbix agent 通信。所以，第一步是创建主机，如下图： 第二步，在主机列表中点击“Iterms”进行该主机的监控项设置： 第三步，进入创建监控项页面： 第四步，创建监控项： 这里需要解释其中几个选项为什么要那样填：\nType：是 Zabbix server 采集指标的类型，我们选择的是 Zabbix agent，如上文所说。 Key：由于我们要监控的指标并不是 Zabbix 预定义的。所以，需要使用用户自定义参数来实现监控 Jenkins 指标。Key 填的值为：jenkins.metrics[gauges.jenkins.node.count.value.value]。jenkins.metrics是需要执行的真正的 Key 名称。而 [] 内是传给该 Key 对应的命令的参数。对于初学者，Zabbix 这部分概念非常不好理解。也许这样会更好理解：在使用用户自定义参数来实现监控的情况下，Zabbix server 会将这个 Key 发送给 agent，然后 agent 根据这个 Key 执行指定的 逻辑 以获取指标数据。这个 逻辑 通常是一段脚本（shell命令或Python脚本等）。而脚本也是可以传参的，[]中的值就是传给脚本的参数。具体更多细节，下文会继续介绍。 Type of information：监控数据的数据类型，由于我们监控的是 Jenkins node 节点的个数，所以，使用数字整型。 Update interval：指 Zabbix server 多长时间向 agent 获取一次数据，为方便实验，我们设置为 2s。 到此，Zabbix server 端已经配置完成。\n2.2 配置 Zabbix agent 使其有能力从 Jenkins 获取指标数据 当 Zabbix agent 接收到 server 端的请求，如 jenkins.metrics[gauges.jenkins.node.count.value.value]。Zabbix agent 会读取自己的配置（agent 启动时会配置），配置内容如下：\n## Zabbix Agent Configuration File for Jenkins Master UserParameter=jenkins.metrics[*], python /usr/lib/zabbix/externalscripts/jenkins.metrics.py $1 根据 Key 名称（jenkins.metrics）找到相应的命令，即：python /usr/lib/zabbix/externalscripts/jenkins.metrics.py $1。并执行它，同时将参数 gauges.jenkins.node.count.value.value 传入到脚本 jenkins.metrics.py 中。jenkins.metrics.py 需要我们在 Jenkins agent 启动前放到 /usr/lib/zabbix/externalscripts/ 目录下。\njenkins.metrics.py 的源码在 jenkins-zabbix 实验环境中可以找到，篇幅有限，这里就简单介绍一下其中的逻辑。\njenkins.metrics.py 所做的事情，无非就是从 Jenkins master 的 metrics api 获取指标数据。但是由于 api 返回的是 JSON 结构，并不是 Zabbix server 所需要的格式。所以，jenkins.metrics.py 还做了一件事情，就是将 JSON 数据进行扁平化，比如原来的数据为：{\u0026quot;gauges\u0026quot;:{\u0026quot;jenkins.node.count.value\u0026quot;: { \u0026quot;value\u0026quot;: 1 }}} 扁平化后变成： gauges.jenkins.node.count.value.value=1。\n如果 jenkins.metrics.py 脚本没有接收参数的执行，它将一次性返回所有的指标如：\n...... histograms.vm.memory.pools.Metaspace.used.window.15m.stddev=0.0 histograms.vm.file.descriptor.ratio.x100.window.5m.p75=0.0 histograms.vm.memory.pools.PS-Old-Gen.used.window.5m.count=4165 gauges.vm.runnable.count.value=10 timers.jenkins.task.waiting.duration.mean=0.0 histograms.vm.memory.non-heap.committed.history.p99=123797504.0 gauges.vm.memory.pools.PS-Eden-Space.used.value=19010928 gauges.jenkins.node.count.value.value=1 histograms.vm.memory.pools.Code-Cache.used.window.15m.mean=44375961.6 ...... 但是，如果接收到具体参数，如 gauges.jenkins.node.count.value.value ，脚本只返回该参数的值。本例中，它将只返回 1。\njenkins.metrics.py 脚本之所以对 JSON 数据进行扁平化，是因为 Zabbix server 一次只拿一个指标的值（这点需要向熟悉 Zabbix 的人求证，笔者从文档中没有找到明确的说明）。\n注意：在 2.1 节中，如果 Key 值设置为：jenkins.metrics，Zabbix server 不会拿 jenkins.metrics.py 返回的所有的指标值自动创建对应的监控项。所以，Key 值必须设置为类似于 jenkins.metrics[gauges.jenkins.node.count.value.value] 这样的值。\n3. 配置 Zabbix server 监控指标，并告警 在经过 2.2 节的配置后，如果 Zabbix server 采集到数据，可通过_Monitoring -\u0026gt; Latest data -\u0026gt; Graph_菜单（如下图），看到图形化的报表：\n图形化的报表： 有了指标数据就可以根据它进行告警了。告警在 Zabbix 中称为触发器（trigger）。如下图，我们创建了一个当 Jenkins node 小于 2 时，就触发告警的触发器：\n至于最终触发器的后续行为是发邮件，还是发短信，属于细节部分，读者朋友可根据自己的情况进行设置。\n小结 在理解了 Zabbix server 与 agent 之间的通信原理的前提下，使用 Zabbix 监控 Jenkins 是不难的。笔者认为难点在于自动化整个过程。上文中，我们创建主机和添加监控项的过程，是手工操作的。虽然 Zabbix 能通过自动发现主机，自动关联模板来自动化上述过程，但是创建”自动化发现主机“和”自动关联动作“依然是手工操作。这不符合”自动化一切“的”追求“。\n最后，如果读者朋友不是历史包袱原因而选择 Zabbix，笔者在这里推荐 Prometheus，一款《Google 运维解密》推荐的开源监控系统。\n附录 Metrics 插件：https://wiki.jenkins.io/display/JENKINS/Metrics+Plugin dropwizard/metrics：https://metrics.dropwizard.io/4.0.0/ Zabbix 监控项类型：https://www.zabbix.com/documentation/3.4/zh/manual/config/items/itemtypes metrics.json： https://github.com/zacker330/jenkins-zabbix/blob/master/metrics.json jenkins-zabbix 实验环境：https://github.com/zacker330/jenkins-zabbix Prometheus：https://prometheus.io/ ","permalink":"https://showme.codes/zh-cn/2019-04-10-jenkins-zabbix-monitor/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文假设读者已经了解 Jenkins 基本概念及插件安装，Zabbix 基础概念。基于 Zabbix 3.4，Jenkins 2.8 做实验\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e笔者最近的工作涉及到使用 Zabbix 监控 Jenkins。在谷歌上搜索到的文章非常少，能操作的就更少了。所以决定写一篇文章介绍如何使用 Zabbix 监控 Jenkins。\u003c/p\u003e\n\u003cp\u003e下图为整体架构图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/292372-ecc7d290dd4d0f0f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e整体并不复杂，大体步骤如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在 Jenkins 上安装 Metrics 插件，使 Jenkins 暴露 metrics api。\u003c/li\u003e\n\u003cli\u003e配置 Zabbix server 及 agent 以实现监控及告警\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e为方便读者实验，笔者将自己做实验的代码上传到了 GitHub，链接在文章末尾。使用的是 Docker Compose 技术（方便一次性启动所有的系统）。\u003c/p\u003e\n\u003cp\u003e接下来，我们详细介绍 Metrics插件及如何实现 Zabbix 监控 Jenkins。\u003c/p\u003e\n\u003ch2 id=\"1-使-jenkins-暴露-metrics-api\"\u003e1. 使 Jenkins 暴露 metrics api\u003c/h2\u003e\n\u003cp\u003e安装 Metrics 插件，在系统配置中，会多出“Metrics”的配置，如下图：\n\u003cimg loading=\"lazy\" src=\"/assets/images/292372-ba867bb2509c6fc4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e配置项不复杂。我们需要点击“Generate\u0026hellip;”生成一个 Access Key（生成后，记得要保存）。这个 Key 用于身份校验，后面我们会用到。\u003c/p\u003e\n\u003cp\u003e保存后，我们在浏览器中输入URL：\u003ccode\u003ehttp://localhost:8080/metrics/\u0026lt;刚生成的 Access Key\u0026gt;\u003c/code\u003e 验证 Jenkins 是否已经暴露 metrics。如果看到如下图，就说明可以进行下一步了。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/292372-011d7fc64d176d63.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"11-metrics-插件介绍\"\u003e1.1 Metrics 插件介绍\u003c/h3\u003e\n\u003cp\u003eMetrics 插件是基于 dropwizard/metrics 实现。它通过4个接口暴露指标数据：/metrics，/ping，/threads，/healthcheck。\u003c/p\u003e","title":"使用 Zabbix 监控 Jenkins"},{"content":" 审校：LinuxSuRen（https://github.com/LinuxSuRen）\nGerrit 是一个基于 Git 版本控制的基于 Web 的代码审查工具 。笔者在学习它的过程中发现，要使用好它，第一步就是要理解 Change-Id。\n理解 Change-Id 要理解 Gerrit 的 Change-Id，我们就必须对“一次代码审查任务”有一个定义。通常，我们认为对一次完整的功能实现或 Bug 修复（即一次完整的变更）进行代码审查是合理的。而对一个半成品进行代码审查，得到的结论是不可靠的。因此，一次代码审查任务意味着是对一次变更进行审查。\nGerrit 使用 Change-Id 来标识一次变更。Change-Id 实际上就是一串字符串，类似这样：Ic8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b\n但是，一次变更通常会伴随多次 Git 提交（Commit），而且每次提交的提交是不同的 Commit Id（提交Id）。Gerrit 如何将多次提交关联到同一个 Change-Id 呢？\n我们需要在每次提交时，将 Change-Id 以规定的格式放在提交消息（Commit message）的Footer 部分中（最后一行）。如下图：\nChange-Id 为避免与提交 Id 冲突，通常以大写字母I为前缀。但是，我们怎么才能方便生成 Change-Id 呢？\n使用 Git 钩子生成 Change-Id Change-Id 最好是自动生成，并放到提交消息指定位置，这样才能节约开发者的时间。Gerrit 提供了标准的“commit-msg”钩子来实现。\nGit 提供了4个提交工作流钩子：pre-commit、prepare-commit-msg、commit-msg、post-commit。其中 commit-msg 钩子，会在我们执行 git commit 时被执行。\n本质上，commit-msg 钩子是一段脚本程序，放在 .git/hooks 目录下。commit-msg 脚本可以使用 Shell、Ruby、Python 等语言实现。\nGerrit 的 commit-msg 钩子直接从 Gerrit 下载：\n## 在项目目录下 curl -Lo .git/hooks/commit-msg http://\u0026lt;gerrit服务地址\u0026gt;/tools/hooks/commit-msg chmod u+x .git/hooks/commit-msg 接下来，在我们执行 git commit 后，再执行 git log 就可以看到 Change-Id 了。\n请注意，第一次 clone 代码到本地时，需要重新安装一次 commit-msg 钩子。因为它并不会被提交到版本库中。\nGitLab 也有类似的 Change-Id 在 GitLab 中，每个 Issue 都会有一个 Id。它是如何将 Issue Id 与 Commit Id 关联起来的呢？GitLab 的解决方案与 Gerrit 一样。只不过，GitLab 是在提交消息的第一行开始加入 Issue Id，格式如下：\n\u0026lt;project name\u0026gt;#\u0026lt;Issue Id\u0026gt;: \u0026lt;commit msg\u0026gt; 例如：devops#151: 实现参数化构建。\n接着，就可以在 GitLab 相应的 Issue#151 的详情页下看到下图内容：\n小结 相信不少初次接触 Gerrit 的同学被 Change-Id 搞得一头雾水。希望此文能给读者带来一些帮助。\n最后，可以看出，Change-Id 和 Issue-Id 本质上是同一样东西，都是变更的唯一标识，用于关联变更与代码提交。而变更Id 对于项目管理意义重大，因为它是代码指标与业务指标的连接点。\n参考 Gerrit 的 Change-Id 文档：https://gerrit-review.googlesource.com/Documentation/user-changeid.html Gerrit 的 commit-msg 钩子文档：https://gerrit-review.googlesource.com/Documentation/cmd-hook-commit-msg.html ","permalink":"https://showme.codes/zh-cn/2019-03-24-understand-gerrit-change-id/","summary":"\u003cblockquote\u003e\n\u003cp\u003e审校：LinuxSuRen（https://github.com/LinuxSuRen）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg alt=\"Gerrit workflow\" loading=\"lazy\" src=\"/assets/images/gerrit_workflow.png\"\u003e\u003c/p\u003e\n\u003cp\u003eGerrit 是一个基于 Git 版本控制的基于 Web 的代码审查工具 。笔者在学习它的过程中发现，要使用好它，第一步就是要理解 Change-Id。\u003c/p\u003e\n\u003ch3 id=\"理解-change-id\"\u003e理解 Change-Id\u003c/h3\u003e\n\u003cp\u003e要理解 Gerrit 的 Change-Id，我们就必须对“一次代码审查任务”有一个定义。通常，我们认为对一次完整的功能实现或 Bug 修复（即一次完整的变更）进行代码审查是合理的。而对一个半成品进行代码审查，得到的结论是不可靠的。因此，一次代码审查任务意味着是对一次变更进行审查。\u003c/p\u003e\n\u003cp\u003eGerrit 使用 Change-Id 来标识一次变更。Change-Id 实际上就是一串字符串，类似这样：\u003ccode\u003eIc8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e但是，一次变更通常会伴随多次 Git 提交（Commit），而且每次提交的提交是不同的 Commit Id（提交Id）。Gerrit 如何将多次提交关联到同一个 Change-Id 呢？\u003c/p\u003e\n\u003cp\u003e我们需要在每次提交时，将 Change-Id 以规定的格式放在提交消息（Commit message）的Footer 部分中（最后一行）。如下图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/gerrit-commit-message-with-change-id.png\"\u003e\u003c/p\u003e\n\u003cp\u003eChange-Id 为避免与提交 Id 冲突，通常以大写字母\u003ccode\u003eI\u003c/code\u003e为前缀。但是，我们怎么才能方便生成 Change-Id 呢？\u003c/p\u003e\n\u003ch3 id=\"使用-git-钩子生成-change-id\"\u003e使用 Git 钩子生成 Change-Id\u003c/h3\u003e\n\u003cp\u003eChange-Id 最好是自动生成，并放到提交消息指定位置，这样才能节约开发者的时间。Gerrit 提供了标准的“commit-msg”钩子来实现。\u003c/p\u003e\n\u003cp\u003eGit 提供了4个提交工作流钩子：pre-commit、prepare-commit-msg、commit-msg、post-commit。其中 commit-msg 钩子，会在我们执行 \u003ccode\u003egit commit\u003c/code\u003e 时被执行。\u003c/p\u003e\n\u003cp\u003e本质上，commit-msg 钩子是一段脚本程序，放在 .git/hooks 目录下。commit-msg 脚本可以使用 Shell、Ruby、Python 等语言实现。\u003c/p\u003e\n\u003cp\u003eGerrit 的 commit-msg 钩子直接从 Gerrit 下载：\u003c/p\u003e","title":"理解 Gerrit 的 Change-Id"},{"content":"面向读者：需要了解 Jenkins 流水线的基本语法。\nElectron 是由 Github 开发，用 HTML，CSS 和 JavaScript 来构建跨平台桌面应用程序的一个开源库。\n本文将介绍 Electron 桌面应用的流水线的设计。\n但是如何介绍呢？倒是个大问题。笔者尝试直接贴代码，在代码注释中讲解。这是一次尝试，希望得到你的反馈。\n完整代码 pipeline { // 我们决定每一个阶段指定 agent，所以， // 流水线的 agent 设置为 none，这样不会占用 agent agent none // 指定整条流水线的环境变量 environment { APP_VERSION = \u0026#34;\u0026#34; APP_NAME = \u0026#34;electron-webpack-quick-start\u0026#34; } stages { stage(\u0026#34;生成版本号\u0026#34;){ agent {label \u0026#34;linux\u0026#34; } steps{ script{ APP_VERSION = generateVersion(\u0026#34;1.0.0\u0026#34;) echo \u0026#34;version is ${APP_VERSION}\u0026#34; }} } stage(\u0026#39;并行构建\u0026#39;) { // 快速失败，只要其中一个平台构建失败， // 整次构建算失败 failFast true // parallel 闭包内的阶段将并行执行 parallel { stage(\u0026#39;Windows平台下构建\u0026#39;) { agent {label \u0026#34;windows \u0026amp;\u0026amp; nodejs\u0026#34; } steps { echo \u0026#34;${APP_VERSION}\u0026#34; } } stage(\u0026#39;Linux平台下构建\u0026#39;) { agent {label \u0026#34;linux \u0026amp;\u0026amp; nodejs\u0026#34; } // 不同平台可能存在不同的环境变量 // environment 支持阶段级的环境变量 environment{ SUFFIX = \u0026#34;tar.xz\u0026#34; APP_PLATFORM = \u0026#34;linux\u0026#34; ARTIFACT_PATH = \u0026#34;dist/${APP_NAME}-${APP_PLATFORM}-${APP_VERSION}.${SUFFIX}\u0026#34; } steps { script{ // Jenkins nodejs 插件提供的 nodejs 包装器 // 包装器内可以执行 npm 命令。 // nodejs10.15.2 是在 Jenkins 的全局工具配置中添加的 NodeJS 安装器 nodejs(nodeJSInstallationName: \u0026#39;nodejs10.15.2\u0026#39;) { // 执行具体的构建命令 sh \u0026#34;npm install yarn\u0026#34; sh \u0026#34;yarn version --new-version ${APP_VERSION}\u0026#34; sh \u0026#34;yarn install\u0026#34; sh \u0026#34;yarn dist --linux deb ${SUFFIX}\u0026#34; // 上传制品 uploadArtifact(\u0026#34;${APP_NAME}\u0026#34;, \u0026#34;${APP_VERSION}\u0026#34;, \u0026#34;${ARTIFACT_PATH}\u0026#34;) }}} // 将括号合并是为了让代码看起来紧凑，提升阅读体验。下同。 } stage(\u0026#39;Mac平台下构建\u0026#39;) { agent {label \u0026#34;mac \u0026amp;\u0026amp; nodejs\u0026#34; } stages { stage(\u0026#39;mac 下阶段1\u0026#39;) { steps { echo \u0026#34;staging 1\u0026#34; } } stage(\u0026#39;mac 下阶段2\u0026#39;) { steps { echo \u0026#34;staging 2\u0026#34; } } } } } } stage(\u0026#34;其它阶段，读者可根据情况自行添加\u0026#34;){ agent {label \u0026#34;linux\u0026#34;} steps{ echo \u0026#34;发布\u0026#34; } } } post { always { cleanWs() } } // 清理工作空间 } def generateVersion(def ver){ def gitCommitId = env.GIT_COMMIT.take(7) return \u0026#34;${ver}-${gitCommitId}.${env.BUILD_NUMBER}\u0026#34; } def uploadArtifact(def appName, def appVersion, def artifactPath){ echo \u0026#34;根据参数将制品上传到制品库中，待测试\u0026#34; } 代码补充说明 因为 Electron 是跨平台的，我们需要将构建过程分别放到 Windows、Linux、Mac 各平台下执行。所以，不同平台的构建任务需要执行在不同的 agent 上。我们通过在 stage 内定义 agent 实现。如在“Mac平台下构建”的阶段中，agent {label \u0026quot;mac \u0026amp;\u0026amp; nodejs\u0026quot; } 指定了只有 label 同时包括了 mac 和 nodejs 的 agent 才能执行构建。\n多平台的构建应该是并行的，以提升流水线的效率。我们通过 parallel 指令实现。\n另外，默认 Electron 应用使用的三段式版本号设计，即 Major.Minor.Patch。但是笔者认为三段式的版本号信息还不够追踪应用与构建之间的关系。笔者希望版本号能反应出构建号和源代码的 commit id。函数 generateVersion 用于生成此类版本号。生成的版本号，看起来类似这样：1.0.0-f7b06d0.28。\n完整源码地址：https://github.com/zacker330/electronjs-pipeline-demo\n小结 上例中，Electron 应用的流水线设计思路，不只是针对 Electron 应用，所有的跨平台应用的流水线都可以参考此思路进行设计。设计思路大概如下：\n多平台构建并行化。本文只有操作系统的类型这个维度进行了说明。现实中，还需要考虑其它维度，如系统位数（32位、64位）、各操作系统下的各版本。 各平台下的构建只做一次编译打包。并将制品上传到制品库，以方便后续步骤或阶段使用。 全局变量与平台相关变量进行分离。 最后，希望能给读者带来一些启发。\n参考： 持续交付的八大原则：https://blog.csdn.net/tony1130/article/details/6673741 Jenkins nodejs 插件：https://plugins.jenkins.io/nodejs Electron 版本管理：https://electronjs.org/docs/tutorial/electron-versioning#semver 审校 LinuxSuRen（https://github.com/LinuxSuRen） ","permalink":"https://showme.codes/zh-cn/2019-3-10-electronjs-pipeline-demo/","summary":"\u003cp\u003e面向读者：需要了解 Jenkins 流水线的基本语法。\u003c/p\u003e\n\u003cp\u003eElectron 是由 Github 开发，用 HTML，CSS 和 JavaScript 来构建跨平台桌面应用程序的一个开源库。\u003c/p\u003e\n\u003cp\u003e本文将介绍 Electron 桌面应用的流水线的设计。\u003c/p\u003e\n\u003cp\u003e但是如何介绍呢？倒是个大问题。笔者尝试直接贴代码，在代码注释中讲解。这是一次尝试，希望得到你的反馈。\u003c/p\u003e\n\u003ch3 id=\"完整代码\"\u003e完整代码\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-groovy\" data-lang=\"groovy\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epipeline\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 我们决定每一个阶段指定 agent，所以，\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 流水线的 agent 设置为 none，这样不会占用 agent\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eagent\u003c/span\u003e \u003cspan class=\"n\"\u003enone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 指定整条流水线的环境变量\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eenvironment\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003eAPP_VERSION\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003eAPP_NAME\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;electron-webpack-quick-start\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003estages\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;生成版本号\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eagent\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003elabel\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;linux\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esteps\u003c/span\u003e\u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003escript\u003c/span\u003e\u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"n\"\u003eAPP_VERSION\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003egenerateVersion\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;1.0.0\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"n\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;version is ${APP_VERSION}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;并行构建\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 快速失败，只要其中一个平台构建失败，\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 整次构建算失败\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003efailFast\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// parallel 闭包内的阶段将并行执行\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eparallel\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Windows平台下构建\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eagent\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003elabel\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;windows \u0026amp;\u0026amp; nodejs\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esteps\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"n\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;${APP_VERSION}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Linux平台下构建\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eagent\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003elabel\u003c/span\u003e  \u003cspan class=\"s2\"\u003e\u0026#34;linux \u0026amp;\u0026amp; nodejs\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// 不同平台可能存在不同的环境变量\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// environment 支持阶段级的环境变量\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eSUFFIX\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;tar.xz\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eAPP_PLATFORM\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;linux\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eARTIFACT_PATH\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;dist/${APP_NAME}-${APP_PLATFORM}-${APP_VERSION}.${SUFFIX}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esteps\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"n\"\u003escript\u003c/span\u003e\u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// Jenkins nodejs 插件提供的 nodejs 包装器\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// 包装器内可以执行 npm 命令。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// nodejs10.15.2 是在 Jenkins 的全局工具配置中添加的 NodeJS 安装器\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003enodejs\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003enodeJSInstallationName:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;nodejs10.15.2\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"c1\"\u003e// 执行具体的构建命令\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;npm install yarn\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;yarn version --new-version ${APP_VERSION}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;yarn install\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"n\"\u003esh\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;yarn dist --linux deb ${SUFFIX}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"c1\"\u003e// 上传制品\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              \u003cspan class=\"n\"\u003euploadArtifact\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;${APP_NAME}\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;${APP_VERSION}\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;${ARTIFACT_PATH}\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e}}}\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 将括号合并是为了让代码看起来紧凑，提升阅读体验。下同。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Mac平台下构建\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eagent\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003elabel\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;mac \u0026amp;\u0026amp; nodejs\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estages\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;mac 下阶段1\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003esteps\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;staging 1\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;mac 下阶段2\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003esteps\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;staging 2\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"o\"\u003e}\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;其它阶段，读者可根据情况自行添加\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eagent\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003elabel\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;linux\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esteps\u003c/span\u003e\u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;发布\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e}\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003epost\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003ealways\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003ecleanWs\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 清理工作空间\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003egenerateVersion\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"n\"\u003ever\u003c/span\u003e\u003cspan class=\"o\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"n\"\u003egitCommitId\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eenv\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eGIT_COMMIT\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003etake\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e7\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;${ver}-${gitCommitId}.${env.BUILD_NUMBER}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003euploadArtifact\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"n\"\u003eappName\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"n\"\u003eappVersion\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"n\"\u003eartifactPath\u003c/span\u003e\u003cspan class=\"o\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;根据参数将制品上传到制品库中，待测试\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"代码补充说明\"\u003e代码补充说明\u003c/h3\u003e\n\u003cp\u003e因为 Electron 是跨平台的，我们需要将构建过程分别放到 Windows、Linux、Mac 各平台下执行。所以，不同平台的构建任务需要执行在不同的 agent 上。我们通过在 \u003ccode\u003estage\u003c/code\u003e 内定义 \u003ccode\u003eagent\u003c/code\u003e 实现。如在“Mac平台下构建”的阶段中，\u003ccode\u003eagent {label \u0026quot;mac \u0026amp;\u0026amp; nodejs\u0026quot; }\u003c/code\u003e 指定了只有 label 同时包括了 mac 和 nodejs 的 agent 才能执行构建。\u003c/p\u003e","title":"Electron 应用的流水线设计"},{"content":"注意，下文所说的“老板”通常指业务提出方。\n问题描述 上个星期，在持续交付2.0的群中，群主发出一个别人的提问。在我看来，这个问题在软件工程领域非常的典型，所以想单独写一篇博客来讨论。以下是问题原文：\n我是一个开发部门的管理人员，团队规模还较小，开发需要兼任测试和部分应用运维的工作，但公司业务条线不算少（差不多有4个左右，目前部门中基本是按每个条线分配2-3个开发，出现某个组资源不足以满足需求时再进行调剂），之前这么做还算稳定，因为各业务对交付频率的要求一般很低，但近期各业务都提出了一个较紧急的项目需求，而且都还无法推迟或拒绝，资源一下子变得非常紧张（如果可以推迟某一个当然就会有富裕资源，但是这些项目由于各种原因公司都不能放弃，也很难推迟），如果依然每个组独立完成自己的项目，可能导致大部分项目因无法按时交付失败，而且每个组还必须保留少部分资源用于处理日常业务，如果临时招聘也来不及做培训，我们暂时是让资深一些的程序员和管理人员参与多个项目中的开发，但仍不能完全解决问题（他们抱怨任务太多都来不及测试），请问帮主遇到这类情况应该如何协调？\n提问者所说的情况，在现实中，太普遍了：\n（无法拒绝的）紧急需求的插入，打乱原有的步骤 开发除了需求的开发，还需要处理日常业务 人员不足：临时招聘也解决不了 开发人员报怨任务太多，来不及测试（就上线？） 项目无法按时交付 可以看出，“人员不足”是“项目无法按时交付”这个“果”的其中一个“因”。而提问者希望通过业务线之间人员的调剂和临时招聘的方式增加人员，但仍不能完全解决问题。笔者每当听到增加人员的信息，都会想起《人月神话》所说的：向进度落后的项目中增加人手，只会使进度更加落后。\n而从提问者口中也了解到，原来人员还算足（感觉是刚刚好），只是因为出现了一个无法拒绝的紧急需求，问题就暴露出来了。\n问题解决 面对这样的问题？你是如何解决的呢？以下是我个人提出来的解法，不一定对，只做交流：\n第1是把当前工作的优先级排出来\n把所有的工作内容（包括日常维护和新功能实现）列出来，同时，也要找到这些工作的交集（避免重复开发）。工作内容列出来后，确定它们的业务价值及优先级，并预估其开发难度。有些新功能是老板直接下发的，但是实现难度过高且业务价值又不高（团队及产品经理觉得），能和老板谈就和老板谈。这部分工作是我觉得最难的。\n这个工作的优先级一定要让老板看到。主要是避免老板中间随意的插入需求。当然，有时需要向现实妥协，但不是每次。同时也要让所有人达成共识，遵守这个优先级。\n第2是找出团队平时工作中最耗时的环节（瓶颈），想办法在这个环节上减少耗时（自动化或者别的办法）。一般来说，经常工作在这个耗时环节的人会知道如何优化它。\n第3是慢慢让人可以流动\n意思是人没办法调剂到其它项目，通常是因为他不了解其它项目（业务或者技术）。所以，在平时，就要注意将项目的“知识”尽可能准确地传递给更多人。当然，也可以定向的传递。具体操作方式要看团队平时的协作方式。\n最后，1，2，3步需要重复执行，同时1，2，3步也不是顺序的。\n笔者提出这样的解法并不是笔者猜的，而是有依据的。依据如下：\n人员不足只是表象，我们怎么知道是真的人员不足，还是没有真正发挥每个人的最大潜能呢？第2、3步是为了让每个人发挥最大的潜能。而工具方面，个人建议通过看板可视化人员的工作内容，来达到了解当前资源状态的目的。 即使每个人的潜能都发挥到了极致，但是还是出现人员不足的情况啊。这就是第1步要解决的问题。这时，我们要学会舍弃。但是为什么老板就不会舍弃，老爱插入一些所谓的紧急需求呢？个人认为是因为老板不了解你当前的工作内容及其优先级。所以，这个优先级一定要和老板达成共识。 小结 当我把解法提出来了，群里的同学就提出了质疑：如何确定老板提出新功能是业务价值不高的？毕竟老板从整个公司考虑问题的。\n这位同学提出了一个软件工程领域内经常发生的问题：执行者怀疑业务提出者提出的需求的价值。个人觉得质疑是好事。但是质疑之后，双方有没有讨论及讨论结果才是关键。讨论了就容易形成共识，有了共识，大家才好力往一处使。题外话，“我只要结果，不管过程”的管理理念的适用范围是拿出来讨论的。\n最后，以上解法，不一定适用所有的情况。比如在外包项目管理中，可能就不适合。\n","permalink":"https://showme.codes/zh-cn/2019-3-1-software-engineering-tricky-problem/","summary":"\u003cp\u003e注意，下文所说的“老板”通常指业务提出方。\u003c/p\u003e\n\u003ch3 id=\"问题描述\"\u003e问题描述\u003c/h3\u003e\n\u003cp\u003e上个星期，在持续交付2.0的群中，群主发出一个别人的提问。在我看来，这个问题在软件工程领域非常的典型，所以想单独写一篇博客来讨论。以下是问题原文：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我是一个开发部门的管理人员，团队规模还较小，开发需要兼任测试和部分应用运维的工作，但公司业务条线不算少（差不多有4个左右，目前部门中基本是按每个条线分配2-3个开发，出现某个组资源不足以满足需求时再进行调剂），之前这么做还算稳定，因为各业务对交付频率的要求一般很低，但近期各业务都提出了一个较紧急的项目需求，而且都还无法推迟或拒绝，资源一下子变得非常紧张（如果可以推迟某一个当然就会有富裕资源，但是这些项目由于各种原因公司都不能放弃，也很难推迟），如果依然每个组独立完成自己的项目，可能导致大部分项目因无法按时交付失败，而且每个组还必须保留少部分资源用于处理日常业务，如果临时招聘也来不及做培训，我们暂时是让资深一些的程序员和管理人员参与多个项目中的开发，但仍不能完全解决问题（他们抱怨任务太多都来不及测试），请问帮主遇到这类情况应该如何协调？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e提问者所说的情况，在现实中，太普遍了：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e（无法拒绝的）紧急需求的插入，打乱原有的步骤\u003c/li\u003e\n\u003cli\u003e开发除了需求的开发，还需要处理日常业务\u003c/li\u003e\n\u003cli\u003e人员不足：临时招聘也解决不了\u003c/li\u003e\n\u003cli\u003e开发人员报怨任务太多，来不及测试（就上线？）\u003c/li\u003e\n\u003cli\u003e项目无法按时交付\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e可以看出，“人员不足”是“项目无法按时交付”这个“果”的其中一个“因”。而提问者希望通过业务线之间人员的调剂和临时招聘的方式增加人员，但仍不能完全解决问题。笔者每当听到增加人员的信息，都会想起《人月神话》所说的：向进度落后的项目中增加人手，只会使进度更加落后。\u003c/p\u003e\n\u003cp\u003e而从提问者口中也了解到，原来人员还算足（感觉是刚刚好），只是因为出现了一个无法拒绝的紧急需求，问题就暴露出来了。\u003c/p\u003e\n\u003ch3 id=\"问题解决\"\u003e问题解决\u003c/h3\u003e\n\u003cp\u003e面对这样的问题？你是如何解决的呢？以下是我个人提出来的解法，不一定对，只做交流：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第1是把当前工作的优先级排出来\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e把所有的工作内容（包括日常维护和新功能实现）列出来，同时，也要找到这些工作的交集（避免重复开发）。工作内容列出来后，确定它们的业务价值及优先级，并预估其开发难度。有些新功能是老板直接下发的，但是实现难度过高且业务价值又不高（团队及产品经理觉得），能和老板谈就和老板谈。这部分工作是我觉得最难的。\u003c/p\u003e\n\u003cp\u003e这个工作的优先级一定要让老板看到。主要是避免老板中间随意的插入需求。当然，有时需要向现实妥协，但不是每次。同时也要让所有人达成共识，遵守这个优先级。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第2是找出团队平时工作中最耗时的环节（瓶颈），想办法在这个环节上减少耗时（自动化或者别的办法）。一般来说，经常工作在这个耗时环节的人会知道如何优化它。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第3是慢慢让人可以流动\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e意思是人没办法调剂到其它项目，通常是因为他不了解其它项目（业务或者技术）。所以，在平时，就要注意将项目的“知识”尽可能准确地传递给更多人。当然，也可以定向的传递。具体操作方式要看团队平时的协作方式。\u003c/p\u003e\n\u003cp\u003e最后，1，2，3步需要重复执行，同时1，2，3步也不是顺序的。\u003c/p\u003e\n\u003cp\u003e笔者提出这样的解法并不是笔者猜的，而是有依据的。依据如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e人员不足只是表象，我们怎么知道是真的人员不足，还是没有真正发挥每个人的最大潜能呢？第2、3步是为了让每个人发挥最大的潜能。而工具方面，个人建议通过看板可视化人员的工作内容，来达到了解当前资源状态的目的。\u003c/li\u003e\n\u003cli\u003e即使每个人的潜能都发挥到了极致，但是还是出现人员不足的情况啊。这就是第1步要解决的问题。这时，我们要学会舍弃。但是为什么老板就不会舍弃，老爱插入一些所谓的紧急需求呢？个人认为是因为老板不了解你当前的工作内容及其优先级。所以，这个优先级一定要和老板达成共识。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e当我把解法提出来了，群里的同学就提出了质疑：如何确定老板提出新功能是业务价值不高的？毕竟老板从整个公司考虑问题的。\u003c/p\u003e\n\u003cp\u003e这位同学提出了一个软件工程领域内经常发生的问题：执行者怀疑业务提出者提出的需求的价值。个人觉得质疑是好事。但是质疑之后，双方有没有讨论及讨论结果才是关键。讨论了就容易形成共识，有了共识，大家才好力往一处使。题外话，“我只要结果，不管过程”的管理理念的适用范围是拿出来讨论的。\u003c/p\u003e\n\u003cp\u003e最后，以上解法，不一定适用所有的情况。比如在外包项目管理中，可能就不适合。\u003c/p\u003e","title":"活多人少，每个需求都紧急，多数项目延期，怎么破？"},{"content":"通过脚本命令行批量修改Jenkins任务 最近，笔者所在团队的 Jenkins 所在的服务器经常报硬盘空间不足。经查发现很多任务没有设置“丢弃旧的构建”。通知所有的团队检查自己的 Jenkins 任务有没有设置丢弃旧的构建，有些不现实。\n一开始想到的是使用 Jenkins 的 API 来实现批量修改所有的 Jenkins 任务。笔者对这个解决方案不满意，经 Google 发现有同学和我遇到了同样的问题。他使用的更“技巧”的方式：在 Jenkins 脚本命令行中，通过执行 Groovy 代码操作 Jenkins 任务。\n总的来说，就两步：\n进入菜单：系统管理 \u0026ndash;\u0026gt; 脚本命令行\n在输入框中，粘贴如下代码：\nimport jenkins.model.Jenkins import hudson.model.Job import jenkins.model.BuildDiscarderProperty import hudson.tasks.LogRotator // 遍历所有的任务 Jenkins.instance.allItems(Job).each { job -\u0026gt; if ( job.isBuildable() \u0026amp;\u0026amp; job.supportsLogRotator() \u0026amp;\u0026amp; job.getProperty(BuildDiscarderProperty) == null) { println \u0026#34; \\\u0026#34;${job.fullDisplayName}\\\u0026#34; 处理中\u0026#34; job.addProperty(new BuildDiscarderProperty(new LogRotator (2, 10, 2, 10))) println \u0026#34;$job.name 已更新\u0026#34; } } return; /** LogRotator构造参数分别为： daysToKeep: If not -1, history is only kept up to this days. numToKeep: If not -1, only this number of build logs are kept. artifactDaysToKeep: If not -1 nor null, artifacts are only kept up to this days. artifactNumToKeep: If not -1 nor null, only this number of builds have their artifacts kept. **/ 脚本命令行介绍 脚本命令行（Jenkins Script Console），它是 Jenkins 的一个特性，允许你在 Jenkins master 和 Jenkins agent 的运行时环境执行任意的 Groovy 脚本。这意味着，我们可以在脚本命令行中做任何的事情，包括关闭 Jenkins，执行操作系统命令 rm -rf /（所以不能使用 root 用户运行 Jenkins agent）等危险操作。\n除了上文中的，使用界面来执行 Groovy 脚本，还可以通过 Jenkins HTTP API：/script执行。具体操作，请参考 官方文档。\n问题：代码执行完成后，对任务的修改有没有被持久化？ 当我们代码job.addProperty(new BuildDiscarderProperty(new LogRotator (2, 10, 2, 10)))执行后，这个修改到底有没有持久化到文件系统中呢（Jenkins 的所有配置默认都持久化在文件系统中）？我们看下 hudson.model.Job 的源码，在addProperty方法背后是有进行持久化的：\npublic void addProperty(JobProperty\u0026lt;? super JobT\u0026gt; jobProp) throws IOException { ((JobProperty)jobProp).setOwner(this); properties.add(jobProp); save(); } 小结 本文章只介绍了批量修改“丢弃旧的构建”的配置，如果还希望修改其它配置，可以参考 hudson.model.Job 的 源码\n不得不提醒读者朋友，Jenkins 脚本命令行是一把双刃剑，大家操作前，请考虑清楚影响范围。如果有必要，请提前做好备份。\n","permalink":"https://showme.codes/zh-cn/2019-2-23-jenkins-script-console-in-practice/","summary":"\u003ch3 id=\"通过脚本命令行批量修改jenkins任务\"\u003e通过脚本命令行批量修改Jenkins任务\u003c/h3\u003e\n\u003cp\u003e最近，笔者所在团队的 Jenkins 所在的服务器经常报硬盘空间不足。经查发现很多任务没有设置“丢弃旧的构建”。通知所有的团队检查自己的 Jenkins 任务有没有设置丢弃旧的构建，有些不现实。\u003c/p\u003e\n\u003cp\u003e一开始想到的是使用 Jenkins 的 API 来实现批量修改所有的 Jenkins 任务。笔者对这个解决方案不满意，经 Google 发现有同学和我遇到了同样的问题。他使用的更“技巧”的方式：在 Jenkins 脚本命令行中，通过执行 Groovy 代码操作 Jenkins 任务。\u003c/p\u003e\n\u003cp\u003e总的来说，就两步：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e进入菜单：\u003cem\u003e系统管理 \u0026ndash;\u0026gt; 脚本命令行\u003c/em\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e在输入框中，粘贴如下代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-groovy\" data-lang=\"groovy\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003ejenkins.model.Jenkins\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003ehudson.model.Job\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003ejenkins.model.BuildDiscarderProperty\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003ehudson.tasks.LogRotator\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 遍历所有的任务\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eJenkins\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003einstance\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eallItems\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eJob\u003c/span\u003e\u003cspan class=\"o\"\u003e).\u003c/span\u003e\u003cspan class=\"na\"\u003eeach\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eisBuildable\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esupportsLogRotator\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetProperty\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eBuildDiscarderProperty\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eprintln\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34; \\\u0026#34;${job.fullDisplayName}\\\u0026#34; 处理中\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eaddProperty\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eBuildDiscarderProperty\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eLogRotator\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"o\"\u003e)))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eprintln\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;$job.name 已更新\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e/**\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003eLogRotator构造参数分别为：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003edaysToKeep:  If not -1, history is only kept up to this days.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003enumToKeep: If not -1, only this number of build logs are kept.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003eartifactDaysToKeep: If not -1 nor null, artifacts are only kept up to this days.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003eartifactNumToKeep: If not -1 nor null, only this number of builds have their artifacts kept.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e**/\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"脚本命令行介绍\"\u003e脚本命令行介绍\u003c/h3\u003e\n\u003cp\u003e脚本命令行（Jenkins Script Console），它是 Jenkins 的一个特性，允许你在 Jenkins master 和 Jenkins agent 的运行时环境执行任意的 Groovy 脚本。这意味着，我们可以在脚本命令行中做任何的事情，包括关闭 Jenkins，执行操作系统命令 \u003ccode\u003erm -rf /\u003c/code\u003e（所以不能使用 root 用户运行 Jenkins agent）等危险操作。\u003c/p\u003e","title":"批量修改Jenkins任务的技巧"},{"content":"李翔商业内参 9月15日（中秋）那期的主题是：最好的礼物是一个真诚的建议。我深表认同。\n两年前，我被另一个资深的同事说了一通（当时我们正结对编程）。原因是我说了另一个同事的代码写得烂，还随手看了下提交记录，发现是当时的leader。\n先不说这位同事教训得是不是。我心里很不爽：我说的就是真话嘛。\n后来，我发邮件给总经理，说了一通，诚恳地向他请教，到底是谁的错。后来总经理给我回了一封邮件，邮件其中一段是这样的：\n有个有意思的故事： 弟子问：”师父您有时候打人、骂人；有时候对人又彬彬有礼，这里面有什么玄机吗？” 师父说：对待上等人直指人心，可打可骂，以真面目待他；对待中等人最多隐喻他，要讲分寸，他受不起打骂；对待下等人要面带微笑，双手合十，他很脆弱、装不下太多指责和训斥，他只配用世俗的礼节 我想说的不是谁是上等人，谁不是，而是受众不一样，说话的方法就会不一样。 中国有句老话，世事洞明皆学问，人情练达即文章。能够把意见说的让人接受是个技能，要不断练习，你自己也多揣摩。 到底是谁的问题不重要， 严于律己，宽以待人对你的职业生涯会很有帮助。 每每对别人过于苛刻时，我都会想起“严于律己，宽以待人”。虽然，有时还会偶尔犯贱。但是它的确改变了我。\n\u0026hellip;不小心翻出自己2016年写的小短文。\n","permalink":"https://showme.codes/zh-cn/2018-12-11-to-be-broad-minded-toward-others/","summary":"\u003cp\u003e李翔商业内参 9月15日（中秋）那期的主题是：最好的礼物是一个真诚的建议。我深表认同。\u003c/p\u003e\n\u003cp\u003e两年前，我被另一个资深的同事说了一通（当时我们正结对编程）。原因是我说了另一个同事的代码写得烂，还随手看了下提交记录，发现是当时的leader。\u003c/p\u003e\n\u003cp\u003e先不说这位同事教训得是不是。我心里很不爽：我说的就是真话嘛。\u003c/p\u003e\n\u003cp\u003e后来，我发邮件给总经理，说了一通，诚恳地向他请教，到底是谁的错。后来总经理给我回了一封邮件，邮件其中一段是这样的：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e   有个有意思的故事：\n   弟子问：”师父您有时候打人、骂人；有时候对人又彬彬有礼，这里面有什么玄机吗？”\n   师父说：对待上等人直指人心，可打可骂，以真面目待他；对待中等人最多隐喻他，要讲分寸，他受不起打骂；对待下等人要面带微笑，双手合十，他很脆弱、装不下太多指责和训斥，他只配用世俗的礼节\n   我想说的不是谁是上等人，谁不是，而是受众不一样，说话的方法就会不一样。\n   中国有句老话，世事洞明皆学问，人情练达即文章。能够把意见说的让人接受是个技能，要不断练习，你自己也多揣摩。\n   到底是谁的问题不重要， 严于律己，宽以待人对你的职业生涯会很有帮助。\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e每每对别人过于苛刻时，我都会想起“严于律己，宽以待人”。虽然，有时还会偶尔犯贱。但是它的确改变了我。\u003c/p\u003e\n\u003cp\u003e\u0026hellip;不小心翻出自己2016年写的小短文。\u003c/p\u003e","title":"最好的礼物是一个真诚的建议"},{"content":"你对你6年跳5次有什么感想？ 上周进行了阿里三面。奇怪的是他们居然迟到10来分钟。上来直接问的都是我职业生涯的问题（不清楚面试官的岗位）。\n从我第一份工作到最近的工作，一个个问入职时间，做了什么，离职时间，为什么离职。我也“老实”一一回复。这个过程，加上视频的网络质量不好，我感觉好像被压着说话。\n最后，面试官问我：你对你6年跳5次是什么感想？（后来算算我应该是毕业6年，工作7年。而5次是指包括这次跳成功）\n听到这个问题，我一下懵了。我从来没有仔细想过这个问题，想了一下，说出了自己的内心的声音：太年轻，太冲动。\n面试官没有说下去。后来草草的结束了20多分钟的面试。\n结束后，我问自己：你为什么在别人眼里就是不稳定？HR 眼里跳槽“多”就是不稳定？\n回想自己的回答，的确给别人不稳定的感觉。因为一次主要是因为对 leader 不爽，一次是因为办公室政治干不过别人。所以，各位提前想好自己跳槽的“借口”很重要。\n但是，另一个问题开始不断萦绕自己：你为什么跳这么多次槽？回想自己的每次跳槽，没有答案。于是，我反过来想：公司如何才能留住我？\n第一家创业公司7人，我的导师走了，我唯一留在这家公司的理由都没有了。 第二家公司做开源软件，Leader 换成了我不喜欢的人。现在想想真幼稚。现在想想，真没必要。 第三家公司是一家咨询+外包的公司，做了一年多的外包，发现自己想做自己的产品，我留在公司的理由是有产品给我做，询问当时的办公室负责人，并没有产品可以做。 第四家公司是做产品了，组织构架的调整我不满意，产品不再是我，细节不方便说，我就想好好做产品，留下来的理由也没有了。 目前这家公司，进来的初衷是从零建设一个敏捷的团队，后来希望破灭。然后我的希望变成能好好写代码。目前留下来的理由是好好磨炼自己的技术。但有机会会看。因为这里是 code is cheap。 企业应该如何留住员工 我尝试把“我”的私人问题扩大到组织上思考：企业应该如何留住员工？\n最近看的《红雀》的台词跳了出来：\n给了别人想要的，你就会得到你想要的。\n留住员工，靠的不是“留”，而是“给”。当然，前提是这个人值得留。\n这时，会有读者想到马斯洛需求层次理论： 这个理论告诉我们如何满足一个人的不同层次的需求。但是，站在公司层面，如何操作呢？毕竟公司里会同时存在不同层次需求的人，而且同一个人不同时期的需求还可能千差万别。\n如何能满足所有人的需求（至少是大部分）？我想到了一句话：海纳百川，有容乃大。但是不可能路边随随便便就收纳，而是能帮助公司实现目标的人。这是海纳百川的前提。\n所以，公司应该像大海，能满足不同的人不同时期的需求。这是什么意思？\n生理需求上，提供上行业内比较有竞争力的薪资，像奈飞。安全需求上，比如提供团体险；社交需求上，让工程师之间有更多的交流的机会，比如谷歌为促进员工的非正式交流，在食堂排队，一般要4分钟。因为时间长了大家会掏出手机来看，时间短了，同事们又聊不起来……等等。\nP.S. 公司文化并不是没有理论的发展，而是要根据公司的发展需要进行调整。\n我再次强调，上述想法的前提：不可能路边随随便便就收纳，而是能帮助公司实现目标的人。所以，要严进。\n同时，上述想法是理想情况，处于企业生命周期不同阶段的公司需要根据自己的具体情况量力而行。\n同时，我也提醒读者啊：我没有办过企业，上述想法都个人臆想。\n教训 给那些还在路上的新人，不要冲动，不要冲动，不要冲动。不要学我。我是反例。\n成本 最后，突然想到，好奇阿里为什么不让HR 先把这些稳定性问题在一面给问了？就可以节约一二面的工程师的时间了。毕竟工程师的时间永远相对 HR 的时间更缺。招工程师比招 HR 更难，如果我没有理解错的话。\n小结 听说30到35岁是大多数人职场的转折点。我相信了。想清自己想要的，是做好职业规划的前提。\n","permalink":"https://showme.codes/zh-cn/2018-6-24-alibaba-interview/","summary":"\u003ch4 id=\"你对你6年跳5次有什么感想\"\u003e你对你6年跳5次有什么感想？\u003c/h4\u003e\n\u003cp\u003e上周进行了阿里三面。奇怪的是他们居然迟到10来分钟。上来直接问的都是我职业生涯的问题（不清楚面试官的岗位）。\u003c/p\u003e\n\u003cp\u003e从我第一份工作到最近的工作，一个个问入职时间，做了什么，离职时间，为什么离职。我也“老实”一一回复。这个过程，加上视频的网络质量不好，我感觉好像被压着说话。\u003c/p\u003e\n\u003cp\u003e最后，面试官问我：你对你6年跳5次是什么感想？（后来算算我应该是毕业6年，工作7年。而5次是指包括这次跳成功）\u003c/p\u003e\n\u003cp\u003e听到这个问题，我一下懵了。我从来没有仔细想过这个问题，想了一下，说出了自己的内心的声音：太年轻，太冲动。\u003c/p\u003e\n\u003cp\u003e面试官没有说下去。后来草草的结束了20多分钟的面试。\u003c/p\u003e\n\u003cp\u003e结束后，我问自己：你为什么在别人眼里就是不稳定？HR 眼里跳槽“多”就是不稳定？\u003c/p\u003e\n\u003cp\u003e回想自己的回答，的确给别人不稳定的感觉。因为一次主要是因为对 leader 不爽，一次是因为办公室政治干不过别人。所以，各位提前想好自己跳槽的“借口”很重要。\u003c/p\u003e\n\u003cp\u003e但是，另一个问题开始不断萦绕自己：你为什么跳这么多次槽？回想自己的每次跳槽，没有答案。于是，我反过来想：公司如何才能留住我？\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e第一家创业公司7人，我的导师走了，我唯一留在这家公司的理由都没有了。\u003c/li\u003e\n\u003cli\u003e第二家公司做开源软件，Leader 换成了我不喜欢的人。现在想想真幼稚。现在想想，真没必要。\u003c/li\u003e\n\u003cli\u003e第三家公司是一家咨询+外包的公司，做了一年多的外包，发现自己想做自己的产品，我留在公司的理由是有产品给我做，询问当时的办公室负责人，并没有产品可以做。\u003c/li\u003e\n\u003cli\u003e第四家公司是做产品了，组织构架的调整我不满意，产品不再是我，细节不方便说，我就想好好做产品，留下来的理由也没有了。\u003c/li\u003e\n\u003cli\u003e目前这家公司，进来的初衷是从零建设一个敏捷的团队，后来希望破灭。然后我的希望变成能好好写代码。目前留下来的理由是好好磨炼自己的技术。但有机会会看。因为这里是 code is cheap。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"企业应该如何留住员工\"\u003e企业应该如何留住员工\u003c/h4\u003e\n\u003cp\u003e我尝试把“我”的私人问题扩大到组织上思考：企业应该如何留住员工？\u003c/p\u003e\n\u003cp\u003e最近看的\u003ca href=\"https://movie.douban.com/subject/25704492/\"\u003e《红雀》\u003c/a\u003e的台词跳了出来：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e给了别人想要的，你就会得到你想要的。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e留住员工，靠的不是“留”，而是“给”。当然，前提是这个人值得留。\u003c/p\u003e\n\u003cp\u003e这时，会有读者想到马斯洛需求层次理论：\n\u003cimg alt=\"马斯洛模型\" loading=\"lazy\" src=\"https://upload-images.jianshu.io/upload_images/292372-8daa25b0ecf990bf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\"\u003e\u003c/p\u003e\n\u003cp\u003e这个理论告诉我们如何满足一个人的不同层次的需求。但是，站在公司层面，如何操作呢？毕竟公司里会同时存在不同层次需求的人，而且同一个人不同时期的需求还可能千差万别。\u003c/p\u003e\n\u003cp\u003e如何能满足所有人的需求（至少是大部分）？我想到了一句话：海纳百川，有容乃大。但是不可能路边随随便便就收纳，而是能帮助公司实现目标的人。这是海纳百川的\u003cstrong\u003e前提\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e所以，公司应该像大海，能满足不同的人不同时期的需求。这是什么意思？\u003c/p\u003e\n\u003cp\u003e生理需求上，提供上行业内比较有竞争力的薪资，像奈飞。安全需求上，比如提供团体险；社交需求上，让工程师之间有更多的交流的机会，比如谷歌为促进员工的非正式交流，在食堂排队，一般要4分钟。因为时间长了大家会掏出手机来看，时间短了，同事们又聊不起来……等等。\u003c/p\u003e\n\u003cp\u003eP.S. 公司文化并不是没有理论的发展，而是要根据公司的发展需要进行调整。\u003c/p\u003e\n\u003cp\u003e我再次强调，上述想法的前提：\u003cstrong\u003e不可能路边随随便便就收纳，而是能帮助公司实现目标的人\u003c/strong\u003e。所以，要严进。\u003c/p\u003e\n\u003cp\u003e同时，上述想法是理想情况，处于\u003ca href=\"https://book.douban.com/subject/1084649/\"\u003e企业生命周期\u003c/a\u003e不同阶段的公司需要根据自己的具体情况量力而行。\u003c/p\u003e\n\u003cp\u003e同时，我也提醒读者啊：我没有办过企业，上述想法都个人臆想。\u003c/p\u003e\n\u003ch4 id=\"教训\"\u003e教训\u003c/h4\u003e\n\u003cp\u003e给那些还在路上的新人，不要冲动，不要冲动，不要冲动。不要学我。我是反例。\u003c/p\u003e\n\u003ch4 id=\"成本\"\u003e成本\u003c/h4\u003e\n\u003cp\u003e最后，突然想到，好奇阿里为什么不让HR 先把这些稳定性问题在一面给问了？就可以节约一二面的工程师的时间了。毕竟工程师的时间永远相对 HR 的时间更缺。招工程师比招 HR 更难，如果我没有理解错的话。\u003c/p\u003e\n\u003ch4 id=\"小结\"\u003e小结\u003c/h4\u003e\n\u003cp\u003e听说30到35岁是大多数人职场的转折点。我相信了。想清自己想要的，是做好职业规划的前提。\u003c/p\u003e","title":"阿里三面后的思考"},{"content":" 提示：本文要求读者有一定的 Ansible 使用经验\n最近一年才有机会在生产环境上使用 Ansible。用的过程中，想把一些小技巧记录下来，避免自己忘记。如果能帮助到其他同学就更好了。如果有同学指出有更好的方法，就更更好了。\n技巧1：校验你的模板文件是否正确 通常我们会使用template module 来生成应用的配置，比如生成 Nginx 的配置或者 sudoers 配置。而像 sudoers 文件内的配置错误可能直接导致无法登录。所以，我们希望在生成这些配置文件后能校验一下它的正确性。如果校验失败，直接停止，不生成该配置文件。\n而 template module 有一个属性 validate 就是为了实现这一需求的：\n- template: src: \u0026#34;user-sudoers\u0026#34; dest: \u0026#34;/etc/sudoers.d/abc\u0026#34; validate: visudo -cf %s 校验 Nginx 配置文件的文件：\n- name: Copy the nginx file template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf validate: \u0026#34;/usr/sbin/nginx -t -c %s\u0026#34; notify: - restart nginx 校验 Prometheus 配置文件：\n- name: Copy Prometheus config template: src: prometheus.yml.j2 dest: \u0026#34;/etc/prometheus.yml\u0026#34; validate: \u0026#34;promtool check config %s\u0026#34; notify: reload prometheus config 校验 Logstash 配置文件：\n- name: template configs template: src: \u0026#34;logstash-filter.conf\u0026#34; dest: \u0026#34;/opt/logstash/conf\u0026#34; validate: \u0026#34;logstash -t -f %s\u0026#34; environment: JAVA_HOME: \u0026#34;{{JAVA_HOME}}\u0026#34; ## logstash 命令需要 JAVA_HOME 环境变量 技巧2：使用 host 变量解决分布式系统中的 id 问题 在部署 Zookeeper 时，通常会部署 3 台组成集群，同时每台 Zookeeper 都需要在配置一个 myid 的文本文件，而这个文件中只放id。而 id 是要求每台机器都是不同的。这时 host 变量派上用场了。定义 host 变量有两种方式：\n第一种：直接在 inventory 文件中定义 [zk] 192.168.1.11 myid=1 192.168.1.12 myid=2 192.168.1.13 myid=3 第二种：在 host_vars 目录中定义 这种方式笔者认为可维护性更高\n├── group_vars ├── host_vars │ ├── 192.168.1.11 │ ├── 192.168.1.12 │ ├── 192.168.1.13 ├── hosts #cat 192.168.1.11 myid: 1 不推荐两种方式都使用，因为变量的作用域问题会把你搞晕\n技巧3：在执行 shell 时需要某个环境变量 某个 shell 需要一个临时变量，可以使用 environment 实现\n- name: install | Build commons daemon. shell: \u0026#34;./configure \u0026amp;\u0026amp; make chdir=/opt/pinpoint/\u0026#34; environment: - JAVA_HOME: \u0026#34;{{ JAVA_HOME }}\u0026#34; 技巧4：Jinjia2 语法：去除最后的逗号 以下方式会生成：a,a,a,a, 注意最后的逗号我们是不需要的：\n{%raw%} {% for f in files %} a, {% endfor %} {% endraw %} 这时，我们可以这样：\n{%raw%} {% for f in files %} a{%- if not loop.last -%},{% endif %} {% endfor %} {% endraw %} 技巧5： 利用 host 变量解决机器连接方式的不统一的问题 机器标准化要求每台机器的ssh连接方式及管理员用户名及密码都是一样的。但是事实中，面对老机器，常常做不过。所以，我们的 Ansible 脚本必须能做到不同的机器可以使用不同的连接方式、管理员用户名和密码。利用 host 变量就可以实现了。\n举个例子，当前的文件内容如下：\n├── group_vars ├── host_vars │ ├── 192.168.1.11 │ ├── 192.168.1.12 │ ├── 192.168.1.13 ├── hosts #cat 192.168.1.11 ansible_ssh_user: abc ansible_become_method: sudo ansible_ssh_private_key_file: /users/abc/id_rsa #cat 192.168.1.12 ansible_ssh_user: bcd ansible_become_method: sudo ansible_ssh_pass: 1234567 #cat 192.168.1.13 ansible_ssh_user: bcd ansible_become_method: su ansible_ssh_private_key_file: /users/bcd/id_rsa ansible_ssh_pass: 1234567 小结 常识和技巧之间的界限很模糊。总之，希望对读者有帮助。\n","permalink":"https://showme.codes/zh-cn/2018-6-22-ansible-in-action-1/","summary":"\u003cblockquote\u003e\n\u003cp\u003e提示：本文要求读者有一定的 Ansible 使用经验\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e最近一年才有机会在生产环境上使用 Ansible。用的过程中，想把一些小技巧记录下来，避免自己忘记。如果能帮助到其他同学就更好了。如果有同学指出有更好的方法，就更更好了。\u003c/p\u003e\n\u003ch3 id=\"技巧1校验你的模板文件是否正确\"\u003e技巧1：校验你的模板文件是否正确\u003c/h3\u003e\n\u003cp\u003e通常我们会使用\u003ccode\u003etemplate\u003c/code\u003e module 来生成应用的配置，比如生成 Nginx 的配置或者 sudoers 配置。而像 sudoers 文件内的配置错误可能直接导致无法登录。所以，我们希望在生成这些配置文件后能校验一下它的正确性。如果校验失败，直接停止，不生成该配置文件。\u003c/p\u003e\n\u003cp\u003e而 \u003ccode\u003etemplate\u003c/code\u003e module 有一个属性 \u003ccode\u003evalidate\u003c/code\u003e 就是为了实现这一需求的：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;user-sudoers\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/etc/sudoers.d/abc\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003evalidate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003evisudo -cf %s\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e校验 Nginx 配置文件的文件：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eCopy the nginx file\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003enginx.conf.j2\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/etc/nginx/nginx.conf\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003evalidate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/usr/sbin/nginx -t -c %s\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003enotify\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e   \u003c/span\u003e- \u003cspan class=\"l\"\u003erestart nginx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e校验 Prometheus 配置文件：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eCopy Prometheus config\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eprometheus.yml.j2\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/etc/prometheus.yml\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003evalidate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;promtool check config %s\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003enotify\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"l\"\u003ereload prometheus config\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e校验 Logstash 配置文件：\u003c/p\u003e","title":"使用Ansible实现自动化运维的一些技巧"},{"content":"注：本文要求读者对Ansible和 Jenkins有一定的认识。\n题记: 幸福的家庭都是相似的 不幸的家庭各有各的不幸\n行业内各巨头的自动化运维架构都各种功能各种酷炫，如下图，让人可望不可及。现在最终的样子大家都知道了，但问题是如何根据自己团队当前的情况一步步向那个目标演进？\n笔者所在团队，三个半开发，要维护几十台云机器，部署了十来个应用，这些应用90%都是遗留系统。应用系统的编译打包基本在程序员自己的电脑上。分支管理也清一色的 dev 分支开发，测试通过后，再合并到 master 分支。生产环境的应用配置要登录上具体的机器看才知道，更不用说配置中心及配置版本化了。\n对了，连基本的机器级别的基础监控都没有。\n我平时的工作是 50% 业务开发，50% 运维。面对这么多问题，我就想啊，如何在低成本情况下实现自动化运维。本文就是总结我在这方面一些经验和实践。希望对读者有帮助。\n别说话，先上监控和告警 事情有轻重缓急，监控和告警是我觉得一开始就要做的，即使业务开发被拖慢。只有知道了当前的情况，你才好做下一步计划。\n现在市面上监控系统很多：Zabbix、Open-Falcon、Prometheus。最终作者选择了 Prometheus。因为：\n它是拉模式的 它方便使用文本方式来配置，有利于配置版本化 插件太多了，想要监控什么，基本都会有现成的 以上三者，我基本都要重新学，我为什么不学一个 Google SRE 书上推荐的呢？ 之前我们已经介绍过，人少机器多，所以，安装 Prometheus 的过程也必须要自动化，同时版本化。笔者使用的是 Ansible + Git 实现。最终样子如下：\n这里需要简单介绍一下：\nPrometheus Server 负责监控数据收集和存储 Prometheus Alert manager 负责根据告警规则进行告警，可集成很多告警通道 node-exporter 的作用就是从机器读取指标，然后暴露一个 http 服务，Prometheus 就是从这个服务中收集监控指标。当然 Prometheus 官方还有各种各样的 exporter。 使用 Ansible 作为部署工具的一个好处是太多现成的 role 了，安装Prometheus 时，我使用的是现成的：prometheus-ansble\n有了监控数据后，我们就可以对数据进行可视化，Grafana 和 Prometheus 集成得非常好，所以，我们又部署了 Grafana:\n在 Grafana 上查看 nodex-exporter 收集的数据的效果图大概如下： 可是，我们不可能24小时盯着屏幕看CPU负载有没有超吧？这时候就要上告警了，Promehtues 默认集成了 N 多告警渠道。可惜没有集成钉钉。但也没有关系，有好心的同学开源了钉钉集成 Prometheus 告警的组件：prometheus-webhook-dingtalk。接着，我们告警也上了： 完成以上工作后，我们的基础监控的架子就完成了。为我们后期上 Redis 监控、JVM 监控等更上层的监控做好了准备。\n配置版本化要从娃娃抓起 在搭建监控系统的过程中，我们已经将配置抽离出来，放到一个单独的代码仓库进行管理。以后所有部署，我们都会将配置和部署逻辑分离。\n关于如何使用 Ansible 进行配置管理，可以参考这篇文章：How to Manage Multistage Environments with Ansible 。我们就是使用这种方式来组织环境变量的。\n├── environments/ # Parent directory for our environment-specific directories │ │ │ ├── dev/ # Contains all files specific to the dev environment │ │ ├── group_vars/ # dev specific group_vars files │ │ │ ├── all │ │ │ ├── db │ │ │ └── web │ │ └── hosts # Contains only the hosts in the dev environment │ │ │ ├── prod/ # Contains all files specific to the prod environment │ │ ├── group_vars/ # prod specific group_vars files │ │ │ ├── all │ │ │ ├── db │ │ │ └── web │ │ └── hosts # Contains only the hosts in the prod environment │ │ │ └── stage/ # Contains all files specific to the stage environment │ ├── group_vars/ # stage specific group_vars files │ │ ├── all │ │ ├── db │ │ └── web │ └── hosts # Contains only the hosts in the stage environment │ 现阶段，我们所有的配置都以文本的方式存储，将来要切换成使用Consul做配置中心，也非常的方便，因为 Ansible2.0以上的版本已经原生集成了consule: consul_module\nTips: Ansible 的配置变量是有层次的，这为我们的配置管理提供了非常大的灵活性。\nJenkins 化：将打包交给 Jenkins 我们要将所有的项目的打包工作交给 Jenkins。当然，现实中我们是先将一些项目放到 Jenkins 上打包，逐步将项目放上 Jenkins。\n首先我们要有 Jenkins。搭建 Jenkins 同样有现成的 Ansible 脚本：ansible-role-jenkins。注意了，在网上看到的大多文章告诉你 Jenkins 都是需要手工安装插件的，而我们使用的这个 ansible-role-jenkins 实现了自动安装插件，你只需要加一个配置变量 jenkins_plugins 就可以了，官方例子如下：\n--- - hosts: all vars: jenkins_plugins: - blueocean - ghprb - greenballs - workflow-aggregator jenkins_plugin_timeout: 120 pre_tasks: - include_tasks: java-8.yml roles: - geerlingguy.java - ansible-role-jenkins 搭建好 Jenkins 后，就要集成 Gitlab 了。我们原来就有Gitlab了，所以，不需要重新搭建。如何集成就不细表了，网络上已经很多文章。\n最终 Jenkins 搭建成以下这个样子： 关于 Jenkins master 与 Jenkins agent 的连接方式，由于网络环境各不相同，网上也有很多种方式，大家自行选择适合的方式。\n好，现在我们需要告诉 Jenkins 如何对我们的业务代码进行编译打包。有两种方法：\n界面上设置 使用 Jenkinsfile：类似于 Dockerfile 的一种文本文件，具体介绍：Using a Jenkinsfile 作者毫不犹豫地选择了第2种，因为一是利于版本化；二是灵活。\nJenkinsfile 类似这样：\npipeline { agent any stages { stage(\u0026#39;Build\u0026#39;) { steps { sh \u0026#39;./gradlew clean build\u0026#39; archiveArtifacts artifacts: \u0026#39;**/target/*.jar\u0026#39;, fingerprint: true } } } } 那么 Jenkinsfile 放哪里呢？和业务代码放在一起，类似这样每个工程各自管理自己的 Jenkinsfile:\n这时，我们就可以在 Jenkins 上创建一个 pipleline Job了：\n关于分支管理，我们人少，所以，建议所有项目统一在 master 分支进行开发并发布。\n让 Jenkins 帮助我们执行 Ansible 之前我们都是在程序员的电脑执行 Ansible 的，现在我们要把这项工作交给 Jenkins。具体操作：\n在 Jenkins 安装 Ansible 插件 在 Jenkinsfile 中执行 withCredentials([sshUserPrivateKey(keyFileVariable:\u0026#34;deploy_private\u0026#34;,credentialsId:\u0026#34;deploy\u0026#34;),file(credentialsId: \u0026#39;vault_password\u0026#39;, variable: \u0026#39;vault_password\u0026#39;)]) { ansiblePlaybook vaultCredentialsId: \u0026#39;vault_password\u0026#39;, inventory: \u0026#34;environments/prod\u0026#34;, playbook: \u0026#34;playbook.yaml\u0026#34;, extraVars:[ ansible_ssh_private_key_file: [value: \u0026#34;${deploy_private}\u0026#34;, hidden: true], build_number: [value: \u0026#34;${params.build_number}\u0026#34;, hidden: false] ] } 这里需要解释下：\nansiblePlaybook 是 Jenkins ansible 插件提供的 pipeline 语法，类似手工执行：ansible-playbook 。 withCredentials 是 Credentials Binding 插件的语法，用于引用一些敏感信息，比如执行 Ansible 时需要的 ssh key 及 Ansible Vault 密码。 一些敏感配置变量，我们使用 Ansible Vault 技术加密。 Ansible 脚本应该放哪？ 我们已经知道各个项目各自负责自己的自动化构建，所以，Jenkinfile 就放到各自项目中。那项目的部署呢？同样的道理，我们觉得也应该由各个项目自行负责，所以，我们的每个要进行部署的项目下都会有一个 ansible 目录，用于存放 Ansible 脚本。类似这样： 但是，怎么用呢？我们会在打包阶段将 Ansible 目录进行 zip 打包。真正部署时，再解压执行里面的 playbook。\n快速为所有的项目生成 Ansible 脚本及Jenkinsfile 上面，我们将一个项目进行 Jenkins 化和 Ansible 化，但是我们还有很多项目需要进行同样的动作。考虑到这是体力活，而且以后我们还会经常做这样事，所以笔者决定使用 cookiecutter 技术自动生成 Jenkinsfile 及 Ansible 脚本，创建一个项目，像这样： 小结 总结下来，我们小团队的自动化运维实施的顺序大概为：\n上基础监控 上 Gitlab 上 Jenkins，并集成 Gitlab 使用 Jenkins 实现自动编译打包 使用 Jenkins 执行 Ansible 以上只是一个架子，基于这个“架子”，就可以向那些大厂的高大上的架构进行演进了。比如：\nCMDB的建设：我们使用 ansible-cmdb 根据 inventory 自动生成当前所有机器的情况 发布管理：Jenkins 上可以对发布的每个阶段进行定制。蓝绿发布等发布方式可以使用通过修改 Ansible 脚本和 Inventory 实现。 自动扩缩容：通过配置 Prometheus 告警规则，调用相应 webhook 就可以实现 ChatOps: ChatOps实战 以上就是笔者关于自动化运维的一些实践。还在演进路上。希望能与大家交流。\n","permalink":"https://showme.codes/zh-cn/2018-6-7-devops-in-action/","summary":"\u003cp\u003e\u003cstrong\u003e注：本文要求读者对Ansible和 Jenkins有一定的认识。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e题记: 幸福的家庭都是相似的 不幸的家庭各有各的不幸\u003c/p\u003e\n\u003cp\u003e行业内各巨头的自动化运维架构都各种功能各种酷炫，如\u003ca href=\"http://www.learnfuture.com/article/1749\"\u003e下图\u003c/a\u003e，让人可望不可及。现在最终的样子大家都知道了，但问题是如何根据自己团队当前的情况一步步向那个目标演进？\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"/assets/images/292372-4a6707c7415fc889.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\"\u003e\u003c/p\u003e\n\u003cp\u003e笔者所在团队，三个半开发，要维护几十台云机器，部署了十来个应用，这些应用90%都是遗留系统。应用系统的编译打包基本在程序员自己的电脑上。分支管理也清一色的 dev 分支开发，测试通过后，再合并到 master 分支。生产环境的应用配置要登录上具体的机器看才知道，更不用说配置中心及配置版本化了。\u003c/p\u003e\n\u003cp\u003e对了，连基本的机器级别的基础监控都没有。\u003c/p\u003e\n\u003cp\u003e我平时的工作是 50% 业务开发，50% 运维。面对这么多问题，我就想啊，如何在低成本情况下实现自动化运维。本文就是总结我在这方面一些经验和实践。希望对读者有帮助。\u003c/p\u003e\n\u003ch4 id=\"别说话先上监控和告警\"\u003e别说话，先上监控和告警\u003c/h4\u003e\n\u003cp\u003e事情有轻重缓急，监控和告警是我觉得一开始就要做的，即使业务开发被拖慢。只有知道了当前的情况，你才好做下一步计划。\u003c/p\u003e\n\u003cp\u003e现在市面上监控系统很多：Zabbix、Open-Falcon、Prometheus。最终作者选择了 Prometheus。因为：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e它是拉模式的\u003c/li\u003e\n\u003cli\u003e它方便使用文本方式来配置，有利于配置版本化\u003c/li\u003e\n\u003cli\u003e插件太多了，想要监控什么，基本都会有现成的\u003c/li\u003e\n\u003cli\u003e以上三者，我基本都要重新学，我为什么不学一个 Google SRE 书上推荐的呢？\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e之前我们已经介绍过，人少机器多，所以，安装 Prometheus 的过程也必须要自动化，同时版本化。笔者使用的是 Ansible + Git 实现。最终样子如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"prometheus\" loading=\"lazy\" src=\"/assets/images/292372-5efa9abb7354b6b2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这里需要简单介绍一下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ePrometheus Server 负责监控数据收集和存储\u003c/li\u003e\n\u003cli\u003ePrometheus Alert manager 负责根据告警规则进行告警，可集成很多告警通道\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/prometheus/node_exporter\"\u003enode-exporter\u003c/a\u003e 的作用就是从机器读取指标，然后暴露一个 http 服务，Prometheus 就是从这个服务中收集监控指标。当然 Prometheus 官方还有各种各样的 exporter。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e使用 Ansible 作为部署工具的一个好处是太多现成的 role 了，安装Prometheus 时，我使用的是现成的：\u003ca href=\"https://github.com/ernestas-poskus/ansible-prometheus\"\u003eprometheus-ansble\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e有了监控数据后，我们就可以对数据进行可视化，Grafana 和 Prometheus 集成得非常好，所以，我们又部署了 Grafana:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-4c78ee26c4ad763f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在 Grafana 上查看 nodex-exporter 收集的数据的效果图大概如下：\n\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-e6027d31ba651dca.png\"\u003e\u003c/p\u003e\n\u003cp\u003e可是，我们不可能24小时盯着屏幕看CPU负载有没有超吧？这时候就要上告警了，Promehtues 默认集成了 N 多告警渠道。可惜没有集成钉钉。但也没有关系，有好心的同学开源了钉钉集成 Prometheus 告警的组件：\u003ca href=\"https://github.com/timonwong/prometheus-webhook-dingtalk\"\u003eprometheus-webhook-dingtalk\u003c/a\u003e。接着，我们告警也上了：\n\u003cimg alt=\"集成告警\" loading=\"lazy\" src=\"/assets/images/292372-b4ec4be4e922d064.png\"\u003e\u003c/p\u003e","title":"一些小团队的自动化运维实践经验"},{"content":" 题记：既生亮何生瑜。\n摘要：标题虽然是为了解释有了 IP 地址，为什么还要用 MAC 地址，但是本文的重点在于理解为什么要有 IP 这样的东西。本文对读者的定位是知道 MAC 地址是什么，IP 地址是什么。\n一开始时，网络中的机器并不多。大家都连到同一个集线器就可以了，就可以实现互通。这时，机器 A 发消息到机器 B ，消息头里附上机器 B 的MAC，集线器收到消息后就广播给所有连到集线器的机器。\n机器 C 收到消息，发现消息里的 MAC 地址和自己的不一样，就丢弃。机器B发现消息里的 MAC 地址和自己一样，就收到下并解析。\n这样机制带来问题很明显：首先每次广播，给所在网络带来不必要的浪费。所以，就出现了交换机。它能识别消息里的目标 MAC 地址后，直接就消息丢到机器 B 所连接的端口中。另一个角度，交换机必须记住所有的 MAC 地址和端口之间的关系。\n这样的机制在网络规规模小的时候是高效的。但是当网络规模扩大到全球的时候，不可能让一台交换机记录下全球这么多的网络设备，也不可能让全球的机器连接到一台交换机上。\n那如果是多台交换机呢？\n想像一下，你是斯坦福的学生，你的电脑 x 的网络直连的是学校的交换机，而学校的交换机又连美国国家网络交换机。而美国国家网络交换机又直接的是中国国家网络交换机，中国服务器 y 直连的是中国国家交换机。\n你想访问中国的服务器 y 中的资源。你了解到服务器 y 的 MAC 地址是00:0C:29:01:00:12，所以你在消息里附上这个 MAC 地址。\n学校交换机收到消息后，拿到 MAC 地址后就愣了，这是要发给谁啊？因为中国服务器 y 并不是直连学校交换机的。这时，学校交换机有一个选择，就是收到不明的 MAC 地址时，一律转发给默认端口。斯坦福交换机就将消息转给美国国家交换机。\n美国国家交换机同样发愣了，因为没有这条 MAC 地址对应的端口。它又直接向默认端口：中国国家网络交换机。\n中国国家网络交换机收到消息，发现自己记录了 MAC 地址 对应的是服务器 y。就直接将你这位斯坦福学生的消息转发到服务器 y 所连接的端口。\n最终，我们的服务器 y 终于收到来自美国斯坦福学生的资源访问请求。\n那么，我们的服务器 y 如何将相应的资源返回给学生呢？将消息中的源MAC 地址作为响应消息的目标 MAC 地址发送给中国国家交换机不就可以了？同样的机制，只不过是把源地址和目标地址反一下。\n这下，我们是不是完美实现使用交换机组建美国网络和中国网络的互通？\n但是美国和中国并不能代表全世界。其他国家也需要加入这个大网络。当日本国家交换机也接入美国国家交换机后，斯坦福学生的消息从学校到达美国国家交换机后就需要进行广播所有直连自己的端口了，因为这时，它没有对外的所谓默认端口了。这里有点烧脑，容各位同学一点时间思考。\n也就是说，当两个网络互接时，MAC 地址 + 交换机还能解决问题广播问题，但是两个以上的网络互连时，MAC 地址 + 交换机就没有办法解决广播问题了。\n这时，我们面临的问题就是无法使用现有的技术—— MAC 地址 + 交换机——解决多网络互连的问题了。所以，需要发明一种新的技术。\n而 IP 协议就是就是解决此问题的一项技术。\n事实上，IP协议的产生并不只是为解决上述的“广播问题”。还解决了很多其他网络传输过程会遇到的问题，比如一次传输的消息过大时，如何对消息进行分组等问题。\n小结 由于历史原因，MAC 地址及相关技术先出现，但是后来发现它并不能解决所有（已知）的问题，所以，先驱们发明了 IP 地址及相关技术来解决。\n另一个角度，个人认为，由于 MAC 地址没有办法表达网络中的子网的概念，而 IP 地址可以。如果网络互换设备（比如路由器）能从目标 MAC 地址中分析出目标网络，而不是只是目标主机，IP 地址还会出现吗？\n有另一个有趣的问题：如果历史反过来，一开始就使用的是 IP 地址，而不是 MAC 地址，我们现在的网络世界会怎么样？\n[未完待续]\n","permalink":"https://showme.codes/zh-cn/2018-5-17-understand-mac-ip/","summary":"\u003cblockquote\u003e\n\u003cp\u003e题记：既生亮何生瑜。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e摘要：标题虽然是为了解释有了 IP 地址，为什么还要用 MAC 地址，但是本文的重点在于理解为什么要有 IP 这样的东西。本文对读者的定位是知道 MAC 地址是什么，IP 地址是什么。\u003c/p\u003e\n\u003cp\u003e一开始时，网络中的机器并不多。大家都连到同一个集线器就可以了，就可以实现互通。这时，机器 A 发消息到机器 B ，消息头里附上机器 B 的MAC，集线器收到消息后就广播给所有连到集线器的机器。\u003c/p\u003e\n\u003cp\u003e机器 C 收到消息，发现消息里的 MAC 地址和自己的不一样，就丢弃。机器B发现消息里的 MAC 地址和自己一样，就收到下并解析。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-dc3520e3cf34d31d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这样机制带来问题很明显：首先每次广播，给所在网络带来不必要的浪费。所以，就出现了交换机。它能识别消息里的目标 MAC 地址后，直接就消息丢到机器 B 所连接的端口中。另一个角度，交换机必须记住所有的 MAC 地址和端口之间的关系。\u003c/p\u003e\n\u003cp\u003e这样的机制在网络规规模小的时候是高效的。但是当网络规模扩大到全球的时候，不可能让一台交换机记录下全球这么多的网络设备，也不可能让全球的机器连接到一台交换机上。\u003c/p\u003e\n\u003cp\u003e那如果是多台交换机呢？\u003c/p\u003e\n\u003cp\u003e想像一下，你是斯坦福的学生，你的电脑 x 的网络直连的是学校的交换机，而学校的交换机又连美国国家网络交换机。而美国国家网络交换机又直接的是中国国家网络交换机，中国服务器 y 直连的是中国国家交换机。\u003c/p\u003e\n\u003cp\u003e你想访问中国的服务器 y 中的资源。你了解到服务器 y 的 MAC 地址是00:0C:29:01:00:12，所以你在消息里附上这个 MAC 地址。\u003c/p\u003e\n\u003cp\u003e学校交换机收到消息后，拿到 MAC 地址后就愣了，这是要发给谁啊？因为中国服务器 y 并不是直连学校交换机的。这时，学校交换机有一个选择，就是收到不明的 MAC 地址时，一律转发给默认端口。斯坦福交换机就将消息转给美国国家交换机。\u003c/p\u003e\n\u003cp\u003e美国国家交换机同样发愣了，因为没有这条 MAC 地址对应的端口。它又直接向默认端口：中国国家网络交换机。\u003c/p\u003e\n\u003cp\u003e中国国家网络交换机收到消息，发现自己记录了 MAC 地址 对应的是服务器 y。就直接将你这位斯坦福学生的消息转发到服务器 y 所连接的端口。\u003c/p\u003e\n\u003cp\u003e最终，我们的服务器 y 终于收到来自美国斯坦福学生的资源访问请求。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-dee29f5c16c55875.png\"\u003e\u003c/p\u003e","title":"通俗解释有了 IP 地址，为什么还要用 MAC 地址？"},{"content":"想法 历史原因，我们一直使用的是阿里云经典网络的 ECS，疲于业务的开发及人力不足，一直没有特别大的动力迁移到 VPC 下。\n而经典网络下的 ECS 的公网 IP 是收费的，而且没有公网 IP 有时会不方便。\n购买公网 IP 时的费用 未购买公网 IP 时的费用 如果是按月购买，每个月将节约：296 - 273 = 23 元。\n而我们的大多服务是内网使用的，所以，公网 IP 的申请完全是浪费。\n可是，我们有时，还是需要公网下载一些东西的。这时怎么办呢？\n我的方案是：没有公网 IP 的机器，使用 HTTP 代理就可以上网了。\n当然，哪些机器需要上网，基于安全上的考虑，需要读者自己决定了。\n怎么做？ 我的具体实现方便使用 Squid 搭建 http proxy 服务。其他机器通过配置环境变量配置，笔者是通过 Ansible 初始化机器时配置的：\n- name: use httpproxy lineinfile: path: \u0026#34;/etc/profile\u0026#34; line: \u0026#34;\u0026lt;\u0026lt;item\u0026gt;\u0026gt;\u0026#34; with_items: - \u0026#34;export http_proxy=http://\u0026lt;\u0026lt; httpproxy.host \u0026gt;\u0026gt;:\u0026lt;\u0026lt; httpproxy.port \u0026gt;\u0026gt;/\u0026#34; - \u0026#34;export https_proxy=http://\u0026lt;\u0026lt; httpproxy.host \u0026gt;\u0026gt;:\u0026lt;\u0026lt; httpproxy.port \u0026gt;\u0026gt;/\u0026#34; when: is_use_httpproxy is defined and is_use_httpproxy == \u0026#39;True\u0026#39; tag: httpproxy 关于 Squid Http Proxy 服务，笔者同样是使用 Ansible 搭建，具体不细表，看官可以自行看代码：Squid-ansible。\n这样就可以节约下公网 IP 的钱了。\n关于 java 应用并没有使用 http Proxy 上文中，我们的机器使用了 http proxy，但是 Java 应用中依然无法请求外网，这时需要在 java 启动时加入相应的变量，如下：\n{{JAVA_HOME}}/bin/java -Dhttp.proxyHost={{ httpproxy.host }} -Dhttp.proxyPort={{ httpproxy.port }} 小结 为什么要节约这点钱，大公司不差你这点钱。再说了，老板也没有让你做啊。你做了老板也不知道。那你为什么还要做？\n笔者认为，不做，就会给自己一次机会养成“浪费”的习惯或者思维模式。这是自己的损失。最终，对企业，对个人都是损失。\n","permalink":"https://showme.codes/zh-cn/2018-5-14-save-ip/","summary":"\u003ch3 id=\"想法\"\u003e想法\u003c/h3\u003e\n\u003cp\u003e历史原因，我们一直使用的是阿里云经典网络的 ECS，疲于业务的开发及人力不足，一直没有特别大的动力迁移到 VPC 下。\u003c/p\u003e\n\u003cp\u003e而经典网络下的 ECS 的公网 IP 是收费的，而且没有公网 IP 有时会不方便。\u003c/p\u003e\n\u003ch4 id=\"购买公网-ip-时的费用\"\u003e购买公网 IP 时的费用\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-c3e63384a16f1863.png\"\u003e\u003c/p\u003e\n\u003ch4 id=\"未购买公网-ip-时的费用\"\u003e未购买公网 IP 时的费用\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-f59626b2a8d6252a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如果是按月购买，每个月将节约：296 - 273 = 23 元。\u003c/p\u003e\n\u003cp\u003e而我们的大多服务是内网使用的，所以，公网 IP 的申请完全是浪费。\u003c/p\u003e\n\u003cp\u003e可是，我们有时，还是需要公网下载一些东西的。这时怎么办呢？\u003c/p\u003e\n\u003cp\u003e我的方案是：没有公网 IP 的机器，使用 HTTP 代理就可以上网了。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-b00de3f909fab15b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e当然，哪些机器需要上网，基于安全上的考虑，需要读者自己决定了。\u003c/p\u003e\n\u003ch3 id=\"怎么做\"\u003e怎么做？\u003c/h3\u003e\n\u003cp\u003e我的具体实现方便使用 Squid 搭建 http proxy 服务。其他机器通过配置环境变量配置，笔者是通过 Ansible 初始化机器时配置的：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003euse httpproxy\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003elineinfile\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/etc/profile\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eline\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u0026lt;\u0026lt;item\u0026gt;\u0026gt;\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith_items\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;export http_proxy=http://\u0026lt;\u0026lt; httpproxy.host \u0026gt;\u0026gt;:\u0026lt;\u0026lt; httpproxy.port \u0026gt;\u0026gt;/\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;export https_proxy=http://\u0026lt;\u0026lt; httpproxy.host \u0026gt;\u0026gt;:\u0026lt;\u0026lt; httpproxy.port \u0026gt;\u0026gt;/\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003ewhen\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eis_use_httpproxy is defined and is_use_httpproxy == \u0026#39;True\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003etag\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ehttpproxy\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e关于 Squid Http Proxy 服务，笔者同样是使用 Ansible 搭建，具体不细表，看官可以自行看代码：\u003ca href=\"https://github.com/zacker330/squid-ansible\"\u003eSquid-ansible\u003c/a\u003e。\u003c/p\u003e","title":"阿里云经典网络下如何节约公网 IP 费用"},{"content":" All problems in computer science can be solved by another level of indirection \u0026ndash; David Wheeler 计算机科学中的任何问题，都可以通过加上一层逻辑层来解决。\u0026ndash; David Wheeler\n总之，分层就是有好处 在计算机领域，“分层” 概念无处不在。比如 web 开发时的 MVC ，网络编程时的 OSI 参考模型和 TCP/IP 协议族。\n但是为什么要进行分层呢？不同的书有不同的说法。\n在《图解TCP/IP》这本书这样说：\n在这一模型中，每个分层都接收它下一层所提供的特定服务，并且负责为自己的上一层提供特定的服务。上下层之间进行交互时所遵循的约定叫做“接口”。\n在我看来，说了等于白说。:-P\n而《企业应用架构模式》开篇第 1 章是这样说的：\n在分解复杂的软件系统时，软件设计者用得最多的技术之一就是分层。\n当用分层的观点来考虑系统时，可以将各个子系统想像成按照“多层蛋糕”的形式来组织，每一层都依托在其下层之上。在这种组织方式下，上层使用了下层定义的各种服务，而下层对上层一无所知。另外，每一层对自己的上层隐藏其下层的细节。\nMarting Fowler在第 1 章后面，又举了一个表现层，领域层，数据源层的例子。但是个人认为依然没有把为什么要分层说透。\n对于为什么要分层，我见过的大多数文章说的只是它带来的好处。比如下层修改实现，不影响上层使用；分离关注点等。但是为什么会带来这些好处？看似很傻的一个问题，其实很难回答。\n两个例子帮助你认识“分层” 我们先通过两个例子给大家一些感性的认识。\n自动驾驶 用分层的思维来看开车这件事情是这样的：\n分层 具体内容 人的意图 前行，不上坡，右转弯 人的操作 踩着油门，瞥一眼后视镜，方向盘打右 汽车运行 根据踩下油门的程度，发动机输出动力，而方向盘打右转动转向拉杆 发动机 进气，压缩，点燃，排气 更底层省略\u0026hellip;. 更底层省略 \u0026hellip;.. 因为有了分层，当我们要实现自动驾驶汽车时，要解决的就只是“人的操作”这一层的问题，而不需要实现“人的操作”以下所有的层，也就是不需要自己从头造汽车（不是绝对，但是绝对不需要从头造所有的部分）。\nIoT 云云对接 总要举一个软件开发领域的例子吧。我们举一个 IoT 云云对接的例子。\n当第三方云想控制 M云的空调时，系统的外观是这样的：\n如果我说用分层的方式，想必有人马上想要说 MVC blabla……。事实上，我不赞同这样。因为在我眼里，MVC 解决只是技术层面的问题。从技术角度分层的优先级比从业务角度分层的优先级低。换句话说，分层可以从多个角度进行，不同的角度有不同的优先级。\n从业务角度如何分层呢？\n我们可以将设备控制器分成：指令生成器和指令下发器。\n我们也来说说这样设计带来的好处 :-P ：\n协议转换器的修改，不会影响设备控制器的逻辑。反之亦然。 第三方云不需要知道如何组织空调的二进制，只需要表达自己的意图：空调20度。使用人类可读的json结构。 不同云之间的对接互不影响。 为什么分层能带来这些好处？ 为什么分层能带来这些好处？刚毕业那会，我经常这样问自己。后来，经过一位高人的指导，再加上看过《面向对象分析与设计》之后，终于明白了：\n开发软件本身是一件很复杂的事情（必须认识到这一点）。而我们人类大脑的能力是有限的，不可能同时处理太多的复杂性。我们可以通过将复杂性分解、抽象、分层，一次只需要处理一个部分复杂性，而不是所有。\n所以，“分层”并不能让复杂性消失，而是让我们的大脑在能力范围内处理相对重要的层面的复杂性，而忽略那些不那么重要的细节。\n比如开发一个HR系统，你的大脑应集中精力放在业务逻辑上，而不是操作系统如何与硬件打交道（并不是说操作系统原理不重要，只是在HR系统上不重要）。\n小结 软件开发所面对的复杂性超出了我们人类大脑一次性能处理的范围，而分层是一种手段，帮助我们人类只需要处理相对重要的层面的复杂性，而忽略相对不重要层面的复杂性。进而使我们以更低的成本达到目的。而好处只是副产品。\n接下来的问题，如何进行分层呢？分层好坏的标准是什么？留给大家思考。\n最后，强烈推介《面向对象分析与设计》这本书。不要以为这又是一本只讲 UML 的书。\n","permalink":"https://showme.codes/zh-cn/2018-5-1-all-problems-in-computer-science-can-be-solved-by-another-level-of-indirection/","summary":"\u003cblockquote\u003e\n\u003cp\u003eAll problems in computer science can be solved by another level of indirection \u0026ndash; David Wheeler\n计算机科学中的任何问题，都可以通过加上一层逻辑层来解决。\u0026ndash; David Wheeler\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"总之分层就是有好处\"\u003e总之，分层就是有好处\u003c/h3\u003e\n\u003cp\u003e在计算机领域，“分层” 概念无处不在。比如 web 开发时的 MVC ，网络编程时的 OSI 参考模型和 TCP/IP 协议族。\u003c/p\u003e\n\u003cp\u003e但是为什么要进行分层呢？不同的书有不同的说法。\u003c/p\u003e\n\u003cp\u003e在《图解TCP/IP》这本书这样说：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在这一模型中，每个分层都接收它下一层所提供的特定服务，并且负责为自己的上一层提供特定的服务。上下层之间进行交互时所遵循的约定叫做“接口”。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在我看来，说了等于白说。:-P\u003c/p\u003e\n\u003cp\u003e而《企业应用架构模式》开篇第 1 章是这样说的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在分解复杂的软件系统时，软件设计者用得最多的技术之一就是分层。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e当用分层的观点来考虑系统时，可以将各个子系统想像成按照“多层蛋糕”的形式来组织，每一层都依托在其下层之上。在这种组织方式下，上层使用了下层定义的各种服务，而下层对上层一无所知。另外，每一层对自己的上层隐藏其下层的细节。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eMarting Fowler在第 1 章后面，又举了一个表现层，领域层，数据源层的例子。但是个人认为依然没有把为什么要分层说透。\u003c/p\u003e\n\u003cp\u003e对于为什么要分层，我见过的大多数文章说的只是它带来的好处。比如下层修改实现，不影响上层使用；分离关注点等。但是为什么会带来这些好处？看似很傻的一个问题，其实很难回答。\u003c/p\u003e\n\u003ch3 id=\"两个例子帮助你认识分层\"\u003e两个例子帮助你认识“分层”\u003c/h3\u003e\n\u003cp\u003e我们先通过两个例子给大家一些感性的认识。\u003c/p\u003e\n\u003ch4 id=\"自动驾驶\"\u003e自动驾驶\u003c/h4\u003e\n\u003cp\u003e用分层的思维来看开车这件事情是这样的：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e分层\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e具体内容\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e人的意图\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e前行，不上坡，右转弯\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e人的操作\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e踩着油门，瞥一眼后视镜，方向盘打右\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e汽车运行\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e根据踩下油门的程度，发动机输出动力，而方向盘打右转动转向拉杆\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e发动机\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e进气，压缩，点燃，排气\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e更底层省略\u0026hellip;.\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e更底层省略 \u0026hellip;..\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e因为有了分层，当我们要实现自动驾驶汽车时，要解决的就只是“人的操作”这一层的问题，而不需要实现“人的操作”以下所有的层，也就是不需要自己从头造汽车（不是绝对，但是绝对不需要从头造所有的部分）。\u003c/p\u003e\n\u003ch4 id=\"iot-云云对接\"\u003eIoT 云云对接\u003c/h4\u003e\n\u003cp\u003e总要举一个软件开发领域的例子吧。我们举一个 IoT 云云对接的例子。\u003c/p\u003e\n\u003cp\u003e当第三方云想控制 M云的空调时，系统的外观是这样的：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-713edf09ffe20c0e.png\"\u003e\u003c/p\u003e","title":"为什么“分层”给我们带来好处——论软件工程的分层概念"},{"content":"\n从聊天说起 有一次和朋友聊天，他说他们有一次部署出事了，影响还挺大，那次事故后，他们公司对于部署流程增加了更多的审批。\n当朋友说完前半句时，我已经猜到下半句，那是很多公司或个人会做出的反应。至于为什么会做出这样的反应，我也不知道。\n我问：为什么那次部署会“出事”？\n他说：当时部署的人忘记了那台机器上有一条 Iptable 规则，导致了事故。\n我就在想，如果有人审批，那次事故就不会发生吗？审批的人就知道那台机器上有一条规则导致事故的发生？然后驳回这次部署吗？连一线的开发和运维都忘记了的 Iptable 规则，“高高在上的审批领导”就更不知道了。\n题外话：增加审批流程并不能避免这次事故，只不过当出现事故时，可以更好的定责。然而我又好奇了，这种“审批”是为了解决问题，解决什么问题？，还是为了逃避责任？谁逃避了责任？谁又有责任？\n对于这类问题，我心里已经有数了，但想知道这位朋友的回答，就接着问：那么怎么杜绝这类问题呢？\n他说：因为那条 Iptable 规则的设置太久远了，是谁都记不起。如果能把每次部署的步骤记录下来，这样下次部署的时候，过一下以前的部署记录，就会知道那个 Iptable 规则了。（作者：大概原意，已经记不清原话）\n这位朋友说的做法，我之前待的一个团队的做法也差不多：会有一个页面专门记录下每次部署的步骤，步骤由开发人员写，然后由运维人员执行。只是我不知道他们会不会回顾之前所有针对这台机器的部署步骤。\n这个团队里有某某大型互联网公司来的架构师和某财务软件公司来的运维，所以，我不负责地推测，我们这个行业很多公司对于配置的管理还没有达到足够的重视，也没有正确的看待。\n我笑了，接着问朋友：那我要知道当前机器的“最终状态”，是不是要找出所有部署记录，还要过滤出对这次部署有影响的每一个细节？比如那条 Iptable 规则。\n接下来的对话细节已经记不清，也不重要了。重要的是找出针对这类运维事故根本原因及解决办法。\n我个人认为这类问题的根本原因在于：\n配置管理的失控： 已经没有人完整知道线上环境配置是什么了？要了解时，只能一个个查。 测试环境与生产环境的配置不一致： 如果那位倒霉的同学在测试环境部署出现这样的问题，到生产环境部署时，自然就会注意相关配置项了。 以上只是我个人认为的，不一定正确，欢迎各位读者讨论。\n那如何杜绝这类问题呢？\n这两个原因可以看作一个，也可以看作两个。但方法都是一样的：\n使用声明式的配置管理方法，而不是脚本式 版本化这些声明的配置 所有环境使用同一套装配置管理方法 使用声明式的配置管理方法，而不是脚本式的 脚本式的配置管理是这样的：\napt-get install build-essential apt-get install libtool cd /usr/local/src wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.37.tar.gz tar -zxvf pcre-8.37.tar.gz cd pcre-8.34 ./configure make make install 而声明式的配置管理是这样的：\n# ./ansible-nginx/tasks/install_nginx.yml # 使用这个7-0.el7版本的yum包 - name: NGINX | Installing NGINX repo rpm yum: name: http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm # 当前机器的nginx的状态应该是最新版本 - name: NGINX | Installing NGINX yum: name: nginx state: latest # 当前机器的 nginx service 的状态应该是已经启动的。至于如何确保 nginx 这个 service，当前是什么状态的，又是如何启动的，我们不需要关心。 - name: NGINX | Starting NGINX service: name: nginx state: started 声明式的配置里写的是当前环境的“状态”，语意上，声明式的配置不论你执行多少次，你得到最终的“状态”就是你所声明的，这也就实现了《持续交付》里说的：\n确保部署流程是幂等的 无论开始部署时目标环境处于何种状态，部署流程应该总是令目标环境达到同样（正确）的状态，并以之为结束点。\n这样，你就不用在第1000次部署时，根据前999次部署脚本找出对这一次部署有影响的细节了。\n具体实践时，我发现 Ansible 就能很好的做到这点。\n版本化这些声明式的配置 将这些配置版本化的好处，就不需要重点说明了。\n所有环境使用同一套装配置管理方法 具体一点的说就是所有环境都使用相同的声明配置，具体到不同环境时，使用变量替换。这样就可以保证所有环境的一致性了。\n具体实践方法，还需要根据所在团队调整。你也可以通过本文附录里链接，参考其他人是如何实践的。\n小结 关于银弹：如果真的按照以上方法就可以确保运维万无一失了吗？很遗憾，答案是否定的。就连亚马逊这样的行业标杆都没有办法做到万无一失。 关于定责：见过一些企业凡事都讲究“定责”。定责可以有，但是定责了，是否真的能解决问题了？是值得讨论的。 关于配置：团队对于“配置”的理解达成共识很重要 关于成本：要实现以上所说的实践成本高吗？是不是需要一个DevOps团队？ 说实话，找到懂这些并有实践经验的人很难，从这一方面看，成本很高。但是这并不是我们不朝这个方向发展的借口。另，对于软件工程的“成本”，大家没有统一的定义，所以也就更不好讨论下去了。 附录 关于配置管理\n关于自动化配置还有什么好说的呢？ 多环境配置管理\n巧用 Ansible 实现配置管理：多环境配置问题 How to Manage Multistage Environments with Ansible ","permalink":"https://showme.codes/zh-cn/2018-3-30-run-while-system-down/","summary":"\u003cp\u003e\u003cimg alt=\"stormtrooper-2296199_640.jpg\" loading=\"lazy\" src=\"/assets/images/292372-d079a1036ccc1e08.jpg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"从聊天说起\"\u003e从聊天说起\u003c/h3\u003e\n\u003cp\u003e有一次和朋友聊天，他说他们有一次部署出事了，影响还挺大，那次事故后，他们公司对于部署流程增加了更多的审批。\u003c/p\u003e\n\u003cp\u003e当朋友说完前半句时，我已经猜到下半句，那是很多公司或个人会做出的反应。至于为什么会做出这样的反应，我也不知道。\u003c/p\u003e\n\u003cp\u003e我问：为什么那次部署会“出事”？\u003c/p\u003e\n\u003cp\u003e他说：当时部署的人忘记了那台机器上有一条 Iptable 规则，导致了事故。\u003c/p\u003e\n\u003cp\u003e我就在想，如果有人审批，那次事故就不会发生吗？审批的人就知道那台机器上有一条规则导致事故的发生？然后驳回这次部署吗？连一线的开发和运维都忘记了的 Iptable 规则，“高高在上的审批领导”就更不知道了。\u003c/p\u003e\n\u003cp\u003e题外话：增加审批流程并不能避免这次事故，只不过当出现事故时，可以更好的定责。然而我又好奇了，这种“审批”是为了解决问题，解决什么问题？，还是为了逃避责任？谁逃避了责任？谁又有责任？\u003c/p\u003e\n\u003cp\u003e对于这类问题，我心里已经有数了，但想知道这位朋友的回答，就接着问：那么怎么杜绝这类问题呢？\u003c/p\u003e\n\u003cp\u003e他说：因为那条 Iptable 规则的设置太久远了，是谁都记不起。如果能把每次部署的步骤记录下来，这样下次部署的时候，过一下以前的部署记录，就会知道那个 Iptable 规则了。（作者：大概原意，已经记不清原话）\u003c/p\u003e\n\u003cp\u003e这位朋友说的做法，我之前待的一个团队的做法也差不多：会有一个页面专门记录下每次部署的步骤，步骤由开发人员写，然后由运维人员执行。只是我不知道他们会不会回顾之前所有针对这台机器的部署步骤。\u003c/p\u003e\n\u003cp\u003e这个团队里有某某大型互联网公司来的架构师和某财务软件公司来的运维，所以，我不负责地推测，我们这个行业很多公司对于配置的管理还没有达到足够的重视，也没有正确的看待。\u003c/p\u003e\n\u003cp\u003e我笑了，接着问朋友：那我要知道当前机器的“最终状态”，是不是要找出所有部署记录，还要过滤出对这次部署有影响的每一个细节？比如那条 Iptable 规则。\u003c/p\u003e\n\u003cp\u003e接下来的对话细节已经记不清，也不重要了。重要的是找出针对这类运维事故根本原因及解决办法。\u003c/p\u003e\n\u003cp\u003e我个人认为这类问题的根本原因在于：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e配置管理的失控：\n已经没有人完整知道线上环境配置是什么了？要了解时，只能一个个查。\u003c/li\u003e\n\u003cli\u003e测试环境与生产环境的配置不一致：\n如果那位倒霉的同学在测试环境部署出现这样的问题，到生产环境部署时，自然就会注意相关配置项了。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e以上只是我个人认为的，不一定正确，欢迎各位读者讨论。\u003c/p\u003e\n\u003cp\u003e那如何杜绝这类问题呢？\u003c/p\u003e\n\u003cp\u003e这两个原因可以看作一个，也可以看作两个。但方法都是一样的：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e使用声明式的配置管理方法，而不是脚本式\u003c/li\u003e\n\u003cli\u003e版本化这些声明的配置\u003c/li\u003e\n\u003cli\u003e所有环境使用同一套装配置管理方法\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"使用声明式的配置管理方法而不是脚本式的\"\u003e使用声明式的配置管理方法，而不是脚本式的\u003c/h3\u003e\n\u003cp\u003e脚本式的配置管理是这样的：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eapt-get install build-essential\napt-get install libtool\ncd /usr/local/src\nwget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.37.tar.gz\ntar -zxvf pcre-8.37.tar.gz\ncd pcre-8.34\n./configure\nmake\nmake install\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e而声明式的配置管理是这样的：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# ./ansible-nginx/tasks/install_nginx.yml\n    # 使用这个7-0.el7版本的yum包\n    - name: NGINX | Installing NGINX repo rpm\n       yum:\n       name: http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm\n\n    # 当前机器的nginx的状态应该是最新版本\n    - name: NGINX | Installing NGINX\n       yum:\n       name: nginx  \n       state: latest\n\n    # 当前机器的 nginx service 的状态应该是已经启动的。至于如何确保 nginx 这个 service，当前是什么状态的，又是如何启动的，我们不需要关心。\n    - name: NGINX | Starting NGINX\n       service:\n       name: nginx\n       state: started\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e声明式的配置里写的是当前环境的“状态”，语意上，声明式的配置不论你执行多少次，你得到最终的“状态”就是你所声明的，这也就实现了《持续交付》里说的：\u003c/p\u003e","title":"出现运维事故后，你会怎么办？"},{"content":"\n没有想到，这本教子经典的书里，除了教我如何与孩子实现真正有效的沟通，还带给我管理方面的启示。\n关于情感 当孩子处于强烈的情感中时，他们听不进任何人的话。他们不会接受任何意见或安慰，也无法接受任何建设性的批评。他们希望我们能够理解他们心里在想什么，希望我们明白在那个特别的时刻他们的心情。\n员工之间难免会有摩擦，当出现这种情况时，我们可以说出各方的感受，引导各方相互理解对方的心情。进而将讨论焦点拉回问题的本质。\n指导孩子时，我们陈述问题以及可能解决问题的方法。我们不会针对孩子本人发表任何观点。\n比如当员工出现工期延迟时，我们首先要做的是陈述这一事实，听听他自己的表述，再讨论问题，而不是指责。\n关于说真话 为什么孩子会撒谎？他们撒谎有时是因为他们不被允许说出真相。\n想想我们所在企业是否允许员工说出真相了？是什么机制导致员工撒谎，不愿意说出真相。有办法改进吗？\n如果我们希望教育孩子诚实的品德，那么我们必须作好心理准备，既要听让人愉快的真话，也要听让人不高兴的真话。\n如果我们的管理者只喜欢好听的话，那么，员工就只会说好听的话。我常常会想为什么有些管理者会只喜欢听好听的话。为什么呢？因为好听的话让人感觉良好，还是因为听到不好听的，代表自己的权威受到威胁了？\n简而言之，我们不能激发孩子防御性的撒谎，我们不能有意制造让孩子撒谎的机会。\n是的，我们在进行组织设计时，要考虑如何才能不激发员工的防御性撒谎。我个人目前能想到的就是在企业中“培育”一种说真话无罪的文化。\n关于责任感 责任感的培养可以从孩子很小的时候就开始。\n在我看来，员工的责任感可以从小事开始，在平时进行，而不是集中在一场大型“洗脑会”中进行。\n培养孩子的责任感，就是要在跟他们有关系的事情上让他们有发言的机会，如果必要，让他们自己作出选择。\n管理层常常报怨员工对公司产品没有责任感。但是孰不知，员工对公司产品没有责任感的根本原因是员工常常只能“服从”，没有任何对公司产品进行发言的机会。\n员工有发言权，但最终选择权是产品Leader的。弄清发言权和选择权两者之间的责任范围很重要。\n我们应该故意制造一些场景，让孩子自己作决定。父母选择场景，孩子作出选择。\n作为技术Leader，我们其实可以大胆让员工自己做一些技术决策。一是让他们更有责任感，二是培养员工将来能独立做技术决策。\n关于聆听 父母需要开明的思想和豁达的心胸，这样才能倾听到所有的事实，不管它们是让人高兴还是让人讨厌。\n“聆听”在我看来是最难做到的，因为我总是那么容易先入为主。同时，我们在聆听时，可以承认对方的感受，但是不一定要同意。\n小结 有人说了，员工不是孩子，不能像小孩那样“带”。然而，很多人的某些方面就是孩子，我们必须面对。你招的是他整个人，而不是他的某部分。\n前两年看完这本书时，我的个人感受：带小孩和带团队有几分相似。要培养他们，要处理他们之间的矛盾，一样要处理他们的情感，还要给他们发言权……然而，带小孩和带团队都知易行难，大家相互勉励。\n强烈推荐各位亲自阅读这本书。\n","permalink":"https://showme.codes/zh-cn/2018-3-27-between-parent-and-child/","summary":"\u003cp\u003e\u003cimg alt=\"孩子把你的手给我\" loading=\"lazy\" src=\"/assets/images/51eEGSgXuOL.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e没有想到，这本教子经典的书里，除了教我如何与孩子实现真正有效的沟通，还带给我管理方面的启示。\u003c/p\u003e\n\u003ch3 id=\"关于情感\"\u003e关于情感\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e当孩子处于强烈的情感中时，他们听不进任何人的话。他们不会接受任何意见或安慰，也无法接受任何建设性的批评。他们希望我们能够理解他们心里在想什么，希望我们明白在那个特别的时刻他们的心情。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e员工之间难免会有摩擦，当出现这种情况时，我们可以说出各方的感受，引导各方相互理解对方的心情。进而将讨论焦点拉回问题的本质。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e指导孩子时，我们陈述问题以及可能解决问题的方法。我们不会针对孩子本人发表任何观点。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e比如当员工出现工期延迟时，我们首先要做的是陈述这一事实，听听他自己的表述，再讨论问题，而不是指责。\u003c/p\u003e\n\u003ch3 id=\"关于说真话\"\u003e关于说真话\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为什么孩子会撒谎？他们撒谎有时是因为他们不被允许说出真相。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e想想我们所在企业是否允许员工说出真相了？是什么机制导致员工撒谎，不愿意说出真相。有办法改进吗？\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e如果我们希望教育孩子诚实的品德，那么我们必须作好心理准备，既要听让人愉快的真话，也要听让人不高兴的真话。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e如果我们的管理者只喜欢好听的话，那么，员工就只会说好听的话。我常常会想为什么有些管理者会只喜欢听好听的话。为什么呢？因为好听的话让人感觉良好，还是因为听到不好听的，代表自己的权威受到威胁了？\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e简而言之，我们不能激发孩子防御性的撒谎，我们不能有意制造让孩子撒谎的机会。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e是的，我们在进行组织设计时，要考虑如何才能不激发员工的防御性撒谎。我个人目前能想到的就是在企业中“培育”一种说真话无罪的文化。\u003c/p\u003e\n\u003ch3 id=\"关于责任感\"\u003e关于责任感\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e责任感的培养可以从孩子很小的时候就开始。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在我看来，员工的责任感可以从小事开始，在平时进行，而不是集中在一场大型“洗脑会”中进行。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e培养孩子的责任感，就是要在跟他们有关系的事情上让他们有发言的机会，如果必要，让他们自己作出选择。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e管理层常常报怨员工对公司产品没有责任感。但是孰不知，员工对公司产品没有责任感的根本原因是员工常常只能“服从”，没有任何对公司产品进行发言的机会。\u003c/p\u003e\n\u003cp\u003e员工有发言权，但最终选择权是产品Leader的。弄清发言权和选择权两者之间的责任范围很重要。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我们应该故意制造一些场景，让孩子自己作决定。父母选择场景，孩子作出选择。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e作为技术Leader，我们其实可以大胆让员工自己做一些技术决策。一是让他们更有责任感，二是培养员工将来能独立做技术决策。\u003c/p\u003e\n\u003ch3 id=\"关于聆听\"\u003e关于聆听\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e父母需要开明的思想和豁达的心胸，这样才能倾听到所有的事实，不管它们是让人高兴还是让人讨厌。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e“聆听”在我看来是最难做到的，因为我总是那么容易先入为主。同时，我们在聆听时，可以承认对方的感受，但是不一定要同意。\u003c/p\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e有人说了，员工不是孩子，不能像小孩那样“带”。然而，很多人的某些方面就是孩子，我们必须面对。你招的是他整个人，而不是他的某部分。\u003c/p\u003e\n\u003cp\u003e前两年看完这本书时，我的个人感受：带小孩和带团队有几分相似。要培养他们，要处理他们之间的矛盾，一样要处理他们的情感，还要给他们发言权……然而，带小孩和带团队都知易行难，大家相互勉励。\u003c/p\u003e\n\u003cp\u003e强烈推荐各位亲自阅读这本书。\u003c/p\u003e","title":"《孩子把你的手给我》给管理带来的启示"},{"content":"\n说在前面 在《持续交付》的第二章配置管理的小结里说到：\n配置管理是本书其他内容的基础。没有配置管理，根本谈不上持续集成、发布管理以及部署流水线。它对交付团队内部的协作也会起到巨大的促进作用。\n再怎么强调配置管理的重要性也不为过，特别是在多环境下。然而大家都知道重要，又少有人告诉我们具体如何做，所以实在难受。\n本文总结了我在多环境配置管理实践方面的一点心得，希望对大家有帮助。\nAnsible 介绍 你可以简单地把它理解为一个自动化运维工具。本文将会使用这个工具下 inventory 概念来实现多环境配置。简单一点来说，inventory是一个文本文件，你可以在这个文件里记录下所有的机器，并对这些机器进行分组（分类）。\n当然，其它的自动化运维工具也可以使用同样的思路来实践。本文只以 Ansible 为例。\n例子 比如我们有两个环境，分别有一台机器。使用Ansible的 inventory 来管理这些机器，就会像下面这样：\n## inventory [aws-prod-app] 10.171.32.158 [aws-test-app] 10.161.158.221 所有的 app 应用都是同一份代码，而且都会涉及操作数据库。当然，不同环境下的 app 读取的数据库的配置项的值是不样的。比如 aws-test 环境下配置是\ndb: url: test.mysql.url username: testu1 password: passwordtest 而生产环境 aws-prod 环境下配置：\ndb: url: prod.mysql.url username: produ1 password: passwordprod 这时，因为机器少，我们可以使用 Ansible 的 inventory 变量实现不同环境的配置隔离，比如：\n## inventory [aws-prod-app] 10.171.32.158 ## [分组名:vars] 这样的写法是 Ansible inventory的约定 ## 按照这个约定来写，Ansible就可以识别了 [aws-prod-app:vars] db.url = test.mysql.url db.username = testu1 db.password = passwordtest [aws-test-app] 10.161.158.221 [aws-test-app:vars] db.url = prod.mysql.url db.username = produ1 db.password = passwordprod 接着，如果环境上要部署新应用呢？而且还是很多呢？我们的 inventory 就会变成这样：\n## inventory [aws-prod-app] 10.171.32.158 [aws-prod-appX] 10.171.32.159 ## 还有更多的 aws-prod-app... [aws-prod-app:vars] db.url = prod.mysql.url db.username = produ1 db.password = passwordprod [aws-prod-appX:vars] db.url = prod.mysql.url db.username = produ1 db.password = passwordprod ## 还有更多的 aws-prod-app:vars ... [aws-test-app] 10.161.158.221 [aws-test-appX] 10.161.158.222 ## 还有更多的 aws-test-app... [aws-test-app:vars] db.url = test.mysql.url db.username = testu1 db.password = passwordtest ## 还有更多的 aws-test-app:vars ... 好吧，面对这种配置冗余，后期维护会很恐怖。有两种办法解决：\n不增加新应用 想办法解决这个问题 不要觉得第一种办法可笑，现实中真的存在，只是不同环境下的具体形态不一样。\n解决这个问题的办法就是使用 Ansible 的分组的分组的变量。说起来是有点绕。简单的说就是对我们刚刚的分组，再进行一次分组，然后再给这一更高层次的分组设置变量。\n接着我们上面的例子，我们使用分组的分组变量进行重写：\n## inventory [aws-prod-app] 10.171.32.158 [aws-prod-appX] 10.171.32.159 ## 注意，我们将所有的生产环境的 ## 分组又加入到 aws-prod分组下 [aws-prod:children] aws-prod-app aws-prod-appX ## 这样，aws-prod 分组的变量又可以 ## 应用到所有的 aws-prod 环境下所有的机器 [aws-prod:vars] db.url = prod.mysql.url db.username = produ1 db.password = passwordprod [aws-test-app] 10.161.158.221 [aws-test-appX] 10.161.158.222 [aws-test:children] aws-test-app aws-test-appX [aws-test:vars] db.url = test.mysql.url db.username = testu1 db.password = passwordtest Ansible 这个分组概念设计得非常妙。仔细看，你会发现，利用分组的概念，你可以轻松地、简单地实现多种环境的配置管理。\n当然，所有的配置都放一个 inventory 里就不合适了，所以，我们使用了Ansible的 group_vars 文件夹来进行管理，重构后如下：\n目录结构 . ├── group_vars │ ├── aws-prod │ └── aws-test └── inventory ## inventory [aws-prod-app] 10.171.32.158 [aws-prod-appX] 10.171.32.159 [aws-prod:children] aws-prod-app aws-prod-appX [aws-test-app] 10.161.158.221 [aws-test-appX] 10.161.158.221 [aws-test:children] aws-test-app aws-test-appX ## group_vars/aws-prod db.url = prod.mysql.url db.username = produ1 db.password = passwordprod 到这里，我们简单的多环境管理的例子就算讲完了。\n如果觉得不够直观，可以访问我在Github的代码样例 ansible-inventory-example。\n小结 当环境少的时候，开发人员和测试人员会争抢环境；当环境多的时候，配置管理又会成为一个头大的问题。不论当哪种情况，都会增加我们研发成本。\n而利用 Ansible 的分组概念同时加上它的自动化，就可以很轻松地解决多环境的配置管理问题，同时又降低我们的研发成本。\n扩展 关于自动化配置还有什么好说的呢？ 我的另一篇关于自动化配置的文章 Puppet，Chef，Ansible的共性 Ansible doc ","permalink":"https://showme.codes/zh-cn/2018-3-11-ansible-inventory-configuration/","summary":"\u003cp\u003e\u003cimg alt=\"ansible logo\" loading=\"lazy\" src=\"/assets/images/292372-65e68fd4479d6e18.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"说在前面\"\u003e说在前面\u003c/h3\u003e\n\u003cp\u003e在《持续交付》的第二章配置管理的小结里说到：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e配置管理是本书其他内容的基础。没有配置管理，根本谈不上持续集成、发布管理以及部署流水线。它对交付团队内部的协作也会起到巨大的促进作用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e再怎么强调配置管理的重要性也不为过，特别是在多环境下。然而大家都知道重要，又少有人告诉我们具体如何做，所以实在难受。\u003c/p\u003e\n\u003cp\u003e本文总结了我在多环境配置管理实践方面的一点心得，希望对大家有帮助。\u003c/p\u003e\n\u003ch3 id=\"ansible-介绍\"\u003eAnsible 介绍\u003c/h3\u003e\n\u003cp\u003e你可以简单地把它理解为一个自动化运维工具。本文将会使用这个工具下 inventory 概念来实现多环境配置。简单一点来说，inventory是一个文本文件，你可以在这个文件里记录下所有的机器，并对这些机器进行分组（分类）。\u003c/p\u003e\n\u003cp\u003e当然，其它的自动化运维工具也可以使用同样的思路来实践。本文只以 Ansible 为例。\u003c/p\u003e\n\u003ch3 id=\"例子\"\u003e例子\u003c/h3\u003e\n\u003cp\u003e比如我们有两个环境，分别有一台机器。使用Ansible的 inventory 来管理这些机器，就会像下面这样：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e## inventory\n[aws-prod-app]\n10.171.32.158\n\n[aws-test-app]\n10.161.158.221\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e所有的 app 应用都是同一份代码，而且都会涉及操作数据库。当然，不同环境下的 app 读取的数据库的配置项的值是不样的。比如 aws-test 环境下配置是\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb:\n  url: test.mysql.url\n  username: testu1\n  password: passwordtest\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e而生产环境 aws-prod 环境下配置：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb:\n  url: prod.mysql.url\n  username: produ1\n  password: passwordprod\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这时，因为机器少，我们可以使用 Ansible 的 inventory 变量实现不同环境的配置隔离，比如：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e## inventory\n[aws-prod-app]\n10.171.32.158\n\n## [分组名:vars] 这样的写法是 Ansible inventory的约定\n## 按照这个约定来写，Ansible就可以识别了\n[aws-prod-app:vars]\ndb.url = test.mysql.url\ndb.username = testu1\ndb.password = passwordtest\n\n[aws-test-app]\n10.161.158.221\n\n[aws-test-app:vars]\ndb.url = prod.mysql.url\ndb.username = produ1\ndb.password = passwordprod\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e接着，如果环境上要部署新应用呢？而且还是很多呢？我们的 inventory 就会变成这样：\u003c/p\u003e","title":"巧用 Ansible 实现配置管理：多环境配置问题"},{"content":"\n坊间一直流传着“康威定律”的概念，但是都是鲜有人能说出真实的反应这一“定律”的例子。本文则尝试弥补这一空缺。\n本文本为几部分：\n康威推论的历史：有必要了解它的历史 康威推论的定义：《人月神话》也只是转载康威的话 康威推论的例子：我的真实经历 启示 康威推论的历史 1967年，一位名叫梅尔文康威（Melvin Conway）的早期计算机科学家、程序员及黑客向《哈佛商业评论》提交了一篇名为“组织是如何进行发明创造的？”的文章，但是立刻被退稿了，理由是他“没能证明该理论”。\n对于软件设计界来说，幸运的是这篇文章随后被Datamation杂志（当时一流的IT杂志）接受，并于1968年发表。后来，Fred Brooks在其开创性作品《人月神话》中，将康威的某些理论归纳为“康威定律”，这个名字就这样流传了下来。\n以文内容来自：《软件之道——软件开发争议问题剖析》。\n康威推论的定义 我们来看看康威推论的原文 How Do Committees Invent? (1968) 最后的总结：\nThe basic thesis of this article is that organizations which design systems (in the broad sense used here) are constrained to produce designs which are copies of the communication structures of these organizations.\n按《软件之道》里的大白话来说就是：\n任何组织设计出来的系统（此处指广义的系统）的结构都会照搬这个组织的沟通。\n说实在的，如果没有经历过，我说的以下两个例子，真心很难体会到康威所表达的。下面，是两个现实鲜活的例子，我正在经历的。\n康威推论的例证 例证一：错误码的设计\n背景是某团队针对同一业务，有Android和iOS两个平台的SDK。\nAndroid SDK和iOS SDK开发人员，他们对于同一业务错误，设计出了两套装错误码。比如对于登录时用户名或密码错误，Android给出的错误码是230，而iOS给出的错误码则是8100。\n为什么会出现两套装错误码系统呢？明明可以使用一套装就可以了。当然可能会有历史原因。但是据我观察，可以肯定的是，他们在设计软件时，并没有就“错误码”达成共识。\n有趣的是，他们是同一个团队。居然还会出现康威现象。如果康威推论是正确的，那么，即使同一个团队下，也会出现不同的沟通结构。\n如果这个例证有些牵强，下面这个例证更能说明事情。\n例证二：IoT系统的设计\nIoT系统有其特殊性。通常它需要进行硬件端、移动端（App）、云端这三端的配合才能更好的完成事情。以空调为例：\n2017年前，硬件端和移动端是（相当于，其实不是）一个团队，而云端则是由另一个公司负责。而真正业务的决定权在硬件端和移动端。所以，平时沟通上，只有硬件端和移动端的事，云端只不过是负责转发。所以，云端的系统设计的最终结果就是一个管道：硬件端给我什么，我就直接转给移动端，移动端给我什么，我就直接转给硬件端。如下图： 这样的系统意味着什么呢？移动端除了要负责展示逻辑，还要对硬件端的细节了解，比如了解某某字段的某些分别代表什么意思。这直接导致整个系统无法快速适应业务的变化。因为硬件端和移动端做了太多的事情。懂点技术的人都知道，硬件端和移动端的修改，周期长！有时硬件端修改还不好实现。\n我将这样系统称为管道系统设计。\n2017年，这家公司意识到云端的重要性。接而将云端的业务接管过来。这时，云端才慢慢地有更多的话语权。也就有了想要理解硬件的“欲望”。\n再后来，云端开始做设备影子（IoT系统的核心）、大数据等都需要理解硬件。这时的云端不再是一个管道系统：\n看吧。这三端的系统结构真的就照搬了它们的沟通结构。我作为一个旁观者和参与者，对于康威推论，体会如此深刻。\n启示 关于康威的那句话，是推论，还是定律。我个人认为还只是推论，不能以“定律”来定义。但是坊间流传的都是“康威定律”。我也很是奇怪。但是不重要了。重要的是它给我们带来了什么启示。\n当我们发现，系统设计不合理时，我们可以考虑是不是组织的沟通结构出了问题 技术Leader或架构师应该理解系统结构，并设计组织的沟通结构。另一个角度说，不了解技术的人，我很好奇他们是如何管理好一个技术团队，比如马云、贝佐斯。 组织构架应该更灵活，以适应不同的系统架构，不同的沟通结构 根据系统架构来决定组织架构，而不是根据权力分配 以上，只是启示。具体操作还是要看当时的组织环境和上下文。大家且行且珍惜。\n","permalink":"https://showme.codes/zh-cn/2018-3-9-conway/","summary":"\u003cp\u003e\u003cimg alt=\"Melvin Conway\" loading=\"lazy\" src=\"/assets/images/292372-399fdb9f12dcd32b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e坊间一直流传着“康威定律”的概念，但是都是鲜有人能说出真实的反应这一“定律”的例子。本文则尝试弥补这一空缺。\u003c/p\u003e\n\u003cp\u003e本文本为几部分：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e康威推论的历史：有必要了解它的历史\u003c/li\u003e\n\u003cli\u003e康威推论的定义：《人月神话》也只是转载康威的话\u003c/li\u003e\n\u003cli\u003e康威推论的例子：我的真实经历\u003c/li\u003e\n\u003cli\u003e启示\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"康威推论的历史\"\u003e康威推论的历史\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e1967年，一位名叫梅尔文康威（Melvin Conway）的早期计算机科学家、程序员及黑客向《哈佛商业评论》提交了一篇名为“组织是如何进行发明创造的？”的文章，但是立刻被退稿了，理由是他“没能证明该理论”。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e对于软件设计界来说，幸运的是这篇文章随后被Datamation杂志（当时一流的IT杂志）接受，并于\u003ca href=\"http://www.melconway.com/Home/pdf/committees.pdf\"\u003e1968年发表\u003c/a\u003e。后来，Fred Brooks在其开创性作品《人月神话》中，将康威的某些理论归纳为“康威定律”，这个名字就这样流传了下来。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e以文内容来自：《软件之道——软件开发争议问题剖析》。\u003c/p\u003e\n\u003ch3 id=\"康威推论的定义\"\u003e康威推论的定义\u003c/h3\u003e\n\u003cp\u003e我们来看看康威推论的原文 \u003ca href=\"http://www.melconway.com/Home/pdf/committees.pdf\"\u003eHow Do Committees Invent? (1968)\u003c/a\u003e 最后的总结：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe basic thesis of this article is that organizations which design systems (in the broad sense used here) are constrained to produce designs which are copies of the communication structures of these organizations.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e按《软件之道》里的大白话来说就是：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e任何组织设计出来的系统（此处指广义的系统）的结构都会照搬这个组织的沟通。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e说实在的，如果没有经历过，我说的以下两个例子，真心很难体会到康威所表达的。下面，是两个现实鲜活的例子，我正在经历的。\u003c/p\u003e\n\u003ch3 id=\"康威推论的例证\"\u003e康威推论的例证\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e例证一：错误码的设计\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e背景是某团队针对同一业务，有Android和iOS两个平台的SDK。\u003c/p\u003e\n\u003cp\u003eAndroid SDK和iOS SDK开发人员，他们对于同一业务错误，设计出了两套装错误码。比如对于登录时用户名或密码错误，Android给出的错误码是230，而iOS给出的错误码则是8100。\u003c/p\u003e\n\u003cp\u003e为什么会出现两套装错误码系统呢？明明可以使用一套装就可以了。当然可能会有历史原因。但是据我观察，可以肯定的是，他们在设计软件时，并没有就“错误码”达成共识。\u003c/p\u003e\n\u003cp\u003e有趣的是，他们是同一个团队。居然还会出现康威现象。如果康威推论是正确的，那么，即使同一个团队下，也会出现不同的沟通结构。\u003c/p\u003e\n\u003cp\u003e如果这个例证有些牵强，下面这个例证更能说明事情。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e例证二：IoT系统的设计\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIoT系统有其特殊性。通常它需要进行硬件端、移动端（App）、云端这三端的配合才能更好的完成事情。以空调为例：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"IoT\" loading=\"lazy\" src=\"/assets/images/292372-3d7a6201125766cb.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2017年前，硬件端和移动端是（相当于，其实不是）一个团队，而云端则是由另一个公司负责。而\u003cstrong\u003e真正业务的决定权\u003c/strong\u003e在硬件端和移动端。所以，平时沟通上，只有硬件端和移动端的事，云端只不过是负责转发。所以，云端的系统设计的最终结果就是一个管道：硬件端给我什么，我就直接转给移动端，移动端给我什么，我就直接转给硬件端。如下图：\n\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-5af3774f1615a94d.png\"\u003e\u003c/p\u003e","title":"现实中的康威推论——IoT系统为例"},{"content":"\n摘要 本文内容主要来自《门后的秘密——卓越管理的故事》，英文名：Behind Closed Doors Secrets of Great Management。在2013年第一次看这本书时就感受益匪浅，现在又重新看，越发觉得这本书写的都是大实话。非常推荐读者亲自去读。\n如何进行有效的指导 指导也是管理者工作内容的一部分，但它着重于提高技术水平和能力。指导是一种帮忙与被帮忙的关系，所以一定要确认对方想要得到你的帮忙。\n指导常常被忘记是管理者工作的一部分。因为管理者本身也会有能力不足的地方，“指导”的过程会暴露这些不足给下属。所以，设计组织时，要想办法让“暴露不足”成为常态，而不是羞耻。\n有效的指导的一些准则：\n帮助员工想出备选方案 有些管理者会想：如果还要我帮他想出备选方案，我还招他做什么。这是老旧的管理思想。我个人有以下理由需要帮助员工想出备选方案： 1. 避免失控，因为你了解方案的上下文，你才有能力判别是否失控。 2. 员工掌握的信息不足，无法做出最优方案。比如如果他不知道公司在长期业务的发展，是不会知道架构上哪些方面应该更灵活，哪些方面应该更固定。需要管理者帮忙他。 3. 与员工协作，而不只是丢一个点子。\n就每一个备选方案的影响进行讨论。不要试图得到某一特定结果；应该鼓励对方从他的视角尽可能深入地去探索每一个备选方案。表达你的观点，但应让你的指导的这名下属根据自己所需，做出选择。\n这个过程，其实，也是管理者学习的过程，学习别人的思考过程，同时，也可以从“思考过程”级别对员工进行指导。\n制定行动计划\n在一对一的会谈时，跟进这一行动计划找出成功的部分，对不成功的方法做出分析，尝试新的技术或者行为。完善并提高奏效的方法，纠正没有起到作用的方法。\n在我的经历中，鲜有组织会对过去做这些分析总结。因为有不少组织文化是不愿意面对失败过去的。也不愿意让员工知道自己的失败，有一部分原因是怕员工知道这些失败，对组织没有信心。\n**小小结：**当一个管理者本身能力不足，就谈不上有指导。比如，你根本就不懂SQL，谈何指导下属提升SQL性能的优化；还有一种情况就是管理者有能力，但不懂得如何有效指导，这是一种浪费。设计组织时，应该考虑如何帮忙这类员工。\n有“绩效管理”思维的人可能会想，让“指导”成为KPI的一部分不就可以了？但是，如何设计这项KPI呢？我个人认为，让“指导”成为组织文化的一部分才是解决之道。\n成功地分配任务 需要分配任务的理由：\n你不可能每件事都亲力亲为，总有一天你需要将管理类或者技术类工作分配给其他人。（不要认为这做是偷懒逃避责任，你是在给别人提供机会。）\n在行业内经常听到：教会他做的工夫，我自己都做完了。我们需要做办法说服这类员工。\n但是为什么会出现这样的现象呢？因为他所在的组织没有“分享”的文化。更有甚者，某些组织或管理者会抑制分享文化，因为分享文化提高了员工能力的透明度，最终会暴露出组织或管理者本身能力的不足。\n成功地分配任务的一些准则：\n明智地选择你的分配任务对象。这名员工应该想要承担更多的责任，并已经确认了自己的职业发展方向，这项工作正好适合他的职业发展方向。不要选择对这项工作不感兴趣的下属接受任务。\n高高在上的管理者是达不到这一准则的。因为他根本不了解自己的下属职业发展方向。\n阐明你对这项工作的期待：什么样的成果才你能接受的。\n在软件行业，特别是互联网行业，很多管理者也不完全知道自己对某项工作的期待。这不重要，重要的是管理者应该将这一点表明给下属听。\n明确表明不被允许的方法\n确定阶段性里程碑。当你做出分配工作的决定后，确保这一决定至少分为两部分：做出备选方案；选择其一。一定要表明哪一（些）部分是你分配下去的，如果在这两部分工作之间的某一点，你想进行检查，请明确地提出来。\n这一点非常重要。“结果导向”的管理方式大行其道，这给很多管理者不进行过程管理的理由（这类人常常将“我只要结果，不管过程”这句话挂在嘴边）。同时，要确定阶段性里程碑，是需要有相关业务的能力的，所以，我常常好奇那些不懂技术的管理者是如何管理软件研发团队的。\n**小小结：**在我个人看来，“分配任务”的能力很能体现一个管理者的能力。他必须有全局观，这样才能知道如何分配，效率才能达到全局最优。他必须有长远眼光，这样才会知道要在长远任务和短期任务之间做平衡（架构师才能在不同的技术方案做出更合理的选择）。\n我也发现，鲜有组织会考虑让员工自行选择任务。让员工自行选择任务，我个人总结有以下几点需要注意：\n找出信任他的理由：小心（可能）没有能力完成，但是又主动要求接的员工。这种情况，不是说不好，而是一定要和他共同制定方案，确定阶段性里程碑。这样，你才有信任他的理由。 检验他的方案：让主动的员工在所有人面前或者和你一对一地阐述自己的解决方案 小结 以上是卓越管理的两条技巧。看似简单，其实不易。比如在有些组织，根本上就没有“分享的基因”，你谈指导，得到的只是不理解。而且，面对中国的国情（面子、关系……），你需要权衡各方的“面子”和权力，更不易。大家且行且珍惜。\n我个人作为一个组织的设计者来看这些“技巧”是有原因的。因为我觉得很多问题，并不是个人问题，而是组织设计的问题。\n","permalink":"https://showme.codes/zh-cn/2018-2-19-secrets-of-great-management-1/","summary":"\u003cp\u003e\u003cimg alt=\"门后的秘密——卓越管理的故事\" loading=\"lazy\" src=\"/assets/images/292372-f7b7c8f66ed37e25.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e摘要\u003c/em\u003e 本文内容主要来自\u003ca href=\"https://www.amazon.cn/dp/B004HFGN8W\"\u003e《门后的秘密——卓越管理的故事》\u003c/a\u003e，英文名：Behind Closed Doors Secrets of Great Management。在2013年第一次看这本书时就感受益匪浅，现在又重新看，越发觉得这本书写的都是大实话。非常推荐读者亲自去读。\u003c/p\u003e\n\u003ch3 id=\"如何进行有效的指导\"\u003e如何进行有效的指导\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e指导也是管理者工作内容的一部分，但它着重于提高技术水平和能力。指导是一种帮忙与被帮忙的关系，所以一定要确认对方想要得到你的帮忙。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e指导常常被忘记是管理者工作的一部分。因为管理者本身也会有能力不足的地方，“指导”的过程会暴露这些不足给下属。\u003cem\u003e所以，设计组织时，要想办法让“暴露不足”成为常态，而不是羞耻。\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e有效的指导的一些准则：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e帮助员工想出备选方案\n有些管理者会想：如果还要我帮他想出备选方案，我还招他做什么。这是老旧的管理思想。我个人有以下理由需要帮助员工想出备选方案：\n1. 避免失控，因为你了解方案的上下文，你才有能力判别是否失控。\n2. 员工掌握的信息不足，无法做出最优方案。比如如果他不知道公司在长期业务的发展，是不会知道架构上哪些方面应该更灵活，哪些方面应该更固定。需要管理者帮忙他。\n3. 与员工协作，而不只是丢一个点子。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e就每一个备选方案的影响进行讨论。不要试图得到某一特定结果；应该鼓励对方从他的视角尽可能深入地去探索每一个备选方案。表达你的观点，但应让你的指导的这名下属根据自己所需，做出选择。\u003c/p\u003e\n\u003cp\u003e这个过程，其实，也是管理者学习的过程，学习别人的思考过程，同时，也可以从“思考过程”级别对员工进行指导。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e制定行动计划\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e在一对一的会谈时，跟进这一行动计划找出成功的部分，对不成功的方法做出分析，尝试新的技术或者行为。完善并提高奏效的方法，纠正没有起到作用的方法。\u003c/p\u003e\n\u003cp\u003e在我的经历中，鲜有组织会对过去做这些分析总结。因为有不少组织文化是不愿意面对失败过去的。也不愿意让员工知道自己的失败，有一部分原因是怕员工知道这些失败，对组织没有信心。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e**小小结：**当一个管理者本身能力不足，就谈不上有指导。比如，你根本就不懂SQL，谈何指导下属提升SQL性能的优化；还有一种情况就是管理者有能力，但不懂得如何有效指导，这是一种浪费。\u003cem\u003e设计组织时，应该考虑如何帮忙这类员工\u003c/em\u003e。\u003c/p\u003e\n\u003cp\u003e有“绩效管理”思维的人可能会想，让“指导”成为KPI的一部分不就可以了？但是，如何设计这项KPI呢？我个人认为，让“指导”成为组织文化的一部分才是解决之道。\u003c/p\u003e\n\u003ch3 id=\"成功地分配任务\"\u003e成功地分配任务\u003c/h3\u003e\n\u003cp\u003e需要分配任务的理由：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e你不可能每件事都亲力亲为，总有一天你需要将管理类或者技术类工作分配给其他人。（不要认为这做是偷懒逃避责任，你是在给别人提供机会。）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在行业内经常听到：教会他做的工夫，我自己都做完了。我们需要做办法说服这类员工。\u003c/p\u003e\n\u003cp\u003e但是为什么会出现这样的现象呢？因为他所在的组织没有“分享”的文化。更有甚者，某些组织或管理者会抑制分享文化，因为分享文化提高了员工能力的透明度，最终会暴露出组织或管理者本身能力的不足。\u003c/p\u003e\n\u003cp\u003e成功地分配任务的一些准则：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e明智地选择你的分配任务对象。这名员工应该想要承担更多的责任，并已经确认了自己的职业发展方向，这项工作正好适合他的职业发展方向。不要选择对这项工作不感兴趣的下属接受任务。\u003c/p\u003e\n\u003cp\u003e高高在上的管理者是达不到这一准则的。因为他根本不了解自己的下属职业发展方向。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e阐明你对这项工作的期待：什么样的成果才你能接受的。\u003c/p\u003e\n\u003cp\u003e在软件行业，特别是互联网行业，很多管理者也不完全知道自己对某项工作的期待。这不重要，重要的是管理者应该将这一点表明给下属听。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e明确表明不被允许的方法\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e确定阶段性里程碑。当你做出分配工作的决定后，确保这一决定至少分为两部分：做出备选方案；选择其一。一定要表明哪一（些）部分是你分配下去的，如果在这两部分工作之间的某一点，你想进行检查，请明确地提出来。\u003c/p\u003e\n\u003cp\u003e这一点非常重要。“结果导向”的管理方式大行其道，这给很多管理者不进行过程管理的理由（这类人常常将“我只要结果，不管过程”这句话挂在嘴边）。同时，要确定阶段性里程碑，是需要有相关业务的能力的，所以，我常常好奇那些不懂技术的管理者是如何管理软件研发团队的。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e**小小结：**在我个人看来，“分配任务”的能力很能体现一个管理者的能力。他必须有全局观，这样才能知道如何分配，效率才能达到全局最优。他必须有长远眼光，这样才会知道要在长远任务和短期任务之间做平衡（架构师才能在不同的技术方案做出更合理的选择）。\u003c/p\u003e\n\u003cp\u003e我也发现，鲜有组织会考虑让员工自行选择任务。让员工自行选择任务，我个人总结有以下几点需要注意：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e找出信任他的理由：小心（可能）没有能力完成，但是又主动要求接的员工。这种情况，不是说不好，而是一定要和他共同制定方案，确定阶段性里程碑。这样，你才有信任他的理由。\u003c/li\u003e\n\u003cli\u003e检验他的方案：让主动的员工在所有人面前或者和你一对一地阐述自己的解决方案\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e以上是卓越管理的两条技巧。看似简单，其实不易。比如在有些组织，根本上就没有“分享的基因”，你谈指导，得到的只是不理解。而且，面对中国的国情（面子、关系……），你需要权衡各方的“面子”和权力，更不易。大家且行且珍惜。\u003c/p\u003e\n\u003cp\u003e我个人作为一个组织的设计者来看这些“技巧”是有原因的。因为我觉得很多问题，并不是个人问题，而是组织设计的问题。\u003c/p\u003e","title":"如何进行有效的指导，如何成功的分配任务"},{"content":"\n申明：缩短出活时长的目的不是为了让他们有更多时间加班。目的是提高团队的生产力。这样我们才有更多时间陪家人。\n作为一个管理者，一定会遇到的一个问题是：如何团队新人更快的“出活”？\n面对这样的问题，行业内，我见过不少管理者，就直接丢给他一堆源码和文档，就什么也不管了。直到一个星期后才询问新人理解了多少，或者直接丢给他一个需求让他去实现。\n这样做意味着：\n一个星期是出不了活的，因为你已经在一个星期后才给他任务 一个星期后，你敢100%保证他所理解的东西能帮忙到他完成你一个星期后给他的需求？很有可能，他所做的需求和这一个星期内看的东西毫无关系 那怎么办呢？我知道你心中开始疑问。接下来，我介绍一些我个人的看法和做法。\n“让新人更快的出活”是一个目标状态，我们应该问的是：达到这个目标状态的前提条件是什么？ 只要保证了这个前提条件，就船到桥头自然直。\n我总结的前提条件有：\n条件1. 对业务有基本的认识。比如做家电IoT时，他必须亲自拿着手机尽可能的玩一遍所有的家电。 条件2. 对于整个系统架构有一个大体的思考框架。比如家电IoT的整体架构有一个Big picture。 条件3. 大概了解每个团队成员的职责，知道哪类问题该找谁。 条件4. 了解自己的职责所在。这样才能有的放矢，至少，知道自己应该重点看哪些文档。\n要达到这些前提条件的做法是什么呢？\n关于条件1：对业务有基本的认识 举例来说就是让他拿着一个家电说明书，然后让家电连上网，再控制他。这个过程，我们如何验证他的学习效果呢？\n让他记录下使用产品这个过程的困惑，因为这些困惑就是真正用户的困惑。换句话，除了让新人了解了业务，还能帮助我们这些“资深用户”找出产品所存在的问题。\n关于条件2：对于整个系统架构有一个大体的思考框架 让团队里最熟悉整个系统架构的人，给这些新人讲解。让新人有一个全局观。同时，这个过程，也是让新人有发现系统构架存在的问题的机会。如何验证新人的学习效果呢？\n让新人当着所有人的面新口说一遍整个系统架构，并在白板上画出来。当然，如果所在团队没有这样的条件，就让他给另一个团队成员讲，也是可以的。\n条件3：大概了解每个团队成员的职责，知道哪类问题该找谁 为每个新人分配到一个“辅导员”的同事。这个辅导员有责任回答新人所有问题。\n这样做，有几个好处：\n对新人有更多的人文关怀。特别是对于一个第一次来公司所在城市的外地人员。 新人可更快融入团队。因为这样新人就不会觉得“怯场”。也可以让新人更快的了解团队的文化。 作为团队的“老人”，也能更深入的了解这位新人。最终达到团队match的状态。 这点，我所经历的ThoughtWorkers就做得非常好。\n条件4：了解自己的职责所在 当了解系统的整体架构后，我们只要画出他的职责的那块，他就可以很快地理解了。也就知道他接下来要做什么了。\n再说了，我们的辅导员也有责任让他知道他接下来要做什么。\n让新人更快出活的小技巧 以下是我自己总结的一些小技巧。\n让新人和老人结对编程。比如让老人完整的实现一个需求，新人就坐在旁边看。这样，新人就很快熟悉在当前团队中一个完整的开发流程是怎样的了。 让新人去测试老人实现的需求。比如老人实现了一个需求，也自测了。但是我们团队有一个要求，只有别人测试过了，才算测试通过。在测试人员不足时，可以这么做。 让新人做测试即可以让他更快的熟悉业务，又可以培养他的测试思维。\n小结 一个新人要多快出活，除了新人本身的素质外，我们作为管理者需要不断地思考，如何缩短他们的出活时长。\n再次申明：缩短出活时长的目的不是为了让他们有更多时间加班。目的是提高团队的生产力。这样我们才有更多时间陪家人。\n图源：https://www.pexels.com/photo/clown-fish-swimming-128756/\n","permalink":"https://showme.codes/zh-cn/2018-1-15-new-fish/","summary":"\u003cp\u003e\u003cimg alt=\"new fish\" loading=\"lazy\" src=\"/assets/images/292372-b30e6f1c3262d43e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e申明：缩短出活时长的目的不是为了让他们有更多时间加班。目的是提高团队的生产力。这样我们才有更多时间陪家人。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e作为一个管理者，一定会遇到的一个问题是：如何团队新人更快的“出活”？\u003c/p\u003e\n\u003cp\u003e面对这样的问题，行业内，我见过不少管理者，就直接丢给他一堆源码和文档，就什么也不管了。直到一个星期后才询问新人理解了多少，或者直接丢给他一个需求让他去实现。\u003c/p\u003e\n\u003cp\u003e这样做意味着：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e一个星期是出不了活的，因为你已经在一个星期后才给他任务\u003c/li\u003e\n\u003cli\u003e一个星期后，你敢100%保证他所理解的东西能帮忙到他完成你一个星期后给他的需求？很有可能，他所做的需求和这一个星期内看的东西毫无关系\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e那怎么办呢？我知道你心中开始疑问。接下来，我介绍一些我个人的看法和做法。\u003c/p\u003e\n\u003cp\u003e“让新人更快的出活”是一个\u003cstrong\u003e目标状态\u003c/strong\u003e，我们应该问的是：\u003cstrong\u003e达到这个目标状态的前提条件是什么？\u003c/strong\u003e 只要保证了这个前提条件，就船到桥头自然直。\u003c/p\u003e\n\u003cp\u003e我总结的前提条件有：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e条件1.\u003c/strong\u003e 对业务有基本的认识。比如做家电IoT时，他必须亲自拿着手机尽可能的玩一遍所有的家电。\n\u003cstrong\u003e条件2.\u003c/strong\u003e  对于整个系统架构有一个大体的思考框架。比如家电IoT的整体架构有一个Big picture。\n\u003cstrong\u003e条件3.\u003c/strong\u003e 大概了解每个团队成员的职责，知道哪类问题该找谁。\n\u003cstrong\u003e条件4.\u003c/strong\u003e 了解自己的职责所在。这样才能有的放矢，至少，知道自己应该重点看哪些文档。\u003c/p\u003e\n\u003cp\u003e要达到这些前提条件的做法是什么呢？\u003c/p\u003e\n\u003ch4 id=\"关于条件1对业务有基本的认识\"\u003e关于条件1：对业务有基本的认识\u003c/h4\u003e\n\u003cp\u003e举例来说就是让他拿着一个家电说明书，然后让家电连上网，再控制他。这个过程，我们如何验证他的学习效果呢？\u003c/p\u003e\n\u003cp\u003e让他记录下使用产品这个过程的困惑，因为这些困惑就是真正用户的困惑。换句话，除了让新人了解了业务，还能帮助我们这些“资深用户”找出产品所存在的问题。\u003c/p\u003e\n\u003ch4 id=\"关于条件2对于整个系统架构有一个大体的思考框架\"\u003e关于条件2：对于整个系统架构有一个大体的思考框架\u003c/h4\u003e\n\u003cp\u003e让团队里最熟悉整个系统架构的人，给这些新人讲解。让新人有一个全局观。同时，这个过程，也是让新人有发现系统构架存在的问题的机会。如何验证新人的学习效果呢？\u003c/p\u003e\n\u003cp\u003e让新人当着所有人的面新口说一遍整个系统架构，并在白板上画出来。当然，如果所在团队没有这样的条件，就让他给另一个团队成员讲，也是可以的。\u003c/p\u003e\n\u003ch4 id=\"条件3大概了解每个团队成员的职责知道哪类问题该找谁\"\u003e条件3：大概了解每个团队成员的职责，知道哪类问题该找谁\u003c/h4\u003e\n\u003cp\u003e为每个新人分配到一个“辅导员”的同事。这个辅导员有责任回答新人所有问题。\u003c/p\u003e\n\u003cp\u003e这样做，有几个好处：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e对新人有更多的人文关怀。特别是对于一个第一次来公司所在城市的外地人员。\u003c/li\u003e\n\u003cli\u003e新人可更快融入团队。因为这样新人就不会觉得“怯场”。也可以让新人更快的了解团队的文化。\u003c/li\u003e\n\u003cli\u003e作为团队的“老人”，也能更深入的了解这位新人。最终达到团队match的状态。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这点，我所经历的ThoughtWorkers就做得非常好。\u003c/p\u003e\n\u003ch4 id=\"条件4了解自己的职责所在\"\u003e条件4：了解自己的职责所在\u003c/h4\u003e\n\u003cp\u003e当了解系统的整体架构后，我们只要画出他的职责的那块，他就可以很快地理解了。也就知道他接下来要做什么了。\u003c/p\u003e\n\u003cp\u003e再说了，我们的辅导员也有责任让他知道他接下来要做什么。\u003c/p\u003e\n\u003ch4 id=\"让新人更快出活的小技巧\"\u003e让新人更快出活的小技巧\u003c/h4\u003e\n\u003cp\u003e以下是我自己总结的一些小技巧。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e让新人和老人结对编程。比如让老人完整的实现一个需求，新人就坐在旁边看。这样，新人就很快熟悉在当前团队中一个完整的开发流程是怎样的了。\u003c/li\u003e\n\u003cli\u003e让新人去测试老人实现的需求。比如老人实现了一个需求，也自测了。但是我们团队有一个要求，只有别人测试过了，才算测试通过。在测试人员不足时，可以这么做。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e让新人做测试即可以让他更快的熟悉业务，又可以培养他的测试思维。\u003c/p\u003e\n\u003ch4 id=\"小结\"\u003e小结\u003c/h4\u003e\n\u003cp\u003e一个新人要多快出活，除了新人本身的素质外，我们作为管理者需要不断地思考，如何缩短他们的出活时长。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e再次申明：缩短出活时长的目的不是为了让他们有更多时间加班。目的是提高团队的生产力。这样我们才有更多时间陪家人。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e图源：https://www.pexels.com/photo/clown-fish-swimming-128756/\u003c/p\u003e","title":"如何充分“使用”研发团队新人"},{"content":"\n想象离开目前这家公司，你还能去哪？ 如果你发现，你只有和这家公司流程、政治、上下文等等强相关的能力，那么，你也只能靠“办公室政治”保留住自己的位子。因为，离开了这家公司，你什么也不是。\n但是，也有例外，就是另一家公司没有分辨能力，你又特能忽悠。像这位“高管”：假证假名假文凭，小学文化男子应聘入职月薪7万公司高管。\n最后，当你思考这个问题时，如果感到害怕，你就要小心了。\n忠告：保持独立思考的能力，但隐藏起来 市面上有一些管理者是不允许你和他有不同意见的。通常，独立思考的人凡事都会有自己的看法。而如果你在别人面前和管理者提出不一样的意见，领导会觉得没面子。那么，今后，你可能就没好吃的了。\n那么，正确的做法是什么呢？即使大环境下，所有的人都附和管理者，你同样不要被这种环境麻痹自己独立思考的能力。观察你的管理者是开明的人，还是职场“老油条”？选择一个合适的时机说出你的不同意见。\n这时，你就知道“察言观色”能力的重要性了。\n当存在不同意见的时候，作为组织和管理者需要思考，是不是管理者没有表达清楚，是不是这个“不同意见”更合理，如何说服这个不同意见的人，其他人是不是也有不同的意见？\n当然，有些人会觉得这样会导致企业的执行力下降。我想说的是，知识工作者执行任务不像体力劳动者，你告诉他把这个箱子从这里搬到那里，这么准确。任务定义不准确往往是组织管理者自己都还没有想清楚自己到底要什么。最后得不到自己想要的，又反过来说企业的执行力差。\n现象：通过建立信息壁垒的手段，保住自己的位置 有一次，我问一个同行：为什么整个系统，这么多个工程，没有一个人知道全景图。（这个系统并不算大，也就30多个子工程）。我们能不能组织所有人建立一个全景图？\n他说了：别人是不会告诉你，他自己那块是怎么做的。\n我问：为什么呢？有个全景图大家不就可以更好的协作吗？出现问题，也不需要等着某一个人了。\n他浅笑，眼看前方：因为如果给你懂了他的那块系统，你就可以替代他了。\n这下，我才明白。行业内，还有人通过建立“信息壁垒”，让公司离不开你；让管理者觉得你是有用的。\n看懂这点后，你会发现，那些不干“实事”的人，为什么能一直“保住位置”。往往因为他们掌握了信息的源头。\n这种协作机制，有好处，比如两个组织之间的对接方式明确，出现问题马上知道找谁解决。坏处一是信息的准确度会在对接人之间严重打折，带来的就是低效率。坏处二就是会滋生建立信息壁垒的职场老油条。\n作为个人，我们要问自己，离开当前这家公司，你建立的信息壁垒，还会有价值吗？\n作为公司，信息不透明所带来的坏处，我就不想说了。管理企业和管理国家都会遇到这个问题。\n忠告：小心那些威胁到别人“KPI”的行为 当公司的IT系统管理员做得不好时，不要直接在公司的大群里说。因为你这样做可能会威胁到他的KPI或他在管理者眼里的印象了。所以，私自跟这位IT系统管理员沟通就好了。\n为什么：不管功劳，苦劳，一定要让管理者看到 过年了，相信不少人要开始表现得非常辛苦了。很简单，因为年底了，年终奖的多少就看这一个月了。当然，我说的不是绝对的。但是一定存在这种现象。\n这种现象背后的原因是多种的：\n管理者对人、工作的判断，依赖的是主观印象，而不是客观因素。\n所以，你会经常听到有人在朋友圈、饭局上说自己最近因为工作太猛不舒服、没有时间陪小孩、过劳胖……\n当出现这种现象时，作为管理者就需要反思自己的管理方式了：组织的效率是不是有问题；作为个人，小心使用这种行为，不要让它麻痹了你独立思考的能力。\n知识工作者的生产活动存在于大脑中，看不到摸不着。\n比如没有好的技术管理的团队，即使你写出优秀的代码，对于你的“年终奖”也没有什么益处。\n作为组织，我们需要思考，员工的表现一定要让管理者看到，而不是让所有人都看到。\n玩笑：准时下班，肯定是工作不饱和 这是一句玩笑话。但是背后有它道理。\n我们知识工作者有一个特点，就是从表面上，你是看不出这个人是否在工作的，因为他的真正工作存在于他的大脑中。他一天坐在那里，你怎么就知道他是否在工作呢？再者，绝大多数人的注意力没法做到连续一小时。所以，公司pay你的一天八小时，是打折的。\n然后，我们软件行业里就有了这么一个玩笑话。\n事实上，这背后更深层的含义是：\n组织、管理者无法评估知识工作者的工作量 组织、管理者还没有理解体力劳动者和知识工作者之间的区别 那怎么知道一个程序员的工作是否饱和了呢？哈哈。这样的好问题，留给喜欢思考的人。P.S. 我不提倡加班文化，让程序员工作饱和的目的是让程序员不加班。\n招聘：交叉组织面试是合理的 以前听说腾讯面试，你面试的是A部门，然后企业HR会在某个环节里随机其他部门的人来面试。这种机制能有效避免有人利用手上权利招聘一些利益相关的人。比如把自己的并不能胜任能力的表弟招进来。\n而企业中，我发现这种交叉组织面试真的是值得的。\n现象：管理者让你把数据拷出来 朋友打电话来诉苦，说他上级要求他把公司的大数据拷出来。这时，你会怎么做呢？我也不知道，说实在的。\n但是这个问题是所有企业都会遇到的问题：如何管理无形资产？这个问题有些大。今天不讨论。\n小结 其实，还有很多职场经验、组织管理上的思考。这些只是其中一些。其中有些话，是有些绝对，你对号入座了，反正我不负责。\n作为一个软件工程师，我是不是有些不务正业？我是不是该执行上级领导给的需求，什么也别想就可以了？\n向独立思考者致敬。\n","permalink":"https://showme.codes/zh-cn/2018-1-6-work-for-work-1/","summary":"\u003cp\u003e\u003cimg alt=\"coding\" loading=\"lazy\" src=\"/assets/images/292372-ae08cceed4d9122c.png\"\u003e\u003c/p\u003e\n\u003ch4 id=\"想象离开目前这家公司你还能去哪\"\u003e想象离开目前这家公司，你还能去哪？\u003c/h4\u003e\n\u003cp\u003e如果你发现，你只有和这家公司流程、政治、上下文等等强相关的能力，那么，你也只能靠“办公室政治”保留住自己的位子。因为，离开了这家公司，你什么也不是。\u003c/p\u003e\n\u003cp\u003e但是，也有例外，就是另一家公司没有分辨能力，你又特能忽悠。像这位“高管”：\u003ca href=\"http://www.thepaper.cn/newsDetail_forward_1852714\"\u003e假证假名假文凭，小学文化男子应聘入职月薪7万公司高管\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e最后，当你思考这个问题时，如果感到害怕，你就要小心了。\u003c/p\u003e\n\u003ch4 id=\"忠告保持独立思考的能力但隐藏起来\"\u003e忠告：保持独立思考的能力，但隐藏起来\u003c/h4\u003e\n\u003cp\u003e市面上有一些管理者是不允许你和他有不同意见的。通常，独立思考的人凡事都会有自己的看法。而如果你在别人面前和管理者提出不一样的意见，领导会觉得没面子。那么，今后，你可能就没好吃的了。\u003c/p\u003e\n\u003cp\u003e那么，正确的做法是什么呢？即使大环境下，所有的人都附和管理者，你同样不要被这种环境麻痹自己独立思考的能力。观察你的管理者是开明的人，还是职场“老油条”？选择一个合适的时机说出你的不同意见。\u003c/p\u003e\n\u003cp\u003e这时，你就知道“察言观色”能力的重要性了。\u003c/p\u003e\n\u003cp\u003e当存在不同意见的时候，作为组织和管理者需要思考，是不是管理者没有表达清楚，是不是这个“不同意见”更合理，如何说服这个不同意见的人，其他人是不是也有不同的意见？\u003c/p\u003e\n\u003cp\u003e当然，有些人会觉得这样会导致企业的执行力下降。我想说的是，知识工作者执行任务不像体力劳动者，你告诉他把这个箱子从这里搬到那里，这么准确。任务定义不准确往往是组织管理者自己都还没有想清楚自己到底要什么。最后得不到自己想要的，又反过来说企业的执行力差。\u003c/p\u003e\n\u003ch4 id=\"现象通过建立信息壁垒的手段保住自己的位置\"\u003e现象：通过建立信息壁垒的手段，保住自己的位置\u003c/h4\u003e\n\u003cp\u003e有一次，我问一个同行：为什么整个系统，这么多个工程，没有一个人知道全景图。（这个系统并不算大，也就30多个子工程）。我们能不能组织所有人建立一个全景图？\u003c/p\u003e\n\u003cp\u003e他说了：别人是不会告诉你，他自己那块是怎么做的。\u003c/p\u003e\n\u003cp\u003e我问：为什么呢？有个全景图大家不就可以更好的协作吗？出现问题，也不需要等着某一个人了。\u003c/p\u003e\n\u003cp\u003e他浅笑，眼看前方：因为如果给你懂了他的那块系统，你就可以替代他了。\u003c/p\u003e\n\u003cp\u003e这下，我才明白。行业内，还有人通过建立“信息壁垒”，让公司离不开你；让管理者觉得你是有用的。\u003c/p\u003e\n\u003cp\u003e看懂这点后，你会发现，那些不干“实事”的人，为什么能一直“保住位置”。往往因为他们掌握了信息的源头。\u003c/p\u003e\n\u003cp\u003e这种协作机制，有好处，比如两个组织之间的对接方式明确，出现问题马上知道找谁解决。坏处一是信息的准确度会在对接人之间严重打折，带来的就是低效率。坏处二就是会滋生建立信息壁垒的职场老油条。\u003c/p\u003e\n\u003cp\u003e作为个人，我们要问自己，离开当前这家公司，你建立的信息壁垒，还会有价值吗？\u003c/p\u003e\n\u003cp\u003e作为公司，信息不透明所带来的坏处，我就不想说了。管理企业和管理国家都会遇到这个问题。\u003c/p\u003e\n\u003ch4 id=\"忠告小心那些威胁到别人kpi的行为\"\u003e忠告：小心那些威胁到别人“KPI”的行为\u003c/h4\u003e\n\u003cp\u003e当公司的IT系统管理员做得不好时，不要直接在公司的大群里说。因为你这样做可能会威胁到他的KPI或他在管理者眼里的印象了。所以，私自跟这位IT系统管理员沟通就好了。\u003c/p\u003e\n\u003ch4 id=\"为什么不管功劳苦劳一定要让管理者看到\"\u003e为什么：不管功劳，苦劳，一定要让管理者看到\u003c/h4\u003e\n\u003cp\u003e过年了，相信不少人要开始表现得非常辛苦了。很简单，因为年底了，年终奖的多少就看这一个月了。当然，我说的不是绝对的。但是一定存在这种现象。\u003c/p\u003e\n\u003cp\u003e这种现象背后的原因是多种的：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e管理者对人、工作的判断，依赖的是主观印象，而不是客观因素。\u003c/p\u003e\n\u003cp\u003e所以，你会经常听到有人在朋友圈、饭局上说自己最近因为工作太猛不舒服、没有时间陪小孩、过劳胖……\u003c/p\u003e\n\u003cp\u003e当出现这种现象时，作为管理者就需要反思自己的管理方式了：组织的效率是不是有问题；作为个人，小心使用这种行为，不要让它麻痹了你独立思考的能力。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e知识工作者的生产活动存在于大脑中，看不到摸不着。\u003c/p\u003e\n\u003cp\u003e比如没有好的技术管理的团队，即使你写出优秀的代码，对于你的“年终奖”也没有什么益处。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e作为组织，我们需要思考，员工的表现一定要让管理者看到，而不是让所有人都看到。\u003c/p\u003e\n\u003ch4 id=\"玩笑准时下班肯定是工作不饱和\"\u003e玩笑：准时下班，肯定是工作不饱和\u003c/h4\u003e\n\u003cp\u003e这是一句玩笑话。但是背后有它道理。\u003c/p\u003e\n\u003cp\u003e我们知识工作者有一个特点，就是从表面上，你是看不出这个人是否在工作的，因为他的真正工作存在于他的大脑中。他一天坐在那里，你怎么就知道他是否在工作呢？再者，绝大多数人的注意力没法做到连续一小时。所以，公司pay你的一天八小时，是打折的。\u003c/p\u003e\n\u003cp\u003e然后，我们软件行业里就有了这么一个玩笑话。\u003c/p\u003e\n\u003cp\u003e事实上，这背后更深层的含义是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e组织、管理者无法评估知识工作者的工作量\u003c/li\u003e\n\u003cli\u003e组织、管理者还没有理解体力劳动者和知识工作者之间的区别\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e那怎么知道一个程序员的工作是否饱和了呢？哈哈。这样的好问题，留给喜欢思考的人。P.S. 我不提倡加班文化，让程序员工作饱和的目的是让程序员不加班。\u003c/p\u003e\n\u003ch4 id=\"招聘交叉组织面试是合理的\"\u003e招聘：交叉组织面试是合理的\u003c/h4\u003e\n\u003cp\u003e以前听说腾讯面试，你面试的是A部门，然后企业HR会在某个环节里随机其他部门的人来面试。这种机制能有效避免有人利用手上权利招聘一些利益相关的人。比如把自己的并不能胜任能力的表弟招进来。\u003c/p\u003e\n\u003cp\u003e而企业中，我发现这种交叉组织面试真的是值得的。\u003c/p\u003e\n\u003ch4 id=\"现象管理者让你把数据拷出来\"\u003e现象：管理者让你把数据拷出来\u003c/h4\u003e\n\u003cp\u003e朋友打电话来诉苦，说他上级要求他把公司的大数据拷出来。这时，你会怎么做呢？我也不知道，说实在的。\u003c/p\u003e\n\u003cp\u003e但是这个问题是所有企业都会遇到的问题：如何管理无形资产？这个问题有些大。今天不讨论。\u003c/p\u003e\n\u003ch4 id=\"小结\"\u003e小结\u003c/h4\u003e\n\u003cp\u003e其实，还有很多职场经验、组织管理上的思考。这些只是其中一些。其中有些话，是有些绝对，你对号入座了，反正我不负责。\u003c/p\u003e\n\u003cp\u003e作为一个软件工程师，我是不是有些不务正业？我是不是该执行上级领导给的需求，什么也别想就可以了？\u003c/p\u003e\n\u003cp\u003e向独立思考者致敬。\u003c/p\u003e","title":"一个软件工程师对职场、管理、企业的思考——上篇"},{"content":"\n在管理这个行当里，存在两个基本理论，X理论和Y理论。希望大家认真看一看，这样你就理解了，为什么公司会有这样那样的政策。\nX理论 Y理论 一般人的本性是懒惰的，工作越少越好，可能的话会逃避工作。 人们在工作上体力和脑力的投入就跟在娱乐和休闲上的投入一样，工作是很自然的事——大部分人并不抗拒工作。 大部分人对集体（公司，机构，单位或组织等）的目标不关心，因此管理者需要以强迫，威胁处罚，指导，金钱利益等诱因激发人们的工作源动力。 即使没有外界的压力和处罚的威胁，他们一样会努力工作以期达到目的——人们具有自我调节和自我监督的能力。 一般人缺少进取心，只有在指导下才愿意接受工作，因此管理者需要对他们施加压力。 人们愿意为集体的目标而努力，在工作上会尽最大的努力，以发挥创造力，才智——人们希望在工作上获得认同感，会自觉遵守规定。 在适当的条件下，人们不仅愿意接受工作上的责任，并会寻求更大的责任。 许多人具有相当高的创新能力去解决问题。 在大多数的机构里面，人们的才智并没有充分发挥。 | 资料来自维基百科\n是的，不少公司的管理人员或多或少基于其中一个理论。有些人在决策时甚至意识不到XY理论的存在。\n这两个理论，我都赞同，也都不赞同。就如同我赞同人有善的一面，也有恶的一面。而且，在不同的时候，有不同的一面。\n所以，当人们表现为X理论的一面的时候，我不是马上认定这个人100%就是X理论里所说的人。而认为是我们的工作流程或机制的设计出现了问题。\n为什么这样说呢？我们先来看看公交卡的设计。我们都知道公交卡可以分为老人卡、学生卡、普通卡。\n我们如何避免不是老人的人刷老人卡坐公交呢？可以想象的方法有这些：\n很明显，政府出一个“公文”不允许非老人刷老人卡，是最没用的方法。 老人卡使用特殊的颜色，这样刷卡时，司机就可以辨认刷卡人是否为老人。可是广大人民的智慧是无穷，只要加个卡套就好了。P.S. 很少人会想到这样会导致下属人员作假。 在刷卡的地方，加一个专门的人去检查卡人是否一致。这样，大多数人可能上下班可能都会迟到。P.S. 很少人会想到这样还会导致腐败、人与人的矛盾。 不要笑，我们不少公司管理方法的背后的思维模式和这些“方法”的没有多大区别。\n如果我们用X理论来度量所有坐公车的人，我们的公车效率就会变得低效。如果我们用Y理论来度量所有坐公车的人，我们的公车效率是好了，但是赚的钱变少了，而且可能亏本。\n这就是不少企业经常遇到的问题：抓过紧就死，放太松就散。\n公交卡问题如何破局？说到这里，坐过公交车的人已经知道答案了：老人卡刷卡时，语音播报：老人卡。其它特殊的卡依此类推。\n很长一段时间，我都在思考：为什么？\n后来，我想通了。语音播报时，刷老人卡时，如果这个人不是老人，他一定会脸红（羞耻心）。因为公交上其他人都看到了。语音播报利用了社会人所具有的羞耻心来解决公交卡问题。\n回到本文的主题，这样的“公交卡”给我们的管理带来什么启示呢？\n一个人不应该用XY理论分类 企业管理做决策时，不能完全基于X理论，也不能完全基于Y理论 利用人性引导人们趋向于Y理论，而不是相反 让信息透明会让我们收益良多 利用人性引导人们趋向于Y理论看似卑鄙，但是总比一些游戏利用人性让你上瘾更让人接受。\n信息的不透明是很多管理难题的根本问题所在。以后我们再谈。\n小结 公交卡的设计给我带来的不止是启示，更有意义的是背后的思维模式。而思维模式能带给我在任何情况下随机应变的能力。\n最后我提个问题，留给读者思考。问题是：如果10人软件开发团队里，服务经常因为开发人员在自己的机器上打包而出现bug，你会如何解决这个问题？\n注意，解决这个问题的同时，会反应出你的思维模式。\n","permalink":"https://showme.codes/zh-cn/2017-12-16-management-in-bus/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-f0fd6db66ffe5397.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在管理这个行当里，存在两个基本理论，X理论和Y理论。希望大家认真看一看，这样你就理解了，为什么公司会有这样那样的政策。\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eX理论\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eY理论\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e一般人的本性是懒惰的，工作越少越好，可能的话会逃避工作。\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e人们在工作上体力和脑力的投入就跟在娱乐和休闲上的投入一样，工作是很自然的事——大部分人并不抗拒工作。\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e大部分人对集体（公司，机构，单位或组织等）的目标不关心，因此管理者需要以强迫，威胁处罚，指导，金钱利益等诱因激发人们的工作源动力。\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e即使没有外界的压力和处罚的威胁，他们一样会努力工作以期达到目的——人们具有自我调节和自我监督的能力。\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e一般人缺少进取心，只有在指导下才愿意接受工作，因此管理者需要对他们施加压力。\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e人们愿意为集体的目标而努力，在工作上会尽最大的努力，以发挥创造力，才智——人们希望在工作上获得认同感，会自觉遵守规定。\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e在适当的条件下，人们不仅愿意接受工作上的责任，并会寻求更大的责任。\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e许多人具有相当高的创新能力去解决问题。\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e在大多数的机构里面，人们的才智并没有充分发挥。\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e| 资料来自维基百科\u003c/p\u003e\n\u003cp\u003e是的，不少公司的管理人员或多或少基于其中一个理论。有些人在决策时甚至意识不到XY理论的存在。\u003c/p\u003e\n\u003cp\u003e这两个理论，我都赞同，也都不赞同。就如同我赞同人有善的一面，也有恶的一面。而且，在不同的时候，有不同的一面。\u003c/p\u003e\n\u003cp\u003e所以，当人们表现为X理论的一面的时候，我不是马上认定这个人100%就是X理论里所说的人。而认为是我们的工作流程或机制的设计出现了问题。\u003c/p\u003e\n\u003cp\u003e为什么这样说呢？我们先来看看公交卡的设计。我们都知道公交卡可以分为老人卡、学生卡、普通卡。\u003c/p\u003e\n\u003cp\u003e我们如何避免不是老人的人刷老人卡坐公交呢？可以想象的方法有这些：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e很明显，政府出一个“公文”不允许非老人刷老人卡，是最没用的方法。\u003c/li\u003e\n\u003cli\u003e老人卡使用特殊的颜色，这样刷卡时，司机就可以辨认刷卡人是否为老人。可是广大人民的智慧是无穷，只要加个卡套就好了。P.S. 很少人会想到这样会导致下属人员作假。\u003c/li\u003e\n\u003cli\u003e在刷卡的地方，加一个专门的人去检查卡人是否一致。这样，大多数人可能上下班可能都会迟到。P.S. 很少人会想到这样还会导致腐败、人与人的矛盾。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e不要笑，我们不少公司管理方法的背后的思维模式和这些“方法”的没有多大区别。\u003c/p\u003e\n\u003cp\u003e如果我们用X理论来度量所有坐公车的人，我们的公车效率就会变得低效。如果我们用Y理论来度量所有坐公车的人，我们的公车效率是好了，但是赚的钱变少了，而且可能亏本。\u003c/p\u003e\n\u003cp\u003e这就是不少企业经常遇到的问题：抓过紧就死，放太松就散。\u003c/p\u003e\n\u003cp\u003e公交卡问题如何破局？说到这里，坐过公交车的人已经知道答案了：老人卡刷卡时，\u003cstrong\u003e语音播报：老人卡\u003c/strong\u003e。其它特殊的卡依此类推。\u003c/p\u003e\n\u003cp\u003e很长一段时间，我都在思考：为什么？\u003c/p\u003e\n\u003cp\u003e后来，我想通了。语音播报时，刷老人卡时，如果这个人不是老人，他一定会脸红（羞耻心）。\u003cstrong\u003e因为公交上其他人都看到了\u003c/strong\u003e。语音播报利用了社会人所具有的\u003cstrong\u003e羞耻心\u003c/strong\u003e来解决公交卡问题。\u003c/p\u003e\n\u003cp\u003e回到本文的主题，这样的“公交卡”给我们的管理带来什么启示呢？\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e一个人不应该用XY理论分类\u003c/li\u003e\n\u003cli\u003e企业管理做决策时，不能完全基于X理论，也不能完全基于Y理论\u003c/li\u003e\n\u003cli\u003e利用人性引导人们趋向于Y理论，而不是相反\u003c/li\u003e\n\u003cli\u003e让信息透明会让我们收益良多\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003e利用人性引导人们趋向于Y理论\u003c/strong\u003e看似卑鄙，但是总比一些游戏利用人性让你上瘾更让人接受。\u003c/p\u003e\n\u003cp\u003e信息的不透明是很多管理难题的根本问题所在。以后我们再谈。\u003c/p\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e公交卡的设计给我带来的不止是启示，更有意义的是背后的\u003cstrong\u003e思维模式\u003c/strong\u003e。而思维模式能带给我在任何情况下随机应变的能力。\u003c/p\u003e\n\u003cp\u003e最后我提个问题，留给读者思考。问题是：如果10人软件开发团队里，服务经常因为开发人员在自己的机器上打包而出现bug，你会如何解决这个问题？\u003c/p\u003e\n\u003cp\u003e注意，解决这个问题的同时，会反应出你的思维模式。\u003c/p\u003e","title":"公交卡带来的管理启示"},{"content":"面对需求，我们首先想到的是什么 在家电IoT这个领域里，通常都会需要实现家电的分享。比如老婆分享家里的电饭煲给老公，让老公控制电饭煲。\n拿到这样一个需求，通常大脑里想到的就是增加一张家电分享表来实现：\nappliance_user_shared master_user_id shared_user_id appliance_id 然后再修改家电列表的实现，因为需要将别人分享给自己的家电也要能获取到。\n同时，别忘记了，我们还要修改所有对于家电的操作的实现。比如业务规则中说明，家电的主人才能对家电的名称进行修改。\n就这样，我们实现了用户对于一个家电的分享。这时的业务模型可以表示如下：\n过不了多久，产品经理跟你说，如果一个用户家里有10个家电以上，一个个家电的分享，这种体验太糟糕了，我希望将一个家庭分享给其他人，这样，可以将家庭下所有的家庭一下子分享给另一个人了。\n当然，我们同样可以将这个需求，看作是添加一个家庭分享表，再修改一下家电列表、家电名称修改、家电控制……这是A方案。\nA方案的领域模型表示如下：\n但是，我们还有一个B方案。\n当我们仔细思考时，我们似乎掉进了产品经理挖的坑：产品觉得通过增加一个“分享家庭”的概念来实现多个家电的分享。\n事实上，我们服务器端实现并不一定需要这样做。前端APP可以有一个家电分享的操作界面，但是，服务器在实现这个web api时，只需要在找到这个家庭下的所有家电，然后重用原来单家电分享的概念了，就可以了。\n这样做，就非常符合软件设计的开闭原则：对扩展开放，对修改关闭。\n基于B方案，我们不需要对家电列表等功能进行修改。同时，你还会发现，某天产品经理抽风，觉得一次性分享所有的家电不好，如果能在一个家庭基础下实现部分家电分享的功能，是不是更好？A方案就头大了。而B方案可以很轻松的应对。\nB方案的领域模型表示如下：\n说回来，我们应该警惕产品经理或需求人员帮我们做软件设计。\n面对需求时，我们的大脑里，第一想到的是数据库表如何设计、如何实现改动最小、是使用微服务呢，还是使用七边形架构……这类技术问题时，我们的设计是技术驱动设计的。\n如果我们大脑里第一思考的是什么单家电分享、家庭分享和单家电分享之间是什么关系……这类领域问题，然后基于这些思考，建立一个领域相关的知识体系——以领域模型为体现。如果我们的软件是基于此领域模型进行设计，就是：领域模型驱动软件设计。\n按《领域驱动设计》中，作者所说的：\n领域驱动设计是一种思维方式，也是一组优先任务。\n领域建模 要搞清楚，什么是领域建模，就必须搞清楚什么是软件的核心。\n我们来看看《DDD》前言里所写的：\n一些设计因素是技术上的。软件的网络、数据库和其他技术方面的设计耗费了人们大量的精力。很多书籍都介绍过如何解决这些问题。大批开发人员很注意培养自己的技术，并紧跟每一次技术进步。\n然而很多应用程序最主要的复杂性并不在技术上，而是来自领域本身、用户的活动或业务。当这种领域复杂性在设计中没有得到解决时，基础技术的构思再好也是无济于事。成功的设计必须系统地考虑软件的这个核心方面。\n这个核心方面指的就是领域知识。\n而领域建模就是消化吸收大量知识（领域相关），最后产生一个反映深层次领域知识并聚集于关键概念的模型。这也就是领域驱动设计的实质。\n上文中，关于家电分享的例子，从A方案到B方案，实际上就是随着我们对领域知识的理解更深入，领域建模的一个过程。\n小结 就如《DDD》作者所言：很多因素可能会导致项目偏离轨道，如官僚主义、目标不清、资源缺乏，等等。但是真正决定软件复杂性的是设计方法。当复杂性失去控制时，开发人员就无法很好的理解软件，因此无法轻易、安全地更改和扩展它。\n领域驱动设计这种思维方式，再加上一整套的设计实践、技术和原则，就能帮助我们控制真正的复杂性。这就是领域驱动设计的作用。\n而这种思维要求我们在面对领域问题时，优先考虑的是领域问题，而不是技术问题。\n注意，这并不是说我们不考虑技术如何实现。这是优先级问题。\n最后，以上举的例子并不是最终的模型。因为家电分享这个概念并不是最根本的概念。更根本概念是家电的权限。这是一个更层次的领域模型：\n","permalink":"https://showme.codes/zh-cn/2017-11-22-ddd/","summary":"\u003ch3 id=\"面对需求我们首先想到的是什么\"\u003e面对需求，我们首先想到的是什么\u003c/h3\u003e\n\u003cp\u003e在家电IoT这个领域里，通常都会需要实现家电的分享。比如老婆分享家里的电饭煲给老公，让老公控制电饭煲。\u003c/p\u003e\n\u003cp\u003e拿到这样一个需求，通常大脑里想到的就是增加一张家电分享表来实现：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eappliance_user_shared\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003emaster_user_id\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eshared_user_id\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eappliance_id\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e然后再修改家电列表的实现，因为需要将别人分享给自己的家电也要能获取到。\u003c/p\u003e\n\u003cp\u003e同时，别忘记了，我们还要修改所有对于家电的操作的实现。比如业务规则中说明，家电的主人才能对家电的名称进行修改。\u003c/p\u003e\n\u003cp\u003e就这样，我们实现了用户对于一个家电的分享。这时的业务模型可以表示如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-b0f267aac02cc764.png\"\u003e\u003c/p\u003e\n\u003cp\u003e过不了多久，产品经理跟你说，如果一个用户家里有10个家电以上，一个个家电的分享，这种体验太糟糕了，我希望将一个家庭分享给其他人，这样，可以将家庭下所有的家庭一下子分享给另一个人了。\u003c/p\u003e\n\u003cp\u003e当然，我们同样可以将这个需求，看作是添加一个家庭分享表，再修改一下家电列表、家电名称修改、家电控制……这是A方案。\u003c/p\u003e\n\u003cp\u003eA方案的领域模型表示如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-08c6e7c392933b2d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e但是，我们还有一个B方案。\u003c/p\u003e\n\u003cp\u003e当我们仔细思考时，我们似乎掉进了产品经理挖的坑：产品觉得通过增加一个“分享家庭”的概念来实现多个家电的分享。\u003c/p\u003e\n\u003cp\u003e事实上，我们服务器端实现并不一定需要这样做。前端APP可以有一个家电分享的操作界面，但是，服务器在实现这个web api时，只需要在找到这个家庭下的所有家电，然后\u003cstrong\u003e重用原来单家电分享的概念\u003c/strong\u003e了，就可以了。\u003c/p\u003e\n\u003cp\u003e这样做，就非常符合软件设计的开闭原则：对扩展开放，对修改关闭。\u003c/p\u003e\n\u003cp\u003e基于B方案，我们不需要对家电列表等功能进行修改。同时，你还会发现，某天产品经理抽风，觉得一次性分享所有的家电不好，如果能在一个家庭基础下实现部分家电分享的功能，是不是更好？A方案就头大了。而B方案可以很轻松的应对。\u003c/p\u003e\n\u003cp\u003eB方案的领域模型表示如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-8ba81fde1a3106ae.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e说回来，我们应该警惕产品经理或需求人员帮我们做软件设计。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e面对需求时，我们的大脑里，第一想到的是数据库表如何设计、如何实现改动最小、是使用微服务呢，还是使用七边形架构……这类技术问题时，我们的设计是技术驱动设计的。\u003c/p\u003e\n\u003cp\u003e如果我们大脑里第一思考的是什么单家电分享、家庭分享和单家电分享之间是什么关系……这类领域问题，然后基于这些思考，建立一个领域相关的知识体系——以领域模型为体现。如果我们的软件是基于此领域模型进行设计，就是：领域模型驱动软件设计。\u003c/p\u003e\n\u003cp\u003e按《领域驱动设计》中，作者所说的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e领域驱动设计是一种思维方式，也是一组优先任务。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"领域建模\"\u003e领域建模\u003c/h3\u003e\n\u003cp\u003e要搞清楚，什么是领域建模，就必须搞清楚什么是软件的核心。\u003c/p\u003e\n\u003cp\u003e我们来看看《DDD》前言里所写的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e一些设计因素是技术上的。软件的网络、数据库和其他技术方面的设计耗费了人们大量的精力。很多书籍都介绍过如何解决这些问题。大批开发人员很注意培养自己的技术，并紧跟每一次技术进步。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e然而很多应用程序最主要的复杂性并不在技术上，而是来自领域本身、用户的活动或业务。当这种领域复杂性在设计中没有得到解决时，基础技术的构思再好也是无济于事。成功的设计必须系统地考虑软件的这个核心方面。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这个核心方面指的就是领域知识。\u003c/p\u003e\n\u003cp\u003e而领域建模就是消化吸收大量知识（领域相关），最后产生一个反映深层次领域知识并聚集于关键概念的模型。这也就是领域驱动设计的实质。\u003c/p\u003e\n\u003cp\u003e上文中，关于家电分享的例子，从A方案到B方案，实际上就是随着我们对领域知识的理解更深入，领域建模的一个过程。\u003c/p\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e就如《DDD》作者所言：很多因素可能会导致项目偏离轨道，如官僚主义、目标不清、资源缺乏，等等。但是\u003cstrong\u003e真正决定软件复杂性的是设计方法\u003c/strong\u003e。当复杂性失去控制时，开发人员就无法很好的理解软件，因此无法轻易、安全地更改和扩展它。\u003c/p\u003e\n\u003cp\u003e领域驱动设计这种思维方式，再加上一整套的设计实践、技术和原则，就能帮助我们控制真正的\u003cstrong\u003e复杂性\u003c/strong\u003e。这就是领域驱动设计的作用。\u003c/p\u003e\n\u003cp\u003e而这种思维要求我们在面对领域问题时，优先考虑的是领域问题，而不是技术问题。\u003c/p\u003e\n\u003cp\u003e注意，这并不是说我们不考虑技术如何实现。这是优先级问题。\u003c/p\u003e\n\u003cp\u003e最后，以上举的例子并不是最终的模型。因为家电分享这个概念并不是最根本的概念。更根本概念是家电的权限。这是一个更层次的领域模型：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-e0f23cbb45d2e226.png\"\u003e\u003c/p\u003e","title":"这就是领域驱动设计(DDD)的作用？"},{"content":"那一年，我在西安。现在在深圳。但，时常怀念在西安的生活。\n每到一个处吃饭（除了星级酒店），不管它是面馆、快餐店、排挡，还是路边小摊，我都会观察它们是如何经营的。\n那一年，西安火车站旁，看上去非常普通的面馆，大概60平米左右，却使用了不一样的经营方法。让我一下子感觉发现了新大陆。\n当时，在上火车前，想吃点什么。因为没时间，就直接走进了路边的一家面馆。面馆处于火车站正大门前的一个丁字路口边上，人流量自然没得说。\n推开门，径直走向前台。可是前台就几个客人，再环视面馆，座位基本都满了。当我走到前台时，那几个客人已经开始找座位。我跟前台说：你好，来碗臊子面。\n前台：你自己找个座位先坐下来，然后叫服务员点菜。\n我：不用拿号吗？\n前台：不用。\n然后，我一会儿就找到一个空座位。心中有种侥幸感：还好有座位。即使，我是与一个看起来很凶的大汉共享这张二人桌。\n现在想想，这是多年在火车站旁消费带来的条件反射：在火车站旁，有位置就不错了。\n桌上有菜单和基本上每个“快餐店”都会有的桌号。\n我举手示意服务员，她1分钟左右就过来了，后来观察到，她是专门负责点餐的。\n点完就马上付钱。\n这位点餐员提醒我：你千万不要乱换座位，要不等下找不到你的。\n面也没有等多久就上来了。这时，我才发现，店里，没有我们经常听到的大声吆喝：5号，5号，臊子面……看到送餐员从厨房后面出来，直接就走到了相应的客户桌上，十分精准。\n当我吃完，要去赶火车时，我也不担心要排队买单。老板也不用担心我跑单。因为在点餐时已经付过了。\n这家面馆，在我看来属于快餐店一类。现在我们来看看这店与一般的快餐店有什么不同：\n它没有客户号，只有桌号 这样，送餐时，菜，桌一一对应，节约送餐员的送菜时间，也缩短了客户取餐的时间。\n一般的快餐店送餐员全店的找“4号客户”，实在找不到，就大声吆喝，这给我们带来不好的用餐体验不说，我们的快餐店的生意居然寄托于服务员的眼力和短时间记忆力！\n客户先找座位，再点餐 做面是需要时间的，并不是你点了，马上就可以拿到。像KFC，在前台完成点餐，取餐，买单所有流程的。在面馆里不适合。\n后来，我也就明白了，为什么有些快餐店会把饭类和面类分到不同的点餐通道，我想就是因为饭类和面类之间从点餐到取餐之间的速度是不一样的，需要分开处理。\n多说一句，我们做软件架构时，也会把慢速流程和快速流程分开处理。\n先付款，后消费 平时，大家都喜欢先消费，后付款。一是不知道还会不会加点，二是，这种消费体验更好。想想哪个高级酒店，不是先消费，后付款。\n但是对于一家火车站旁的小快餐店，是不成立的。第一，火车站旁，大多数人，想要的是快点走，点了又点的概率太小；第二，火车站旁，消费者要的是快，而不是“更高级”。\n“先付款”节约了完成消费时的计算时间和叫服务员买单时间。最后，节约了客户的时间。至少，我们的生意，也不用寄托于服务员的记忆力。\n最后的好处是：火车站，鱼龙混杂，“先付款”，防止了跑单。\n给我带来了什么启示 后来，我见识不少火车站旁的快餐店，但是基本上都会有手托的服务员，费时费力的找“客户号”。我知道西安火车站旁的这一小面馆的经营方法，并不是行业的“共识”。虽然每家店都想赚更多的钱。\n这家小面馆的经营方法并不一定适合于所有的面馆。况且，世上有没有适用于所有面馆的经营方法，也是个问题。\n但是，这家小面馆，一定是看清了自己的价值流：从客户进店，到客户出店。然后，不断优化这个价值流的节点，使其更顺畅，最终实现赚更多的钱。\n对于我们做软件产品的呢？我们的价值流又是什么呢？好问题！\n另一个问题：发现价值流后，如何持续优化？我的答案是：现场管理。但是软件产品的生产过程，如何做到现场管理呢？好问题！\n如果一个不懂软件开发的人，如何管理好软件产品生产过程呢？好问题！\n最后提一个问题：这家面馆的经营方法算不算精益？\n图片来网络。如果侵权，必删。\n","permalink":"https://showme.codes/zh-cn/2017-10-14-lean-noodle/","summary":"\u003cp\u003e那一年，我在西安。现在在深圳。但，时常怀念在西安的生活。\u003c/p\u003e\n\u003cp\u003e每到一个处吃饭（除了星级酒店），不管它是面馆、快餐店、排挡，还是路边小摊，我都会观察它们是如何经营的。\u003c/p\u003e\n\u003cp\u003e那一年，西安火车站旁，看上去非常普通的面馆，大概60平米左右，却使用了不一样的经营方法。让我一下子感觉发现了新大陆。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-41462348a00f7fc3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e当时，在上火车前，想吃点什么。因为没时间，就直接走进了路边的一家面馆。面馆处于火车站正大门前的一个丁字路口边上，人流量自然没得说。\u003c/p\u003e\n\u003cp\u003e推开门，径直走向前台。可是前台就几个客人，再环视面馆，座位基本都满了。当我走到前台时，那几个客人已经开始找座位。我跟前台说：你好，来碗臊子面。\u003c/p\u003e\n\u003cp\u003e前台：你自己找个座位先坐下来，然后叫服务员点菜。\u003c/p\u003e\n\u003cp\u003e我：不用拿号吗？\u003c/p\u003e\n\u003cp\u003e前台：不用。\u003c/p\u003e\n\u003cp\u003e然后，我一会儿就找到一个空座位。心中有种侥幸感：还好有座位。即使，我是与一个看起来很凶的大汉共享这张二人桌。\u003c/p\u003e\n\u003cp\u003e现在想想，这是多年在火车站旁消费带来的条件反射：在火车站旁，有位置就不错了。\u003c/p\u003e\n\u003cp\u003e桌上有菜单和基本上每个“快餐店”都会有的桌号。\u003c/p\u003e\n\u003cp\u003e我举手示意服务员，她1分钟左右就过来了，后来观察到，她是专门负责点餐的。\u003c/p\u003e\n\u003cp\u003e点完就马上付钱。\u003c/p\u003e\n\u003cp\u003e这位点餐员提醒我：你千万不要乱换座位，要不等下找不到你的。\u003c/p\u003e\n\u003cp\u003e面也没有等多久就上来了。这时，我才发现，店里，没有我们经常听到的大声吆喝：5号，5号，臊子面……看到送餐员从厨房后面出来，直接就走到了相应的客户桌上，十分精准。\u003c/p\u003e\n\u003cp\u003e当我吃完，要去赶火车时，我也不担心要排队买单。老板也不用担心我跑单。因为在点餐时已经付过了。\u003c/p\u003e\n\u003cp\u003e这家面馆，在我看来属于快餐店一类。现在我们来看看这店与一般的快餐店有什么不同：\u003c/p\u003e\n\u003ch4 id=\"它没有客户号只有桌号\"\u003e它没有客户号，只有桌号\u003c/h4\u003e\n\u003cp\u003e这样，送餐时，菜，桌一一对应，节约送餐员的送菜时间，也缩短了客户取餐的时间。\u003c/p\u003e\n\u003cp\u003e一般的快餐店送餐员全店的找“4号客户”，实在找不到，就大声吆喝，这给我们带来不好的用餐体验不说，我们的快餐店的生意居然寄托于服务员的眼力和短时间记忆力！\u003c/p\u003e\n\u003ch4 id=\"客户先找座位再点餐\"\u003e客户先找座位，再点餐\u003c/h4\u003e\n\u003cp\u003e做面是需要时间的，并不是你点了，马上就可以拿到。像KFC，在前台完成点餐，取餐，买单所有流程的。在面馆里不适合。\u003c/p\u003e\n\u003cp\u003e后来，我也就明白了，为什么有些快餐店会把饭类和面类分到不同的点餐通道，我想就是因为饭类和面类之间从点餐到取餐之间的速度是不一样的，需要分开处理。\u003c/p\u003e\n\u003cp\u003e多说一句，我们做软件架构时，也会把慢速流程和快速流程分开处理。\u003c/p\u003e\n\u003ch4 id=\"先付款后消费\"\u003e先付款，后消费\u003c/h4\u003e\n\u003cp\u003e平时，大家都喜欢先消费，后付款。一是不知道还会不会加点，二是，这种消费体验更好。想想哪个高级酒店，不是先消费，后付款。\u003c/p\u003e\n\u003cp\u003e但是对于一家火车站旁的小快餐店，是不成立的。第一，火车站旁，大多数人，想要的是快点走，点了又点的概率太小；第二，火车站旁，消费者要的是快，而不是“更高级”。\u003c/p\u003e\n\u003cp\u003e“先付款”节约了完成消费时的计算时间和叫服务员买单时间。最后，节约了客户的时间。至少，我们的生意，也不用寄托于服务员的记忆力。\u003c/p\u003e\n\u003cp\u003e最后的好处是：火车站，鱼龙混杂，“先付款”，防止了跑单。\u003c/p\u003e\n\u003ch4 id=\"给我带来了什么启示\"\u003e给我带来了什么启示\u003c/h4\u003e\n\u003cp\u003e后来，我见识不少火车站旁的快餐店，但是基本上都会有手托的服务员，费时费力的找“客户号”。我知道西安火车站旁的这一小面馆的经营方法，并不是行业的“共识”。虽然每家店都想赚更多的钱。\u003c/p\u003e\n\u003cp\u003e这家小面馆的经营方法并不一定适合于所有的面馆。况且，世上有没有适用于所有面馆的经营方法，也是个问题。\u003c/p\u003e\n\u003cp\u003e但是，这家小面馆，一定是看清了自己的价值流：从客户进店，到客户出店。然后，不断优化这个价值流的节点，使其更顺畅，最终实现赚更多的钱。\u003c/p\u003e\n\u003cp\u003e对于我们做软件产品的呢？我们的价值流又是什么呢？好问题！\u003c/p\u003e\n\u003cp\u003e另一个问题：发现价值流后，如何持续优化？我的答案是：现场管理。但是软件产品的生产过程，如何做到现场管理呢？好问题！\u003c/p\u003e\n\u003cp\u003e如果一个不懂软件开发的人，如何管理好软件产品生产过程呢？好问题！\u003c/p\u003e\n\u003cp\u003e最后提一个问题：这家面馆的经营方法算不算\u003cstrong\u003e精益\u003c/strong\u003e？\u003c/p\u003e\n\u003cp\u003e图片来网络。如果侵权，必删。\u003c/p\u003e","title":"西安火车站旁一小面馆带给我的启示"},{"content":"\n传统软件开发方法 传统软件开发方法的共同特点是强调计划、管控和结构化的工程方法，并遵循严格的生命周期概念，把软件开发分割为顺序阶段构成的过程，瀑布式开发方法是其中的代表之一。 到了上世纪90年代初，CMMI和PMI项目管理知识体系成为传统产品开发管理方法的典型代表。\n我个人认为，传统软件开发方法的假设是：需求确定后就不会变了。然而，时代变了，这个假设是否还站得住脚？\n从另一个角度看，我们也就理解了为什么项目经理会对需求变更如此痛恨。你想嘛，项目经理的职责是让项目按时完成（想想KPI怎么定的？），立项后，工期就定死了，我一开始把项目计划做好了，需要变更了，你让我的项目如何按时完成？\n所以，具有这样思维模式的项目经理面对敏捷和精益这样的概念时，第一念头就是：它们能帮助让项目按时完成吗？\n当然，我并不是说项目按时完成不重要。而是想表达：我们应该清楚自己最终追求的是项目按时完成，还是做好的产品服务用户。\n那么，如果“需求确定后就不会变了”这个假设不成立，我们的产品开发应该如何应对呢？\n《精益产品开发》 从原则，方法，实施三个方面来说明我们应该如何应对。\n产品开发与生产制造的不同 产品开发相对生产有两个最本质的不同：其一，价值的不确定性，它决定我们无法一开始就明确定义价值，或者说“价值定义”的过程应该是一个持续探索的过程，因此才有了精益创业、精益数据分析等实践体系；其二，过程的不确定性，如每个任务的处理时长不等，且可能在过程中发生变化，它决定了价值流动的管理和改进方法不同，如产品开发中看板方法就与生产中的十分不同。\n在软件行业，产品开发与生产的本质不同，深有体会啊。生产常常有批量的含义，而产品开发没有，你听说过“批量生产软件”的说法吗？\n部署与发布的区别 为更好地管理发布，团队应该区分发布和部署。部署属于技术范畴的概念，发布是属于市场范畴的概念。它们具有如下意义：\n部署（deployment）：将软件安装到一个特定的环境 发布（release）：让一个或一组特性对应用可见和可用。 上家公司时，我们每周会部署两次，没做完的新功能，我们同时会部署上去，只不过，用户是看不到这些功能的，应该feature toggle是off的，偶尔也会向部分用户打开。\n等我们确定功能完全OK了，就会把相应的feature toggle完全打开，所有用户就可以看这个新功能了。\n这是区分了部署和发布两个概念的做法。\n而现在所在家公司由于历史原因，服务器要跟着APP的版本走。APP端的人每个月发布一次，以至于管理人员也认为服务器端也必须一个月发布一次。导致的问题之一就是服务器端一次性部署大量的服务，不论这些服务是否有变更接口外在行为。\n这是没有区分部署和发布两个概念的做法。\n看板系统的设计、站会的设计 看板系统能全面地反映需求交付过程吗？ 瓶颈和问题能在看板墙上得到即时体现吗？ 团队可以根据看板墙上的信息协作和做决定吗？ 只要问这三个问题，你就知道你的看板系统设计如何。\n何勉老师在书提到了一个案例：一家企业网盘的需求被两端——前端和后端——分别实现。团队也按前端和后端来区分。每个团队还有各自的测试人员。\n面对这样的情况，我们如何处理，才能更好的交付产品价值呢？我目前所在公司也遇到同样的问题，一个产品的开发甚至涉及到服务器端、APP端、Web端、硬件端。\n一个需求同时涉及这么多的端。看板系统如何设计？如何站会？如何测试？好问题。\n从这本书里，我得到的答案是：\n只要这个需求的涉及人员的任务卡片都应该放在同一个看板上，有时甚至包括市场人员。 站会时，只要这个需求的涉及人员都应该在一起站，有时甚至包括市场人员。 小结 这些只是一部分，书中的很多想法击中了我。因为现实，我就遇到这样那样的问题，苦于不知如何解决。\n另，回过头来看，不管传统软件开发方法，精益，还是敏捷，我们都应该始终记得不论开发、测试、项目管理人员，公司决策层，我们的最终目标：为用户提供更好的服务。按时完成任务只是我们达到这个目标的手段。\n但是如何才能达到这个共识呢？留给读者一个问题。\n","permalink":"https://showme.codes/zh-cn/2017-10-8-lean-product-development/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-f1c25e67f36fe86b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-1e244e6767fdf93f.png\"\u003e\u003c/p\u003e\n\u003ch4 id=\"传统软件开发方法\"\u003e传统软件开发方法\u003c/h4\u003e\n\u003cblockquote\u003e\n\u003cp\u003e传统软件开发方法的共同特点是强调计划、管控和结构化的工程方法，并遵循严格的生命周期概念，把软件开发分割为顺序阶段构成的过程，瀑布式开发方法是其中的代表之一。\n到了上世纪90年代初，CMMI和PMI项目管理知识体系成为传统产品开发管理方法的典型代表。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我个人认为，传统软件开发方法的假设是：需求确定后就不会变了。然而，时代变了，这个假设是否还站得住脚？\u003c/p\u003e\n\u003cp\u003e从另一个角度看，我们也就理解了为什么项目经理会对需求变更如此痛恨。你想嘛，项目经理的职责是让项目按时完成（想想KPI怎么定的？），立项后，工期就定死了，我一开始把项目计划做好了，需要变更了，你让我的项目如何按时完成？\u003c/p\u003e\n\u003cp\u003e所以，具有这样思维模式的项目经理面对敏捷和精益这样的概念时，第一念头就是：它们能帮助让项目按时完成吗？\u003c/p\u003e\n\u003cp\u003e当然，我并不是说项目按时完成不重要。而是想表达：\u003cstrong\u003e我们应该清楚自己最终追求的是项目按时完成，还是做好的产品服务用户。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e那么，如果“需求确定后就不会变了”这个假设不成立，我们的产品开发应该如何应对呢？\u003c/p\u003e\n\u003cp\u003e《精益产品开发》 从原则，方法，实施三个方面来说明我们应该如何应对。\u003c/p\u003e\n\u003ch4 id=\"产品开发与生产制造的不同\"\u003e产品开发与生产制造的不同\u003c/h4\u003e\n\u003cblockquote\u003e\n\u003cp\u003e产品开发相对生产有两个最本质的不同：其一，价值的不确定性，它决定我们无法一开始就明确定义价值，或者说“价值定义”的过程应该是一个持续探索的过程，因此才有了精益创业、精益数据分析等实践体系；其二，过程的不确定性，如每个任务的处理时长不等，且可能在过程中发生变化，它决定了价值流动的管理和改进方法不同，如产品开发中看板方法就与生产中的十分不同。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在软件行业，产品开发与生产的本质不同，深有体会啊。生产常常有批量的含义，而产品开发没有，你听说过“批量生产软件”的说法吗？\u003c/p\u003e\n\u003ch4 id=\"部署与发布的区别\"\u003e部署与发布的区别\u003c/h4\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为更好地管理发布，团队应该区分发布和部署。部署属于技术范畴的概念，发布是属于市场范畴的概念。它们具有如下意义：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e部署（deployment）：将软件安装到一个特定的环境\u003c/li\u003e\n\u003cli\u003e发布（release）：让一个或一组特性对应用可见和可用。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e上家公司时，我们每周会部署两次，没做完的新功能，我们同时会部署上去，只不过，用户是看不到这些功能的，应该feature toggle是off的，偶尔也会向部分用户打开。\u003c/p\u003e\n\u003cp\u003e等我们确定功能完全OK了，就会把相应的feature toggle完全打开，所有用户就可以看这个新功能了。\u003c/p\u003e\n\u003cp\u003e这是区分了部署和发布两个概念的做法。\u003c/p\u003e\n\u003cp\u003e而现在所在家公司由于历史原因，服务器要跟着APP的版本走。APP端的人每个月发布一次，以至于管理人员也认为服务器端也必须一个月发布一次。导致的问题之一就是服务器端一次性部署大量的服务，不论这些服务是否有变更接口外在行为。\u003c/p\u003e\n\u003cp\u003e这是没有区分部署和发布两个概念的做法。\u003c/p\u003e\n\u003ch4 id=\"看板系统的设计站会的设计\"\u003e看板系统的设计、站会的设计\u003c/h4\u003e\n\u003cblockquote\u003e\n\u003col\u003e\n\u003cli\u003e看板系统能全面地反映需求交付过程吗？\u003c/li\u003e\n\u003cli\u003e瓶颈和问题能在看板墙上得到即时体现吗？\u003c/li\u003e\n\u003cli\u003e团队可以根据看板墙上的信息协作和做决定吗？\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e只要问这三个问题，你就知道你的看板系统设计如何。\u003c/p\u003e\n\u003cp\u003e何勉老师在书提到了一个案例：一家企业网盘的需求被两端——前端和后端——分别实现。团队也按前端和后端来区分。每个团队还有各自的测试人员。\u003c/p\u003e\n\u003cp\u003e面对这样的情况，我们如何处理，才能更好的交付产品价值呢？我目前所在公司也遇到同样的问题，一个产品的开发甚至涉及到服务器端、APP端、Web端、硬件端。\u003c/p\u003e\n\u003cp\u003e一个需求同时涉及这么多的端。看板系统如何设计？如何站会？如何测试？好问题。\u003c/p\u003e\n\u003cp\u003e从这本书里，我得到的答案是：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e只要这个需求的涉及人员的任务卡片都应该放在同一个看板上，有时甚至包括市场人员。\u003c/li\u003e\n\u003cli\u003e站会时，只要这个需求的涉及人员都应该在一起站，有时甚至包括市场人员。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"小结\"\u003e小结\u003c/h4\u003e\n\u003cp\u003e这些只是一部分，书中的很多想法击中了我。因为现实，我就遇到这样那样的问题，苦于不知如何解决。\u003c/p\u003e\n\u003cp\u003e另，回过头来看，不管传统软件开发方法，精益，还是敏捷，我们都应该始终记得不论开发、测试、项目管理人员，公司决策层，我们的最终目标：为用户提供更好的服务。按时完成任务只是我们达到这个目标的手段。\u003c/p\u003e\n\u003cp\u003e但是如何才能达到这个共识呢？留给读者一个问题。\u003c/p\u003e","title":"《精益产品开发》老翟读后感"},{"content":"\nChatOps概念在国内已经有一些文章谈过，但是都处于理论范畴。而本文则是一篇ChatOps实践的文章。\n有必要说明我对ChatOps的理解，ChatOps表面上就是在一个聊天窗口中，发送一个命令给运维机器人bot，然后bot根据我们预定义的操作进行执行，并返回执行结果。至于更深层次的作用，就是将重复性的手工的运维工作自动化了，开发人员、运维人员可以按需执行一些运维操作。\n另外，我做到了自动化搭建这一套东西（感谢Github上那么多开源项目，让我少写很多Ansible脚本）。为什么要自动化搭建呢？因为我懒，我不想每次通过一条条shell手工搭建。\n本文主题 在RocketChat的聊天窗口中命令Hubot执行一次Jenkins构建任务。\n工具介绍 有必要简单说明一下我们此次实现ChatOps的这几个工具。\nRocketChat 可以把RocketChat想像成一个具有更多功能的IRC或者微信。它依赖于MongoDB，所以，我们还将自动化安装MongoDB。\n如果你了解过Slack的话，它可以作为Slack的开源替代表。\nHubot Hubot是Github出品的一个运维机器人。本质上就是一个接收命令消息，执行预定义操作的一个程序。而接收命令消息的这个组件在Hubot中被称为Adapter。比如我们希望Hubot接收来自RocketChat聊天窗口里的消息，我们就必须为Hubot安装一个RocketChat的Adapter。市面上，已经有很多Adapter了，我们很少需要自己实现自定义Adapter。\n那么，Hubot接收到命令消息后，怎么知道执行哪些操作呢？这部分是需要我们实现了。本质上就是通过正则表达式匹配命令消息，然后操作。实际上通过写Coffescript脚本实现。比如：\nrobot.respond /open the (.*) doors/i, (res) -\u0026gt; doorType = res.match[1] if doorType is \u0026#34;pod bay\u0026#34; res.reply \u0026#34;I\u0026#39;m afraid I can\u0026#39;t let you do that.\u0026#34; else res.reply \u0026#34;Opening #{doorType} doors\u0026#34; Jenkins 就这个就不用多介绍了。值得一提是Github已经有不少自动化搭建Jenkins的Ansible脚本了（完全不需要人工干预），本文使用的是geerlingguy的。\nAnsible 能让开发人员快速上手的自动化运维工具。我们使用Ansible实现自动化。想简单了解Anbible，可以看看简单易懂Ansible系列 —— 解决了什么。\n准备环境 需要准备几台机器：\nIP OS 安装 192.168.61.11 CentOS7 Jenkins,Openresty(for Jenkins) 192.168.61.14 CentOS7 Openresty(for RocketChat) 192.168.61.15 CentOS7 RocketChat Server, MongoDB，Hubot 因为我是在本地做实验的，所以需要在本机虚拟化3台机器。我使用Vagrant + VirtualBox的方式来实现。具体Vagrant如何使用，不在本文讨论范围。你也可以手工在VirtualBox或Vmware上创建相应的虚拟机。Vagrant只不过是自动化了这个过程。Vagrant会基于一个称为Vagrantfile的文件来创建机器。\nVagrantfile部分内容如下（想看全文件点这）：\nVagrant.configure(2) do |config| ANSIBLE_RAW_SSH_ARGS = [] VAGRANT_VM_PROVIDER = \u0026#34;virtualbox\u0026#34; machine_box = \u0026#34;CentOS-7.1.1503-x86_64-netboot\u0026#34; config.vm.define \u0026#34;p1\u0026#34; do |machine| machine.vm.box = machine_box machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.61.11\u0026#34; machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node| node.name = \u0026#34;p1\u0026#34; node.memory = 2000 node.cpus = 2 end end ##### 此处省略其它机器的配置 end 因为我本地已经存在相应的Vagrant box了，所以，直接使用命令就可以启动这几台机器：\nvagrant up p1 vagrant up p4 vagrant up p5 搭建环境 clone 项目 git clone https://github.com/zacker330/devops-platform.git cd devops-platform 执行Ansible自动化部署所有的应用及配置 ansible-playbook -i chatops-inventory chatops-playbook.yml chatops-inventory 是一个类ini文件，用于描述机器，其实就是对机器进行分组。 chatops-playbook.yml是一个yaml文件，用于描述如何部署我们的应用及配置。\n就这样，我们的Jenkins，RocketChat，Hubot就已经搭建完成了。没错，就只需要扫行一条命令。是不是很爽~\nRocketChat web客户端：http://192.168.61.14:3000/，初次登录时，需要先注册一个超级管理员。 Jenkins: http://192.168.61.11/jenkins，默认账号密码：admin/admin\n至于是如何搭建的，感兴趣的同学可以看Ansible代码。\n以下是集成方法及需要注意的地方：\nHubot与RocketChat集成 设置Hubot运维机器人 现在需要在RocketChat中添加一个User作为运维机器人，我们选择 RocketChat默认用户rocket.cat作为运维机器人，这里需要注意的是:\nrocket.cat必须具有的角色：admin、bot rocket.cat必须设置密码，我设置了为123456 邮箱必须verified，设置时只要勾选上就可以了 安装hubot-rocketchat adapter\n启动时需要指定这几个环境变量以便Hubot能登录上RocketChat：\nexport ROCKETCHAT_URL=\u0026#34;http://192.168.61.15:3000\u0026#34; export ROCKETCHAT_ROOM=\u0026#39;\u0026#39; export LISTEN_ON_ALL_PUBLIC=true export ROCKETCHAT_USER=rocket.cat export ROCKETCHAT_PASSWORD=123456 export ROCKETCHAT_AUTH=password 验证 因为我们安装了hubot-friendly脚本，hey一下hubot，它有回应，就说明我们成功集成了RocketChat和Hubot。\nHubot与Jenkins集成 安装hubot脚本：hubot-jenkins\n配置hubot连接Jenkins的环境变量：\nexport HUBOT_JENKINS_URL=192.168.61.14/jenkins export HUBOT_JENKINS_AUTH=admin:admin 在RocketChat中，操作Jenkins的job: 比如列出当前Jenkins的job列表：\n再比如执行chatops-demo这个job: Jenkins与RocketChat集成 Jenkins与RocketChat集成主要用于当Jenkins的job发生变化时主动推送消息到RocketChat中。\n在Jenkins中安装Jenkins插件rocketchatnotifier\n在系统设置中，设置rocketchatnotifier参数： 在构建job中设置post build action: 如果你使用的是Jenkins pipeline，rocketchatnotifier也支持\nrocketSend channel: \u0026#39;general\u0026#39;, emoji: \u0026#39;:sob:\u0026#39;, message: \u0026#39;My message\u0026#39;, rawMessage: true 验证 在Jenkins上手工点击构建按钮，RocketChat的ci channel应该会有消息提醒： 小结 本文如有不足，欢迎来邮讨论。\n至此，我们简单的ChatOps框架算是搭好了。剩下的就是根据你们自己业务进行改造了。\n另外多说一句：思维模式不应该被职位所局限。\n","permalink":"https://showme.codes/zh-cn/2017-10-08-chatops-in-action/","summary":"\u003cp\u003e\u003cimg alt=\"image.png\" loading=\"lazy\" src=\"/assets/images/292372-9f8cdc1cc6e15975.png\"\u003e\u003c/p\u003e\n\u003cp\u003eChatOps概念在国内已经有一些文章谈过，但是都处于理论范畴。而本文则是一篇ChatOps实践的文章。\u003c/p\u003e\n\u003cp\u003e有必要说明我对ChatOps的理解，ChatOps表面上就是在一个聊天窗口中，发送一个命令给运维机器人bot，然后bot根据我们预定义的操作进行执行，并返回执行结果。至于更深层次的作用，就是将重复性的手工的运维工作自动化了，开发人员、运维人员可以按需执行一些运维操作。\u003c/p\u003e\n\u003cp\u003e另外，我做到了自动化搭建这一套东西（感谢Github上那么多开源项目，让我少写很多Ansible脚本）。为什么要自动化搭建呢？因为我懒，我不想每次通过一条条shell手工搭建。\u003c/p\u003e\n\u003ch3 id=\"本文主题\"\u003e本文主题\u003c/h3\u003e\n\u003cp\u003e在RocketChat的聊天窗口中命令Hubot执行一次Jenkins构建任务。\u003c/p\u003e\n\u003ch3 id=\"工具介绍\"\u003e工具介绍\u003c/h3\u003e\n\u003cp\u003e有必要简单说明一下我们此次实现ChatOps的这几个工具。\u003c/p\u003e\n\u003ch4 id=\"rocketchat\"\u003eRocketChat\u003c/h4\u003e\n\u003cp\u003e可以把\u003ca href=\"https://github.com/RocketChat/Rocket.Chat\"\u003eRocketChat\u003c/a\u003e想像成一个具有更多功能的IRC或者微信。它依赖于MongoDB，所以，我们还将自动化安装MongoDB。\u003c/p\u003e\n\u003cp\u003e如果你了解过Slack的话，它可以作为Slack的开源替代表。\u003c/p\u003e\n\u003ch4 id=\"hubot\"\u003eHubot\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"https://hubot.github.com/\"\u003eHubot\u003c/a\u003e是Github出品的一个运维机器人。本质上就是一个接收命令消息，执行预定义操作的一个程序。而接收命令消息的这个组件在Hubot中被称为Adapter。比如我们希望Hubot接收来自RocketChat聊天窗口里的消息，我们就必须为Hubot安装一个RocketChat的Adapter。市面上，已经有很多\u003ca href=\"https://hubot.github.com/docs/adapters/\"\u003eAdapter\u003c/a\u003e了，我们很少需要自己实现自定义Adapter。\u003c/p\u003e\n\u003cp\u003e那么，Hubot接收到命令消息后，怎么知道执行哪些操作呢？这部分是需要我们实现了。本质上就是通过正则表达式匹配命令消息，然后操作。实际上通过写\u003ca href=\"http://coffeescript.org/\"\u003eCoffescript\u003c/a\u003e脚本实现。比如：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode class=\"language-coffescript\" data-lang=\"coffescript\"\u003erobot.respond /open the (.*) doors/i, (res) -\u0026gt;\n    doorType = res.match[1]\n    if doorType is \u0026#34;pod bay\u0026#34;\n      res.reply \u0026#34;I\u0026#39;m afraid I can\u0026#39;t let you do that.\u0026#34;\n    else\n      res.reply \u0026#34;Opening #{doorType} doors\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003ch4 id=\"jenkins\"\u003eJenkins\u003c/h4\u003e\n\u003cp\u003e就这个就不用多介绍了。值得一提是Github已经有不少自动化搭建Jenkins的Ansible脚本了（完全不需要人工干预），本文使用的是\u003ca href=\"https://github.com/geerlingguy\"\u003egeerlingguy\u003c/a\u003e的。\u003c/p\u003e\n\u003ch4 id=\"ansible\"\u003eAnsible\u003c/h4\u003e\n\u003cp\u003e能让开发人员快速上手的自动化运维工具。我们使用Ansible实现自动化。想简单了解Anbible，可以看看\u003ca href=\"http://showme.codes/2017-06-12/ansible-introduce/\"\u003e简单易懂Ansible系列 —— 解决了什么\u003c/a\u003e。\u003c/p\u003e\n\u003ch3 id=\"准备环境\"\u003e准备环境\u003c/h3\u003e\n\u003cp\u003e需要准备几台机器：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eIP\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eOS\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e安装\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e192.168.61.11\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCentOS7\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJenkins,Openresty(for Jenkins)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e192.168.61.14\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCentOS7\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOpenresty(for RocketChat)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e192.168.61.15\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCentOS7\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRocketChat Server, MongoDB，Hubot\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e因为我是在本地做实验的，所以需要在本机虚拟化3台机器。我使用Vagrant + VirtualBox的方式来实现。具体Vagrant如何使用，不在本文讨论范围。你也可以手工在VirtualBox或Vmware上创建相应的虚拟机。Vagrant只不过是自动化了这个过程。Vagrant会基于一个称为\u003ccode\u003eVagrantfile\u003c/code\u003e的文件来创建机器。\u003c/p\u003e","title":"ChatOps实战"},{"content":"我们在搭建Hadoop完全分布式环境时，Hadoop的name node节点（理解为master节点）需要无密码登录到所有的data node节点。\n当然，我们使用手工的方式很容易就实现了：\n在name node节点上生成ssh key：ssh-keygen 将public key copy到所有的data node节点上：ssh-copy-id slave1 同时，你还必须设置~/.ssh/config，以防止登录时不停的问yes or no：\n```yml Host * StrictHostKeyChecking no ``` 完了，还要设置这个文件的权限为400。\n以上步骤当然可以手工一步步执行。但是，总有那么一些人：希望所有的操作都可以版本化，所有的操作都应该自动化。我属于这些人。\n再说了，我发现在搭建Jenkins环境时，也遇到了同样的问题：需要将Jenkins master的public key加入到Jenkins agent机器中。\n可以预见到将来我还会遇到类似的问题。于是，我找到一个方法来自动化以上操作。\n在name node机器上执行task如下 创建用户的时候生成ssh_key：\n- name: create hadoop user user: name: \u0026#34;{{hadoop_user}}\u0026#34; group: \u0026#34;{{hadoop_group}}\u0026#34; createhome: yes generate_ssh_key: yes ssh_key_bits: 2048 ssh_key_file: .ssh/id_rsa tags: - hadoop 将id_rsa.pub拉取到ansible执行机器上\n- name: fetch public key fetch: src: \u0026#34;/home/{{hadoop_user}}/.ssh/id_rsa.pub\u0026#34; dest: /tmp/ flat: yes tags: - hadoop 设置StrictHostKeyChecking no 因为我们只想修改这个用户的ssh行为，所以我们的ssh的配置只是针对当前这个用户的：\n- name: namenode ssh config template: src: ssh.conf dest: \u0026#34;{{hadoop_user_home}}/.ssh/config\u0026#34; mode: \u0026#34;400\u0026#34; owner: \u0026#34;{{ hadoop_user }}\u0026#34; group: \u0026#34;{{ hadoop_group }}\u0026#34; tags: - hadoop ssh.conf 的内容如下：\n```yml Host * StrictHostKeyChecking no ``` 在data node机器上执行的task如下 将public key加入到data node的机器中，/tmp/id_rsa.pub就是刚由name node机器生成将拉取到本地的key\n## 此时，会在data node机器中相应的用户目录的.ssh文件夹中生成authorized_keys文件，并将public key内容放到里面 - name: add master public key to slaves authorized_key: user: \u0026#34;{{hadoop_user}}\u0026#34; key: \u0026#34;{{ lookup(\u0026#39;file\u0026#39;, \u0026#39;/tmp/id_rsa.pub\u0026#39;) }}\u0026#34; tags: - hadoop 设置.ssh目录的权限为700 不清楚为什么authorized_key模块自动生成的.ssh的权限过高，所以还需要将目录设置成700：\n- name: make .ssh folder 700 file: path: \u0026#34;{{hadoop_user_home}}/.ssh/\u0026#34; state: directory mode: \u0026#34;700\u0026#34; owner: \u0026#34;{{ hadoop_user }}\u0026#34; group: \u0026#34;{{ hadoop_group }}\u0026#34; tags: - hadoop ","permalink":"https://showme.codes/zh-cn/2017-8-19-ansible-manage-sshkey/","summary":"\u003cp\u003e我们在搭建Hadoop完全分布式环境时，Hadoop的name node节点（理解为master节点）需要无密码登录到所有的data node节点。\u003c/p\u003e\n\u003cp\u003e当然，我们使用手工的方式很容易就实现了：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在name node节点上生成ssh key：ssh-keygen\u003c/li\u003e\n\u003cli\u003e将public key copy到所有的data node节点上：ssh-copy-id slave1\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e同时，你还必须设置\u003ccode\u003e~/.ssh/config\u003c/code\u003e，以防止登录时不停的问yes or no：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e```yml\nHost *\n    StrictHostKeyChecking no\n```\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e完了，还要设置这个文件的权限为\u003cstrong\u003e400\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e以上步骤当然可以手工一步步执行。但是，总有那么一些人：希望所有的操作都可以版本化，所有的操作都应该自动化。我属于这些人。\u003c/p\u003e\n\u003cp\u003e再说了，我发现在搭建Jenkins环境时，也遇到了同样的问题：需要将Jenkins master的public key加入到Jenkins agent机器中。\u003c/p\u003e\n\u003cp\u003e可以预见到将来我还会遇到类似的问题。于是，我找到一个方法来自动化以上操作。\u003c/p\u003e\n\u003ch3 id=\"在name-node机器上执行task如下\"\u003e在name node机器上执行task如下\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e创建用户的时候生成ssh_key：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yml\" data-lang=\"yml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ecreate hadoop user\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{hadoop_user}}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003egroup\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;{{hadoop_group}}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ecreatehome\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003eyes\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003egenerate_ssh_key\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003eyes\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003essh_key_bits\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e2048\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003essh_key_file\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e.ssh/id_rsa\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003ehadoop\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e将id_rsa.pub拉取到ansible执行机器上\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yml\" data-lang=\"yml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003efetch public key\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003efetch\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/home/{{hadoop_user}}/.ssh/id_rsa.pub\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/tmp/\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eflat\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003eyes\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003ehadoop\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e设置\u003ccode\u003eStrictHostKeyChecking no\u003c/code\u003e\n因为我们只想修改这个用户的ssh行为，所以我们的ssh的配置只是针对当前这个用户的：\u003c/p\u003e","title":"简单易懂Ansible系列 —— 实现ssh key主机之间复制"},{"content":"\n不知什么时候，Ansible的slogan从“IT Automation Software for System Administrators”变成了“AUTOMATION FOR EVERYONE”。\n从一个给系统管理员使用的工具变成了给所有人使用的工具。\n但是，现实中，发现了解Ansible的人，还是太少了。同时，自己断断续续学习Ansible也有一段时间，希望拿出来和大家交流。所以就决定不定期写写一个关于Ansible的系列。如果你觉得我写得还可以，到文末扫码请我喝杯茶。\n此文为“简单易懂Ansible”系列文章的开篇 —— Ansible解决了什么\nAnsible解决了什么 首先，它是一个运维工具。当然要解决运维过程中遇到的问题了。运维过程遇到了什么问题？\n想像一下，你要在一台新的机器上安装Tomcat，你会怎么样呢，条件反射的：\nssh user@111.111.111.111 wget -c http://apache.fayea.com/tomcat/tomcat-8.5.15.tar.gz tar -zxf apache-tomcat-8.5.15.tar.gz .....省略 好，10分钟后你愉快地完成了老板给你的任务。但是现在你需要给100台机器安装Tomcat呢？手工的重复100次？\n而Ansible能让我们只定义一次，理论上可以在无限台机器上执行。换句话：减少运维工作中的重复工作。\n同时，如果是人工执行100次，那么失误是难免的！自动化运维工具会严格根据我们所给指令来执行，而不会因为失恋而手抖执行了：sudo rm -rf /。\n不少人反对自动化，认为那样太危险，因为一不小心就在上百台机器删错文件。显然，他们没有注意到：自动化实现的是准确地执行指令，解决人类执行任务时存在的指令理解不正确、执行不严格的问题。而机器不会出现这些问题的概念几乎为零。\n没有达到预期效果，往往是我们人类下达的指令不正确。\n所以，Ansible还解决了人执行指令不准确的问题。\n如果使用Ansible来实现上述的运维需求，怎么做呢？你需要做三件事情：\n定义目标机器的列表：一种被称为inventory的类ini文件 定义这些机器的配置：使用YAML格式的文件来描述你机器的配置 执行 ansible-playbook -i inventory playbook.yml 以下是inventory文件：\n[tomcat-servers] 111.111.111.111 112.112.112.112 .... 而这些ip的配置写在一种被称为playbook的YAML文件中：\n--- - hosts: tomcat-servers tasks: - name: download tomcat get_url: url: http://apache.fayea.com/tomcat/tomcat-8.5.15.tar.gz dest: /tmp - name: unarchive tomcat to /usr/local unarchive: src: /tmp/apache-tomcat-8.5.15.tar.gz dest: /usr/local/ remote_src: true .....省略 如果你想再添加100台机器，你需要做的，也只是在inventory文件里添加100个ip，再执行一遍ansible-playbook命令。\n当然，写shell写得不错的人，也能实现上面的功能。\n但是，使用Ansible有什么优势？模块化和标准化！ 手工写shell，甚至手工写python，要做到模块化和标准化，太困难了。\nAnsible将大部分运维工作都抽象并标准化成一个个模块（module）。所有的模块都以这样形式使用：\n- name: \u0026lt;描述说明(option)\u0026gt; \u0026lt;模块名\u0026gt; \u0026lt;属性名\u0026gt;: \u0026lt;属性值\u0026gt; 比如使用Ansible的file模块创建文件夹，而且file模块会自行判断该文件夹是否存在：\n- name: create a directory file: path: \u0026#34;/tmp/aa\u0026#34; state: directory owner: \u0026#34;centos\u0026#34; group: \u0026#34;centos\u0026#34; mode: \u0026#34;ug=rwx,o=rx\u0026#34; 显然，不同的人使用shell或python在方法名上可能都不一样，是驼峰，还是使用下划线？是使用createDir？使用createDirectory？还是create_folder？\n小结 我们小结一下Ansible到底解决了什么问题？\n自动化：避免运维工作中重复的工作，以及人的不确定性问题 模块化：大部分运维工作能做到模块化，直接使用shell脚本或者python，还是过于低级，比如： if [ ! -d \u0026#34;/tmp/aa\u0026#34; ]; then mkdir /tmp/aa fi .... 标准化：所有的模块的使用方式都是一样的，减少学习成本 然后，我个人认为Ansible解决以上问题都是为了实现一个最根本的目标：自动化配置！关于自动化配置，你可以看看我写的另一篇文章：关于自动化配置还有什么好说的呢？\n最后，这篇文章存在一个假设：手工运维、非模块化、非标准是问题，需要解决。如果你觉得这些都不是问题的话，这篇文章所说的也就都不成立了。\n","permalink":"https://showme.codes/zh-cn/2017-06-12-ansible-introduce/","summary":"\u003cp\u003e\u003cimg alt=\"Ansible\" loading=\"lazy\" src=\"/assets/images/292372-a504ab242c05db6a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e不知什么时候，Ansible的slogan从“IT Automation Software for System Administrators”变成了“AUTOMATION FOR EVERYONE”。\u003c/p\u003e\n\u003cp\u003e从一个给系统管理员使用的工具变成了给所有人使用的工具。\u003c/p\u003e\n\u003cp\u003e但是，现实中，发现了解Ansible的人，还是太少了。同时，自己断断续续学习Ansible也有一段时间，希望拿出来和大家交流。所以就决定不定期写写一个关于Ansible的系列。如果你觉得我写得还可以，到文末扫码请我喝杯茶。\u003c/p\u003e\n\u003cp\u003e此文为“简单易懂Ansible”系列文章的开篇 —— Ansible解决了什么\u003c/p\u003e\n\u003ch2 id=\"ansible解决了什么\"\u003eAnsible解决了什么\u003c/h2\u003e\n\u003cp\u003e首先，它是一个运维工具。当然要解决运维过程中遇到的问题了。运维过程遇到了什么问题？\u003c/p\u003e\n\u003cp\u003e想像一下，你要在一台新的机器上安装Tomcat，你会怎么样呢，条件反射的：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003essh user@111.111.111.111\n\nwget -c http://apache.fayea.com/tomcat/tomcat-8.5.15.tar.gz\n\ntar -zxf apache-tomcat-8.5.15.tar.gz\n.....省略\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e好，10分钟后你愉快地完成了老板给你的任务。但是现在你需要给100台机器安装Tomcat呢？手工的重复100次？\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"懵逼满屏\" loading=\"lazy\" src=\"/assets/images/292372-b33ed81e9618ca8a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e而Ansible能让我们只定义一次，理论上可以在无限台机器上执行。换句话：\u003cstrong\u003e减少运维工作中的重复工作\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e同时，如果是人工执行100次，那么失误是难免的！自动化运维工具会严格根据我们所给指令来执行，而不会因为失恋而手抖执行了：\u003ccode\u003esudo rm -rf /\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e不少人反对自动化，认为那样太危险，因为一不小心就在上百台机器删错文件。显然，他们没有注意到：自动化实现的是\u003cstrong\u003e准确地执行指令\u003c/strong\u003e，解决人类执行任务时存在的指令理解不正确、执行不严格的问题。而机器不会出现这些问题的概念几乎为零。\u003c/p\u003e\n\u003cp\u003e没有达到预期效果，往往是我们人类下达的指令不正确。\u003c/p\u003e\n\u003cp\u003e所以，Ansible还解决了\u003cstrong\u003e人执行指令不准确\u003c/strong\u003e的问题。\u003c/p\u003e\n\u003cp\u003e如果使用Ansible来实现上述的运维需求，怎么做呢？你需要做三件事情：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e定义目标机器的列表：一种被称为inventory的类ini文件\u003c/li\u003e\n\u003cli\u003e定义这些机器的配置：使用\u003ca href=\"https://en.wikipedia.org/wiki/YAML\"\u003eYAML\u003c/a\u003e格式的文件来描述你机器的配置\u003c/li\u003e\n\u003cli\u003e执行 \u003ccode\u003eansible-playbook -i inventory playbook.yml\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e以下是inventory文件：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[tomcat-servers]\n111.111.111.111\n112.112.112.112\n....\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e而这些ip的配置写在一种被称为playbook的YAML文件中：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e---\n- hosts: tomcat-servers\n  tasks:\n    - name: download tomcat\n      get_url:\n          url: http://apache.fayea.com/tomcat/tomcat-8.5.15.tar.gz\n          dest: /tmp\n          \n    - name: unarchive tomcat to /usr/local\n      unarchive:\n          src: /tmp/apache-tomcat-8.5.15.tar.gz\n          dest: /usr/local/\n          remote_src: true\n.....省略\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e如果你想再添加100台机器，你需要做的，也只是在inventory文件里添加100个ip，再执行一遍ansible-playbook命令。\u003c/p\u003e","title":"简单易懂Ansible系列 —— 解决了什么"},{"content":" 图片来源：http://www.imdb.com/title/tt0395699/\n回顾自己的技术成长之路，具体技术真心没有一样敢说精通，对于一个像我这样工作6、7年的人来说，实在有些难以启齿。\n现在中国整个的技术环境看重的是技术深度，而我从一开始就认为应该先广度再深度，自然在同行中被认为是异类。我没记错的话，大神左耳朵耗子的观点就是深度优先。\n为什么要广度优先，而不是深度优先。我有自己理由：\n技术变化太快，当你还没有深挖到某一个框架的本质，这个框架就可能已经过时了，特别是JS框架 容易只见树木、不见森林：比如你花很多时间去研究如何分布式存储你业务应用中的文件，但是你可能不知道世界上还有AWS S3这样的东西 手里有把锤子，全世界都是钉子：精通写bash脚本的人，所有的运维工作都倾向于写bash来解决运维问题，不知道世界上还有Ansible这样方便的东西，也不知道有时候根本问题不在运维，而在开发 我说出这些理由，并不是说我们就不需要深入研究某个框架和技术，只是想说明我们的选择的优先级会决定，至少会影响我们的思维方式。\n这几年，我开始输出一些体现我思维方式的文章，比如：\n《耦合的本质》 《关于自动化配置还有什么好说的呢？》 《也许，这样理解HTTPS更容易》 《Puppet，Chef，Ansible的共性》 （根据自己的一次分享整理） 《信息检索中，索引的本质》 很少人发现这些文章的真正价值，因为看起来和他们的实际工作没有任何关系，这些文章不会告诉你怎么快速搭建好https环境，也不告诉你怎么用Ansible copy一个文件到所有的目标机器上。\n在一次面试时一位老架构师两次问我：《耦合的本质》真的是你自己写的？显然他不相信写这篇文章的人30不到。确认之后，他说他不完全认同“耦合的本质是假设”，但是他欣赏这样的思维方式。\n我头一回感觉到有人看懂这篇文章。\n总的来说，这些文章体现出来思维方式是：\n利用概念推导、还原事物的形成过程、找共性这3个手段来找到事物的本质，再从这个本质推导基于此事物的上层建筑。\n比如我根据我们实际运维过程所要做的事情，推导出要实现自动化所要解决的问题，然后再通过“找共性”的方法，最终找到了这Puppet，Chef，Ansible 三款工具之间的共性。\n但是有什么用呢？其实，找到共性后，当遇到第四种自动化运维工具Salt时，我们就很容易提问了：\nSalt如何与受控机器通信 如何组织机器的？ 使用什么DSL来描述这些机器的配置 最后根据这些问题进行深入地学习，这样我们就可以从被动学习变成主动学习，有方法论的学习方式。甚至找到这些工具的知识边界。\n然而这只是我的个人学习方式，不一定适用于所有人。也不代表我的学习方式就是好的。\n我只想说明：深度优先和广度优先的选择会改变我们的思维方式。\n按道理，使用这样的思维方式（有点像方法论），任何一门技术都可以做到精通，但是我目前就是没做到精通。\n因为我排斥用脑袋记东西。我认为记不了的东西或者能不记的东西，它就不值得记忆。比如如何将字符串ip转成一个整型数字、Ansible里某个module的具体用法。\n而现实中，我对比其他的运维人员，我发现我用Ansible用得已经非常好了，Ansible里的概念我基本已经理解透了。但是我仍然不敢说精通Ansible。我实在记不了unarchive这个module的所有参数。\n所以，即将三十了，我仍然不敢说我精通任何一项技术。这成为我的困境。\n这时，很多人就会说了，你应该考虑转管理了。\n但是，我要问了：为什么要转管理呢？\n不少人的回答：\n因为你老了，你没有精力去学习更多的新语言、新框架了，你拼不过小鲜肉了。\n这个观点里有，有两个假设：第一，到三十后，你学不会，或学得慢新语言、新框架是因为没有精力；第二，小鲜肉没有能力做管理；\n第一点假设不成立，因为那只是借口——不想做的人，会找理由，想做的人，会找办法。第二点假设只是概率性问题，小鲜肉也可以做管理。\n转不转管理，决定于你是否真的Ready好了，是否真的喜欢做管理。和你年龄没有任何关系。\n说到底，写不写代码，做不做管理，都是个非常私人的问题。我们没必要那么在意别人怎么看。\n最后，我深爱着写代码。这不会因为我目前或将来是否精通某项技术而改变。\n","permalink":"https://showme.codes/zh-cn/2017-05-10-30-years-old/","summary":"\u003cp\u003e\u003cimg alt=\"超级奶爸\" loading=\"lazy\" src=\"/assets/images/292372-fad0235657317f1c.png\"\u003e\n图片来源：http://www.imdb.com/title/tt0395699/\u003c/p\u003e\n\u003cp\u003e回顾自己的技术成长之路，具体技术真心没有一样敢说精通，对于一个像我这样工作6、7年的人来说，实在有些难以启齿。\u003c/p\u003e\n\u003cp\u003e现在中国整个的技术环境看重的是技术深度，而我从一开始就认为应该先广度再深度，自然在同行中被认为是异类。我没记错的话，大神左耳朵耗子的观点就是深度优先。\u003c/p\u003e\n\u003cp\u003e为什么要广度优先，而不是深度优先。我有自己理由：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e技术变化太快，当你还没有深挖到某一个框架的本质，这个框架就可能已经过时了，特别是JS框架\u003c/li\u003e\n\u003cli\u003e容易只见树木、不见森林：比如你花很多时间去研究如何分布式存储你业务应用中的文件，但是你可能不知道世界上还有AWS S3这样的东西\u003c/li\u003e\n\u003cli\u003e手里有把锤子，全世界都是钉子：精通写bash脚本的人，所有的运维工作都倾向于写bash来解决运维问题，不知道世界上还有Ansible这样方便的东西，也不知道有时候根本问题不在运维，而在开发\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e我说出这些理由，并不是说我们就不需要深入研究某个框架和技术，只是想说明我们的选择的优先级会决定，至少会影响我们的思维方式。\u003c/p\u003e\n\u003cp\u003e这几年，我开始输出一些体现我思维方式的文章，比如：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"http://showme.codes/2015-12-29/the-nature-of-coupling/\"\u003e《耦合的本质》\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://showme.codes/2016-08-12/automation-configuration/\"\u003e《关于自动化配置还有什么好说的呢？》\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://showme.codes/2017-02-20/understand-https/\"\u003e《也许，这样理解HTTPS更容易》\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://showme.codes/2016-01-02/the-nature-of-ansible-puppet-chef/\"\u003e《Puppet，Chef，Ansible的共性》\u003c/a\u003e  （根据自己的一次分享整理）\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://my.oschina.net/zjzhai/blog/464446\"\u003e《信息检索中，索引的本质》\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e很少人发现这些文章的真正价值，因为看起来和他们的实际工作没有任何关系，这些文章不会告诉你怎么快速搭建好https环境，也不告诉你怎么用Ansible copy一个文件到所有的目标机器上。\u003c/p\u003e\n\u003cp\u003e在一次面试时一位老架构师两次问我：《耦合的本质》真的是你自己写的？显然他不相信写这篇文章的人30不到。确认之后，他说他不完全认同“耦合的本质是假设”，但是他欣赏这样的思维方式。\u003c/p\u003e\n\u003cp\u003e我头一回感觉到有人看懂这篇文章。\u003c/p\u003e\n\u003cp\u003e总的来说，这些文章体现出来思维方式是：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e利用概念推导、还原事物的形成过程、找共性这3个手段来找到事物的本质，再从这个本质推导基于此事物的上层建筑。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e比如我根据我们实际运维过程所要做的事情，推导出要实现自动化所要解决的问题，然后再通过“找共性”的方法，最终找到了这Puppet，Chef，Ansible 三款工具之间的共性。\u003c/p\u003e\n\u003cp\u003e但是有什么用呢？其实，找到共性后，当遇到第四种自动化运维工具Salt时，我们就很容易提问了：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSalt如何与受控机器通信\u003c/li\u003e\n\u003cli\u003e如何组织机器的？\u003c/li\u003e\n\u003cli\u003e使用什么DSL来描述这些机器的配置\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e最后根据这些问题进行深入地学习，这样我们就可以从被动学习变成主动学习，有方法论的学习方式。甚至找到这些工具的知识边界。\u003c/p\u003e\n\u003cp\u003e然而这只是我的个人学习方式，不一定适用于所有人。也不代表我的学习方式就是好的。\u003c/p\u003e\n\u003cp\u003e我只想说明：深度优先和广度优先的选择会改变我们的思维方式。\u003c/p\u003e\n\u003cp\u003e按道理，使用这样的思维方式（有点像方法论），任何一门技术都可以做到精通，但是我目前就是没做到精通。\u003c/p\u003e\n\u003cp\u003e因为我排斥用脑袋记东西。我认为记不了的东西或者能不记的东西，它就不值得记忆。比如如何将字符串ip转成一个整型数字、Ansible里某个module的具体用法。\u003c/p\u003e\n\u003cp\u003e而现实中，我对比其他的运维人员，我发现我用Ansible用得已经非常好了，Ansible里的概念我基本已经理解透了。但是我仍然不敢说精通Ansible。我实在记不了unarchive这个module的所有参数。\u003c/p\u003e\n\u003cp\u003e所以，即将三十了，我仍然不敢说我精通任何一项技术。这成为我的困境。\u003c/p\u003e\n\u003cp\u003e这时，很多人就会说了，你应该考虑转管理了。\u003c/p\u003e\n\u003cp\u003e但是，我要问了：为什么要转管理呢？\u003c/p\u003e\n\u003cp\u003e不少人的回答：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e因为你老了，你没有精力去学习更多的新语言、新框架了，你拼不过小鲜肉了。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这个观点里有，有两个假设：第一，到三十后，你学不会，或学得慢新语言、新框架是因为没有精力；第二，小鲜肉没有能力做管理；\u003c/p\u003e\n\u003cp\u003e第一点假设不成立，因为那只是借口——不想做的人，会找理由，想做的人，会找办法。第二点假设只是概率性问题，小鲜肉也可以做管理。\u003c/p\u003e\n\u003cp\u003e转不转管理，决定于你是否真的Ready好了，是否真的喜欢做管理。和你年龄没有任何关系。\u003c/p\u003e\n\u003cp\u003e说到底，写不写代码，做不做管理，都是个非常私人的问题。我们没必要那么在意别人怎么看。\u003c/p\u003e\n\u003cp\u003e最后，我深爱着写代码。这不会因为我目前或将来是否精通某项技术而改变。\u003c/p\u003e","title":"即将三十，我不敢说我精通任何一项技术"},{"content":" 图截自：http://agilemanifesto.org/iso/zhchs/manifesto.html\n最近，项目上遇到了以前我从来没有遇到的事情：10多个人一个团队（概念上的），要应对9个外部需求提出方；要维护超过10个子系统，这个“大系统”还是从另一个不愿意配合的团队接手过来的；项目管理者中，有倾向于敏捷的，也有倾向于瀑布的；最可怕的是这支团队完成组建才1个多月，只有3个人有站会经验，平均工作经验在7年以上😱。\n所有的这些条件混合在一起，管理就变得异常复杂，困难。面对这样复杂的乱麻，谁都很难有勇气一刀切。\n然而，事情还要做。比如站会。上周我自荐主持一次站会。说实在，那次站会是失败的，因为期间还是有两个人拿手机来刷。\n有人拿手机出来刷，说明站会上的内容和他们无关，进一步说明站会是无效的。\n但是，为什么呢？我会后一直都在思考这个问题。\n我想起自己一年多前，也是带团队从零开始实践敏捷开发。为什么不会出现这样的情况？\n突然，一个词蹦出来：共同语言！\n站会成为形式的根本原因，就是整个团队没有共同语言！。没有共同语言使站会沦为形式。\n好，现在我必须解释两个问题：\n为什么整个团队没有共同语言导致站会成为形式？ 为什么整个团队没有共同语言？ 我先解释为什么整个团队没有共同语言，再解释为什么没有共同语言的团队站会是形式。\n为什么整个团队没有共同语言 团队的沟通模型 第一个使整个团队没有共同语言的因素是：团队的沟通模型。\n为了方便讨论，我们假设团队的沟通模型为： 项目管理A，对接需求方1、2、3，然后再将任务拆分给Q、W、E。项目管理B、C依此类推。\n这样的沟通模型下，为什么团队成员会没有共同语言？\n在这样的沟通模型下，开发人员Q平时只与A沟通需求，尽管可能私底下与其他开发人员沟通一下实现，可以说，开发人员Q与项目管理A才会有共同语言。依此类推，每个开发人员只与他的直接上级有共同语言。\n我的结论是：趋向于单向沟通的团队沟通模型决定团队成员之间没有共同语言。而且这种单向沟通的结构时间越长，团队成员之间共同语言就越少！现象是，同处一个团队，你不知道你隔壁坐的同学到底在做什么。\n没有统一业务术语 第二个整个团队没有共同语言的因素是：团队内部没有统一业务术语。\n我们假设站会时，移动团队里的iOS、Anroid、H5三个小组一起参加同一个站会。而在站会时，iOS针对功能A使用了“激活”业务术语，而Android的同学对同一功能A却使用“上线”业务术语。\n不统一业务术语不仅导致成员之间没有共同语言，导致更严重的问题是：沟通效率低下。\n为什么整个团队没有共同语言导致站会成为形式 其实道理很简单，你问问自己，你喜欢与自己有更多共同语言的人交谈，还是反之？这是人性！\n站会时，我们更倾向于听我们关心的，和我们听得懂的。但是因为没有共同语言，所以，我们即不关心，也听不懂！\n站会当然也就是形式而已。\n怎么破？ 这下肯定会有人问，那为什么要站会？取消不就可以了。问这样的人是因为不了解站会的本质：站会一种团队快速反馈的机制。\n至于为什么需要快速反馈，很简单：（真正有效的）每日站会的团队可以每天根据站会内容（反馈）来对人员、需求、发布时间进行调整，调整的时间是以天计。而如果只有周会的团队，那么，这个团队调整的时间是以周计，那你觉得哪种团队面对变化时更敏捷，迅速？\n说回来，如何让站会更有效，而不至于成为一种形式呢？\n至少可以肯定的是这不是一个主持人就能解决的。\n剩下的先留给大家思考，我们下篇文章再讨论。\n你也可以先读读我之前写过的文章：\n每日站会、代码审查、结对编程 之开源中国实践 反馈机制在企业中的作用？ 如何防止程序员上班迟到？ ","permalink":"https://showme.codes/zh-cn/2017-05-07-no-standup-no-problem/","summary":"\u003cp\u003e\u003cimg alt=\"敏捷宣言的那些大叔\" loading=\"lazy\" src=\"/assets/images/292372-97ded69e8d2535c7.png\"\u003e\n图截自：http://agilemanifesto.org/iso/zhchs/manifesto.html\u003c/p\u003e\n\u003cp\u003e最近，项目上遇到了以前我从来没有遇到的事情：10多个人一个团队（概念上的），要应对9个外部需求提出方；要维护超过10个子系统，这个“大系统”还是从另一个不愿意配合的团队接手过来的；项目管理者中，有倾向于敏捷的，也有倾向于瀑布的；最可怕的是这支团队完成组建才1个多月，只有3个人有站会经验，平均工作经验在7年以上😱。\u003c/p\u003e\n\u003cp\u003e所有的这些条件混合在一起，管理就变得异常复杂，困难。面对这样复杂的乱麻，谁都很难有勇气一刀切。\u003c/p\u003e\n\u003cp\u003e然而，事情还要做。比如站会。上周我自荐主持一次站会。说实在，那次站会是失败的，因为期间还是有两个人拿手机来刷。\u003c/p\u003e\n\u003cp\u003e有人拿手机出来刷，说明站会上的内容和他们无关，进一步说明站会是无效的。\u003c/p\u003e\n\u003cp\u003e但是，为什么呢？我会后一直都在思考这个问题。\u003c/p\u003e\n\u003cp\u003e我想起自己一年多前，也是带团队从零开始实践敏捷开发。为什么不会出现这样的情况？\u003c/p\u003e\n\u003cp\u003e突然，一个词蹦出来：共同语言！\u003c/p\u003e\n\u003cp\u003e站会成为形式的根本原因，就是\u003cstrong\u003e整个团队没有共同语言\u003c/strong\u003e！。没有共同语言使站会沦为形式。\u003c/p\u003e\n\u003cp\u003e好，现在我必须解释两个问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e为什么整个团队没有共同语言导致站会成为形式？\u003c/li\u003e\n\u003cli\u003e为什么整个团队没有共同语言？\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我先解释为什么整个团队没有共同语言，再解释为什么没有共同语言的团队站会是形式。\u003c/p\u003e\n\u003ch2 id=\"为什么整个团队没有共同语言\"\u003e为什么整个团队没有共同语言\u003c/h2\u003e\n\u003ch3 id=\"团队的沟通模型\"\u003e团队的沟通模型\u003c/h3\u003e\n\u003cp\u003e第一个使整个团队没有共同语言的因素是：团队的沟通模型。\u003c/p\u003e\n\u003cp\u003e为了方便讨论，我们假设团队的沟通模型为：\n项目管理A，对接需求方1、2、3，然后再将任务拆分给Q、W、E。项目管理B、C依此类推。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"团队结构\" loading=\"lazy\" src=\"/assets/images/292372-cedb061dc583584b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这样的沟通模型下，为什么团队成员会没有共同语言？\u003c/p\u003e\n\u003cp\u003e在这样的沟通模型下，开发人员Q平时只与A沟通需求，尽管可能私底下与其他开发人员沟通一下实现，可以说，开发人员Q与项目管理A才会有共同语言。依此类推，每个开发人员只与他的直接上级有共同语言。\u003c/p\u003e\n\u003cp\u003e我的结论是：趋向于单向沟通的团队沟通模型决定团队成员之间没有共同语言。而且这种单向沟通的结构时间越长，团队成员之间共同语言就越少！现象是，同处一个团队，你不知道你隔壁坐的同学到底在做什么。\u003c/p\u003e\n\u003ch3 id=\"没有统一业务术语\"\u003e没有统一业务术语\u003c/h3\u003e\n\u003cp\u003e第二个整个团队没有共同语言的因素是：团队内部没有统一业务术语。\u003c/p\u003e\n\u003cp\u003e我们假设站会时，移动团队里的iOS、Anroid、H5三个小组一起参加同一个站会。而在站会时，iOS针对功能A使用了“激活”业务术语，而Android的同学对同一功能A却使用“上线”业务术语。\u003c/p\u003e\n\u003cp\u003e不统一业务术语不仅导致成员之间没有共同语言，导致更严重的问题是：沟通效率低下。\u003c/p\u003e\n\u003ch2 id=\"为什么整个团队没有共同语言导致站会成为形式\"\u003e为什么整个团队没有共同语言导致站会成为形式\u003c/h2\u003e\n\u003cp\u003e其实道理很简单，你问问自己，你喜欢与自己有更多共同语言的人交谈，还是反之？这是人性！\u003c/p\u003e\n\u003cp\u003e站会时，我们更倾向于听我们关心的，和我们听得懂的。但是因为没有共同语言，所以，我们即不关心，也听不懂！\u003c/p\u003e\n\u003cp\u003e站会当然也就是形式而已。\u003c/p\u003e\n\u003ch2 id=\"怎么破\"\u003e怎么破？\u003c/h2\u003e\n\u003cp\u003e这下肯定会有人问，那为什么要站会？取消不就可以了。问这样的人是因为不了解站会的本质：站会一种团队快速反馈的机制。\u003c/p\u003e\n\u003cp\u003e至于为什么需要快速反馈，很简单：（真正有效的）每日站会的团队可以每天根据站会内容（反馈）来对人员、需求、发布时间进行调整，调整的时间是以天计。而如果只有周会的团队，那么，这个团队调整的时间是以周计，那你觉得哪种团队面对变化时更敏捷，迅速？\u003c/p\u003e\n\u003cp\u003e说回来，如何让站会更有效，而不至于成为一种形式呢？\u003c/p\u003e\n\u003cp\u003e至少可以肯定的是这不是一个主持人就能解决的。\u003c/p\u003e\n\u003cp\u003e剩下的先留给大家思考，我们下篇文章再讨论。\u003c/p\u003e\n\u003cp\u003e你也可以先读读我之前写过的文章：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://showme.codes/2016-04-01/standup-codereview-pair-in-oschina/\"\u003e每日站会、代码审查、结对编程 之开源中国实践\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://showme.codes/2016-12-10/feedback-in-company/\"\u003e反馈机制在企业中的作用？\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://showme.codes/2017-03-03/prevent-late-for-work/\"\u003e如何防止程序员上班迟到？\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"为什么站会会成为形式"},{"content":" 图片来源：link\n摘要：当我们要考虑如何让项目不延期时，我们是否做到让每个员工都满负荷了？我们追求的是不延期，还是追求更卓越的产品？\n这一两个星期和同事讨论如何使用看板进行项目管理时，总的来说，我遇到最频繁的问题有：\n如何能看出项目是否延期？ 如何拆任务？ 其实，我遇到的问题是：如何能看出项目是否延期？然后经我将问题深挖，才发现他们更本质的问题是：拿到需求，如何拆任务，拆到什么粒度。\n讨论这类问题，最好举个例子，否则整个讨论过程会很虚。\n比如我们的项目经理从产品经理那里拿到一个需求：改版APP。这款APP有12个界面，所有的界面都需要改。而你手下有6个人。\n这时，可以以两种粒度来拆分：\n以界面为粒度 拆分成更可以量化的粒度。 关于什么是可以量化的粒度，下文会阐述。\n按界面粒度来拆分 可以看出，以界面粒度来拆分，简单粗暴：24人天的任务，我们有6个人，所以，理论上我们只需要4天完成“改版APP”。我们可以很容易看出这个项目是否延期，只要每个界面都没有延期。\n放到看板上，理所当然，每个界面一张卡。\n现实中，我们的项目经理可能还会这样分到人头上：\n为什么一定要分到人头上？除了方便KPI（表面上），背后还有一定的文化因素：因为当项目延期时，我们就可以找出那个相应的人进行问责。这种问责的机制导致的后果：人们更愿意推卸责任，而不是共同协作。\n放大一些这个问题，公司内部多个技术部门也会因为这种问责的文化，导致部门之间更趋向责怪对方不按期，而不是共同协作完成一件事情。\n再再放大一些这个问题：在人们的意识里往往认为，问责后，坏的事情就可以避免问题再发生。放到我们本篇文章讨论的上下文里，也就是问责可以避免延期。但是，可能吗？因为延期已经发生，我们应该在延期发生前进行协调资源来解决延期。\n我们举个例子：在项目进行的过程中，人员B在做界面3，4时，在第3天时被一个问题卡住了。而人员C其实在第3天时就已经完成了，第4天开始优化。其他人准时完成了自己的任务。最后人员B的延期导致项目延期了2天。这时，如果你问责人员B，那么，这次的延期能倒退吗？也许你会说，问责后，这个人下次就不会延期了。\n我想说：\n延期不延期和你问责没有任何关系。如果有关系，你在项目开始时，就每个人问责一下，这样项目就不会延期了？ 我们应该追求的是每个项目都不延期，而不是下一个项目不延期 我们追求的是不延期，还是追求更卓越的产品？ 回头看这次延期，也许我们是可以避免的，比如在第3天的站会上，人员C说出自己被某个问题卡住了。这时，可能其他人员一句话就点通人员C的问题了。还有可能是人员C遇到的问题是需要其它部门来协助才能根本解决，这时项目经理就需要与其它部门沟通了。\n回到问题“按界面粒度来拆分任务”这个问题本身。\n将界面再拆分成可量化的粒度 这种方式要我们的项目经理拿到需求后，让最熟悉这个APP的人或团队对需求再进行拆分成一系列工作单元，然后再分别估算这些工作单元在现有的人员基础上需要多少天。最后估算出一个总的交付时间点。我们假设完成这个需求，我们同样需要4天完成。\n至于拆分到什么程度，就是我们上文提到的可量化的程度。\n什么叫可量化？ 上面我们看到将需求拆分成一系列工作单元后，我们可以更灵活的安排优先级。同时，这样也帮助我们发现界面1和界面2有一个工作单元3是有交集的。有交集的工作单元，我们应该让同一个人来完成以避免其中的沟通成本。总的来说，拆分成一系列可量化的工作单元后，我们可以：\n更灵活的优先级调控 发现有交集的工作单元，也就能发现可减少沟通成本的空间。 但是，什么样的工作单元叫可量化？\n代码行数是最简单的，估计完成APP改版需要写10万行代码。一个工作单元，我们定1万行？这种工作单元是可以量化，但是写完那么多行代码，你就是完成APP改版这个任务了？\n我们举个例子来说明什么样的工作单元叫可量化，比如对于界面1，我们需要：\n把“完成”按钮的颜色从绿色改成蓝色 当完成值为100时，不显示100，显示成“恭喜，已完成” 缓存从服务器获得的任务完成值，对于多次操作，只向服务器请求一次，以提升用户操作的流畅感 从这个例子，我们可以看出，每个工作单元都应该是：\n准确的：将绿色改成蓝色，而不是红色 不可分割的：不显示100，显示成“恭喜，已完成”，这个工作单元，你不能再分割了 体现了业务含义：代码行数并不能体现业务含义，但是提升用户操作的流畅感有业务含义的。 可量化的工作单元、站会与看板 有了可量化的工作单元后，再结合站会和看板，这样，我们每天都可以知道（可视化）团队的工作状态了。延不延期，大家都可以看得到，大家都是成年人了：\n谁做得快，谁捡更多的卡来做的。而且可以捡优先更高的卡先做，也降低延期的风险。我们可以从这个过程中识别人才。 站会的第3天，人员B还在做_#3_卡，我们其他成员可以加快速度做其它卡以弥补人员B的慢速度，同时项目经理也可以更早的介入这个可能延期的卡中帮助人员B 当出现质量问题时，人员D的卡会被打回Todo多次，因为有站会，我们所有人都很感觉到_#5_这张卡可能存在一定难度或者人员D在协作方式存在问题，这时，我们其他人就会主动帮助人员D解决问题，而不是责怪他。 慢慢地，团队的协作方式变得以解决问题为导向，而不是以问责为导向。\n拆分成可量化的工作单元，一样会延期 但是，我个人的经验看来即使我们将需求拆分成可量化的工作单元，项目一样可能会延期。\n看板只能帮助我们更可视化，更容易地了解到项目当前的状态，对于这个状态，我们的项目经理要如何反应，完成是个人问题了。\n同时，看板也能帮助我们找到延期的根本原因，比如是某个人的卡在In Progress上拖了很长时间、某个人请假了、其它部门中间改需求了、项目人员在某项技术的能力问题……\n所以，要延期的项目一定会延期，我们应该正确面对，找到原因并根本解决。我们要做的只是保证每个人每个工作日都是满负荷的。\n这里，留给大家一个思考题：如果其它外部条件不变，每个人每个工作日都是满负荷了？如何不延期？\n拆成可量化的工作单元会增加项目经理的工作量？ 然而，又会有人说了，这么多项目，我每个项目都要拆分成可量化的，我们项目经理会增加很多工作量。\n其实，如果真的有作用了，这些工作量是值得的，只要你真的理解可量化工作单元的作用。同时，当出现多个项目时，你忙不过来时，说明现在是你培养另一个项目经理的时机了。你可以尝试将一些项目管理的工作交给团队成员来完成。但前提是项目经理本身也是超负荷工作，影响正常工作了。\n小结 想让项目不延期，我们首先关注的是如何将需求拆分成可量化的工作单元，然后想办法保证这些工作单元真正被有效的执行。办法通常可以有：\n使用看板可视化所有的工作单元 通过站会了解工作单元执行过程可能的风险 通过协作来取长补短 通过优先级来降低延期时的风险 通过打包有交集的工作单元减少沟通成本 通过以上方法可以将团队“调”到可能的最优状态。但是如果还是延期，原因可能就不在团队了。\n最后，项目延期是客观存在的事实，你会选择红药丸，还是蓝药丸？\n","permalink":"https://showme.codes/zh-cn/2017-4-14-split-task-by-project-manager/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/292372-f7b4cab4640af3a7.png\"\u003e\n图片来源：\u003ca href=\"http://img1.mydrivers.com/img/20140106/s_18f35cda6a9045b4b2024ec231ecdb4c.jpg\"\u003elink\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e摘要：当我们要考虑如何让项目不延期时，我们是否做到让每个员工都满负荷了？我们追求的是不延期，还是追求更卓越的产品？\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e这一两个星期和同事讨论如何使用看板进行项目管理时，总的来说，我遇到最频繁的问题有：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e如何能看出项目是否延期？\u003c/li\u003e\n\u003cli\u003e如何拆任务？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e其实，我遇到的问题是：如何能看出项目是否延期？然后经我将问题深挖，才发现他们更本质的问题是：拿到需求，如何拆任务，拆到什么粒度。\u003c/p\u003e\n\u003cp\u003e讨论这类问题，最好举个例子，否则整个讨论过程会很虚。\u003c/p\u003e\n\u003cp\u003e比如我们的项目经理从产品经理那里拿到一个需求：改版APP。这款APP有12个界面，所有的界面都需要改。而你手下有6个人。\u003c/p\u003e\n\u003cp\u003e这时，可以以两种粒度来拆分：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e以界面为粒度\u003c/li\u003e\n\u003cli\u003e拆分成更可以量化的粒度。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e关于什么是可以量化的粒度，下文会阐述。\u003c/p\u003e\n\u003ch3 id=\"按界面粒度来拆分\"\u003e按界面粒度来拆分\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"按界面来拆\" loading=\"lazy\" src=\"/assets/images/292372-8d26158ad9cdc5d5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e可以看出，以界面粒度来拆分，简单粗暴：24人天的任务，我们有6个人，所以，理论上我们只需要4天完成“改版APP”。我们可以很容易看出这个项目是否延期，只要每个界面都没有延期。\u003c/p\u003e\n\u003cp\u003e放到看板上，理所当然，每个界面一张卡。\u003c/p\u003e\n\u003cp\u003e现实中，我们的项目经理可能还会这样分到人头上：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"将卡分到人\" loading=\"lazy\" src=\"/assets/images/292372-edb8c59b4d5ecd71.png\"\u003e\u003c/p\u003e\n\u003cp\u003e为什么一定要分到人头上？除了方便KPI（表面上），背后还有一定的文化因素：因为当项目延期时，我们就可以找出那个相应的人进行\u003cstrong\u003e问责\u003c/strong\u003e。这种问责的机制导致的后果：人们更愿意推卸责任，而不是共同协作。\u003c/p\u003e\n\u003cp\u003e放大一些这个问题，公司内部多个技术部门也会因为这种问责的文化，导致部门之间更趋向责怪对方不按期，而不是共同协作完成一件事情。\u003c/p\u003e\n\u003cp\u003e再再放大一些这个问题：\u003cstrong\u003e在人们的意识里往往认为，问责后，坏的事情就可以避免问题再发生\u003c/strong\u003e。放到我们本篇文章讨论的上下文里，也就是问责可以避免延期。但是，可能吗？因为延期已经发生，我们应该在延期发生前进行协调资源来解决延期。\u003c/p\u003e\n\u003cp\u003e我们举个例子：在项目进行的过程中，人员B在做界面3，4时，在第3天时被一个问题卡住了。而人员C其实在第3天时就已经完成了，第4天开始优化。其他人准时完成了自己的任务。最后人员B的延期导致项目延期了2天。这时，如果你问责人员B，那么，这次的延期能倒退吗？也许你会说，问责后，这个人下次就不会延期了。\u003c/p\u003e\n\u003cp\u003e我想说：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e延期不延期和你问责没有任何关系。如果有关系，你在项目开始时，就每个人问责一下，这样项目就不会延期了？\u003c/li\u003e\n\u003cli\u003e我们应该追求的是每个项目都不延期，而不是下一个项目不延期\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e我们追求的是不延期，还是追求更卓越的产品？\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e回头看这次延期，也许我们是可以避免的，比如在第3天的站会上，人员C说出自己被某个问题卡住了。这时，可能其他人员一句话就点通人员C的问题了。还有可能是人员C遇到的问题是需要其它部门来协助才能根本解决，这时项目经理就需要与其它部门沟通了。\u003c/p\u003e\n\u003cp\u003e回到问题“按界面粒度来拆分任务”这个问题本身。\u003c/p\u003e\n\u003ch3 id=\"将界面再拆分成可量化的粒度\"\u003e将界面再拆分成可量化的粒度\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"可量化的粒度\" loading=\"lazy\" src=\"/assets/images/292372-3070c3fcde6922f5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这种方式要我们的项目经理拿到需求后，让最熟悉这个APP的人或团队对需求再进行拆分成一系列工作单元，然后再分别估算这些工作单元在现有的人员基础上需要多少天。最后估算出一个总的交付时间点。我们假设完成这个需求，我们同样需要4天完成。\u003c/p\u003e\n\u003cp\u003e至于拆分到什么程度，就是我们上文提到的\u003cstrong\u003e可量化\u003c/strong\u003e的程度。\u003c/p\u003e\n\u003ch4 id=\"什么叫可量化\"\u003e什么叫可量化？\u003c/h4\u003e\n\u003cp\u003e上面我们看到将需求拆分成一系列工作单元后，我们可以更灵活的安排优先级。同时，这样也帮助我们发现界面1和界面2有一个工作单元3是有交集的。有交集的工作单元，我们应该让同一个人来完成以避免其中的沟通成本。总的来说，拆分成一系列可量化的工作单元后，我们可以：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e更灵活的优先级调控\u003c/li\u003e\n\u003cli\u003e发现有交集的工作单元，也就能发现可减少沟通成本的空间。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e但是，\u003cstrong\u003e什么样的工作单元叫可量化？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e代码行数是最简单的，估计完成APP改版需要写10万行代码。一个工作单元，我们定1万行？这种工作单元是可以量化，但是写完那么多行代码，你就是完成APP改版这个任务了？\u003c/p\u003e\n\u003cp\u003e我们举个例子来说明什么样的工作单元叫可量化，比如对于界面1，我们需要：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e把“完成”按钮的颜色从绿色改成蓝色\u003c/li\u003e\n\u003cli\u003e当完成值为100时，不显示100，显示成“恭喜，已完成”\u003c/li\u003e\n\u003cli\u003e缓存从服务器获得的任务完成值，对于多次操作，只向服务器请求一次，以提升用户操作的流畅感\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e从这个例子，我们可以看出，每个工作单元都应该是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e准确的：将绿色改成蓝色，而不是红色\u003c/li\u003e\n\u003cli\u003e不可分割的：不显示100，显示成“恭喜，已完成”，这个工作单元，你不能再分割了\u003c/li\u003e\n\u003cli\u003e体现了业务含义：代码行数并不能体现业务含义，但是提升用户操作的流畅感有业务含义的。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"可量化的工作单元站会与看板\"\u003e可量化的工作单元、站会与看板\u003c/h3\u003e\n\u003cp\u003e有了可量化的工作单元后，再结合站会和看板，这样，我们每天都可以知道（可视化）团队的工作状态了。延不延期，大家都可以看得到，大家都是成年人了：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e谁做得快，谁捡更多的卡来做的。而且可以捡优先更高的卡先做，也降低延期的风险。我们可以从这个过程中识别人才。\u003c/li\u003e\n\u003cli\u003e站会的第3天，人员B还在做_#3_卡，我们其他成员可以加快速度做其它卡以弥补人员B的慢速度，同时项目经理也可以更早的介入这个可能延期的卡中\u003cstrong\u003e帮助\u003c/strong\u003e人员B\u003c/li\u003e\n\u003cli\u003e当出现质量问题时，人员D的卡会被打回Todo多次，因为有站会，我们所有人都很感觉到_#5_这张卡可能存在一定难度或者人员D在协作方式存在问题，这时，我们其他人就会主动帮助人员D解决问题，而不是责怪他。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/292372-f27551e5e27c6c91.png\"\u003e\u003c/p\u003e\n\u003cp\u003e慢慢地，团队的协作方式变得以解决问题为导向，而不是以问责为导向。\u003c/p\u003e\n\u003ch3 id=\"拆分成可量化的工作单元一样会延期\"\u003e拆分成可量化的工作单元，一样会延期\u003c/h3\u003e\n\u003cp\u003e但是，我个人的经验看来即使我们将需求拆分成可量化的工作单元，项目一样可能会延期。\u003c/p\u003e\n\u003cp\u003e看板只能帮助我们更可视化，更容易地了解到项目当前的状态，对于这个状态，我们的项目经理要如何反应，完成是个人问题了。\u003c/p\u003e\n\u003cp\u003e同时，看板也能帮助我们找到延期的根本原因，比如是某个人的卡在In Progress上拖了很长时间、某个人请假了、其它部门中间改需求了、项目人员在某项技术的能力问题……\u003c/p\u003e\n\u003cp\u003e所以，要延期的项目一定会延期，我们应该正确面对，找到原因并根本解决。我们要做的只是保证每个人每个工作日都是满负荷的。\u003c/p\u003e\n\u003cp\u003e这里，留给大家一个思考题：如果其它外部条件不变，每个人每个工作日都是满负荷了？如何不延期？\u003c/p\u003e\n\u003ch3 id=\"拆成可量化的工作单元会增加项目经理的工作量\"\u003e拆成可量化的工作单元会增加项目经理的工作量？\u003c/h3\u003e\n\u003cp\u003e然而，又会有人说了，这么多项目，我每个项目都要拆分成可量化的，我们项目经理会增加很多工作量。\u003c/p\u003e\n\u003cp\u003e其实，如果真的有作用了，这些工作量是值得的，只要你真的理解可量化工作单元的作用。同时，当出现多个项目时，你忙不过来时，说明现在是你培养另一个项目经理的时机了。你可以尝试将一些项目管理的工作交给团队成员来完成。但前提是项目经理本身也是超负荷工作，影响正常工作了。\u003c/p\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e想让项目不延期，我们首先关注的是如何将需求拆分成可量化的工作单元，然后想办法保证这些工作单元真正被有效的执行。办法通常可以有：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e使用看板可视化所有的工作单元\u003c/li\u003e\n\u003cli\u003e通过站会了解工作单元执行过程可能的风险\u003c/li\u003e\n\u003cli\u003e通过协作来取长补短\u003c/li\u003e\n\u003cli\u003e通过优先级来降低延期时的风险\u003c/li\u003e\n\u003cli\u003e通过打包有交集的工作单元减少沟通成本\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e通过以上方法可以将团队“调”到可能的最优状态。但是如果还是延期，原因可能就不在团队了。\u003c/p\u003e","title":"什么？项目延期有解药？"},{"content":"P.S. 这里的“迟到”指的是故意迟到。\n看看满大街的招聘信息上都写着：\n弹性上班，不打卡\n我们还有必要思考如何防止程序员上班迟到吗？我不讨论有没有必要，因为肯定有不少公司存在员工上班迟到的同时，没把事情做好的。\n也许你又会问了：如果是这样，上KPI不就好了，给他一个活，规定好时间不就可以了？\n这个问题，我觉得不在本文讨论范围内。我只想讨论：如何防止程序员上班迟到。\n了解我的人，都知道，当我遇到问题时，我往往先想的是为什么，然后再想怎么。只有知道为什么，才能根治。\n那么他们为什么要迟到呢？这个问题似乎是无解的。就似常常迟到的小学生，被老师问起原因一样，每个小学生，每一天都有自己的理由。\n好吧，对于似乎无解的问题，我们暂且放一放。\n回到问题本身：怎么“防”？\n一提这个问题，绝大数人就想到了：上打卡机呗。\n以前，我也是这绝大数人的其中一个。可是最近，另一个疑问进入到我的大脑：\n为什么去年我带团队时，没有迟到现象？\n晨会——这个词突然击中我。是的，因为团队每天早上上班时间点过20分钟都会准时进行晨会。\n晨会就是指所有团队成员站着过任务卡，晨会一般都会很短。好处什么的，具体可以看我的另一篇博客：每日站会、代码审查、结对编程 之开源中国实践\n晨会是如何“防止”程序员上班迟到的呢？ 因为我们团队达成一致：上班时间点过20分钟进行晨会。假如10点上班，你一个人10点20了还没来到，你好意思吗？\n不知道有人想到其中的腻味？人是会不好意思的，在团队这个交际圈里，除非你不想在这个团队待了。换句话说，这样的晨会在一定程度上利用了人性对交际的焦虑来实现“防迟到”。\n但是，我要申明，我要申明，我要申明：晨会的真正目的不是为了防止程序员上班迟到！晨会达到自己的目的的同时，恰好解决了“迟到”这个企业难题。\n有人会问，为什么10点上班，10点20才开始晨会？因为我们需要给团队成员一点时间进入工作状态，给团队成员一些空间融合。\n小结 做水利工程时，与其围堵，不如疏导。这样的战略方针，在企业管理中同样有用。我们在思考如何“防”时，不应该只想着如何围堵，疏导可能是更好的解决方案。而晨会就是一种疏导方案。\n题外话，员工为什么会故意迟到？这是另一个有更有深度的问题，留给大家。:)\n","permalink":"https://showme.codes/zh-cn/2017-3-3-prevent-late-for-work/","summary":"\u003cp\u003e\u003cstrong\u003e\u003cem\u003eP.S. 这里的“迟到”指的是故意迟到。\u003c/em\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e看看满大街的招聘信息上都写着：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e弹性上班，不打卡\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我们还有必要思考如何防止程序员上班迟到吗？我不讨论有没有必要，因为肯定有不少公司存在员工上班迟到的同时，没把事情做好的。\u003c/p\u003e\n\u003cp\u003e也许你又会问了：如果是这样，上KPI不就好了，给他一个活，规定好时间不就可以了？\u003c/p\u003e\n\u003cp\u003e这个问题，我觉得不在本文讨论范围内。我只想讨论：如何防止程序员上班迟到。\u003c/p\u003e\n\u003cp\u003e了解我的人，都知道，当我遇到问题时，我往往先想的是\u003cstrong\u003e为什么\u003c/strong\u003e，然后再想\u003cstrong\u003e怎么\u003c/strong\u003e。只有知道为什么，才能根治。\u003c/p\u003e\n\u003cp\u003e那么他们为什么要迟到呢？这个问题似乎是无解的。就似常常迟到的小学生，被老师问起原因一样，每个小学生，每一天都有自己的理由。\u003c/p\u003e\n\u003cp\u003e好吧，对于\u003cstrong\u003e似乎无解\u003c/strong\u003e的问题，我们暂且放一放。\u003c/p\u003e\n\u003cp\u003e回到问题本身：怎么“防”？\u003c/p\u003e\n\u003cp\u003e一提这个问题，绝大数人就想到了：上打卡机呗。\u003c/p\u003e\n\u003cp\u003e以前，我也是这绝大数人的其中一个。可是最近，另一个疑问进入到我的大脑：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为什么去年我带团队时，没有迟到现象？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e晨会\u003c/strong\u003e——这个词突然击中我。是的，因为团队每天早上上班时间点过20分钟都会准时进行晨会。\u003c/p\u003e\n\u003cp\u003e晨会就是指所有团队成员站着过任务卡，晨会一般都会很短。好处什么的，具体可以看我的另一篇博客：\u003ca href=\"http://showme.codes/2016-04-01/standup-codereview-pair-in-oschina/\"\u003e每日站会、代码审查、结对编程 之开源中国实践\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"晨会时面对的看板\" loading=\"lazy\" src=\"/assets/images/2016-4-team.jpg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"晨会是如何防止程序员上班迟到的呢\"\u003e晨会是如何“防止”程序员上班迟到的呢？\u003c/h3\u003e\n\u003cp\u003e因为我们团队达成一致：上班时间点过20分钟进行晨会。假如10点上班，你一个人10点20了还没来到，你好意思吗？\u003c/p\u003e\n\u003cp\u003e不知道有人想到其中的腻味？人是会不好意思的，在团队这个\u003cstrong\u003e交际圈\u003c/strong\u003e里，除非你不想在这个团队待了。换句话说，这样的晨会在一定程度上利用了人性对交际的焦虑来实现“防迟到”。\u003c/p\u003e\n\u003cp\u003e但是，\u003cstrong\u003e我要申明，我要申明，我要申明\u003c/strong\u003e：晨会的真正目的不是为了防止程序员上班迟到！晨会达到自己的目的的同时，恰好解决了“迟到”这个企业难题。\u003c/p\u003e\n\u003cp\u003e有人会问，为什么10点上班，10点20才开始晨会？因为我们需要给团队成员一点时间进入工作状态，给团队成员一些空间融合。\u003c/p\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e做水利工程时，与其围堵，不如疏导。这样的战略方针，在企业管理中同样有用。我们在思考如何“防”时，不应该只想着如何围堵，疏导可能是更好的解决方案。而晨会就是一种疏导方案。\u003c/p\u003e\n\u003cp\u003e题外话，员工为什么会故意迟到？这是另一个有更有深度的问题，留给大家。:)\u003c/p\u003e","title":"如何防止程序员上班迟到？"},{"content":"\nP.S. 如果你没有了解过互联网产品，下文可能不适合你，因为我没打算写得能让所有人都懂。\nAirbnb，就不详细介绍了。而Joe Gebbia则是这家公司的首席产品官兼共同创始人。\n他在Ted上有一个Talk：How Airbnb designs for trust。而在网易公开课上翻译成：如何与陌生人建立信任？ 这个标题翻译得是否合适，仁者见仁了。\n但从Talk本身，我学到了不少关于**“做产品”**的东西。以下是我所学到的，但对你来说这是二手知识，推荐你自己先看一遍视频，再继续阅读本文。\n是什么驱动产品设计 我们的社会从小给就我们灌输了陌生人 = 危险的观念。同时，家是一个人最私密的地方，你怎么才会将这个私密的地方公开给一个陌生人住呢？\n而Joe知道Airbnb这款产品的本质是什么。是信任！如果不打破人们陌生人=危险这个观念，Airbnb不可能成功（至少当前是成功的）。\n\b他们(似乎)研究了如何增加陌生人之间的信任。Talk中，他说：\n事实证明：一个精心设计的信誉体系，是建立信任的关键。\nP.S. 我想到了支付宝\n然后才有Airbnb不一样的评论机制：只有房东和租客都评论后，评论才展示。这里有个问题需要你来思考：为什么要这样控制评论的展示时机？而不是追求评论数？哈哈。\n说到这里，我最想说的是：原来这就是产品的本质驱动产品设计。\n\b但是怎么做？ 但是当我们知道产品的本质后，如何做？或者说是如何在做产品的过程中慢慢发现这个本质？\nTalk中，Joe说了Airbnb与斯坦福大学合作。不知道怎么合作，反正他们发现：\n我们更喜欢与我们相似的人，与我们差异越大，我们越是不信任他们。这是人们与生俱来的天性。正确的设计可以帮助我们克服人们扎根心底的认知偏见。\nP.S. 看到这里，我第一反应是：他们怎么会想到和大学合作？\n最后，他们通过数据分析发现：\n当评论大于10条时，高的信誉评论比高的相似度更可信！ 房客的自我介绍是如何影响自己的被接受率的 是的，通过数据分析，我们就可以做各种实验并进行实验对照，以找到增加陌生人之间的信任度的方法。\n这似乎是个老掉牙的问题了。\n亲近你的用户 产品初期，Joe用自己的手机号码做起了客服。所以，他才会知道现有产品会有哪些不足。\n这也是个老掉牙的问题了。\n后记 上文绝属我个人虚构，Joe是不是这样想的，只有他知道。😂 本次Talk还有讲共享经济，而我只说了“做产品”这部分。\n部分内容摘自字幕，如有侵权，麻烦告知。谢谢。\n","permalink":"https://showme.codes/zh-cn/2017-2-24-learn-from-joe/","summary":"\u003cp\u003e\u003cimg alt=\"Joe Gebbia\" loading=\"lazy\" src=\"/assets/images/2017-2-24-292372-48fd2f20843d1122.png\"\u003e\u003c/p\u003e\n\u003cp\u003eP.S. 如果你没有了解过互联网产品，下文可能不适合你，因为我没打算写得能让所有人都懂。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://zh.airbnb.com/about/about-us\"\u003eAirbnb\u003c/a\u003e，就不详细介绍了。而\u003ca href=\"https://zh.airbnb.com/about/founders\"\u003eJoe Gebbia\u003c/a\u003e则是这家公司的首席产品官兼共同创始人。\u003c/p\u003e\n\u003cp\u003e他在Ted上有一个Talk：\u003ca href=\"http://www.ted.com/talks/joe_gebbia_how_airbnb_designs_for_trust/transcript?language=en\"\u003eHow Airbnb designs for trust\u003c/a\u003e。而在网易公开课上翻译成：\u003ca href=\"http://open.163.com/movie/2016/3/P/C/MBIJ802BI_MBIJ8J2PC.html\"\u003e如何与陌生人建立信任？\u003c/a\u003e 这个标题翻译得是否合适，仁者见仁了。\u003c/p\u003e\n\u003cp\u003e但从Talk本身，我学到了不少关于**“做产品”**的东西。以下是我所学到的，但对你来说这是二手知识，推荐你自己先看一遍视频，再继续阅读本文。\u003c/p\u003e\n\u003ch3 id=\"是什么驱动产品设计\"\u003e是什么驱动产品设计\u003c/h3\u003e\n\u003cp\u003e我们的社会从小给就我们灌输了\u003ccode\u003e陌生人 = 危险\u003c/code\u003e的观念。同时，家是一个人最私密的地方，你怎么才会将这个私密的地方公开给一个陌生人住呢？\u003c/p\u003e\n\u003cp\u003e而Joe知道Airbnb这款产品的本质是什么。是\u003cstrong\u003e信任\u003c/strong\u003e！如果不打破人们\u003ccode\u003e陌生人=危险\u003c/code\u003e这个观念，Airbnb不可能成功（至少当前是成功的）。\u003c/p\u003e\n\u003cp\u003e\b他们(似乎)研究了如何增加陌生人之间的信任。Talk中，他说：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e事实证明：一个精心设计的信誉体系，是建立信任的关键。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eP.S. 我想到了支付宝\u003c/p\u003e\n\u003cp\u003e然后才有Airbnb不一样的评论机制：只有房东和租客都评论后，评论才展示。这里有个问题需要你来思考：为什么要这样控制评论的展示时机？而不是追求评论数？哈哈。\u003c/p\u003e\n\u003cp\u003e说到这里，我最想说的是：原来这就是产品的本质驱动产品设计。\u003c/p\u003e\n\u003ch3 id=\"但是怎么做\"\u003e\b但是怎么做？\u003c/h3\u003e\n\u003cp\u003e但是当我们知道产品的本质后，如何做？或者说是如何在做产品的过程中慢慢发现这个本质？\u003c/p\u003e\n\u003cp\u003eTalk中，Joe说了Airbnb与斯坦福大学合作。不知道怎么合作，反正他们发现：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我们更喜欢与我们相似的人，与我们差异越大，我们越是不信任他们。这是人们与生俱来的天性。正确的设计可以帮助我们克服人们扎根心底的认知偏见。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eP.S. 看到这里，我第一反应是：他们怎么会想到和大学合作？\u003c/p\u003e\n\u003cp\u003e最后，他们通过\u003cstrong\u003e数据分析\u003c/strong\u003e发现：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e当评论大于10条时，高的信誉评论比高的相似度更可信！\u003c/li\u003e\n\u003cli\u003e房客的自我介绍是如何影响自己的被接受率的\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e是的，通过\u003cstrong\u003e数据分析\u003c/strong\u003e，我们就可以做各种实验并进行实验对照，以找到增加陌生人之间的信任度的方法。\u003c/p\u003e\n\u003cp\u003e这似乎是个老掉牙的问题了。\u003c/p\u003e\n\u003ch3 id=\"亲近你的用户\"\u003e亲近你的用户\u003c/h3\u003e\n\u003cp\u003e产品初期，Joe用自己的手机号码做起了客服。所以，他才会知道现有产品会有哪些不足。\u003c/p\u003e\n\u003cp\u003e这也是个老掉牙的问题了。\u003c/p\u003e\n\u003ch3 id=\"后记\"\u003e后记\u003c/h3\u003e\n\u003cp\u003e上文绝属我个人虚构，Joe是不是这样想的，只有他知道。😂\n本次Talk还有讲共享经济，而我只说了“做产品”这部分。\u003c/p\u003e\n\u003cp\u003e部分内容摘自字幕，如有侵权，麻烦告知。谢谢。\u003c/p\u003e","title":"我从Airbnb联合创始人的Talk里学到的"},{"content":"摘要：本文尝试一步步还原HTTPS的设计过程，以理解为什么HTTPS最终会是这副模样。但是这并不代表HTTPS的真实设计过程。在阅读本文时，你可以尝试放下已有的对HTTPS的理解，这样更利于“还原”过程。\n我们先不了聊HTTP，HTTPS，我们先从一个聊天软件说起，我们要实现A能发一个hello消息给B： 如果我们要实现这个聊天软件，本文只考虑安全性问题，要实现\nA发给B的hello消息包，即使被中间人拦截到了，也无法得知消息的内容\n如何做到真正的安全？ 这个问题，很多人马上就想到了各种加密算法，什么对称加密、非对称加密、DES、RSA、XX、噼里啪啦~\n而我想说，加密算法只是解决方案，我们首先要做的是理解我们的问题域——什么是安全？\n我个人的理解是：\nA与B通信的内容，有且只有A和B有能力看到通信的真正内容\n好，问题域已经定义好了（现实中当然不止这一种定义）。\b对于解决方案，很容易就想到了对消息进行加密。\n题外话，但是只有这一种方法吗？我看未必，说不定在将来会出现一种物质打破当前世界的通信假设，实现真正意义上的保密。\n对于A与B这样的简单通信模型，我们很容易做出选择： 这就是对称加密算法，其中图中的密钥S同时扮演加密和解密的角色。具体细节不是本文范畴。\n只要这个密钥S不公开给第三者，同时密钥S足够安全，我们就解决了我们一开始所定问题域了。因为世界上有且只有A与B知道如何加密和解密他们之间的消息。\n但是，在WWW环境下，我们的Web服务器的通信模型没有这么简单：\n如果服务器端对所有的客户端通信都使用同样的对称加密算法，无异于没有加密。那怎么办呢？**即能使用对称加密算法，又不公开密钥？**请读者思考21秒钟。😜\n\b答案是：Web服务器与每个客户端使用不同的对称加密算法：\n如何确定对称加密算法 慢着，另一个问题来了，我们的服务器端怎么告诉客户端该使用哪种对称加密算法？\n当然是通过协商。\n但是，你协商的过程是没有加密的，还是会被中间人拦截。那我们再对这个协商过程进行对称加密就好了，那你对协商过程加密的加密还是没有加密，怎么办？再加密不就好了……好吧，进行鸡生蛋蛋生鸡的问题了。\n如何对协商过程进行加密 新问题来了，如何对协商过程进行加密？\b密码学领域中，有一种称为“非对称加密”的加密算法，特点是私钥加密后的密文，只要是公钥，都可以解密，但是公钥加密后的密文，只有私钥可以解密。私钥只有一个人有，而公钥可以发给所有的人。\n虽然服务器端向A、B……的方向还是不安全的，但是至少A、B向服务器端方向是安全的。\n好了，如何协商加密算法的问题，我们解决了：使用非对称加密算法进行对称加密算法协商过程。\n这下，你明白为什么HTTPS同时需要对称加密算法和非对称加密算法了吧？\n协商什么加密算法 要达到Web服务器针对每个客户端使用不同的对称加密算法，同时，我们也不能让第三者知道这个对称加密算法是什么，怎么办？\n使用随机数，就是使用随机数来生成对称加密算法。这样就可以做到服务器和客户端每次交互都是新的加密算法、只有在交互的那一该才确定加密算法。\n这下，你明白为什么HTTPS协议握手阶段会有这么多的随机数了吧。\n如何得到公钥？ 细心的人可能已经注意到了如果使用非对称加密算法，我们的客户端A，B需要一开始就持有公钥，要不没法开展加密行为啊。\n这下，我们又遇到新问题了，如何让A、B客户端安全地得到公钥？\n我能想到的方案只有这些：\n方案1. 服务器端将公钥发送给每一个客户端\n方案2. 服务器端将公钥放到一个远程服务器，客户端可以请求得到\n我们选择方案1，因为方案2又多了一次请求，还要另外处理公钥的放置问题。\n公钥被调包了怎么办？又是一个鸡生蛋蛋生鸡问题？ 但是方案1有个问题：如果服务器端发送公钥给客户端时，被中间人调包了，怎么办？\n我画了张图方便理解：\n显然，让每个客户端的每个浏览器默认保存所有网站的公钥是不现实的。\n使用第三方机构的公钥解决鸡生蛋蛋生鸡问题 公钥被调包的问题出现，是因为我们的客户端无法分辨返回公钥的人到底是中间人，还是真的服务器。这其实就是密码学中提的身份验证问题。\n如果让你来解决，你怎么解决？如果你了解过HTTPS，会知道使用数字证书来解决。但是你想过证书的本质是什么么？请放下你对HTTPS已有的知识，自己尝试找到解决方案。\n我是这样解决的。既然服务器需要将公钥传给客户端，这个过程本身是不安全，那么我们为什么不对这个过程本身再加密一次？可是，你是使用对称加密，还是非对称加密？这下好了，我感觉又进了鸡生蛋蛋生鸡问题了。\n问题的难点是如果我们选择直接将公钥传递给客户端的方案，我们始终无法解决公钥传递被中间人调包的问题。\n所以，我们不能直接将服务器的公钥传递给客户端，而是第三方机构使用它的私钥对我们的公钥进行加密后，再传给客户端。客户端再使用第三方机构的公钥进行解密。\n下图就是我们设计的第一版“数字证书”，证书中只有服务器交给第三方机构的公钥，而且这个公钥被第三方机构的私钥加密了：\n如果能解密，就说明这个公钥没有被中间人调包。因为如果中间人使用自己的私钥加密后的东西传给客户端，客户端是无法使用第三方的公钥进行解密的。\n话到此，我以为解决问题了。但是现实中HTTPS，还有一个数字签名的概念，我没法理解它的设计理由。\n原来，我漏掉了一个场景：第三方机构不可能只给你一家公司制作证书，它也可能会给中间人这样有坏心思的公司发放证书。这样的，中间人就有机会对你的证书进行调包，客户端在这种情况下是无法分辨出是接收的是你的证书，还是中间人的。因为不论中间人，还是你的证书，都能使用第三方机构的公钥进行解密。像下面这样：\n第三方机构向多家公司颁发证书的情况： 客户端能解密同一家第三机构颁发的所有证书： 最终导致其它持有同一家第三方机构证书的中间人可以进行调包：\n数字签名，解决同一机构颁发的不同证书被篡改问题 要解决这个问题，我们首先要想清楚一个问题，辨别同一机构下不同证书的这个职责，我们应该放在哪？\n只能放到客户端了。意思是，客户端在拿到证书后，自己就有能力分辨证书是否被篡改了。如何才能有这个能力呢？\n我们从现实中找灵感。比如你是HR，你手上拿到候选人的学历证书，证书上写了持证人，颁发机构，颁发时间等等，同时证书上，还写有一个最重要的：证书编号！我们怎么鉴别这张证书是的真伪呢？只要拿着这个证书编号上相关机构去查，如果证书上的持证人与现实的这个候选人一致，同时证书编号也能对应上，那么就说明这个证书是真实的。\n我们的客户端能不能采用这个机制呢？像这样： 可是，这个“第三方机构”到底是在哪呢？是一个远端服务？不可能吧？如果是个远端服务，整个交互都会慢了。所以，这个第三方机构的验证功能只能放在客户端的本地了。\n客户端本地怎么验证证书呢？ 客户端本地怎么验证证书呢？答案是证书本身就已经告诉客户端怎么验证证书的真伪。\n也就是证书上写着如何根据证书的内容生成证书编号。客户端拿到证书后根据证书上的方法自己生成一个证书编号，如果生成的证书编号与证书上的证书编号相同，那么说明这个证书是真实的。\n同时，为避免证书编号本身又被调包，所以使用第三方的私钥进行加密。\n这地方有些抽象，我们来个图帮助理解：\n证书的制作如图所示。证书中的“编号生成方法MD5”就是告诉客户端：你使用MD5对证书的内容求值就可以得到一个证书编号。\n当客户端拿到证书后，开始对证书中的内容进行验证，如果客户端计算出来的证书编号与证书中的证书编号相同，则验证通过：\n但是第三方机构的公钥怎么跑到了客户端的机器中呢？世界上这么多机器。\n其实呢，现实中，浏览器和操作系统都会维护一个权威的第三方机构列表（包括它们的公钥）。因为客户端接收到的证书中会写有颁发机构，客户端就根据这个颁发机构的值在本地找相应的公钥。\n题外话：如果浏览器和操作系统这道防线被破了，就没办法。想想当年自己装过的非常规XP系统，都害怕。\n说到这里，想必大家已经知道上文所说的，证书就是HTTPS中数字证书，证书编号就是数字签名，而第三方机构就是指数字证书签发机构（CA）。\nCA如何颁发数字证书给服务器端的？ 当我听到这个问题时，我误以为，我们的SERVER需要发网络请求到CA部门的服务器来拿这个证书。😭 到底是我理解能力问题，还是。。\n其实，问题应该是CA如何颁发给我们的网站管理员，而我们的管理员又如何将这个数字证书放到我们的服务器上。\n我们如何向CA申请呢？每个CA机构都大同小异，我在网上找了一个： 拿到证书后，我们就可以将证书配置到自己的服务器上了。那么如何配置？这是具体细节了，留给大家google了。\n也许我们需要整理一下思路 我们通过推算的方式尝试还原HTTPS的设计过程。这样，我们也就明白了为什么HTTPS比HTTP多那么多次的交互，为什么HTTPS的性能会差，以及找到HTTPS的性能优化点。\n而上面一大堆工作都是为了让客户端与服务器端安全地协商出一个对称加密算法。这就是HTTPS中的SSL/TLS协议主要干的活。剩下的就是通信时双方使用这个对称加密算法进行加密解密。\n以下是一张HTTPS协议的真实交互图（从网上copy的，忘了从哪了，如果侵权麻烦告知）：\n能不能用一句话总结HTTPS？ 答案是不能，因为HTTPS本身实在太复杂。但是我还是尝试使用一段话来总结HTTPS:\nHTTPS要使客户端与服务器端的通信过程得到安全保证，必须使用的对称加密算法，但是协商对称加密算法的过程，需要使用非对称加密算法来保证安全，然而直接使用非对称加密的过程本身也不安全，会有中间人篡改公钥的可能性，所以客户端与服务器不直接使用公钥，而是使用数字证书签发机构颁发的证书来保证非对称加密过程本身的安全。这样通过这些机制协商出一个对称加密算法，就此双方使用该算法进行加密解密。从而解决了客户端与服务器端之间的通信安全问题。\n好长的一段话。\n后记 以上是个人为理解HTTPS而编造出来的自圆其说的看法。顶多只能算是HTTPS的科普文章。如有错误，请指出，万分感谢。\n那么，我为什么会觉得以这种方式理解HTTPS会更容易呢？我个人给出的答案是：当你自己为一家人做一次菜时，你就会理解妈妈天天做菜的不易了。\n学习资料：\nHTTPS为什么安全 \u0026amp;分析 HTTPS 连接建立全过程 数字证书的基础知识 理解 HTTPS HTTPS 是如何保证安全的？ 图解SSL/TLS协议 The First Few Milliseconds of an HTTPS Connection SSL/TLS原理详解 ","permalink":"https://showme.codes/zh-cn/2017-2-20-understand-https/","summary":"\u003cp\u003e\u003cem\u003e摘要：本文尝试一步步\u003cstrong\u003e还原\u003c/strong\u003eHTTPS的设计过程，以理解为什么HTTPS最终会是这副模样。但是这并不代表HTTPS的真实设计过程。在阅读本文时，你可以尝试放下已有的对HTTPS的理解，这样更利于“还原”过程。\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e我们先不了聊HTTP，HTTPS，我们先从一个聊天软件说起，我们要实现A能发一个hello消息给B：\n\u003cimg loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-b93c4670333eb8d9.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如果我们要实现这个聊天软件，本文只考虑安全性问题，要实现\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eA发给B的hello消息包，即使被中间人拦截到了，也无法得知消息的内容\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"如何做到真正的安全\"\u003e如何做到真正的安全？\u003c/h3\u003e\n\u003cp\u003e这个问题，很多人马上就想到了各种加密算法，什么对称加密、非对称加密、DES、RSA、XX、噼里啪啦~\u003c/p\u003e\n\u003cp\u003e而我想说，加密算法只是\u003cstrong\u003e解决方案\u003c/strong\u003e，我们首先要做的是理解我们的\u003cstrong\u003e问题域\u003c/strong\u003e——\u003cstrong\u003e什么是安全？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e我个人的理解是：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eA与B通信的内容，有且只有A和B有能力看到通信的真正内容\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e好，问题域已经定义好了（现实中当然不止这一种定义）。\b对于解决方案，很容易就想到了对消息进行加密。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e题外话，但是只有这一种方法吗？我看未必，说不定在将来会出现一种物质打破当前世界的通信假设，实现真正意义上的保密。\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e对于A与B这样的简单通信模型，我们很容易做出选择：\n\u003cimg alt=\"对称加密算法\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-9d943956300560f2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这就是\u003cstrong\u003e对称加密算法\u003c/strong\u003e，其中图中的\u003cstrong\u003e密钥S\u003c/strong\u003e同时扮演加密和解密的角色。具体细节不是本文范畴。\u003c/p\u003e\n\u003cp\u003e只要这个密钥S不公开给第三者，同时密钥S足够安全，我们就解决了我们一开始所定问题域了。因为世界上有且只有A与B知道如何加密和解密他们之间的消息。\u003c/p\u003e\n\u003cp\u003e但是，在WWW环境下，我们的Web服务器的通信模型没有这么简单：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Web服务器使用对称加密算法\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-528782117a69a5dc.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如果服务器端对所有的客户端通信都使用同样的对称加密算法，无异于没有加密。那怎么办呢？**即能使用对称加密算法，又不公开密钥？**请读者思考21秒钟。😜\u003c/p\u003e\n\u003cp\u003e\b答案是：Web服务器与每个客户端使用不同的对称加密算法：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Web服务器与每个客户端使用不同对称加密算法\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-d38dbcf3633a1cc9.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"如何确定对称加密算法\"\u003e如何确定对称加密算法\u003c/h3\u003e\n\u003cp\u003e慢着，另一个问题来了，\u003cstrong\u003e我们的服务器端怎么告诉客户端该使用哪种对称加密算法？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e当然是通过协商。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"协商对称加密算法\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-88ce3bd16ac6006d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e但是，你协商的过程是没有加密的，还是会被中间人拦截。那我们再对这个协商过程进行对称加密就好了，那你对协商过程加密的加密还是没有加密，怎么办？再加密不就好了……好吧，进行鸡生蛋蛋生鸡的问题了。\u003c/p\u003e\n\u003ch3 id=\"如何对协商过程进行加密\"\u003e如何对协商过程进行加密\u003c/h3\u003e\n\u003cp\u003e新问题来了，如何对协商过程进行加密？\b密码学领域中，有一种称为“非对称加密”的加密算法，特点是\u003cstrong\u003e私钥加密后的密文，只要是公钥，都可以解密，但是公钥加密后的密文，只有私钥可以解密。私钥只有一个人有，而公钥可以发给所有的人\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"非对称加密\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-ae678fa7d569dac9.png\"\u003e\u003c/p\u003e\n\u003cp\u003e虽然服务器端向A、B……的方向还是不安全的，但是至少A、B向服务器端方向是安全的。\u003c/p\u003e\n\u003cp\u003e好了，如何协商加密算法的问题，我们解决了：使用非对称加密算法进行对称加密算法协商过程。\u003c/p\u003e\n\u003cp\u003e这下，你明白为什么HTTPS同时需要对称加密算法和非对称加密算法了吧？\u003c/p\u003e\n\u003ch3 id=\"协商什么加密算法\"\u003e协商什么加密算法\u003c/h3\u003e\n\u003cp\u003e要达到Web服务器针对每个客户端使用不同的对称加密算法，同时，我们也不能让第三者知道这个对称加密算法是什么，怎么办？\u003c/p\u003e\n\u003cp\u003e使用随机数，就是使用随机数来生成对称加密算法。这样就可以做到服务器和客户端每次交互都是新的加密算法、只有在交互的那一该才确定加密算法。\u003c/p\u003e\n\u003cp\u003e这下，你明白为什么HTTPS协议握手阶段会有这么多的随机数了吧。\u003c/p\u003e\n\u003ch3 id=\"如何得到公钥\"\u003e如何得到公钥？\u003c/h3\u003e\n\u003cp\u003e细心的人可能已经注意到了如果使用非对称加密算法，我们的客户端A，B需要一开始就持有公钥，要不没法开展加密行为啊。\u003c/p\u003e\n\u003cp\u003e这下，我们又遇到新问题了，如何让A、B客户端安全地得到公钥？\u003c/p\u003e\n\u003cp\u003e我能想到的方案只有这些：\u003c/p\u003e\n\u003cp\u003e方案1. 服务器端将公钥发送给每一个客户端\u003c/p\u003e\n\u003cp\u003e方案2. 服务器端将公钥放到一个远程服务器，客户端可以请求得到\u003c/p\u003e\n\u003cp\u003e我们选择方案1，因为方案2又多了一次请求，还要另外处理公钥的放置问题。\u003c/p\u003e\n\u003ch3 id=\"公钥被调包了怎么办又是一个鸡生蛋蛋生鸡问题\"\u003e公钥被调包了怎么办？又是一个鸡生蛋蛋生鸡问题？\u003c/h3\u003e\n\u003cp\u003e但是方案1有个问题：如果服务器端发送公钥给客户端时，被中间人调包了，怎么办？\u003c/p\u003e\n\u003cp\u003e我画了张图方便理解：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"中间人将公钥拦截了\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-8f0d8228af2f6480.png\"\u003e\u003c/p\u003e\n\u003cp\u003e显然，让每个客户端的每个浏览器默认保存所有网站的公钥是不现实的。\u003c/p\u003e\n\u003ch3 id=\"使用第三方机构的公钥解决鸡生蛋蛋生鸡问题\"\u003e使用第三方机构的公钥解决鸡生蛋蛋生鸡问题\u003c/h3\u003e\n\u003cp\u003e公钥被调包的问题出现，是因为我们的客户端无法分辨返回公钥的人到底是中间人，还是真的服务器。这其实就是密码学中提的\u003cstrong\u003e身份验证\u003c/strong\u003e问题。\u003c/p\u003e\n\u003cp\u003e如果让你来解决，你怎么解决？如果你了解过HTTPS，会知道使用数字证书来解决。但是你想过证书的本质是什么么？请放下你对HTTPS已有的知识，自己尝试找到解决方案。\u003c/p\u003e\n\u003cp\u003e我是这样解决的。既然服务器需要将公钥传给客户端，这个过程本身是不安全，那么我们为什么不对这个过程本身再加密一次？可是，你是使用对称加密，还是非对称加密？这下好了，我感觉又进了鸡生蛋蛋生鸡问题了。\u003c/p\u003e\n\u003cp\u003e问题的难点是如果我们选择直接将公钥传递给客户端的方案，我们始终无法解决公钥传递被中间人调包的问题。\u003c/p\u003e\n\u003cp\u003e所以，我们不能直接将服务器的公钥传递给客户端，而是第三方机构使用它的私钥对我们的公钥进行加密后，再传给客户端。客户端再使用第三方机构的公钥进行解密。\u003c/p\u003e\n\u003cp\u003e下图就是我们设计的第一版“数字证书”，证书中只有服务器交给第三方机构的公钥，而且这个公钥被第三方机构的私钥加密了：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"第一版数字证书的内容\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-f3dd4b7370df950e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如果能解密，就说明这个公钥没有被中间人调包。因为如果中间人使用自己的私钥加密后的东西传给客户端，客户端是无法使用第三方的公钥进行解密的。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"开始引入第三方机构\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-3b7c21b3525c0e64.png\"\u003e\u003c/p\u003e\n\u003cp\u003e话到此，我以为解决问题了。但是现实中HTTPS，还有一个数字签名的概念，我没法理解它的设计理由。\u003c/p\u003e\n\u003cp\u003e原来，我漏掉了一个场景：第三方机构不可能只给你一家公司制作证书，它也可能会给中间人这样有坏心思的公司发放证书。这样的，中间人就有机会对你的证书进行调包，客户端在这种情况下是无法分辨出是接收的是你的证书，还是中间人的。因为不论中间人，还是你的证书，都能使用第三方机构的公钥进行解密。像下面这样：\u003c/p\u003e\n\u003cp\u003e第三方机构向多家公司颁发证书的情况：\n\u003cimg alt=\"第三方机构向多家公司颁发证书的情况\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-66e00dc26cea3112.png\"\u003e\u003c/p\u003e\n\u003cp\u003e客户端能解密同一家第三机构颁发的所有证书：\n\u003cimg alt=\"客户端能解密同一家第三机构颁发的所有证书\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-5bf2c898914e749f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e最终导致其它持有同一家第三方机构证书的中间人可以进行调包：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"证书依然可以被中间人调包\" loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-b2bd3805bc25fd2c.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"数字签名解决同一机构颁发的不同证书被篡改问题\"\u003e数字签名，解决同一机构颁发的不同证书被篡改问题\u003c/h3\u003e\n\u003cp\u003e要解决这个问题，我们首先要想清楚一个问题，辨别同一机构下不同证书的这个职责，我们应该放在哪？\u003c/p\u003e\n\u003cp\u003e只能放到客户端了。意思是，客户端在拿到证书后，自己就有能力分辨证书是否被篡改了。如何才能有这个能力呢？\u003c/p\u003e\n\u003cp\u003e我们从现实中找灵感。比如你是HR，你手上拿到候选人的学历证书，证书上写了持证人，颁发机构，颁发时间等等，同时证书上，还写有一个最重要的：证书编号！我们怎么鉴别这张证书是的真伪呢？只要拿着这个证书编号上相关机构去查，如果证书上的持证人与现实的这个候选人一致，同时证书编号也能对应上，那么就说明这个证书是真实的。\u003c/p\u003e\n\u003cp\u003e我们的客户端能不能采用这个机制呢？像这样：\n\u003cimg loading=\"lazy\" src=\"/assets/images/2017-2-20-292372-ac2b0d452a4e8b05.png\"\u003e\u003c/p\u003e\n\u003cp\u003e可是，这个“第三方机构”到底是在哪呢？是一个远端服务？不可能吧？如果是个远端服务，整个交互都会慢了。所以，这个第三方机构的验证功能只能放在客户端的本地了。\u003c/p\u003e","title":"也许，这样理解HTTPS更容易"},{"content":"导致CPU100%的原因很多，而程序中出现死循环就是原因之一。然而，并不是每个人在工作中都有机会踩中这个坑。我就是其中一个没踩过的。人生似乎有些不完整。\n所以，我做了一个很重要的决定：在程序中写一个死循环。看看会发生什么事情。\n当然，不是在生产环境。😜 我搭建了一个实验环境来做实验。只是这个实验环境不仅可以用于这个死循环实验。以下是这个环境的结构图：\n还是老样子，使用Vagrant + Virtualbox + Ansible自动化搭环境。代码及搭建步骤在文末。\n我们会写一个简单的Spring MVC 应用，然后其中一个接口里会有死循环代码：\n@RequestMapping(value = \u0026#34;/loop\u0026#34;, method = RequestMethod.GET, produces = \u0026#34;application/json; charset=UTF-8\u0026#34;) public void endlessLoop() { int i = 0; while (true) { System.out.println(i += 1); } } 以下是我自己尝试找出这个死循环的过程。\n使用top，查看是哪个进程的问题 我请求一次：http://192.168.88.10:9898/web/loop\n然后，我打开新窗口，又请求一次\n这里，我好奇CPU没有到200%。一直在120%和130%之间。P.S. 我一定是某个知识点不牢固，要不，不会有这个疑问。\n堆空间 因为不涉及JVM堆空间问题，执行 jstat -gcutil 32593 1s 没看出什么问题。32593为Java进程ID，1s指1秒抽样一次。\n栈 堆没问题，就看看是哪个线程占用得高。\n列出java进程的线程，top -H -p \u0026lt;java 进程pid\u0026gt; 将jvm的栈dump下来 jstack -l \u0026lt;其中一个线程PID\u0026gt; \u0026gt;\u0026gt; stack.log，这里我选3596。\n在日志中，找到相应的线程 我们需要从栈日志中找到相应的线程，但由于栈日志中使用的16进制，但是top中的PID又是10进制，所以，需要手工将10进制的PID转成16进制。3596的16进制转是0xe0c 小结 好吧。我没有因为写这个死循环去看10小时的无聊电影。\n附录：\n代码：performance-labs 准备环境：虚拟机的账号密码都是_vagrant_ git clone git@github.com:zacker330/performance-labs.git vagrant up download jdk8 to ansible/roles/jdk8/files: https://pan.baidu.com/s/1bpxfpvD ansible-playbook ./ansible/playbook.yml -i ./ansible/inventory -u vagrant -k ansible-playbook ./ansible/init-mysql.yml -i ./ansible/inventory -u vagrant -k cd ansible;chmode +x ./buildwarfile.sh;./buildwarfile.sh \u0026ndash;\u0026gt; 将会提示输入vagrant密码 访问：http://192.168.88.10:9898/web/ ","permalink":"https://showme.codes/zh-cn/2017-2-17-endless-loop-cpu100/","summary":"\u003cp\u003e导致CPU100%的原因很多，而程序中出现死循环就是原因之一。然而，并不是每个人在工作中都有机会踩中这个坑。我就是其中一个没踩过的。人生似乎有些不完整。\u003c/p\u003e\n\u003cp\u003e所以，我做了一个很重要的决定：\u003cstrong\u003e在程序中写一个死循环。看看会发生什么事情\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e当然，不是在生产环境。😜 我搭建了一个实验环境来做实验。只是这个实验环境\u003cstrong\u003e不仅\u003c/strong\u003e可以用于这个死循环实验。以下是这个环境的结构图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"实验室结构\" loading=\"lazy\" src=\"/assets/images/2017-2-17-292372-4c451d9ef3b37ab1.png\"\u003e\n还是老样子，使用Vagrant + Virtualbox + Ansible自动化搭环境。代码及搭建步骤在文末。\u003c/p\u003e\n\u003cp\u003e我们会写一个简单的Spring MVC 应用，然后其中一个接口里会有死循环代码：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e    @RequestMapping(value = \u0026#34;/loop\u0026#34;, method = RequestMethod.GET, produces = \u0026#34;application/json; charset=UTF-8\u0026#34;)\n    public void endlessLoop() {\n        int i = 0;\n        while (true) {\n            System.out.println(i += 1);\n        }\n    }\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e以下是我自己尝试找出这个死循环的过程。\u003c/p\u003e\n\u003ch3 id=\"使用top查看是哪个进程的问题\"\u003e使用top，查看是哪个进程的问题\u003c/h3\u003e\n\u003cp\u003e我请求一次：\u003ccode\u003ehttp://192.168.88.10:9898/web/loop\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"07:13:36 cpu100%了\" loading=\"lazy\" src=\"/assets/images/2017-2-17-292372-e7afb1a7f5524fec.png\"\u003e\u003c/p\u003e\n\u003cp\u003e然后，我打开新窗口，又请求一次\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"07:22:28 CPU120%~130%之间\" loading=\"lazy\" src=\"/assets/images/2017-2-17-292372-864eaa6befc3bdd5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这里，我好奇CPU没有到200%。一直在120%和130%之间。P.S. 我一定是某个知识点不牢固，要不，不会有这个疑问。\u003c/p\u003e\n\u003ch3 id=\"堆空间\"\u003e堆空间\u003c/h3\u003e\n\u003cp\u003e因为不涉及JVM堆空间问题，执行 \u003ccode\u003ejstat -gcutil 32593 1s\u003c/code\u003e 没看出什么问题。32593为Java进程ID，1s指1秒抽样一次。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"查看堆空间GC情况\" loading=\"lazy\" src=\"/assets/images/2017-2-17-292372-a4d954f013d39fb6.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"栈\"\u003e栈\u003c/h3\u003e\n\u003cp\u003e堆没问题，就看看是哪个线程占用得高。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e列出java进程的线程，\u003ccode\u003etop -H -p \u0026lt;java 进程pid\u0026gt;\u003c/code\u003e\n\u003cimg alt=\"找到CPU占用高的线程PID\" loading=\"lazy\" src=\"/assets/images/2017-2-17-292372-1af95af5be74988e.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e将jvm的栈dump下来\n\u003ccode\u003ejstack -l \u0026lt;其中一个线程PID\u0026gt; \u0026gt;\u0026gt; stack.log\u003c/code\u003e，这里我选3596。\u003c/p\u003e","title":"我故意写了个死循环"},{"content":"摘要：John McCarthy身为Lisp之父和人工智能之父，同时，他也是GC之父。1960年，他在其论文中首次发布了GC算法（其实是委婉的提出😂）。而Java的前身Oak是在1990发布的，利用JVM实现了跨平台。GC因此一举成名。\n最近想复习一下JVM的知识。然后发现网上不少文章在写JVM的垃圾回收算法时，都比较偏向于具体实现，而少有站在更高角度来看垃圾回收算法的文章。而我本人想对垃圾回收算法有个全景的认识，所以，就找到了这本《垃圾回收的算法与实现》（以下简称《垃圾回收》）。本篇博客就是尝试对“全景”的总结。\n以下为方便讨论，垃圾回收缩写成GC。\n为什么要有GC 我时而听到C++程序员说我们是被GC惯坏了的一代。的确是这样的，我本人在学习GC算法时，大脑里第一问题就是为什么需要GC这样的东西。说明我已经认为GC是理所当然了。😂\n总的一句话：没有GC的世界，我们需要手动进行内存管理，而手动内存管理是纯技术活，又容易出错。\n既然我们写的大多程序都是为了解决现实业务问题，那么，我们为什么不把这种纯技术活自动化呢？但是自动化，也是有代价的。 这是我的个人理解，不代表John McCarthy本人的理解。\n“垃圾”的定义 首先，我们要给个“垃圾”的定义，才能进行回收吧。书中给出的定义：\n把分配到堆中那些不能通过程序引用的对象称为非活动对象，也就是死掉的对象，我们称为“垃圾”。\nGC的定义 因为我们期望让内存管理变得自动（只管用内存，不管内存的回收），我们就必须做两件事情：\n找到内存空间里的垃圾 回收垃圾，让程序员能再次利用这部分空间 [1] 只要满足这两项功能的程序，就是GC，不论它是在JVM中，还是在Ruby的VM中。 但这只是两个需求，并没有说明GC应该何时找垃圾，何时回收垃圾等等更具体的问题，各类GC算法就是在这些更具体问题的处理方式上施展手脚。\nGC的历史 John McCarthy身为Lisp之父和人工智能之父，同时，他也是GC之父。1960年，他在其论文中首次发布了GC算法（其实是委婉的提出😂）。\n标记-清除算法 由John McCarthy在1960年提出 引用计数法 由George E. Collins在1960年提出 此算法会有循环引用问题，Harold McBeth 1963年指出。 复制算法 由Marvin L. Minsky在1963年提出 《垃圾回收》的作者认为：\n从50年前GC算法首次发布以来，众多研究者对其进行了各种各样的研究，因此许多GC算法也得以发布。[2] 但事实上，这些算法只不过是把前文中提到的三种算法进行组合或应用。也可以这么说，1963年GC复制算法诞生时，GC的根本性内容就已经完成了。[3]\n那我们常常听说的分代垃圾回收又是怎么回事？作者是这样说的：\n人们从众多程序案例中总结出了一个经验：“大部分的对象在生成后马上就变成了垃圾，很少有对象能活得很久”。分代垃圾回收利用该经验，在对象中导入了“年龄”的概念，经历过一次GC后活下来的对象年龄为1岁。[4]\n分代垃圾回收中把对象分类成几代，针对不同的代使用不同的GC算法，我们把刚生成的对象称为新生代对象，到达一定年龄的对象则称为老年代对象。[5]\n好了，这下我总算知道为什么要分代了，我的总结是： 将对象根据存活概率进行分类，对存活时间长一些的对象，可以减少扫描“垃圾”的时间，以减少GC频率和时长。根本思路就是对对象进行分类，才能针对各个分类采用不同的垃圾回收算法，以对各算法进行扬长避短。\n留一个问题给读者：我们知道分代垃圾回收所采用的堆结构是：\n为什么新生代空间要分成“生成空间”和“幸存空间”，而幸存空间又分成两块大小相等的幸存空间1，幸存空间2?\n这些GC算法共同解决的问题 上面我们说了，GC的定义只给出了需求，三种算法都为实现这个需求，那么它们总会遇到共同要解决的问题吧？ 我尝试总结出：\n如何分辨出什么是垃圾？ 如何、何时搜索垃圾？ 如何、何时清除垃圾？ 这样，只要涉及到垃圾回收，我就可以从这2点需求，3个共同问题（两点三共）出发来讨论、学习。\n如何评价GC算法？ 如果没有评价标准，我们当然无法评估这些GC算法的性能。作者给出了4个标准：\n吞吐量: 单位时间内的处理能力 最大暂停时间：GC执行过程中，应用暂停的时长。 较大的吞吐量和较短的最大暂停时间不可兼得 堆的使用效率：就是堆空间的利用率。 可用的堆越大，GC运行越快；相反，越想有效地利用有限的堆，GC花费的时间就越长。 访问的局部性：把具有引用关系的对象安排在堆中较近的位置，就能提高在缓存中读取到想利用的数据的概率。 好吧。两点三共，四标~\n小结 搞清楚为什么要GC，要实现GC都要解决什么问题，而各类算法又是怎么解决的，最后怎么评价这些算法。GC原来是这么回事。\n但是这不是GC的全部。但是提供我一个思考GC的思考框架。\n以上就是《垃圾回收的算法与实现》的读书笔记。如果想更深入，可以阅读《垃圾回收算法手册:自动内存管理的艺术》。\n[1] 《垃圾回收的算法与实现》 P2\n[2][3] 《垃圾回收的算法与实现》 P4\n[4] 《垃圾回收的算法与实现》 P141\n[5] 《垃圾回收的算法与实现》 P142\n","permalink":"https://showme.codes/zh-cn/2017-2-4-what-is-gc/","summary":"\u003cp\u003e摘要：John McCarthy身为Lisp之父和人工智能之父，同时，他也是GC之父。1960年，他在其\u003ca href=\"http://www-formal.stanford.edu/jmc/recursive/node4.html#tex2html8\"\u003e论文\u003c/a\u003e中首次发布了GC算法（其实是委婉的提出😂）。而\u003ca href=\"https://zh.wikipedia.org/wiki/Java\"\u003eJava\u003c/a\u003e的前身Oak是在1990发布的，利用JVM实现了跨平台。GC因此一举成名。\u003c/p\u003e\n\u003cp\u003e最近想复习一下JVM的知识。然后发现网上不少文章在写JVM的垃圾回收算法时，都比较偏向于具体实现，而少有站在更高角度来看垃圾回收算法的文章。而我本人想对垃圾回收算法有个全景的认识，所以，就找到了这本《垃圾回收的算法与实现》（以下简称《垃圾回收》）。本篇博客就是尝试对“全景”的总结。\u003c/p\u003e\n\u003cp\u003e以下为方便讨论，垃圾回收缩写成GC。\u003c/p\u003e\n\u003ch3 id=\"为什么要有gc\"\u003e为什么要有GC\u003c/h3\u003e\n\u003cp\u003e我时而听到C++程序员说我们是被GC惯坏了的一代。的确是这样的，我本人在学习GC算法时，大脑里第一问题就是为什么需要GC这样的东西。说明我已经认为GC是理所当然了。😂\u003c/p\u003e\n\u003cp\u003e总的一句话：没有GC的世界，我们需要手动进行内存管理，而手动内存管理是纯技术活，又容易出错。\u003c/p\u003e\n\u003cp\u003e既然我们写的大多程序都是为了解决现实业务问题，那么，我们为什么不把这种纯技术活自动化呢？但是自动化，也是有代价的。\n\u003cstrong\u003e这是我的个人理解，不代表John McCarthy本人的理解\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 id=\"垃圾的定义\"\u003e“垃圾”的定义\u003c/h3\u003e\n\u003cp\u003e首先，我们要给个“垃圾”的定义，才能进行回收吧。书中给出的定义：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e把分配到堆中那些不能通过程序引用的对象称为非活动对象，也就是死掉的对象，我们称为“垃圾”。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"gc的定义\"\u003eGC的定义\u003c/h3\u003e\n\u003cp\u003e因为我们期望让内存管理变得自动（只管用内存，不管内存的回收），我们就必须做两件事情：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003col\u003e\n\u003cli\u003e找到内存空间里的垃圾\u003c/li\u003e\n\u003cli\u003e回收垃圾，让程序员能再次利用这部分空间 [1]\n只要满足这两项功能的程序，就是GC，不论它是在JVM中，还是在Ruby的VM中。\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e但这只是两个需求，并没有说明GC应该何时找垃圾，何时回收垃圾等等更具体的问题，各类GC算法就是在这些更具体问题的处理方式上施展手脚。\u003c/p\u003e\n\u003ch3 id=\"gc的历史\"\u003eGC的历史\u003c/h3\u003e\n\u003cp\u003eJohn McCarthy身为Lisp之父和人工智能之父，同时，他也是GC之父。1960年，他在其\u003ca href=\"http://www-formal.stanford.edu/jmc/recursive/node4.html#tex2html8\"\u003e论文\u003c/a\u003e中首次发布了GC算法（其实是委婉的提出😂）。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"http://www-formal.stanford.edu/jmc/recursive/recursive.html\"\u003e标记-清除算法\u003c/a\u003e 由John McCarthy在1960年提出\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://dl.acm.org/citation.cfm?id=367501\u0026amp;dl=ACM\u0026amp;coll=DL\u0026amp;CFID=895960203\u0026amp;CFTOKEN=65936422\"\u003e引用计数法\u003c/a\u003e 由George E. Collins在1960年提出\n此算法会有循环引用问题，\u003ca href=\"http://dl.acm.org/citation.cfm?id=367649\"\u003eHarold McBeth\u003c/a\u003e 1963年指出。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://dl.acm.org/citation.cfm?id=888858\"\u003e复制算法\u003c/a\u003e 由Marvin L. Minsky在1963年提出\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e《垃圾回收》的作者认为：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e从50年前GC算法首次发布以来，众多研究者对其进行了各种各样的研究，因此许多GC算法也得以发布。[2]\n但事实上，这些算法只不过是把前文中提到的三种算法进行组合或应用。也可以这么说，1963年GC复制算法诞生时，GC的根本性内容就已经完成了。[3]\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e那我们常常听说的分代垃圾回收又是怎么回事？作者是这样说的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e人们从众多程序案例中总结出了一个经验：“大部分的对象在生成后马上就变成了垃圾，很少有对象能活得很久”。分代垃圾回收利用该经验，在对象中导入了“年龄”的概念，经历过一次GC后活下来的对象年龄为1岁。[4]\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e分代垃圾回收中把对象分类成几代，针对不同的代使用不同的GC算法，我们把刚生成的对象称为新生代对象，到达一定年龄的对象则称为老年代对象。[5]\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e好了，这下我总算知道为什么要分代了，我的总结是： 将对象根据存活概率进行分类，对存活时间长一些的对象，可以减少扫描“垃圾”的时间，以减少GC频率和时长。\u003cstrong\u003e根本思路就是对对象进行分类，才能针对各个分类采用不同的垃圾回收算法，以对各算法进行扬长避短。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e留一个问题给读者：我们知道分代垃圾回收所采用的堆结构是：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Paste_Image.png\" loading=\"lazy\" src=\"/assets/images/gc1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e为什么新生代空间要分成“生成空间”和“幸存空间”，而幸存空间又分成两块大小相等的幸存空间1，幸存空间2?\u003c/p\u003e\n\u003ch3 id=\"这些gc算法共同解决的问题\"\u003e这些GC算法共同解决的问题\u003c/h3\u003e\n\u003cp\u003e上面我们说了，GC的定义只给出了需求，三种算法都为实现这个需求，那么它们总会遇到共同要解决的问题吧？ 我尝试总结出：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e如何分辨出什么是垃圾？\u003c/li\u003e\n\u003cli\u003e如何、何时搜索垃圾？\u003c/li\u003e\n\u003cli\u003e如何、何时清除垃圾？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这样，只要涉及到垃圾回收，我就可以从这2点需求，3个共同问题（两点三共）出发来讨论、学习。\u003c/p\u003e\n\u003ch3 id=\"如何评价gc算法\"\u003e如何评价GC算法？\u003c/h3\u003e\n\u003cp\u003e如果没有评价标准，我们当然无法评估这些GC算法的性能。作者给出了4个标准：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e吞吐量: 单位时间内的处理能力\u003c/li\u003e\n\u003cli\u003e最大暂停时间：GC执行过程中，应用暂停的时长。\n较大的吞吐量和较短的最大暂停时间不可兼得\u003c/li\u003e\n\u003cli\u003e堆的使用效率：就是堆空间的利用率。\n可用的堆越大，GC运行越快；相反，越想有效地利用有限的堆，GC花费的时间就越长。\u003c/li\u003e\n\u003cli\u003e访问的局部性：把具有引用关系的对象安排在堆中较近的位置，就能提高在缓存中读取到想利用的数据的概率。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e好吧。两点三共，四标~\u003c/p\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e搞清楚为什么要GC，要实现GC都要解决什么问题，而各类算法又是怎么解决的，最后怎么评价这些算法。GC原来是这么回事。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e但是这不是GC的全部。但是提供我一个思考GC的思考框架。\u003c/p\u003e\n\u003cp\u003e以上就是《垃圾回收的算法与实现》的读书笔记。如果想更深入，可以阅读《垃圾回收算法手册:自动内存管理的艺术》。\u003c/p\u003e\n\u003cp\u003e[1] 《垃圾回收的算法与实现》 P2\u003c/p\u003e","title":"垃圾回收原来是这么回事"},{"content":"像学习Spark这类大数据平台，搭建环境，是一件很费时费力的事情。特别是当你想使用多台机器模拟真实生产环境时。\n为了更有效的学习Spark，我决定将自己的学习环境按生产环境的要求来搭建。但是真实生产环境的群集往往由多个集群组成：Hadoop/Hbase集群、Zookeeper集群、Spark集群。掐指一算，至少需要6台机器了。\n我们真的需要买6台机器吗？当然不是，我们只需要在自己的电脑上虚拟化出6台就好了。而我的电脑只有16G，虚拟化6台太吃力了。最终，我决定搭建成以下结构：\n以下是搭建过程：\n环境的搭建 按以前学习像Spring这些类Web，开发环境的搭建非常简单，也就引入几个依赖，添加几项配置，就好了。\n但是学习Spark，我敢\b肯定不少人在环境搭建这一环节踩坑。正因为这样，才会有此博客。\nSpark不是一框架，而是一个分布式的计算平台。所以，你不能通过引入依赖，添加配置就完成搭建。需要先将Spark这个平台部署起来。Spark支持4种部署方式：\n单机：同一台机器，同一进程，不同线程运行Master和Worker 伪分布式：同一台机器，不同进程分别运行Master和Worker Standalone方式：多台机器分别运行Master和Worker，自己解决资源调度 Yarn和Mesos方式：将资源调度这一职责外包出去 虽然Spark的单机部署方式很简单，但是没有人会在生产环境上使用单机部署方式。而伪分布式我见不少人搭建，以此为基础来学习Spark。但我不推荐。\n因为在线上真正运行的是Standalone、Yarn、Mesos方式，也称为完全分布式的方式。只有一开始就使用完全分布式的方式来进行开发调试，你才会学习到生产环境会遇到什么问题。\n机器准备 \b如果采取完全分布式的部署方式来学习，你必须准备\b很多台机器，就像上面所说的。\n我想大多数人都会选择虚拟化方案来得到多台机器。我推荐Virtualbox。\n我见不少人手工的创建一台机器，然后安装操作系统，接着想要多少台机器，就复制几台，甚至还要分别进入机器修改每台机器的IP。。。\n这样的方式，效率低，又很难与你的同事分享你的环境（也就是统一一个团队的开发环境，以避免不同开发环境不同引起的问题）。\n所以，我一开始就使用Vagrant。把机器的虚拟化这一动作进行自动化和版本化（提交到git仓库中）。使用了Vagrant，你只需要在Vagrantfile中定义机器数据、机器的系统镜像、CPU个数、内存，然后执行vagrant up，就可以得到你想要的机器了。要与同事统一这些机器，只需要他使用相同的Vagrantfile就好了。 同时，这样，还能实现：统一开发环境与生产环境使用同样或相近的机器环境。\n以下是一个Vagrantfile样例：\nVagrant.configure(2) do |config| VAGRANT_VM_PROVIDER = \u0026#34;virtualbox\u0026#34; machine_box = \u0026#34;boxcutter/ubuntu1604\u0026#34; -\u0026gt; 系统镜像 config.vm.define \u0026#34;offlinenode1\u0026#34; do |machine| machine.vm.box = machine_box machine.vm.hostname = \u0026#34;offlinenode1\u0026#34; machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.11.151\u0026#34; -\u0026gt; 指定IP machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node| node.name = \u0026#34;offlinenode1\u0026#34; node.memory = 4096 -\u0026gt; 指定内存 node.cpus = 2 -\u0026gt; 指定CPU个数 end end config.vm.define \u0026#34;offlinenode2\u0026#34; do |machine| machine.vm.box = machine_box machine.vm.hostname = \u0026#34;offlinenode2\u0026#34; machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.11.152\u0026#34; machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node| node.name = \u0026#34;offlinenode2\u0026#34; node.memory = 4096 node.cpus = 2 end end ....... 还可以定义很多这样的机器 end 搭建Spark集群 在准备好机器后，接下来做的就是搭建Spark集群。我会选择Ansible来实现自动化搭建，而不是一台台机器登上去，一条条命令的执行安装。\n那么，只是学习阶段，我为什么要自动化呢？正因为在学习阶段，我们更要自动化搭建过程。作为新手很容易把环境弄乱了，又没法一下子查到原因。但是自动化后，意味着版本化了搭建脚本，查原因时，只要对比版本库就好了。\n同时，也因为我要搭建的是Spark完全分布式，需要上3台机器，除了安装Spark，还需要安装Hadoop。如果不自动化这整个过程，学习过程会浪费很多时间在重复工作上。\n题外话：很多人反对项目开始时就考虑自动化所有的部署流程，理由是成本高（指人力成本），先实现功能再说。这两点理由是站不住脚的，因为如果一开始不自动化，你后期返回来再补，成本会更高。因为会有历史负担！\n监控集群 为什么我们要学习过程中就加上监控？写出刚刚能运行的Spark应用，不难，但是谁知道你写的应用的性能如何，有没有发挥所有机器的作用呢？所以，我在一开始就会加上监控。 目前，我还没有完成这部分工作。\n自动化Submit提交Spark应用 在搭建好了Spark集群后，我们就可以写Spark应用，然后将应用提交到Spark集群中运行。我们采用集群模式来submit spark应用，在集群中某台Spark node上手工执行命令来提交：\n./bin/spark-submit \\ --class codes.showme.HbaseExample \\ --master spark://192.168.11.153:7077 \\ --deploy-mode cluster --executor-memory 1G \\ --total-executor-cores 2 \\ /home/spark/spark/example.jar 如果不自动这个过程，你需要做：\n在开发环境将应用打成jar包 手工将jar包copy上指定机器指定路径 执行命令 所以，我又将这个过程写成了Ansible脚本，你只需要在./ansible/下执行： ./deploy-hbase-example.sh 就完成submit的操作了。\n最后，我们的应用如果要上CI，完全没有压力！\n小结 以上是我个人的Spark学习环境搭建方法。希望有经验的同学能多多指教。 这是最终搭建好的环境：spark2-hadoop2.6-hbase-labs\n祝大家学习愉快。\n","permalink":"https://showme.codes/zh-cn/2017-1-31-setup-spark-dev-env/","summary":"\u003cp\u003e像学习Spark这类大数据平台，搭建环境，是一件很费时费力的事情。特别是当你想使用多台机器模拟真实生产环境时。\u003c/p\u003e\n\u003cp\u003e为了更有效的学习Spark，我决定将自己的学习环境按生产环境的要求来搭建。但是真实生产环境的群集往往由多个集群组成：Hadoop/Hbase集群、Zookeeper集群、Spark集群。掐指一算，至少需要6台机器了。\u003c/p\u003e\n\u003cp\u003e我们真的需要买6台机器吗？当然不是，我们只需要在自己的电脑上虚拟化出6台就好了。而我的电脑只有16G，虚拟化6台太吃力了。最终，我决定搭建成以下结构：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"spark-hadoop-hbase\" loading=\"lazy\" src=\"/assets/images/spark-hadoop-hbase.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e以下是搭建过程：\u003c/p\u003e\n\u003ch3 id=\"环境的搭建\"\u003e环境的搭建\u003c/h3\u003e\n\u003cp\u003e按以前学习像Spring这些类Web，开发环境的搭建非常简单，也就引入几个依赖，添加几项配置，就好了。\u003c/p\u003e\n\u003cp\u003e但是学习Spark，我敢\b肯定不少人在环境搭建这一环节踩坑。正因为这样，才会有此博客。\u003c/p\u003e\n\u003cp\u003eSpark不是一框架，而是一个分布式的计算平台。所以，你不能通过引入依赖，添加配置就完成搭建。需要先将Spark这个平台部署起来。Spark支持4种部署方式：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e单机：同一台机器，同一进程，不同线程运行Master和Worker\u003c/li\u003e\n\u003cli\u003e伪分布式：同一台机器，不同进程分别运行Master和Worker\u003c/li\u003e\n\u003cli\u003eStandalone方式：多台机器分别运行Master和Worker，自己解决资源调度\u003c/li\u003e\n\u003cli\u003eYarn和Mesos方式：将资源调度这一职责外包出去\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e虽然Spark的单机部署方式很简单，但是没有人会在生产环境上使用单机部署方式。而伪分布式我见不少人搭建，以此为基础来学习Spark。但我不推荐。\u003c/p\u003e\n\u003cp\u003e因为在线上真正运行的是Standalone、Yarn、Mesos方式，也称为完全分布式的方式。只有一开始就使用完全分布式的方式来进行开发调试，你才会学习到生产环境会遇到什么问题。\u003c/p\u003e\n\u003ch3 id=\"机器准备\"\u003e机器准备\u003c/h3\u003e\n\u003cp\u003e\b如果采取完全分布式的部署方式来学习，你必须准备\b很多台机器，就像上面所说的。\u003c/p\u003e\n\u003cp\u003e我想大多数人都会选择虚拟化方案来得到多台机器。我推荐Virtualbox。\u003c/p\u003e\n\u003cp\u003e我见不少人手工的创建一台机器，然后安装操作系统，接着想要多少台机器，就复制几台，甚至还要分别进入机器修改每台机器的IP。。。\u003c/p\u003e\n\u003cp\u003e这样的方式，效率低，又很难与你的同事分享你的环境（也就是统一一个团队的开发环境，以避免不同开发环境不同引起的问题）。\u003c/p\u003e\n\u003cp\u003e所以，我一开始就使用Vagrant。把机器的虚拟化这一动作进行自动化和版本化（提交到git仓库中）。使用了Vagrant，你只需要在Vagrantfile中定义机器数据、机器的系统镜像、CPU个数、内存，然后执行\u003ccode\u003evagrant up\u003c/code\u003e，就可以得到你想要的机器了。要与同事统一这些机器，只需要他使用相同的Vagrantfile就好了。\n同时，这样，还能实现：统一开发环境与生产环境使用同样或相近的机器环境。\u003c/p\u003e\n\u003cp\u003e以下是一个\u003ca href=\"https://github.com/bigdata-labs/spark2-hadoop2.6-hbase-labs/blob/master/Vagrantfile\"\u003eVagrantfile样例\u003c/a\u003e：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eVagrant.configure(2) do |config|\n  VAGRANT_VM_PROVIDER = \u0026#34;virtualbox\u0026#34;\n  machine_box = \u0026#34;boxcutter/ubuntu1604\u0026#34;  -\u0026gt; 系统镜像\n\n  config.vm.define \u0026#34;offlinenode1\u0026#34; do |machine|\n    machine.vm.box = machine_box \n    machine.vm.hostname = \u0026#34;offlinenode1\u0026#34;\n    machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.11.151\u0026#34; -\u0026gt; 指定IP\n    machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node|\n        node.name = \u0026#34;offlinenode1\u0026#34;\n        node.memory = 4096 -\u0026gt; 指定内存\n        node.cpus = 2 -\u0026gt; 指定CPU个数\n    end\n   end\n\n   config.vm.define \u0026#34;offlinenode2\u0026#34; do |machine|\n     machine.vm.box = machine_box\n     machine.vm.hostname = \u0026#34;offlinenode2\u0026#34;\n     machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.11.152\u0026#34;\n     machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node|\n         node.name = \u0026#34;offlinenode2\u0026#34;\n         node.memory = 4096\n         node.cpus = 2\n     end\n    end\n....... 还可以定义很多这样的机器\nend\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"搭建spark集群\"\u003e搭建Spark集群\u003c/h3\u003e\n\u003cp\u003e在准备好机器后，接下来做的就是搭建Spark集群。我会选择Ansible来实现自动化搭建，而不是一台台机器登上去，一条条命令的执行安装。\u003c/p\u003e","title":"这样搭建Spark学习环境效率似乎更高"},{"content":"先申明：我反驳“以结果为导向”的某些理由，并不代表我认为管理应该不制定目标，管理不应该有结果。\n前些天与朋友聊天，聊到管理。虽然两人所处不同行业，他是保险业，我是软件开发，当聊到管理的“以结果为导向”。我的话匣就打开了。\n后来，我总感觉自己思路还不够清晰，就总结此文。\n首先，我们需要统一对“以结果为导向”这个概念的理解。要不，我们的讨论可能就不在同一个频道上。\n我听过的最多的，对于“以结果为导向”的理解就是：我只管结果，不管你中间过程怎么做。\n这个“我”，是当事人。这里的假设是，这个“我”是一个管理者。\n但是，为什么呢？只管理结果，不管过程呢？我得到的理由是：\n人多了，会管不过来 作为“领导”（很多人常常把领导和管理两个概念混淆），下属当然要完成领导指派的“结果”，否则，我要你做什么？ 我们要鼓励员工的主人翁精神 微管理会束缚员工的创造性 我不知道， 上述的有没有统一大家的理解。但我还是要尝试接着逐一讨论。\n人多了，会管不过来 对于“人多了，会管不过”这个观点，我听到的阐述是：\n当你手下只有3个人的时候，你当然可以管得过来，你可以很容易知道他们上班时间里有没有干与工作不相关的事情，甚至知道他们什么时候去厕所了。\n但是当你做到事业部经理时，管理4个部门，每个部门都有100个人的时候，你怎么管。\n这些支言片语中，我粗鲁地认为这些言语包含以下几点假设：\n管理中，“监督”工作占很大的比例 管理一个3人团队和管理4个100人部门使用的是同一套管理方式 管不过来，是因为人多 我的个人观点：\n以上几点假设之间相关的，因为如果管理400人时和管理3人时的方法一样，管不过来，当然是因为人多。同时，人数不是决定管理方法的唯一因素，还有你的团队成员职能结构组成等等。\n我是领导，我只管结果 我擅自将“作为领导，下属当然要完成领导指派的‘结果’，否则，我要你做什么？”这句长句抽象成：我是领导，我只管结果。这个“结果”的同义词是目标，也被称为KPI。\n首先，我们得承认，某些“领导”有这个权利那样做。\n接着，我从这个观点中看到以下几点假设：\n下属足够做得好，怎么样都能完成我（领导）定好的“结果” 领导的工作就是“管结果” 领导定好的“KPI”就是正确的 我的观点：\n定好KPI是一种能力。如果一个没有定好KPI能力的领导说他只管结果，下面的员工就只能呵呵了。\n管理是有级别的，过程终究是要管理的，只不过，不同的级别，管理的过程不一样。一线项目经理手下可能有前端、后端，如果他只管结果，不管，前后端是如何协作的，他是怎么知道他们团队的工作效率的？如果连自己的团队的工作效率都评估不了，我要这个项目经理做什么？😜\n所以，并不是每种“领导”都有权利说“只管结果”\n我们要鼓励员工的主人翁精神 这句话的假设是：\n管理过程了，就是不鼓励员工的主人翁精神 只管结果，员工才会有主人翁精神 微管理会束缚员工的创造性 这句话的假设是：\n只管结果，不管过程就不是微管理了 管过程就是微管理，那么，又引申出来假设：管过程会束缚员工的创造性 微管理会束缚员工的创造性 思考这些理由背后的假设是否成立 反驳一个人的观点时，一个有效的方法就是找到他的观点背后的“假设”。所以，我尝试去找到那些理由背后的假设。因为如果假设不成立，他的理由当然也就不成立了。\n以上的“假设”在不同的公司环境，结果可能不一样。比如管理中，“监督”工作占很大的比例，在软件行业，管理活动中，“监督”所占的比例当然没有劳动力密集型工厂里的管理所占比例大。不过，说回来，软件行业也有劳动力密集型的。\n软件行业的过程管理 说到底，一线软件开发团队当然需要过程管理，要不，你没办法评估它的开发效率。\n当然，有些人就会反驳，你制定一个目标，如果他完成了，那么不是说明效率可以吗？\n我的回答是：你的假设是完成目标等于效率高，你仔细想想，这两者是等于关系吗？\n但是管理到什么程度算“微”，我目前没有想到好的度量方法。不过，过去一年中，我发现使用“看板”来进行微管理，效果不错。既能管理结果（软件产品），也能管理过程（团队成员协作）。本文的目的不在讨论软件行业如何进行过程管理，所以，就不深入讨论了。\n小结 倡导“以结果导向”的管理方式的人，本质上是在倡导宏观管理。其实，倡导宏观管理，就是在假设宏观管理和微观管理是两个矛盾的东西。但是，我不认为这个假设是正确的，因为我们的目标是管理好一家公司以实现盈利，总之就是要管理好，为了实现这个目标，需要进行宏观管理时，就进行宏观管理，需要进行微观管理时，就进行微观管理。我们不能因“宏观”和“微观”的概念之分而忘记管理的初心。\n上述观点纯属个人理解，不一定正确。欢迎讨论。\n扩展阅读：平庸的领导忙着无数小事，卓越的领导从来只管大事\n","permalink":"https://showme.codes/zh-cn/2017-1-20-about-result-orientation/","summary":"\u003cp\u003e\u003cstrong\u003e\u003cem\u003e先申明：我反驳“以结果为导向”的某些理由，并不代表我认为管理应该不制定目标，管理不应该有结果。\u003c/em\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e前些天与朋友聊天，聊到管理。虽然两人所处不同行业，他是保险业，我是软件开发，当聊到管理的“以结果为导向”。我的话匣就打开了。\u003c/p\u003e\n\u003cp\u003e后来，我总感觉自己思路还不够清晰，就总结此文。\u003c/p\u003e\n\u003cp\u003e首先，我们需要统一对“以结果为导向”这个概念的理解。要不，我们的讨论可能就不在同一个频道上。\u003c/p\u003e\n\u003cp\u003e我听过的最多的，对于“以结果为导向”的理解就是：我只管结果，不管你中间过程怎么做。\u003c/p\u003e\n\u003cp\u003e这个“我”，是当事人。这里的假设是，这个“我”是一个管理者。\u003c/p\u003e\n\u003cp\u003e但是，为什么呢？只管理结果，不管过程呢？我得到的理由是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e人多了，会管不过来\u003c/li\u003e\n\u003cli\u003e作为“领导”（很多人常常把领导和管理两个概念混淆），下属当然要完成领导指派的“结果”，否则，我要你做什么？\u003c/li\u003e\n\u003cli\u003e我们要鼓励员工的主人翁精神\u003c/li\u003e\n\u003cli\u003e微管理会束缚员工的创造性\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我不知道， 上述的有没有统一大家的理解。但我还是要尝试接着逐一讨论。\u003c/p\u003e\n\u003ch3 id=\"人多了会管不过来\"\u003e人多了，会管不过来\u003c/h3\u003e\n\u003cp\u003e对于“人多了，会管不过”这个观点，我听到的阐述是：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e当你手下只有3个人的时候，你当然可以管得过来，你可以很容易知道他们上班时间里有没有干与工作不相关的事情，甚至知道他们什么时候去厕所了。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e但是当你做到事业部经理时，管理4个部门，每个部门都有100个人的时候，你怎么管。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这些支言片语中，我粗鲁地认为这些言语包含以下几点假设：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e管理中，“监督”工作占很大的比例\u003c/li\u003e\n\u003cli\u003e管理一个3人团队和管理4个100人部门使用的是同一套管理方式\u003c/li\u003e\n\u003cli\u003e管不过来，是因为人多\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我的个人观点：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e以上几点假设之间相关的，因为如果管理400人时和管理3人时的方法一样，管不过来，当然是因为人多。同时，人数不是决定管理方法的唯一因素，还有你的团队成员职能结构组成等等。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"我是领导我只管结果\"\u003e我是领导，我只管结果\u003c/h3\u003e\n\u003cp\u003e我擅自将“作为\u003ccode\u003e领导\u003c/code\u003e，下属当然要完成领导指派的‘结果’，否则，我要你做什么？”这句长句抽象成：\u003ccode\u003e我是领导，我只管结果\u003c/code\u003e。这个“结果”的同义词是目标，也被称为KPI。\u003c/p\u003e\n\u003cp\u003e首先，我们得承认，某些“领导”有这个权利那样做。\u003c/p\u003e\n\u003cp\u003e接着，我从这个观点中看到以下几点假设：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e下属足够做得好，怎么样都能完成我（领导）定好的“结果”\u003c/li\u003e\n\u003cli\u003e领导的工作就是“管结果”\u003c/li\u003e\n\u003cli\u003e领导定好的“KPI”就是正确的\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我的观点：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e定好KPI是一种能力。如果一个没有定好KPI能力的领导说他只管结果，下面的员工就只能呵呵了。\u003c/p\u003e\n\u003cp\u003e管理是有级别的，过程终究是要管理的，只不过，不同的级别，管理的过程不一样。一线项目经理手下可能有前端、后端，如果他只管结果，不管，前后端是如何协作的，他是怎么知道他们团队的工作效率的？如果连自己的团队的工作效率都评估不了，我要这个项目经理做什么？😜\u003c/p\u003e\n\u003cp\u003e所以，并不是每种“领导”都有权利说“只管结果”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"我们要鼓励员工的主人翁精神\"\u003e我们要鼓励员工的主人翁精神\u003c/h3\u003e\n\u003cp\u003e这句话的假设是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e管理过程了，就是不鼓励员工的主人翁精神\u003c/li\u003e\n\u003cli\u003e只管结果，员工才会有主人翁精神\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"微管理会束缚员工的创造性\"\u003e微管理会束缚员工的创造性\u003c/h3\u003e\n\u003cp\u003e这句话的假设是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e只管结果，不管过程就不是微管理了\u003c/li\u003e\n\u003cli\u003e管过程就是微管理，那么，又引申出来假设：管过程会束缚员工的创造性\u003c/li\u003e\n\u003cli\u003e微管理会束缚员工的创造性\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"思考这些理由背后的假设是否成立\"\u003e思考这些理由背后的假设是否成立\u003c/h3\u003e\n\u003cp\u003e反驳一个人的观点时，一个有效的方法就是找到他的观点背后的“假设”。所以，我尝试去找到那些理由背后的假设。因为如果假设不成立，他的理由当然也就不成立了。\u003c/p\u003e\n\u003cp\u003e以上的“假设”在不同的公司环境，结果可能不一样。比如管理中，“监督”工作占很大的比例，在软件行业，管理活动中，“监督”所占的比例当然没有劳动力密集型工厂里的管理所占比例大。不过，说回来，软件行业也有劳动力密集型的。\u003c/p\u003e\n\u003ch3 id=\"软件行业的过程管理\"\u003e软件行业的过程管理\u003c/h3\u003e\n\u003cp\u003e说到底，一线软件开发团队当然需要过程管理，要不，你没办法评估它的开发效率。\u003c/p\u003e\n\u003cp\u003e当然，有些人就会反驳，你制定一个目标，如果他完成了，那么不是说明效率可以吗？\u003c/p\u003e\n\u003cp\u003e我的回答是：你的假设是完成目标等于效率高，你仔细想想，这两者是等于关系吗？\u003c/p\u003e\n\u003cp\u003e但是管理到什么程度算“微”，我目前没有想到好的度量方法。不过，过去一年中，我发现使用“看板”来进行微管理，效果不错。既能管理结果（软件产品），也能管理过程（团队成员协作）。本文的目的不在讨论软件行业如何进行过程管理，所以，就不深入讨论了。\u003c/p\u003e\n\u003ch3 id=\"小结\"\u003e小结\u003c/h3\u003e\n\u003cp\u003e倡导“以结果导向”的管理方式的人，本质上是在倡导宏观管理。其实，倡导宏观管理，就是在假设宏观管理和微观管理是两个矛盾的东西。但是，我不认为这个假设是正确的，因为我们的目标是管理好一家公司以实现盈利，总之就是要管理好，为了实现这个目标，需要进行宏观管理时，就进行宏观管理，需要进行微观管理时，就进行微观管理。我们不能因“宏观”和“微观”的概念之分而忘记管理的初心。\u003c/p\u003e\n\u003cp\u003e上述观点纯属个人理解，不一定正确。欢迎讨论。\u003c/p\u003e\n\u003cp\u003e扩展阅读：\u003ca href=\"https://mp.weixin.qq.com/s?__biz=MjM5NzY4MzQyMQ==\u0026amp;mid=2650079046\u0026amp;idx=1\u0026amp;sn=6c8b843c94da7923a27c9f2e599f69a3\u0026amp;chksm=bed616f489a19fe265c207389addd8c1345769f8c9502a75baa3d9db4fd4d8361bea759f533d\u0026amp;scene=0\u0026amp;key=49c0ab0571729aaba8d8d6ffc130266278bae406360140dc8ce9001178824e299526e2b8909d1fed4a4a4ade6d072d4befc3b576e2d2b7e90bd16d40ed95c0ac9eae1981e8ee8f1998918a88696c2d86\u0026amp;ascene=0\u0026amp;uin=MTcyODMxNTUxMQ%3D%3D\u0026amp;devicetype=iMac+MacBookPro11%2C1+OSX+OSX+10.10.5+build(14F2009)\u0026amp;version=12010210\u0026amp;nettype=WIFI\u0026amp;fontScale=100\u0026amp;pass_ticket=RmnX9YnOZyNScv7ImFfO2P7KD7zaaVKWBK%2Ft%2BbIvSOMp1hms8FgVuVrwryVJuNQ7\"\u003e平庸的领导忙着无数小事，卓越的领导从来只管大事\u003c/a\u003e\u003c/p\u003e","title":"关于“以结果为导向”的管理方式的碎碎语"},{"content":"为什么想做这个东西 一直好奇像亚马逊这类网站的搜索是如何做到推荐的，最近刚好看到一篇文章：Redis 与搜索热词推荐，然而只写了思路。所以，就是想自己实现一个。\n先上个效果图，再聊：\nP.S. 按四年前，要写这样的前端效果，对于我这个后台开发，还是挺困难的。而现在，简单的学了下Vue.js，再加上同事的小小指点，就搞定了。😂\n热词推荐的本质 假如你预先就知道了用户输入：s、sz、shen、深这些字时，就是想搜“深圳”，那是不是说，我们只要提前将这些字放到一个Map结构中，将用户的输入想像出一个key，value就是“深圳”。\n说到底，热词推荐的本质就是一个大大的Map。难点就在于如何更新这个Map，以至于让用户觉得“智能”，或觉得我们在给他们做“推荐”。\n这个Map，常常被人称为“索引”。其实使用“索引” 这个名词也更准确一些。Map中的Key是不能重复的。但是我们数据结构是要求可重复的，为什么呢？因为，在系统中，s、sh、shen、深等等这些都是key，而它们对应的value，可能相同，又可能不同。举个例子：\nhotword:0\u0026gt;zrevrange s 0 10 1) 鼠蛟 2) 鼠场乡 3) 鳝鱼 4) 鳝溪校区 5) 鳝溪农场 6) 鳝溪 7) 骚子营社区 8) 骚子营 9) 驷马镇 10) 驷马桥街道 11) 驷马桥 hotword:0\u0026gt;zrevrange sh 0 10 1) 鼠蛟 2) 鼠场乡 3) 鳝鱼 4) 鳝溪校区 5) 鳝溪农场 6) 鳝溪 7) 首院胡同 8) 首阳镇 9) 首阳山镇 10) 首阳山 11) 首钢试验厂 仔细看到其中的不同了吗？同时，这里还有一个问题，那就是当用户输入s时，出现了10个value，我们如何给这些value如何排序呢？\n为了与排序模型解耦，我们为每个value都给出一个分数score。score越大，越排前面。最终索引结构就变成了这样子：\nP.S. 这些score之所以都为0，是因为数据问题。\n总的来说，关于热词推荐，我们需要解决以下问题：\n如何存储索引的数据？\n如何构建索引？也就是一开始时，我们怎么知道用户输入“s” 就是要搜“深圳”呢？\n如何根据用户的反馈行为来更新索引？当用户输入 “s” 出现了“1 沙河”和“2 深圳”，用户选择了“深圳”，那么当其他用户输入“s”时，我们是不是应该将“深圳”这个词放到前面呢？\n​\n基于Solr实现的弊端 美团在几年前也写了一篇文章来介绍自己的热词推荐：搜索引擎关键字智能提示的一种实现。然而这种实现，个人觉得有个设计非常不好。因为Solr在整个系统中，即做了“存储索引”的角色，又做了“构建索引”的角色。违反了职责单一原则。因为当我们想改变构建索引的算法时，同时会影响到“存储索引”的逻辑。\n以下是他们的实现逻辑截图：\n另一种基于Redis的实现 我目前只写了一个简单实现，而且还没有实现“根据用户反馈来更新索引”的功能。这个功能可实现得很简单，也可以实现得很复杂。本文不讨论。\n同时，生产环境会更复杂一些。比如要实现高可用。我个人能力有限，还没有能实现。但是思路是有的：所有出现单点的地方都要做成分布式的，比如Redis就做成Redis Cluster。\n以下是架构图：\n图中，InitWorker负责将我准备好的全国地名大全的数据，构建成索引，然后写到Redis中。用户则可以通过基于Openresty写的APP去查询Redis中的数据。\n使用本系统的方法：\nP.S. 本系统使用Ansible做自动化部署，所以，请提前安装好Ansible。\ngit clone https://github.com/zacker330/hot-word-recommend.git 准备两个Ubuntu 16的机器，如果你懂Vagrant的话，直接使用我的Vagrantfile就好了 进入到项目中，执行ansible-playbook ./ansible/playbook.yml -i ./ansible/inventory -u vagrant -k 来自动化部署所有组件。如果使用Vagrant来搭建的环境，密码是 vagrant，以下同，将不在重述。 打包我们的InitWorker项目：mvn assembly:assembly 部署InitWorker: ansible-playbook ./ansible/deploy-worker.yml -i ./ansible/inventory -u vagrant -k 打开链接测试：http://192.168.10.11/index.lsp 。IP换成你自己部署的机器的IP。 具体代码，自己看了。为方便阅读，我觉得有必要注释一下项目结构：\n├── README.md ├── Vagrantfile ├── ansible │ ├── deploy-front-app.yml // 单独部署 前端app │ ├── deploy-local.yml //本地开发使用 │ ├── deploy-worker.yml // 执行worker，写索引到redis中 │ ├── inventory │ ├── playbook.yml // 安装所有必要的组件 │ ├── roles │ │ ├── common │ │ ├── front-app // 安装前端APP │ │ ├── jdk8 // 安装Jdk8 │ │ ├── openresty // 安装Openresty │ │ └── redis // 安装redis的脚本 │ └── vars │ └── base-env.yml // 配置变量存放文件 ├── autocomplete-worker │ ├── pom.xml │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── codes │ │ │ │ └── showme │ │ │ │ └── autocomplete │ │ │ │ ├── InitWorker.java │ │ │ │ └── common │ │ │ └── resources │ │ │ └── env.properties │ │ └── test │ └── target ├── doc // 文档需要用到的一些文件 └── files └── places.txt.zip //全国地名数据 小结 热词推荐的“智能”所在处就在于索引的构建算法。简单一点的做法就是每当用户点击某搜索结果时，我们就给这个索引条目加权1。感兴趣的同学可以实现来玩玩。\n以上内容均为个人看法，如果有不对的地方，还请斧正，谢谢了。\n","permalink":"https://showme.codes/zh-cn/2016-12-31-hot-word-recommend-demo/","summary":"\u003ch4 id=\"为什么想做这个东西\"\u003e为什么想做这个东西\u003c/h4\u003e\n\u003cp\u003e一直好奇像亚马逊这类网站的搜索是如何做到推荐的，最近刚好看到一篇文章：\u003ca href=\"http://blog.jobbole.com/95780/\"\u003eRedis 与搜索热词推荐\u003c/a\u003e，然而只写了思路。所以，就是想自己实现一个。\u003c/p\u003e\n\u003cp\u003e先上个效果图，再聊：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/index.lsp.gif\"\u003e\u003c/p\u003e\n\u003cp\u003eP.S. 按四年前，要写这样的前端效果，对于我这个后台开发，还是挺困难的。而现在，简单的学了下Vue.js，再加上同事的小小指点，就搞定了。😂\u003c/p\u003e\n\u003ch4 id=\"热词推荐的本质\"\u003e热词推荐的本质\u003c/h4\u003e\n\u003cp\u003e假如你预先就知道了用户输入：s、sz、shen、深这些字时，就是想搜“深圳”，那是不是说，我们只要提前将这些字放到一个Map结构中，将用户的输入想像出一个key，value就是“深圳”。\u003c/p\u003e\n\u003cp\u003e说到底，热词推荐的本质就是一个大大的Map。\u003cstrong\u003e难点就在于如何更新这个Map\u003c/strong\u003e，以至于让用户觉得“智能”，或觉得我们在给他们做“推荐”。\u003c/p\u003e\n\u003cp\u003e这个Map，常常被人称为“索引”。其实使用“索引” 这个名词也更准确一些。Map中的Key是不能重复的。但是我们数据结构是要求可重复的，为什么呢？因为，在系统中，s、sh、shen、深等等这些都是key，而它们对应的value，可能相同，又可能不同。举个例子：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ehotword:0\u0026gt;zrevrange s 0 10\n1) 鼠蛟\n2) 鼠场乡\n3) 鳝鱼\n4) 鳝溪校区\n5) 鳝溪农场\n6) 鳝溪\n7) 骚子营社区\n8) 骚子营\n9) 驷马镇\n10) 驷马桥街道\n11) 驷马桥\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ehotword:0\u0026gt;zrevrange sh 0 10\n1) 鼠蛟\n2) 鼠场乡\n3) 鳝鱼\n4) 鳝溪校区\n5) 鳝溪农场\n6) 鳝溪\n7) 首院胡同\n8) 首阳镇\n9) 首阳山镇\n10) 首阳山\n11) 首钢试验厂\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e仔细看到其中的不同了吗？同时，这里还有一个问题，那就是当用户输入s时，出现了10个value，我们如何给这些value如何排序呢？\u003c/p\u003e\n\u003cp\u003e为了与排序模型解耦，我们为每个value都给出一个分数score。score越大，越排前面。最终索引结构就变成了这样子：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/assets/images/key-value-score.png\"\u003e\u003c/p\u003e\n\u003cp\u003eP.S. 这些score之所以都为0，是因为数据问题。\u003c/p\u003e\n\u003cp\u003e总的来说，关于热词推荐，我们需要解决以下问题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e如何存储索引的数据？\u003c/p\u003e","title":"一个热词推荐的简单实现"},{"content":" 关键绩效指标（英语：Key Performance Indicators，简称KPI），又称主要绩效指标、重要绩效指标、绩效评核指标等，是指衡量一个管理工作成效最重要的指标，是一项数据化管理的工具，必须是客观、可衡量的绩效指标。这个名词往往用于财政、一般行政事务的衡量。是将公司、员工、事务在某时期表现量化与质化的一种指标。可协助将优化组织表现，并规划愿景。\nKPI是指衡量一个管理工作成效的最重要指标。注意，是管理工作成效。实现中，管理工作当然由管理者来做，也就是管理者是拿来KPI来管理自己的工作成效。同时，KPI也要选择最重要的指标，而不是什么都选 ，因为并不是每项KPI都能体现管理工作成效。\n光看这个定义，似乎很多公司都把KPI理解成：“员工完成了管理者安排的工作” == “管理者的管理工作有成效”！而且，只止于此。\n翻看历史你就理解我说的了：宋太宗“雄韬武略”遥控指挥军队差点全军覆没！ 作为管理者宋太宗的管理工作成效应该是打赢仗，而不是军队有没有按照他的“阵图”来行军。（我又甩锅了。呵呵）\n光看KPI是没有问题的，谁都想知道自己的工作成效如何，尤其作为管理者。\n但，KPI的选择并没有那么简单。根据自己对KPI的理解，管理者为自己找KPI时必须做到：\n管理者必须理解KPI的目标：衡量自己的管理工作成效。 管理者必须理解公司的业务目标是什么，避免为了KPI而KPI。 管理者必须知道业务指标是什么？比如500万的销售额 管理者必须知道执行哪些管理工作才能帮助实现业务指标，如果不知道，那么就需要做实验，再验证！ 管理者必须知道自己有没有100%执行相应的管理工作 定义个KPI都这么难，所以，管理者不好当啊。你也看到了，我一句也没有提下属的KPI。因为下属的KPI是需要管理者与其下属共同制定的。下属对自己的管理成效本质上就是很多公司希望的“自我管理”。而自我管理的成效是没有办法量化的，作为管理者，也只能看下属的自我管理后的表象。\n看到这里，相信还是有很多人疑惑，到底怎么做KPI啊。\n首先申明，KPI不是制定出来的，而是找出来的。因为指标本来就存在了，问题只是你选择什么指标作为管理成效的KPI。它是一个名词，不是动词。（想起人们常常开的玩笑：你今天被KPI了吗？）\n那如何找KPI呢？我的回答是：你清晰地知道你的业务目标，以及如何达到，你就自然而然地知道你的KPI有哪些了。\n小结 KPI是一个名词，不是一个动词。管理者的管理成效不等于员工的100%执行。KPI本质上是自我管理，不是上级对下级的工作安排。工作如何安排，不在本文讨论范围。\n抱怨KPI是没有用的，现实世界中，KPI在企业中会与企业的其它因素起化学反应，远没有本文说的那么简单。\n最后，两个思考题：\n“如果新浪要求自己员工每天要发2条以上微博” 这条能成为新浪员工的KPI吗？ “主导XXX系统（或模块）开发” 这条能成为你的KPI吗？ 请停留思考，然后再看：SMART原则。\n","permalink":"https://showme.codes/zh-cn/2016-12-21-how-to-save-my-kpi/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e关键绩效指标\u003c/strong\u003e（英语：Key Performance Indicators，简称\u003cstrong\u003eKPI\u003c/strong\u003e），又称\u003cstrong\u003e主要绩效指标\u003c/strong\u003e、\u003cstrong\u003e重要绩效指标\u003c/strong\u003e、\u003cstrong\u003e绩效评核指标\u003c/strong\u003e等，是指衡量一个管理工作成效最重要的指标，是一项\u003ca href=\"https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E5%8C%96\"\u003e数据化\u003c/a\u003e管理的工具，必须是客观、可衡量的\u003ca href=\"https://zh.wikipedia.org/w/index.php?title=%E7%B8%BE%E6%95%88%E6%8C%87%E6%A8%99\u0026amp;action=edit\u0026amp;redlink=1\"\u003e绩效指标\u003c/a\u003e。这个名词往往用于\u003ca href=\"https://zh.wikipedia.org/wiki/%E8%B4%A2%E6%94%BF\"\u003e财政\u003c/a\u003e、一般\u003ca href=\"https://zh.wikipedia.org/wiki/%E8%A1%8C%E6%94%BF\"\u003e行政\u003c/a\u003e事务的衡量。是将公司、员工、事务在某时期表现\u003ca href=\"https://zh.wikipedia.org/wiki/%E9%87%8F%E5%8C%96\"\u003e量化\u003c/a\u003e与\u003ca href=\"https://zh.wikipedia.org/wiki/%E8%B3%AA%E5%8C%96\"\u003e质化\u003c/a\u003e的一种指标。可协助将优化\u003ca href=\"https://zh.wikipedia.org/wiki/%E7%B5%84%E7%B9%94\"\u003e组织\u003c/a\u003e表现，并规划愿景。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eKPI是指衡量一个\u003cstrong\u003e管理工作\u003c/strong\u003e成效的\u003cstrong\u003e最重要\u003c/strong\u003e指标。注意，是管理工作成效。实现中，管理工作当然由管理者来做，也就是管理者是拿来KPI来管理自己的工作成效。同时，KPI也要选择\u003cstrong\u003e最\u003c/strong\u003e重要的指标，而不是什么都选 ，因为并不是每项KPI都能体现管理工作成效。\u003c/p\u003e\n\u003cp\u003e光看这个定义，似乎很多公司都把KPI理解成：“员工完成了管理者安排的工作” == “管理者的管理工作有成效”！而且，只止于此。\u003c/p\u003e\n\u003cp\u003e翻看历史你就理解我说的了：\u003ca href=\"https://mt.sohu.com/d20161012/115928211_486566.shtml\"\u003e宋太宗“雄韬武略”遥控指挥军队差点全军覆没！\u003c/a\u003e 作为管理者宋太宗的管理工作成效应该是打赢仗，而不是军队有没有按照他的“阵图”来行军。（我又甩锅了。呵呵）\u003c/p\u003e\n\u003cp\u003e光看KPI是没有问题的，谁都想知道自己的工作成效如何，尤其作为管理者。\u003c/p\u003e\n\u003cp\u003e但，KPI的选择并没有那么简单。根据自己对KPI的理解，管理者为自己\u003cstrong\u003e找\u003c/strong\u003eKPI时必须做到：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e管理者必须理解KPI的目标：衡量\u003cstrong\u003e自己\u003c/strong\u003e的管理工作成效。\u003c/li\u003e\n\u003cli\u003e管理者必须理解公司的业务目标是什么，避免为了KPI而KPI。\u003c/li\u003e\n\u003cli\u003e管理者必须知道业务指标是什么？比如500万的销售额\u003c/li\u003e\n\u003cli\u003e管理者必须知道执行哪些管理工作才能帮助实现业务指标，如果不知道，那么就需要做实验，再验证！\u003c/li\u003e\n\u003cli\u003e管理者必须知道自己有没有100%执行相应的管理工作\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e定义个KPI都这么难，所以，管理者不好当啊。你也看到了，我一句也没有提下属的KPI。因为下属的KPI是需要管理者与其下属共同制定的。下属对自己的管理成效本质上就是很多公司希望的“自我管理”。而自我管理的成效是没有办法量化的，作为管理者，也只能看下属的自我管理后的表象。\u003c/p\u003e\n\u003cp\u003e看到这里，相信还是有很多人疑惑，到底怎么做KPI啊。\u003c/p\u003e\n\u003cp\u003e首先申明，KPI不是制定出来的，而是找出来的。因为指标本来就存在了，问题只是你选择什么指标作为管理成效的KPI。它是一个名词，不是动词。（想起人们常常开的玩笑：你今天被KPI了吗？）\u003c/p\u003e\n\u003cp\u003e那如何找KPI呢？我的回答是：你清晰地知道你的业务目标，以及如何达到，你就自然而然地知道你的KPI有哪些了。\u003c/p\u003e\n\u003ch4 id=\"小结\"\u003e小结\u003c/h4\u003e\n\u003cp\u003eKPI是一个名词，不是一个动词。管理者的管理成效不等于员工的100%执行。KPI本质上是自我管理，不是上级对下级的工作安排。工作如何安排，不在本文讨论范围。\u003c/p\u003e\n\u003cp\u003e抱怨KPI是没有用的，现实世界中，KPI在企业中会与企业的其它因素起化学反应，远没有本文\u003cstrong\u003e说\u003c/strong\u003e的那么简单。\u003c/p\u003e\n\u003cp\u003e最后，两个思考题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e“如果新浪要求自己员工每天要发2条以上微博” 这条能成为新浪员工的KPI吗？\u003c/li\u003e\n\u003cli\u003e“主导XXX系统（或模块）开发” 这条能成为你的KPI吗？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e请停留思考，然后再看：\u003ca href=\"http://wiki.mbalib.com/wiki/SMART%E5%8E%9F%E5%88%99\"\u003eSMART原则\u003c/a\u003e。\u003c/p\u003e","title":"似乎百分之七十的人都理解错了KPI"},{"content":"进入ThoughtWork后，每天都会有人向你提反馈。比如站会，你这样说会更好；你代码这样写也许会更好；你这段时间英语进步得非常快……这些反馈中，有对你优点反馈，也会对你的缺点进行反馈。\n这个过程，我才发现自己以前很多没有发现的不足，某些固化的思维方式，我的视野也一下开了好多。\n后来，离开了ThoughtWorks。我尝试将反馈机制带入到自己的团队。怎么尝试呢？\n首先，你需要让团队成员达成共识：只有成长，我们的生活才更美好，而反馈就是在帮助对方成长。当然人们对于“美好”的理解不同，有些人觉得有钱就是美好，有些人觉得得真正学到东西就是美好。这些都无所谓。因为成长了，这些美好都会有。\n接着，从自己做起。每天尝试给团队成员反馈。只要找到合适的时机及时给予反馈。举例子：\n一个成员写代码时，不加思索的就上个大大的if-else，这种情况，最好马上指出，同时告诉他背后的原因。但是现实中，有时，我们需要考虑多一些，比如这个成员是不是超级要面子的，这个成员是不是团队受尊重的老员工。这时就要考虑沟通方式了。 团队中两个成员因为某件事情吵了起来。而这件事，也不是特别大的事情，也没有吵多久。这时，你就要看他们是不是还气在头。合适的时机是等他们的气消了后，再分别给他们反馈。 然后，争取每隔一段时间就每一位成员进行一次一对一沟通。沟通的内容包括生活和工作，主要是了解他们对当前工作的看法，生活上有什么需要帮忙，学习上有没有遇到困难，工作上有什么不顺心……这个过程其实是成员向你反馈：\n工作安排得合理不合理？ 与同事相处是否舒服？ 有没有学习的焦虑？ …… 《创业维艰》这本书专门有一节谈“一对一的沟通”，有兴趣的朋友可以看看。\n最后，引导并指导团队成员相互之间进行反馈。\n话说这里了。那反馈机制在企业中到底起到了什么作用？我不知道专业的管理行家如何回答。但是，我自己的理解是这样的：\n马斯洛的需求理论，反馈能满足马斯洛的需求理论中的的安全需求（你可以和他一对一沟通）、归属需求（Leader是把他当一个团队成员来对待的）、尊重需求（尊重了他）。 人天生就是通过反馈来成长，就像小孩通过你他的反馈来调整自己的行为。而如果团队成员得到了成长，就刚好可以满足了他的“自我实现”需求。 需求得到了满足，在其中工作，当然就会快乐。 有人就会说了，员工在工作中能否找到快乐，那是员工的事。这个观点即对，也不对。快乐的确是每一个人的。但是员工毕竟是人，不是机器。\n不快乐的工作，同样可以完成机械的活，像珠三角大批的工厂工人。而创造性的工作，如软件开发，做产品，不是机械完成就好了的，也不是能通过机械地堆人就能完成的。\n同时，反馈带给员工的成长，会正反馈到工作中，员工工作效率高了（学习新的技术、知道更多的解决方案），我们软件开发速度及开发质量会不会提高呢？\n小结 我个人认为，以上只是“反馈机制”给企业带来表面利益，它给企业的是更深层次的东西：企业相互学习、相互帮助的文化。不是说企业有了“反馈机制”就有了文化，而是说企业文化就是由这些道不清说不明的东西，通过时间一点点积累起来的。\n以上是个人习得的，希望能和大家交流。\n","permalink":"https://showme.codes/zh-cn/2016-12-10-feedback-in-company/","summary":"\u003cp\u003e进入ThoughtWork后，每天都会有人向你提反馈。比如站会，你这样说会更好；你代码这样写也许会更好；你这段时间英语进步得非常快……这些反馈中，有对你优点反馈，也会对你的缺点进行反馈。\u003c/p\u003e\n\u003cp\u003e这个过程，我才发现自己以前很多没有发现的不足，某些固化的思维方式，我的视野也一下开了好多。\u003c/p\u003e\n\u003cp\u003e后来，离开了ThoughtWorks。我尝试将反馈机制带入到自己的团队。怎么尝试呢？\u003c/p\u003e\n\u003cp\u003e首先，你需要让团队成员达成共识：只有成长，我们的生活才更美好，而反馈就是在帮助对方成长。当然人们对于“美好”的理解不同，有些人觉得有钱就是美好，有些人觉得得真正学到东西就是美好。这些都无所谓。因为成长了，这些美好都会有。\u003c/p\u003e\n\u003cp\u003e接着，从自己做起。每天尝试给团队成员反馈。只要找到合适的时机\u003cstrong\u003e及时\u003c/strong\u003e给予反馈。举例子：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e一个成员写代码时，不加思索的就上个大大的if-else，这种情况，最好马上指出，同时告诉他背后的原因。但是现实中，有时，我们需要考虑多一些，比如这个成员是不是超级要面子的，这个成员是不是团队受尊重的老员工。这时就要考虑沟通方式了。\u003c/li\u003e\n\u003cli\u003e团队中两个成员因为某件事情吵了起来。而这件事，也不是特别大的事情，也没有吵多久。这时，你就要看他们是不是还气在头。合适的时机是等他们的气消了后，再分别给他们反馈。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e然后，争取每隔一段时间就每一位成员进行一次一对一沟通。沟通的内容包括生活和工作，主要是了解他们对当前工作的看法，生活上有什么需要帮忙，学习上有没有遇到困难，工作上有什么不顺心……这个过程其实是成员向你反馈：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e工作安排得合理不合理？\u003c/li\u003e\n\u003cli\u003e与同事相处是否舒服？\u003c/li\u003e\n\u003cli\u003e有没有学习的焦虑？\u003c/li\u003e\n\u003cli\u003e……\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e《创业维艰》这本书专门有一节谈“一对一的沟通”，有兴趣的朋友可以看看。\u003c/p\u003e\n\u003cp\u003e最后，引导并指导团队成员相互之间进行反馈。\u003c/p\u003e\n\u003cp\u003e话说这里了。那反馈机制在企业中到底起到了什么作用？我不知道专业的管理行家如何回答。但是，我自己的理解是这样的：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e马斯洛的需求理论，反馈能满足马斯洛的需求理论中的的安全需求（你可以和他一对一沟通）、归属需求（Leader是把他当一个团队成员来对待的）、尊重需求（尊重了他）。\u003c/li\u003e\n\u003cli\u003e人天生就是通过反馈来成长，就像小孩通过你他的反馈来调整自己的行为。而如果团队成员得到了成长，就刚好可以满足了他的“自我实现”需求。\u003c/li\u003e\n\u003cli\u003e需求得到了满足，在其中工作，当然就会快乐。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e有人就会说了，员工在工作中能否找到快乐，那是员工的事。这个观点即对，也不对。快乐的确是每一个人的。但是员工毕竟是\u003cstrong\u003e人\u003c/strong\u003e，不是机器。\u003c/p\u003e\n\u003cp\u003e不快乐的工作，同样可以完成机械的活，像珠三角大批的工厂工人。而创造性的工作，如软件开发，做产品，不是机械完成就好了的，也不是能通过机械地堆人就能完成的。\u003c/p\u003e\n\u003cp\u003e同时，反馈带给员工的成长，会正反馈到工作中，员工工作效率高了（学习新的技术、知道更多的解决方案），我们软件开发速度及开发质量会不会提高呢？\u003c/p\u003e\n\u003ch5 id=\"小结\"\u003e小结\u003c/h5\u003e\n\u003cp\u003e我个人认为，以上只是“反馈机制”给企业带来表面利益，它给企业的是更深层次的东西：企业相互学习、相互帮助的文化。不是说企业有了“反馈机制”就有了文化，而是说企业文化就是由这些道不清说不明的东西，通过时间一点点积累起来的。\u003c/p\u003e\n\u003cp\u003e以上是个人习得的，希望能和大家交流。\u003c/p\u003e","title":"反馈机制在企业中的作用？"},{"content":"看了开涛的Nginx+Lua开发教程，很是感兴趣。所以，自己也把环境搭建起来玩。\n跟开涛的不同，我使用Vagrant + Ansible来搭建（不要问我为什么不使用Docker）。这样，所有的人只要两条命令就可以搭建好了，而不需要手工一条命令一条命令打。\n所谓使用Openresty来做读服务，是指Openresty直接从数据源读数据，然后渲染输出，而不经过应用服务器，比如Tomcat服务器。Openresty 是一个基于Nginx和LuaJIT的动态Web开发平台。我不知道京东是否是直接使用Openresty还是自己编译Nginx + Lua。反正，我直接使用Openresty。\n本次文章就是根据开涛的教程，实现使用lua-resty-template 做模块引擎，使用Redis做数据源。我把Openresty和Redis都安装在同一台机器上，以方便做实验，当然，如果你想装在不同的服务器，只需要修改下配置就好了。以下是架构：\n搭建的步骤： 安装Openresty及其相关的Openresty module: lua-resty-template、lua-resty-redis 安装Redis，启动Redis 配置Openresty，启动Openresty 写页面逻辑代码 整个步骤我都写成了Ansible自动化配置脚本。所以，你已经不需要自己搭建。所有的代码都托管在：http://git.oschina.net/zacker330/openresty-lab 。\n启动方法 启动前，你必须安装Vagrant 和 Ansible 2.0+。\ngit clone https://git.oschina.net/zacker330/openresty-lab.git cd openresty-lab vagrant up ansible-playbook ./ansible/playbook.yml -i ./ansible/inventory -u vagrant -k \u0026gt;\u0026gt; 输入ssh密码 `vagrant` PS. ansible-playbook需要通过ssh登录上目标机器来执行我们的任务。\n接下来，我们解释下代码。\nOpenresty的配置如下：\n## 省去了一些不重要的nginx配置 http { default_type application/octet-stream; ## 省去了一些不重要的nginx配置 ## 初始化所需要对象 init_by_lua \u0026#39; require \u0026#34;resty.core\u0026#34; redis = require \u0026#34;resty.redis\u0026#34; template = require \u0026#34;resty.template\u0026#34; template.caching(false); -- you may remove this on production \u0026#39;; server{ listen 80; server_name 192.168.8.10; charset utf-8; ## 指定 模块路径\tset $template_root \u0026#34;/usr/local/openresty/nginx/html/templates\u0026#34;; location ~ \\.lsp$ { default_type text/html; content_by_lua \u0026#39;template.render(ngx.var.uri)\u0026#39;; ## 访问index.lsp，将使用index.lsp模板 } } } 页面逻辑代码 index.lsp： {% raw %}\n{% layout = \u0026#34;layouts/default.lsp\u0026#34; -- 模板 local blogid= ngx.var.arg_blogId local title = \u0026#34;博客标题\u0026#34; local author = {name = \u0026#34;fooname\u0026#34;, gender = \u0026#34;female\u0026#34;, level= 3} local description = \u0026#34;\u0026lt;script\u0026gt;alert(1);\u0026lt;/script\u0026gt;\u0026#34; local content = \u0026#34;java8的流式处理极大了简化我们对于集合、数组等结构的操作，让我们可以以函数式的思想去操作，\u0026lt;br/\u0026gt;本篇文章将探讨java8的流式数据处理的基本使用。\u0026#34; local tags = {\u0026#34;life\u0026#34;, \u0026#34;lua\u0026#34;, \u0026#34;openresty\u0026#34;} local radar = {lua = 90, openresty = 80, nginx = 70} -- 使用nginx的内置变量 local a = ngx.var.arg_a local b = ngx.var.arg_b local ip = ngx.var.remote_addr -- 使用redis读数据源 local red = redis:new() red:set_timeout(1000) local ok, err = red:connect(\u0026#34;127.0.0.1\u0026#34;, 6379) if not ok then ngx.say(\u0026#34;failed to connect: \u0026#34;, err) return end local ok, err = red:lpush(\u0026#34;list\u0026#34;, a, b) local member, err = red:llen(\u0026#34;list\u0026#34;) %} \u0026lt;div\u0026gt; member: {{ member }}\u0026lt;br/\u0026gt; remote ip: {{ ip }} blogId: {{blogid}}\u0026lt;br/\u0026gt; 作者: {{author.name}} {{author.gender}} level: {{author.level}}\u0026lt;br/\u0026gt; description: {{description}} \u0026lt;br/\u0026gt; tags: {% for i = 1, #tags do %} {% if i \u0026gt; 1 then %},{% end %} {* tags[i] *} {% end %}\u0026lt;br/\u0026gt; \u0026lt;/div\u0026gt; {% endraw %}\n最终访问效果：http://192.168.8.10/index.lsp?a=12\u0026amp;b=asdfasdf\u0026amp;blogId=111\n小结 这种不经过应用服务器的方式，读的速度似乎更快，毕竟省去了中间的Tomcat服务。但是，谁来填充数据给Redis和什么时机填充数据，又是另一回事了。\n开发过程似乎有些麻烦，因为修改nginx配置后，不能像普通的页面开发那样立马看到效果，还要nginx -s reload一下。这个，我想到的解决方案是使用Ruby的guard gem来监控文件变动，然后reload nginx配置，最后使用浏览器的livereload来自刷新页面。https://github.com/guard/guard-livereload 目前还没有时间实现，希望有热心朋友实现提pr。或者开涛能分享下他们的实践。希望他本人能看到这篇博客。:P\n参考： 第五章 常用Lua开发库3-模板渲染\nlua-resty-template\n2016.10 于深圳西丽人民医院\n","permalink":"https://showme.codes/zh-cn/2016-10-15-copy-jd-openresty-redis/","summary":"\u003cp\u003e看了\u003ca href=\"http://www.iteye.com/blogs/subjects/nginx-lua\"\u003e开涛的Nginx+Lua开发教程\u003c/a\u003e，很是感兴趣。所以，自己也把环境搭建起来玩。\u003c/p\u003e\n\u003cp\u003e跟开涛的不同，我使用Vagrant + Ansible来搭建（不要问我为什么不使用Docker）。这样，所有的人只要两条命令就可以搭建好了，而不需要手工一条命令一条命令打。\u003c/p\u003e\n\u003cp\u003e所谓使用Openresty来做读服务，是指Openresty直接从数据源读数据，然后渲染输出，而不经过应用服务器，比如Tomcat服务器。\u003ca href=\"http://openresty.org/en/\"\u003eOpenresty\u003c/a\u003e 是一个基于Nginx和LuaJIT的动态Web开发平台。我不知道京东是否是直接使用Openresty还是自己编译Nginx + Lua。反正，我直接使用Openresty。\u003c/p\u003e\n\u003cp\u003e本次文章就是根据开涛的教程，实现使用\u003ca href=\"https://github.com/bungle/lua-resty-template\"\u003elua-resty-template\u003c/a\u003e 做模块引擎，使用Redis做数据源。我把Openresty和Redis都安装在同一台机器上，以方便做实验，当然，如果你想装在不同的服务器，只需要修改下配置就好了。以下是架构：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"openresty_redis\" loading=\"lazy\" src=\"/assets/images/openresty_redis-1.png\"\u003e\u003c/p\u003e\n\u003ch4 id=\"搭建的步骤\"\u003e搭建的步骤：\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e安装Openresty及其相关的Openresty module: lua-resty-template、lua-resty-redis\u003c/li\u003e\n\u003cli\u003e安装Redis，启动Redis\u003c/li\u003e\n\u003cli\u003e配置Openresty，启动Openresty\u003c/li\u003e\n\u003cli\u003e写页面逻辑代码\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e整个步骤我都写成了Ansible自动化配置脚本。所以，你已经不需要自己搭建。所有的代码都托管在：http://git.oschina.net/zacker330/openresty-lab 。\u003c/p\u003e\n\u003ch4 id=\"启动方法\"\u003e启动方法\u003c/h4\u003e\n\u003cp\u003e启动前，你必须安装Vagrant 和 Ansible 2.0+。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit clone https://git.oschina.net/zacker330/openresty-lab.git\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e openresty-lab\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evagrant up \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eansible-playbook ./ansible/playbook.yml -i ./ansible/inventory -u vagrant -k\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026gt;\u0026gt; 输入ssh密码 \u003cspan class=\"sb\"\u003e`\u003c/span\u003evagrant\u003cspan class=\"sb\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003ePS. ansible-playbook需要通过ssh登录上目标机器来执行我们的任务。\u003c/p\u003e\n\u003cp\u003e接下来，我们解释下代码。\u003c/p\u003e\n\u003cp\u003eOpenresty的配置如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-nginx\" data-lang=\"nginx\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e## 省去了一些不重要的nginx配置\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ehttp\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kn\"\u003edefault_type\u003c/span\u003e  \u003cspan class=\"s\"\u003eapplication/octet-stream\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e## 省去了一些不重要的nginx配置\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e## 初始化所需要对象\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kn\"\u003einit_by_lua\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003erequire\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;resty.core\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003eredis\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003erequire\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;resty.redis\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003etemplate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003erequire\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;resty.template\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003etemplate.caching(false)\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"kn\"\u003e--\u003c/span\u003e \u003cspan class=\"s\"\u003eyou\u003c/span\u003e \u003cspan class=\"s\"\u003emay\u003c/span\u003e \u003cspan class=\"s\"\u003eremove\u003c/span\u003e \u003cspan class=\"s\"\u003ethis\u003c/span\u003e \u003cspan class=\"no\"\u003eon\u003c/span\u003e \u003cspan class=\"s\"\u003eproduction\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   \u003cspan class=\"kn\"\u003eserver{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003elisten\u003c/span\u003e       \u003cspan class=\"mi\"\u003e80\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"kn\"\u003eserver_name\u003c/span\u003e  \u003cspan class=\"n\"\u003e192.168.8.10\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"kn\"\u003echarset\u003c/span\u003e        \u003cspan class=\"s\"\u003eutf-8\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e      \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \t  \u003cspan class=\"c1\"\u003e## 指定 模块路径\t\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"kn\"\u003eset\u003c/span\u003e \u003cspan class=\"nv\"\u003e$template_root\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;/usr/local/openresty/nginx/html/templates\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"kn\"\u003elocation\u003c/span\u003e \u003cspan class=\"p\"\u003e~\u003c/span\u003e \u003cspan class=\"sr\"\u003e\\.lsp$\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kn\"\u003edefault_type\u003c/span\u003e \u003cspan class=\"s\"\u003etext/html\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kn\"\u003econtent_by_lua\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#39;template.render(ngx.var.uri)\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e## 访问index.lsp，将使用index.lsp模板\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e页面逻辑代码 index.lsp：\n{% raw %}\u003c/p\u003e","title":"模仿京东使用Openresty+Redis做读服务"},{"content":"大概一年前，我做了产品负责人——负责产品的所有。其中一项最重要的工作：产品设计。\n产品设计是以前我从来没有做过的。怎么办呢？除了，在网上了解别人怎么做外，自己还找了一些书来看，比如《谷歌和亚马逊如何做产品》，《启示录——打造用户喜爱的产品》，张小龙的文章，张小龙推荐的《乌合之众》……从这些书中，的确学到一些产品设计上有用的东西，但始终都是隔靴挠痒，不得其法。\n而后，想到公司终究是要赚钱的，我们做产品的意义大部分就是为赚钱。而看到的文章或书，鲜有谈论产品设计时，如何赚更多钱的问题。在现实中，我表达出这样的困惑，但是常常得到的答案就是，你先做好产品再想这个问题好吧；产品都没做好，就想着怎么赚钱……\n商业模式是什么？ 我无力反驳，也没有人给我指导，自己又想不通（时间也不允许），就只能找书来看了。关于如何赚钱的书，抽象一些就是“商业模式”的问题，就找到了《商业模式全史》。\n而这本书首先给我们讲了什么是商业：\n把采购或生产出的价值提供给他人，以换取同等的价值\n而商业模式：\n这些要素的组合就是商业模式\n我从中总结出：\n产品设计的过程，始终需要想清楚，你的产品提供给用户什么价值？是通过采购的，还是生产出的？\n我想到开源中国的友商，像开发者头条、掘金，属于通过生产（用户生产内容）为用户提供高质量的内容，像凤凰网这类综合传统媒体则是官方生产内容，都是生产出的。\n采购的例子，可以是类似简书、CSDN的签约作者，从这些作者采购高质量的内容。\n媒体公司到底怎么赚钱，广告还是付费内容？ 可，还是老问题？我们怎么以换取同等价值——赚钱？我们为什么要免费为广大用户采购和生产高质量内容呢？想想以前人们想要高质量的内容都要自己掏钱买杂志的。\n《商业模式全史》说到90年代发起的一个商业模式：广告模式，来自美国的哥伦比亚广播公司。免费播出节目，然后吸引广告主投放广告。是不是想起央视？还有早上地铁上免费但但满是小广告的报纸？\n最近两年出了一种传统广告模式的变种：百度百家的广告分成稿酬模式。也就是百度的广告赚钱了会和作者分成。（思考题：知乎应该算是高质量的内容社区，为什么他们现在还没有大量投放广告？）\n这下，我终于明白了。开源中国社区本质上和广播公司一样，也是一家媒体公司。\n媒体公司除了靠广告赚钱，还可以像《男人装》、罗胖的得到APP那样做付费内容赚钱。\n媒体公司的两种商业模式：\n广告模式 付费内容 那你到底采用广告模式还是付费内容呢？要看你的用户群体和你的内容价值。我自己画了图，以方便大家理解。\n而内容价值这个容易产生歧义，我解释下：\n用户对内容的渴望度和稀缺度决定了内容的价值。用户对内容的渴望度和内容的稀缺度同时高，内容的价值才会高。\n这里举个例子：1953年创办的《花花公子》杂志。2016年开始在找买家了。我猜其中一个原因是不是因为人们对其内容的渴望度降低，而是稀缺性变低了。不是么，路边摊随随便都可以买到满足欲望的片子。\n说回正题，商业模式也会有创新，将来说不定又会出现第三种商业模式。\n用户体验为王，就可以赚钱？ 很多人说产品时，上来就说：用户体验为王，用户爽了，我们自然就会赚钱。\n其实，不说商业模式，只说用户体验，就是耍流氓。去过中国政%府办事的人都会吐槽政%府办事效率低（用户体验差），但是你没有办法啊，吐槽后还是必须和他们打交道。政府有垄断性资源，人家根本不需要考虑用户体验。\n内容为王 最最后，你会发现，所有的媒体公司的赚钱方式都是围绕内容，不论你使用哪种商业模式，亦或两者结合。互联网只不过是渠道。知乎不是也出Kindle格式的付费书吗？\n那么，我们在做产品设计，甚至运营时，都应该从内容本身出发，比如你这套积分系统的设计是否有利于网站得到更多优质内容、你的运营策略是否有利于留住那些生产高质量内容的人等等。甚至，我们的设计师也可以根据这点来考虑button放在哪个位置更吸引人生产高质量内容。\n好，现在我们总结出第二个关键点：\n媒体产品都是围绕内容来做，不论什么商业模式。\n问题是什么？ 好了，最后，回到我们的问题：商业模式与产品设计有什么关系？\n我的回答：\n产品必须根据现有或将来的商业模式来设计，否则产品只是功能的堆砌。\n我能想像到有人会反驳我：你看Facebook一开始时不也没有谈商业模式吗？\n我想说，一开始时，你可能是凭自己的兴趣爱好来做产品，可能是为了解决自己的小问题。但是当你成立了一家公司时，你不得不考虑如何养活一家公司时，你不可能不考虑商业模式了——如何赚钱。\n小结 这些都是自己个人的总结，不一定对，希望好心人斧正帮助我成长。谢谢。\n","permalink":"https://showme.codes/zh-cn/2016-9-15-whats-the-relation-between-production-and-business/","summary":"\u003cp\u003e大概一年前，我做了产品负责人——负责产品的所有。其中一项最重要的工作：产品设计。\u003c/p\u003e\n\u003cp\u003e产品设计是以前我从来没有做过的。怎么办呢？除了，在网上了解别人怎么做外，自己还找了一些书来看，比如《谷歌和亚马逊如何做产品》，《启示录——打造用户喜爱的产品》，张小龙的文章，张小龙推荐的《乌合之众》……从这些书中，的确学到一些产品设计上有用的东西，但始终都是隔靴挠痒，不得其法。\u003c/p\u003e\n\u003cp\u003e而后，想到公司终究是要赚钱的，我们做产品的意义大部分就是为赚钱。而看到的文章或书，鲜有谈论产品设计时，如何赚更多钱的问题。在现实中，我表达出这样的困惑，但是常常得到的答案就是，你先做好产品再想这个问题好吧；产品都没做好，就想着怎么赚钱……\u003c/p\u003e\n\u003ch4 id=\"商业模式是什么\"\u003e商业模式是什么？\u003c/h4\u003e\n\u003cp\u003e我无力反驳，也没有人给我指导，自己又想不通（时间也不允许），就只能找书来看了。关于如何赚钱的书，抽象一些就是“商业模式”的问题，就找到了《商业模式全史》。\u003c/p\u003e\n\u003cp\u003e而这本书首先给我们讲了什么是商业：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e把采购或生产出的价值提供给他人，以换取同等的价值\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e而商业模式：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这些要素的组合就是商业模式\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我从中总结出：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e产品设计的过程，始终需要想清楚，你的产品提供给用户什么价值？是通过采购的，还是生产出的？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我想到开源中国的友商，像\u003ccode\u003e开发者头条\u003c/code\u003e、\u003ccode\u003e掘金\u003c/code\u003e，属于通过生产（用户生产内容）为用户提供高质量的内容，像\u003ccode\u003e凤凰网\u003c/code\u003e这类综合传统媒体则是官方生产内容，都是生产出的。\u003c/p\u003e\n\u003cp\u003e采购的例子，可以是类似\u003ccode\u003e简书\u003c/code\u003e、\u003ccode\u003eCSDN\u003c/code\u003e的签约作者，从这些作者采购高质量的内容。\u003c/p\u003e\n\u003ch4 id=\"媒体公司到底怎么赚钱广告还是付费内容\"\u003e媒体公司到底怎么赚钱，广告还是付费内容？\u003c/h4\u003e\n\u003cp\u003e可，还是老问题？我们怎么以换取同等价值——赚钱？我们为什么要免费为广大用户采购和生产高质量内容呢？想想以前人们想要高质量的内容都要自己掏钱买杂志的。\u003c/p\u003e\n\u003cp\u003e《商业模式全史》说到90年代发起的一个商业模式：广告模式，来自美国的哥伦比亚广播公司。免费播出节目，然后吸引广告主投放广告。是不是想起央视？还有早上地铁上免费但但满是小广告的报纸？\u003c/p\u003e\n\u003cp\u003e最近两年出了一种传统广告模式的变种：百度百家的广告分成稿酬模式。也就是百度的广告赚钱了会和作者分成。（思考题：知乎应该算是高质量的内容社区，为什么他们现在还没有大量投放广告？）\u003c/p\u003e\n\u003cp\u003e这下，我终于明白了。开源中国社区本质上和广播公司一样，也是一家媒体公司。\u003c/p\u003e\n\u003cp\u003e媒体公司除了靠广告赚钱，还可以像《男人装》、罗胖的\u003ccode\u003e得到\u003c/code\u003eAPP那样做付费内容赚钱。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e媒体公司的两种商业模式\u003c/strong\u003e：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e广告模式\u003c/li\u003e\n\u003cli\u003e付费内容\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e那你到底采用广告模式还是付费内容呢？要看你的用户群体和你的内容价值。我自己画了图，以方便大家理解。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"媒体公司的商业模式参考\" loading=\"lazy\" src=\"/assets/images/2016-9-business-of-content.png\"\u003e\u003c/p\u003e\n\u003cp\u003e而内容价值这个容易产生歧义，我解释下：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e用户对内容的渴望度和稀缺度决定了内容的价值。用户对内容的渴望度和内容的稀缺度同时高，内容的价值才会高。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这里举个例子：1953年创办的《花花公子》杂志。2016年开始在找买家了。我猜其中一个原因是不是因为人们对其内容的渴望度降低，而是稀缺性变低了。不是么，路边摊随随便都可以买到满足欲望的片子。\u003c/p\u003e\n\u003cp\u003e说回正题，商业模式也会有创新，将来说不定又会出现第三种商业模式。\u003c/p\u003e\n\u003ch4 id=\"用户体验为王就可以赚钱\"\u003e用户体验为王，就可以赚钱？\u003c/h4\u003e\n\u003cp\u003e很多人说产品时，上来就说：用户体验为王，用户爽了，我们自然就会赚钱。\u003c/p\u003e\n\u003cp\u003e其实，不说商业模式，只说用户体验，就是耍流氓。去过中国政%府办事的人都会吐槽政%府办事效率低（用户体验差），但是你没有办法啊，吐槽后还是必须和他们打交道。政府有垄断性资源，人家根本不需要考虑用户体验。\u003c/p\u003e\n\u003ch4 id=\"内容为王\"\u003e内容为王\u003c/h4\u003e\n\u003cp\u003e最最后，你会发现，所有的媒体公司的赚钱方式都是围绕\u003cstrong\u003e内容\u003c/strong\u003e，不论你使用哪种商业模式，亦或两者结合。互联网只不过是渠道。知乎不是也出Kindle格式的付费书吗？\u003c/p\u003e\n\u003cp\u003e那么，我们在做产品设计，甚至运营时，都应该从内容本身出发，比如你这套积分系统的设计是否有利于网站得到更多优质内容、你的运营策略是否有利于留住那些生产高质量内容的人等等。甚至，我们的设计师也可以根据这点来考虑button放在哪个位置更吸引人生产高质量内容。\u003c/p\u003e\n\u003cp\u003e好，现在我们总结出第二个关键点：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e媒体产品都是围绕内容来做，不论什么商业模式。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch4 id=\"问题是什么\"\u003e问题是什么？\u003c/h4\u003e\n\u003cp\u003e好了，最后，回到我们的问题：商业模式与产品设计有什么关系？\u003c/p\u003e\n\u003cp\u003e我的回答：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e产品必须根据现有或将来的商业模式来设计，否则产品只是功能的堆砌。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我能想像到有人会反驳我：你看Facebook一开始时不也没有谈商业模式吗？\u003c/p\u003e\n\u003cp\u003e我想说，一开始时，你可能是凭自己的兴趣爱好来做产品，可能是为了解决自己的小问题。但是当你成立了一家公司时，你不得不考虑如何养活一家公司时，你不可能不考虑商业模式了——如何赚钱。\u003c/p\u003e\n\u003ch4 id=\"小结\"\u003e小结\u003c/h4\u003e\n\u003cp\u003e这些都是自己个人的总结，不一定对，希望好心人斧正帮助我成长。谢谢。\u003c/p\u003e","title":"商业模式与产品设计有什么关系？以媒体公司为例"},{"content":"人们常常说数据如金，可是，能被利用起的数据，才是“金”。而互联网的数据，常常以日志的媒介的形式存在，并需要从中提取其中的\u0026quot;数据\u0026quot;。\n从这些数据中，我们可以做用户画像（每个用户都点了什么广告，对哪些开源技术感兴趣），安全审计，安全防护（如果1小时内登录请求数到达一定值就报警），业务数据统计（如开源中国每天的博客数是多少，可视化编辑格式和markdown格式各占比例是多少）等等。\n之所以能做这些，是因为用户的所有的行为，都将被记录在nginx日志中或其它web服务器的日志中。日志分析要做的就是将这些日志进行结构化，方便我们的业务人员快速查询。日志分析平台要做的就是这些。\n说完这些，你是不是觉得日志分析平台很难做，需要十人的团队加班几个月才能完成？\n自从有了Elasticsearch、Logstash、Kibana，俗称ELK，小公司也可以很轻松地做日志分析了。说白了，1天几G的日志，ELK完全可以吃得消。就像标题说的，只需要1个人半小时就可以搭建好了。前提是你已经熟悉了Ansible。下文也假设你已经熟悉Anbile，如果不熟悉可以看看我的另一篇文章：Puppet，Chef，Ansible的共性\n本文目的就是教你如何在搭建一个日志分析平台的雏形。有了这个雏形，你可以慢慢迭代出更强大，更适合你业务的日志分析平台。同时，提供可执行的源代码：OSC-AdCenter\n简单日志分析架构图 我做了简化，架构图中的每个组件都可以分别放到不同的机器。这里简单介绍下这些你组件：\nyour app：你的应用，我们的源码中，把这个给省略了 Openresty：基于Nginx的Web开发平台，你可以想像它基于Nginx做了很多扩展，类似淘宝的Tengine。为什么我们不直接使用Nginx呢？因为在Openresty上，我们可以做更多事情。 Logstash：日志收集，结构化数据后，push到Elasticsearch中，基于JRuby。可使用其它日志收集工具替代，比如Beats Elasticsearch：分布式搜索引擎，基于Lucene Kibana：用于可视化数据，基于NodeJs 日志分析平台开发所需要工具 Ansible 2.0+：简单的自动化配置工具，运维工具。关于自动化配置还有什么好说的呢？ Vagrant：操作系统虚拟化工具，开发时使用。如果没有听过，Docker总听过吧。这家伙就和Docker完全类似的功能，也早于Docker出现。 一个简单的支持yml格式高亮的文本编辑器，比如Atom 自行下载JDK8:jdk-8u66-linux-x64.tar.gz放到项目路径：provision/roles/jdk8/files/jdk-8u66-linux-x64.tar.gz P.S. 抱歉这个的确需要你自己下。 什么？不用写代码吗？的确不用需要写。如果你要扩展这个雏形就会需要写一些脚本。 启动一台服务器 因为我们需要在本地开发好以后，再部署到生产环境，所以，我们需要一台服务器用来做实验。用Vagrant可以在你的开发机上虚拟化一台。clone 下 OSC-AdCenter后，进入项目目录执行：Vagrant up\n文件Vagrantfile有描述这台机器的配置：\nVagrant.configure(2) do |config| ANSIBLE_RAW_SSH_ARGS = [] machine_box = \u0026#34;trusty-server-cloudimg-amd64-vagrant-disk1\u0026#34; machine_box_url = \u0026#34;https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box\u0026#34; config.vm.define \u0026#34;oscadcenter\u0026#34; do |machine| machine.vm.box = machine_box machine.vm.box_url = machine_box_url machine.vm.hostname = \u0026#34;oscadcenter\u0026#34; machine.vm.network \u0026#34;private_network\u0026#34;, ip: \u0026#34;192.168.4.10\u0026#34; ##指定这台机器的IP，只能宿主机能访问 machine.vm.provider \u0026#34;virtualbox\u0026#34; do |node| node.name = \u0026#34;oscadcenter\u0026#34; node.memory = 4048 node.cpus = 2 end end end 更多关于Vagrantfile：https://www.vagrantup.com/docs/vagrantfile/\nVagrant机器的默认账号密码都是: vagrant，所以你可以使用ssh vagrant@192.168.4.10登录这台机器。也可以使用vagrant命令登录，在Vagrantfile所在目录下执行：vagrant ssh oscadcenter。\n部署日志分析平台 在你的开发机上，安装好ansible：\n服务器准备好了，我们只需要一条命令就可以部署OSC-AdCenter了：\nansible-playbook ./provision/playbook.yml -i ./provision/inventory -u vagrant -k 然后输入ssh登录密码：vagrant。\n简单说明：\nansible-playbook是ansible的一个命令\n./provision/playbook.yml是描述你的服务器配置的文本，你可以想像成所有的部署脚本都写在这个文件中\n./provision/inventory是服务器在playbook在的host与ip的映射表，比如playbook中这么写：\n--- - hosts: adcenter 那么，inventory文件就是这样的：\n[adcenter] 192.168.4.10 具体请看文档：http://docs.ansible.com/ansible/intro_inventory.html\n-u vagrant -k 表示使用vagrant账号ssh登录目标机器\n部署的这个过程，要看你的网速和elastic源的提供速度，可能会很漫长。 参考时长为半小时。建议执行部署后，做些别的事情，比如午休。\n测试部署是否成功 打开Elasticsearch http://192.168.4.10:9200/_plugin/head/ 可看到界面：\n打开Kibana http://192.168.4.10:5601 可看到界面：\n打开各种浏览器，输入url：http://192.168.4.10/1.gif?account=oschina\u0026amp;e=pv\u0026amp;p=p233444\u0026amp;url=www.oschina.net\u0026amp;title=学习\u0026amp;sh=1200\u0026amp;sw=800\u0026amp;cd=400\u0026amp;lang=en，然后可在Elasticsearch中和kibana中看到相应的数据\n我使用Chrome访问了两次url，再使用Safari访问了一次。就这样，Elasticsearch中出现了3条数据，而Kibana中我们可统计出，过去4小时中，Chrome占了2/3，而Safari占 1/3。\n部署过程都执行了什么？ 从部署脚本的入口./provision/playbook.yml看:\n- hosts: analysis sudo: yes vars_files: - ./vars/base-env.yml - ./vars/analysis-logstash.yml roles: - common # 执行一些基础工作 - openresty # 安装openresty - {role: \u0026#34;analysis-openresty-conf\u0026#34;, nginx_server_conf: \u0026#34;analysis.conf\u0026#34;} # 配置openresty - jdk8 # 安装jdk8，并设置JAVA_HOME到 /etc/profile中 - ansible-role-elasticsearch #安装 es - ansible-role-kibana-4 # 安装kibana4 - ansible-logstash # 安装logstash 这里的ELK的role都是从Ansible 的 Galaxy上download下来的。\n然后呢？ 学习Kibana的查询语法，根据业务需求来统计分析日志。 对当前的日志分析平台实施监控，哪天系统挂了，你都不知道。 与现在有的系统结合。 解决当单个Elasticsearch，特别庞大时的扩容问题 最后 好吧，如果你不会Ansible，你半小时可能搞不定。所以，我说的半小时，其实并不科学。但是这也恰恰说明了使用的自动化配置的好处。我一个运维外行，利用Ansible两三天就搭建好了一个简单日志分析平台。\n而且如果你要在生产环境使用这套系统，你只需要在线上准备一台干净的ubuntu服务器，修改inventory文件的IP就可以了。\n现实中的日志分析平台一定不会这么简单的，本次教程，只是抛砖引玉。\n附：项目源代码位置 http://git.oschina.net/zacker330/OSC-AdCenter\n","permalink":"https://showme.codes/zh-cn/2016-9-10-how-to-setup-a-simple-log-analysis-platform-in-half-hour/","summary":"\u003cp\u003e人们常常说数据如金，可是，能被利用起的数据，才是“金”。而互联网的数据，常常以日志的媒介的形式存在，并需要从中提取其中的\u0026quot;数据\u0026quot;。\u003c/p\u003e\n\u003cp\u003e从这些数据中，我们可以做用户画像（每个用户都点了什么广告，对哪些开源技术感兴趣），安全审计，安全防护（如果1小时内登录请求数到达一定值就报警），业务数据统计（如开源中国每天的博客数是多少，可视化编辑格式和markdown格式各占比例是多少）等等。\u003c/p\u003e\n\u003cp\u003e之所以能做这些，是因为用户的所有的行为，都将被记录在nginx日志中或其它web服务器的日志中。日志分析要做的就是将这些日志进行结构化，方便我们的业务人员快速查询。日志分析平台要做的就是这些。\u003c/p\u003e\n\u003cp\u003e说完这些，你是不是觉得日志分析平台很难做，需要十人的团队加班几个月才能完成？\u003c/p\u003e\n\u003cp\u003e自从有了Elasticsearch、Logstash、Kibana，俗称ELK，小公司也可以很轻松地做日志分析了。说白了，1天几G的日志，ELK完全可以吃得消。就像标题说的，只需要1个人半小时就可以搭建好了。前提是你已经熟悉了Ansible。下文也假设你已经熟悉Anbile，如果不熟悉可以看看我的另一篇文章：\u003ca href=\"https://my.oschina.net/zjzhai/blog/600430\"\u003ePuppet，Chef，Ansible的共性\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文目的就是教你如何在搭建一个日志分析平台的雏形。有了这个雏形，你可以慢慢迭代出更强大，更适合你业务的日志分析平台。同时，提供可执行的源代码：\u003ca href=\"http://git.oschina.net/zacker330/OSC-AdCenter\"\u003eOSC-AdCenter\u003c/a\u003e\u003c/p\u003e\n\u003ch4 id=\"简单日志分析架构图\"\u003e简单日志分析架构图\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"简单日志分析架构图\" loading=\"lazy\" src=\"/assets/images/2016-9-elk.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我做了简化，架构图中的每个组件都可以分别放到不同的机器。这里简单介绍下这些你组件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eyour app：你的应用，我们的源码中，把这个给省略了\u003c/li\u003e\n\u003cli\u003eOpenresty：基于Nginx的Web开发平台，你可以想像它基于Nginx做了很多扩展，类似淘宝的Tengine。为什么我们不直接使用Nginx呢？因为在Openresty上，我们可以做更多事情。\u003c/li\u003e\n\u003cli\u003eLogstash：日志收集，结构化数据后，push到Elasticsearch中，基于JRuby。可使用其它日志收集工具替代，比如\u003ca href=\"https://www.elastic.co/guide/en/beats/libbeat/current/index.html\"\u003eBeats\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eElasticsearch：分布式搜索引擎，基于Lucene\u003c/li\u003e\n\u003cli\u003eKibana：用于可视化数据，基于NodeJs\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"日志分析平台开发所需要工具\"\u003e日志分析平台开发所需要工具\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.oschina.net/p/ansible\"\u003eAnsible\u003c/a\u003e 2.0+：简单的自动化配置工具，运维工具。\u003ca href=\"https://my.oschina.net/zjzhai/blog/732120\"\u003e关于自动化配置还有什么好说的呢？\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.oschina.net/p/vagrant\"\u003eVagrant\u003c/a\u003e：操作系统虚拟化工具，开发时使用。如果没有听过，Docker总听过吧。这家伙就和Docker完全类似的功能，也早于Docker出现。\u003c/li\u003e\n\u003cli\u003e一个简单的支持yml格式高亮的文本编辑器，比如Atom\u003c/li\u003e\n\u003cli\u003e自行下载JDK8:\u003cstrong\u003ejdk-8u66-linux-x64.tar.gz\u003c/strong\u003e放到项目路径：\u003ccode\u003eprovision/roles/jdk8/files/jdk-8u66-linux-x64.tar.gz\u003c/code\u003e P.S. 抱歉这个的确需要你自己下。\u003c/li\u003e\n\u003cli\u003e什么？不用写代码吗？的确不用需要写。如果你要扩展这个雏形就会需要写一些脚本。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"启动一台服务器\"\u003e启动一台服务器\u003c/h4\u003e\n\u003cp\u003e因为我们需要在本地开发好以后，再部署到生产环境，所以，我们需要一台服务器用来做实验。用Vagrant可以在你的开发机上虚拟化一台。clone 下 OSC-AdCenter后，进入项目目录执行：\u003ccode\u003eVagrant up\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e文件Vagrantfile有描述这台机器的配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ruby\" data-lang=\"ruby\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"no\"\u003eVagrant\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econfigure\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"no\"\u003eANSIBLE_RAW_SSH_ARGS\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e[]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003emachine_box\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;trusty-server-cloudimg-amd64-vagrant-disk1\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003emachine_box_url\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evm\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edefine\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;oscadcenter\u0026#34;\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"n\"\u003emachine\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emachine\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evm\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebox\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emachine_box\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emachine\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evm\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebox_url\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emachine_box_url\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emachine\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evm\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ehostname\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;oscadcenter\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emachine\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evm\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enetwork\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;private_network\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"ss\"\u003eip\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;192.168.4.10\u0026#34;\u003c/span\u003e \u003cspan class=\"c1\"\u003e##指定这台机器的IP，只能宿主机能访问\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emachine\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evm\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eprovider\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;virtualbox\u0026#34;\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;oscadcenter\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ememory\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e4048\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecpus\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   \u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e更多关于Vagrantfile：https://www.vagrantup.com/docs/vagrantfile/\u003c/p\u003e\n\u003cp\u003eVagrant机器的默认账号密码都是: \u003cstrong\u003evagrant\u003c/strong\u003e，所以你可以使用\u003ccode\u003essh vagrant@192.168.4.10\u003c/code\u003e登录这台机器。也可以使用vagrant命令登录，在Vagrantfile所在目录下执行：\u003ccode\u003evagrant ssh oscadcenter\u003c/code\u003e。\u003c/p\u003e\n\u003ch4 id=\"部署日志分析平台\"\u003e部署日志分析平台\u003c/h4\u003e\n\u003cp\u003e在你的开发机上，安装好ansible：\u003c/p\u003e\n\u003cp\u003e服务器准备好了，我们只需要一条命令就可以部署OSC-AdCenter了：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eansible-playbook ./provision/playbook.yml  -i ./provision/inventory  -u vagrant -k\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e然后输入ssh登录密码：\u003cstrong\u003evagrant\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e简单说明：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eansible-playbook是ansible的一个命令\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e./provision/playbook.yml是描述你的服务器配置的文本，你可以想像成所有的部署脚本都写在这个文件中\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e./provision/inventory是服务器在playbook在的host与ip的映射表，比如playbook中这么写：\u003c/p\u003e","title":"如何在半小时搭建一个简单的日志分析平台？"},{"content":"最近我们团队正在将生产环境的配置进行自动化。简单地说就是使生产环境在任何地方都可以快速的搭建起来，比如程序员在自己的机器上，公司内部的机器上，还有云上。\n本文就是想阐明为什么要自动化配置。\n进行配置自动化的这个过程，我发现，问题不在于程序员懂不懂Ansible、Chef、Puppet这些自动化配置工具的使用。\n问题在于对“配置”本身的理解。我甚至发现还有运维人员也不能理解为什么要将配置自动化。\n配置是什么？ 很多人都将搭建Tomcat、Nginx这类脚本理解成一个个“任务”。这类脚本还不能被称为“配置”。\n而这类Tomcat、Nginx的安装脚本大概是这样：\napt-get install build-essential apt-get install libtool cd /usr/local/src wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.37.tar.gz tar -zxvf pcre-8.37.tar.gz cd pcre-8.34 ./configure make make install ..... 脚本来自：http://www.nginx.cn/install\n运行这类脚本时，大脑里的概念是：我要“安装”Tomcat、Nginx。这类脚本被当成“动词”存在。也就是说在面对一台机器时，我们的思维方式是：我要写一个if来判断Tomcat是不是已经安装，如果没有安装，就是执行apt-get install，blabla……\n但是，现实的自动化配置工具Ansible、Chef、Puppet告诉我们，自动化配置的脚本更应当充当机器环境的状态的描述文档。也就是面对机器时，我们应该使用“形容”词。什么意思呢？来几个配置体会下：\nAnsible:\n# ./ansible-nginx/tasks/install_nginx.yml # 使用这个7-0.el7版本的yum包 - name: NGINX | Installing NGINX repo rpm yum: name: http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm # 当前机器的nginx的状态应该是最新版本 - name: NGINX | Installing NGINX yum: name: nginx state: latest # 当前机器的nginx service的状态应该是已经启动的。至于如何确保nginx这个service是如何启动的，我们不需要关心。 - name: NGINX | Starting NGINX service: name: nginx state: started 配置来自：https://www.nginx.com/blog/installing-nginx-nginx-plus-ansible/\n篇幅有限，我们就只举Ansible的例子。\n配置的本质 相信很多人看了上面的例子，还是一脸懵逼( ¯ □ ¯ ) ，下面来个现实例子。\n现实中，我们组装一台台式电脑，你会这样对装机人员描述：我要4G内存，我要1T的SSD等等；而不是：我打开机箱，找到内存插槽，将4G内存插上，再把1T的SSD插入机架，接着把线接好 blabla。\n我们作为需求方时，我们只需要描述需求。作为实现方时，我们需要根据需求执行相应的动作。\n而配置更像是一份对目标机器（不论多少）的状态需求，我们作为需求方，只要在配置上写清楚目标机器的状态，至于这些状态最终是怎么达到的，由Ansible、Chef、Puppet这类自动化配置工具（实现方）实现。\n也就是说，配置这个概念帮助我们程序员、运维人员更容易站在需求方的角色去思考问题，而不是实现方。\n问题来了？那有什么用呢。\n**配置解放了我们的双手，使我们有更多的时间去思考更高层的问题。**这是最大的好处。\n其它好处还有：\n配置可以将配置和机器进行解耦，同一份配置可适配不同的操作系统。也就是拿着这份电脑配置清单，我们去不同的商家去对比价钱。内存可能来自A商家，SSD可能来自B商家。 配置可以重复使用，这也就是为什么可以自动化的原因。新来的程序员很容易就可以搭建好一个与线上生产环境配置一样的开发环境，来节约培训成本。 可对配置进行更好的版本控制。也就是当前机器的状态，由谁，什么时候修改的，完全可控。不会出现报怨：这是谁干的？什么时候加的这个脚本。 可很轻松的查清当前机器的状态。哪些机器安装了什么，使用了什么nginx配置，一目了然。而不需要程序员专门跑去询问运维人员，当前生产环境是怎么配置的啊，我们在本地重现不了问题。 在现实中，对于“自动化”，我听过最多的话是：我们机器才3台，没有必要使用这些自动化配置工具。我觉得这不能成为不自动化配置的借口，原因有：\n生产/开发环境不可能只部署一次，身边出现过这样的例子，因为机器坏了，然后花3个人天搭建测试环境。 再少的机器也会涉及线上和线下的环境一致性问题。想想你是不是遇到过，在本地怎么测试都不出问题，但是一到线上就问题一大堆，后来才发现原来是线上使用的另一套配置。如果你觉得不是问题，那么想想，项目的历史包袱就是由类似这些“环境问题”的小问题一点点积累起来的。如果机器少时候，不开始做，今后可能就要花数倍的成本去弥补。 节约运维成本，因为你不需要那么依赖于运维人员了（恐怕很多运维的同学不同意） 当业务成长时，我们需要上云了。可是，我们没有一个人完全了解当前的环境的情况。因为所有的修改都没有版本控制，今天一个人登上生产环境apt-get install abc，明天一个人登上去 sudo chown user:group … 最终，你一定会为之前的“随意”买单。 这些问题，在我身边或多或少的发生了。我听得最多的也是：自动化配置解决了什么业务问题嘛，技术是为了业务服务的。我想说：前人犯的错，我们为什么又犯一次？所以，我会一开始就自动化。\n在2012年，美暴雨致亚马逊数据中心断电 Netflix等中断，Instagram也是其中受害者，看看他们的总结，Instagram这个月就5岁了，创始人跟你说说它都经历了哪些大事儿 [来自虎嗅网]：\n另外，我们有时也需要扮演实现方，因为自动化配置工具无法满足需求，就需要自己的写工具的插件，更甚至实现自己的自动化配置工具。\n总结 只有理解了\u0026quot;配置\u0026quot;的概念，才能更好的理解为什么Ansible、Chef、Puppet要这样设计。 配置解放了我们的双手，使我们有更多的时间去思考更高层的问题。 自动化配置是为了提升团队工作效率。 配置也是需要版本控制的。 未雨绸缪胜过做救火leader。 这是我的个人观点，欢迎反驳。\n","permalink":"https://showme.codes/zh-cn/2016-8-12-automation-configuration/","summary":"\u003cp\u003e最近我们团队正在将生产环境的配置进行自动化。简单地说就是使生产环境在任何地方都可以快速的搭建起来，比如程序员在自己的机器上，公司内部的机器上，还有云上。\u003c/p\u003e\n\u003cp\u003e本文就是想阐明为什么要自动化配置。\u003c/p\u003e\n\u003cp\u003e进行配置自动化的这个过程，我发现，问题不在于程序员懂不懂Ansible、Chef、Puppet这些自动化配置工具的使用。\u003c/p\u003e\n\u003cp\u003e问题在于对“配置”本身的理解。我甚至发现还有运维人员也不能理解为什么要将配置自动化。\u003c/p\u003e\n\u003ch3 id=\"配置是什么\"\u003e配置是什么？\u003c/h3\u003e\n\u003cp\u003e很多人都将搭建Tomcat、Nginx这类脚本理解成一个个“任务”。这类脚本还不能被称为“配置”。\u003c/p\u003e\n\u003cp\u003e而这类Tomcat、Nginx的安装脚本大概是这样：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapt-get install build-essential\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapt-get install libtool\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e /usr/local/src\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.37.tar.gz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etar -zxvf pcre-8.37.tar.gz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e pcre-8.34\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./configure\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emake\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emake install\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e.....\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e脚本来自：http://www.nginx.cn/install\u003c/p\u003e\n\u003cp\u003e运行这类脚本时，大脑里的概念是：我要“安装”Tomcat、Nginx。这类脚本被当成“动词”存在。也就是说在面对一台机器时，我们的思维方式是：我要写一个\u003ccode\u003eif\u003c/code\u003e来判断Tomcat是不是已经安装，如果没有安装，就是执行apt-get install，blabla……\u003c/p\u003e\n\u003cp\u003e但是，现实的自动化配置工具Ansible、Chef、Puppet告诉我们，自动化配置的脚本更应当充当机器环境的状态的描述文档。也就是面对机器时，我们应该使用“形容”词。什么意思呢？来几个配置体会下：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAnsible:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# ./ansible-nginx/tasks/install_nginx.yml\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# 使用这个7-0.el7版本的yum包 \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eNGINX | Installing NGINX repo rpm\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003eyum\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ehttp://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# 当前机器的nginx的状态应该是最新版本\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eNGINX | Installing NGINX\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003eyum\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003enginx  \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003elatest\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# 当前机器的nginx service的状态应该是已经启动的。至于如何确保nginx这个service是如何启动的，我们不需要关心。\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eNGINX | Starting NGINX\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003eservice\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003enginx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003estarted\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e配置来自：https://www.nginx.com/blog/installing-nginx-nginx-plus-ansible/\u003c/p\u003e","title":"关于自动化配置还有什么好说的呢？"},{"content":"初做管理，想快速成长，而不想只依赖磨时间来带动“管理”的能力成长。所以，找到此书。\n我期望从这本书中弄清楚的是管理过程中，到底会遇到哪些难题，以及别人解决这些难题的思路是什么样的。很幸运，这本书没有辜负我。\n以下是书摘：\n管理者，是不可能花大量时间去查阅几百本书之后再下决定 商业界却比以往任何时候都要复杂和扑朔迷离，管理者需要在更短的时间内对不同的观点和建议做出判断。但是他们通常工作繁忙，日程紧张，不可能花大量时间去查阅几百本书之后再下决定，他们需要的是马上就能帮助解决问题的答案或思路。\n管理者，每天都有各种问题等着做决策，不论是自己的还是别人提出的。而且，还会面对有不少问题是之前从来没有想过的，也需要做决策。\n这里并不是在抱怨做管理需要不停地做决策，心累。而是要说明，1. 管理者需要有一些原则来指导决策；2. 管理者平时就需要多思考，必须提前对自己提问，而不是等到问题出现再去考虑；3. 管理者必须有能力对问题进行抽象、分类。\n其实我们自己在写代码时，也会不停地做决策。这个类要怎样的命名，这段逻辑到底要不要抽出来等等。编程时的决策大多已经有前人总结出来，只要你多学习前人经验+自己勤思考，基本上也就能成为合格的程序员了。但是管理不一样。很多问题，是必须你即时做决策的，比如员工当面向你提出离职，没有时间留给你查资料后做决定。那么如何才能更即时地做出更正确的决策呢？这个没有答案的问题，我自己是这样看的：很多能力是可以通过练习的方式习得的，在你还没有成为管理者的时候，应该向你的管理者学习如何管理并对其管理方式进行抽象。当然，我不是说你不学编程了就一心只学编程。就像我自己学开车，我不是一开始就找个驾校，而是抓住很一次坐车的机会，向司机学习，同时看着司机开车，自己在大脑里预演自己应该如何开车。\n管理难题的分类 书中是对管理难题这样分类：\n员工层面的难题 团队层面的难题 来自外界的难题 冲突带来的难题 变革中难题 权力、政治和影响力带来的难题 涉及自己的难题 其实，这和你自己当前所在岗位有关。如果你只是一个小小的PM，通常你只会遇到员工和团队层面的难题，涉及自己的难题除外。但是这并不妨碍你，看到公司中有变革中难题时，向那些真正遇到这方面问题的人学习。我想这就是这个分类，对我们这些小PM最大的益处。我甚至从上几家公司学习如何劝一名员工不离职。\n员工层面的难题 最常见的管理难题通常与团队中个人的需求相关，这些个人需求之所以会变成管理难题，原因在于管理者宁愿解决和工作任务相关的问题，也不愿处理一些有关自我认知或照顾他人感受的问题。\n成功的管理者既能意识到这些问题，也有能力解决它们。不管是个人缺乏动力、缺乏信心或者是个人能力不足，这些都是团队或组织沉重的负担。\n每位管理者还必须有能力与员工就棘手的问题进行交流，不管是员工出现了个人变动，或者他们需要得到企业的关注，还是他们做了不可接受或进行了破坏性的行为，你都必须能够开启那些具有挑战性的交流，或者当问题反映到你这里时，你要能够对其做出积极的回应。\n员工层面可能会遇到的难题：\n缺乏动力 缺乏信心 能力不足 虽优秀但“难伺候” 授权与放手 …. 相信每个管理者都遇到过千奇百怪的，比如员工的家人说做IT不好，然后这个员工最离职了。\n遇到员工层面的难题，如何分析呢？我个人从书中总结出的： 出问题是团队新成员，还是老员工？出问题前是这个人是怎样的：有没职位上的变动，能力能否胜任当前工作，有没有新人加入，他和其他团队成员的关系怎么样等等；企业环境是怎样的：公司最近出了什么政策，公司的薪资水平是否有调整等\n但是这些毕竟是你自己猜想。所以，处理员工层面的问题（其实也适用于处理每个层面的问题）最重要的事情就是：沟通，沟通，沟通！\n团队层面的难题 如何有效管理团队并让团队发挥最大功效是管理者要应对的最大也是最常见的管理难题\n很多人将发挥最大功效等同于压榨。我个人想法是大家对于发挥最大功效的理解不同。在体力工作年代，产出的物件越多，就是量越大功效越大。但是知识工作则不同，知识工作不是量越大，功效越大。因为你根本无法度量“知识”这个东西，多大量才算大功效呢？一个月生产了10个ERP软件就算功效大？真实场景是一个月只需要生产一个ERP，然后由销售卖出10个。\n对于知识工作，是否是压榨？取决于自己是否在做重复工作。比如你工作3年了，每年做的工作都是一样的，那么，你就是自己压榨自己！你不学习，你就不能胜任新工作，毕竟学习是自己的事。\n反过来，作为技术管理者，我们应该思考，什么才是技术团队的最大功效？\n权力、政治和影响力带来的难题 在企业生活中，所有东西都带有政治色彩，这是因为总是有公开的或隐藏的组织意图与公开或隐藏的个人意图交叉在一起。这些意图通常并非一心为公，更多的是为了一己私利，队员们追求的是对自己、自己所属团队或部门有利的意图，而非无私地追求企业、企业的客户和顾客有利的意图。\n刚毕业时感受不到这些“权力”、“政治”带来的难题，但是随着年龄增长，慢慢就发现真的就是那样：即使两个人公司也会存在政治。\n带来的思考是“政治”这个东西在什么环境是好的，什么环境是坏的？如果无法消除这种“私心”，那么如何把企业的利益和员工的利益相结合呢？这本书没有回答。但是另一本书叫《联盟》的书，也许能给我们答案。\n总结 整本书最关键的是带给我们一种解决问题的思维方式。书中没有直接说明这点。什么思维方式呢？当我们面对管理上的难题时，可以这样思考：\n首先，想一想 确定行动思路 想什么呢？\n想清楚问题到底是什么？ 事实有哪些？ 理清相关者背后的利益关系 听取当事人的想法 如何确定行动思路？这点，书中倒是没有很好的抽象，只是告诉你怎样做。也许这方面就真的依赖于经验了。\n在“涉及自己的难题”这一章开头：\n管理者面临的一些最大挑战实际上并不是和其他人相关或者与技巧和能力方面的要求相关。这些有形的挑战他们往往都能处理好。对很多管理者来说，最大的挑战是更加无形的，通常都与自我意识有关。有些挑战看起来似乎是直接针对你个人的，并不是以职业身份发起，而是以个人的名义发起的，对于这样高度私人化的难题，你该怎样做出最佳回应？ 自我意识有助于你应对那些针对你个人本身的难题，因为它让你清楚地明白自己是谁以及在做什么。如果你了解自己，就知道什么是重要的，知道自己喜欢怎样工作以及怎样和别人打交道，知道自己作为个体的优势以及哪些方面还有待改善。\n唯有自己真正强大了，难题也就不存在了。\n思考题 公司有些福利政策，但这些政策依赖于个人自觉。如果出现了不自觉的情况，你如何管理？比如灵活的上下班机制，起初是考虑到大家如果真有事或住得远，可以晚到一会。但是慢慢会变成了每天必须晚到。如果你是管理者，你觉得这类问题是问题吗？为什么？\n题外话 有时我会和别人讨论这些管理问题。但常常得到的回答却是：你不知道“不在其位，不谋其政”吗？ 想必看这篇文章其中一部分读者也会这么想。我的个人观点是：机会都是交给那些随时准备着的人的，在做好你当前工作的同时，多思考一些，等机会来的时候，你就不会失去机会。\n老翟书摘说明\n书摘内容完全来自原书，如果原书的作者或出版商觉得我侵权了。请通过开源中国 @翟志军 联系我。\n老翟书摘旨在通过一种书摘的方式让大家花最少的时间了解一本书，从而决定要不要继续读下去。书摘的每一本书都是本人亲自读过并理解的。\n","permalink":"https://showme.codes/zh-cn/2016-7-20-solutions-for-managers-problem/","summary":"\u003cp\u003e初做管理，想快速成长，而不想只依赖磨时间来带动“管理”的能力成长。所以，找到此书。\u003c/p\u003e\n\u003cp\u003e我期望从这本书中弄清楚的是管理过程中，到底会遇到哪些难题，以及别人解决这些难题的思路是什么样的。很幸运，这本书没有辜负我。\u003c/p\u003e\n\u003cp\u003e以下是书摘：\u003c/p\u003e\n\u003ch3 id=\"管理者是不可能花大量时间去查阅几百本书之后再下决定\"\u003e管理者，是不可能花大量时间去查阅几百本书之后再下决定\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e商业界却比以往任何时候都要复杂和扑朔迷离，管理者需要在更短的时间内对不同的观点和建议做出判断。但是他们通常工作繁忙，日程紧张，不可能花大量时间去查阅几百本书之后再下决定，他们需要的是马上就能帮助解决问题的答案或思路。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e管理者，每天都有各种问题等着做决策，不论是自己的还是别人提出的。而且，还会面对有不少问题是之前从来没有想过的，也需要做决策。\u003c/p\u003e\n\u003cp\u003e这里并不是在抱怨做管理需要不停地做决策，心累。而是要说明，1. 管理者需要有一些原则来指导决策；2. 管理者平时就需要多思考，必须提前对自己提问，而不是等到问题出现再去考虑；3. 管理者必须有能力对问题进行抽象、分类。\u003c/p\u003e\n\u003cp\u003e其实我们自己在写代码时，也会不停地做决策。这个类要怎样的命名，这段逻辑到底要不要抽出来等等。编程时的决策大多已经有前人总结出来，只要你多学习前人经验+自己勤思考，基本上也就能成为合格的程序员了。但是管理不一样。很多问题，是必须你即时做决策的，比如员工当面向你提出离职，没有时间留给你查资料后做决定。那么如何才能更即时地做出更正确的决策呢？这个没有答案的问题，我自己是这样看的：很多能力是可以通过练习的方式习得的，在你还没有成为管理者的时候，应该向你的管理者学习如何管理并对其管理方式进行抽象。当然，我不是说你不学编程了就一心只学编程。就像我自己学开车，我不是一开始就找个驾校，而是抓住很一次坐车的机会，向司机学习，同时看着司机开车，自己在大脑里预演自己应该如何开车。\u003c/p\u003e\n\u003ch3 id=\"管理难题的分类\"\u003e管理难题的分类\u003c/h3\u003e\n\u003cp\u003e书中是对管理难题这样分类：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e员工层面的难题\u003c/li\u003e\n\u003cli\u003e团队层面的难题\u003c/li\u003e\n\u003cli\u003e来自外界的难题\u003c/li\u003e\n\u003cli\u003e冲突带来的难题\u003c/li\u003e\n\u003cli\u003e变革中难题\u003c/li\u003e\n\u003cli\u003e权力、政治和影响力带来的难题\u003c/li\u003e\n\u003cli\u003e涉及自己的难题\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e其实，这和你自己当前所在岗位有关。如果你只是一个小小的PM，通常你只会遇到员工和团队层面的难题，\u003ccode\u003e涉及自己的难题\u003c/code\u003e除外。但是这并不妨碍你，看到公司中有\u003ccode\u003e变革中难题\u003c/code\u003e时，向那些真正遇到这方面问题的人学习。我想这就是这个分类，对我们这些小PM最大的益处。我甚至从上几家公司学习\u003ccode\u003e如何劝一名员工不离职\u003c/code\u003e。\u003c/p\u003e\n\u003ch3 id=\"员工层面的难题\"\u003e员工层面的难题\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e最常见的管理难题通常与团队中个人的需求相关，这些个人需求之所以会变成管理难题，原因在于管理者宁愿解决和工作任务相关的问题，也不愿处理一些有关自我认知或照顾他人感受的问题。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e成功的管理者既能意识到这些问题，也有能力解决它们。不管是个人缺乏动力、缺乏信心或者是个人能力不足，这些都是团队或组织沉重的负担。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e每位管理者还必须有能力与员工就棘手的问题进行交流，不管是员工出现了个人变动，或者他们需要得到企业的关注，还是他们做了不可接受或进行了破坏性的行为，你都必须能够开启那些具有挑战性的交流，或者当问题反映到你这里时，你要能够对其做出积极的回应。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e员工层面可能会遇到的难题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e缺乏动力\u003c/li\u003e\n\u003cli\u003e缺乏信心\u003c/li\u003e\n\u003cli\u003e能力不足\u003c/li\u003e\n\u003cli\u003e虽优秀但“难伺候”\u003c/li\u003e\n\u003cli\u003e授权与放手\u003c/li\u003e\n\u003cli\u003e….\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e相信每个管理者都遇到过千奇百怪的，比如员工的家人说做IT不好，然后这个员工最离职了。\u003c/p\u003e\n\u003cp\u003e遇到员工层面的难题，如何分析呢？我个人从书中总结出的：\n出问题是团队新成员，还是老员工？出问题前是这个人是怎样的：有没职位上的变动，能力能否胜任当前工作，有没有新人加入，他和其他团队成员的关系怎么样等等；企业环境是怎样的：公司最近出了什么政策，公司的薪资水平是否有调整等\u003c/p\u003e\n\u003cp\u003e但是这些毕竟是你自己\u003cstrong\u003e猜想\u003c/strong\u003e。所以，处理员工层面的问题（其实也适用于处理每个层面的问题）最重要的事情就是：沟通，沟通，沟通！\u003c/p\u003e\n\u003ch3 id=\"团队层面的难题\"\u003e团队层面的难题\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e如何有效管理团队并让团队发挥最大功效是管理者要应对的最大也是最常见的管理难题\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e很多人将\u003ccode\u003e发挥最大功效\u003c/code\u003e等同于\u003ccode\u003e压榨\u003c/code\u003e。我个人想法是大家对于\u003ccode\u003e发挥最大功效\u003c/code\u003e的理解不同。在体力工作年代，产出的物件越多，就是量越大功效越大。但是知识工作则不同，知识工作不是量越大，功效越大。因为你根本无法度量“知识”这个东西，多大量才算大功效呢？一个月生产了10个ERP软件就算功效大？真实场景是一个月只需要生产一个ERP，然后由销售卖出10个。\u003c/p\u003e\n\u003cp\u003e对于知识工作，是否是压榨？取决于自己是否在做重复工作。比如你工作3年了，每年做的工作都是一样的，那么，你就是自己压榨自己！你不学习，你就不能胜任新工作，毕竟学习是自己的事。\u003c/p\u003e\n\u003cp\u003e反过来，作为技术管理者，我们应该思考，什么才是技术团队的最大功效？\u003c/p\u003e\n\u003ch3 id=\"权力政治和影响力带来的难题\"\u003e权力、政治和影响力带来的难题\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在企业生活中，所有东西都带有政治色彩，这是因为总是有公开的或隐藏的组织意图与公开或隐藏的个人意图交叉在一起。这些意图通常并非一心为公，更多的是为了一己私利，队员们追求的是对自己、自己所属团队或部门有利的意图，而非无私地追求企业、企业的客户和顾客有利的意图。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e刚毕业时感受不到这些“权力”、“政治”带来的难题，但是随着年龄增长，慢慢就发现真的就是那样：即使两个人公司也会存在政治。\u003c/p\u003e\n\u003cp\u003e带来的思考是“政治”这个东西在什么环境是好的，什么环境是坏的？如果无法消除这种“私心”，那么如何把企业的利益和员工的利益相结合呢？这本书没有回答。但是另一本书叫《联盟》的书，也许能给我们答案。\u003c/p\u003e\n\u003ch3 id=\"总结\"\u003e总结\u003c/h3\u003e\n\u003cp\u003e整本书最关键的是带给我们一种解决问题的思维方式。书中没有直接说明这点。什么思维方式呢？当我们面对管理上的难题时，可以这样思考：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e首先，想一想\u003c/li\u003e\n\u003cli\u003e确定行动思路\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e想什么呢？\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e想清楚问题到底是什么？\u003c/li\u003e\n\u003cli\u003e事实有哪些？\u003c/li\u003e\n\u003cli\u003e理清相关者背后的利益关系\u003c/li\u003e\n\u003cli\u003e听取当事人的想法\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e如何确定行动思路？这点，书中倒是没有很好的抽象，只是告诉你怎样做。也许这方面就真的依赖于经验了。\u003c/p\u003e\n\u003cp\u003e在“涉及自己的难题”这一章开头：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e管理者面临的一些最大挑战实际上并不是和其他人相关或者与技巧和能力方面的要求相关。这些有形的挑战他们往往都能处理好。对很多管理者来说，最大的挑战是更加无形的，通常都与自我意识有关。有些挑战看起来似乎是直接针对你个人的，并不是以职业身份发起，而是以个人的名义发起的，对于这样高度私人化的难题，你该怎样做出最佳回应？\n自我意识有助于你应对那些针对你个人本身的难题，因为它让你清楚地明白自己是谁以及在做什么。如果你了解自己，就知道什么是重要的，知道自己喜欢怎样工作以及怎样和别人打交道，知道自己作为个体的优势以及哪些方面还有待改善。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e唯有自己真正强大了，难题也就不存在了。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"思考题\"\u003e思考题\u003c/h3\u003e\n\u003cp\u003e公司有些福利政策，但这些政策依赖于个人自觉。如果出现了不自觉的情况，你如何管理？比如灵活的上下班机制，起初是考虑到大家如果\u003cstrong\u003e真\u003c/strong\u003e有事或住得远，可以晚到一会。但是慢慢会变成了每天\u003cstrong\u003e必须\u003c/strong\u003e晚到。如果你是管理者，你觉得这类问题是问题吗？为什么？\u003c/p\u003e\n\u003ch3 id=\"题外话\"\u003e题外话\u003c/h3\u003e\n\u003cp\u003e有时我会和别人讨论这些管理问题。但常常得到的回答却是：你不知道“不在其位，不谋其政”吗？\n想必看这篇文章其中一部分读者也会这么想。我的个人观点是：机会都是交给那些随时准备着的人的，在做好你当前工作的同时，多思考一些，等机会来的时候，你就不会失去机会。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e老翟书摘说明\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e书摘内容完全来自原书，如果原书的作者或出版商觉得我侵权了。请通过开源中国 \u003ca href=\"http://my.oschina.net/zjzhai\"\u003e@翟志军\u003c/a\u003e 联系我。\u003c/p\u003e\n\u003cp\u003e老翟书摘旨在通过一种书摘的方式让大家花最少的时间了解一本书，从而决定要不要继续读下去。书摘的每一本书都是本人亲自读过并理解的。\u003c/p\u003e","title":"老翟书摘：《50大管理难题解决方案》"},{"content":"在我和同事结对时，发现数据库中多个表中，分别都会有gender这个字段。比如A表，B表，C表。这三个表中，gender字段都是int类型。但是同一性别，在各个表中的值是不同的。比如A表中，1代表男，在B表中却代表了女，在C表中代表未知。\n我突然意识到这背后存在更大的问题。从而引发我对“性别字段存储时应该使用的字符串，还是数字？”这个问题思考。也许已经有前辈思考过这个问题并写在某本书的某页，如果有，请告知。谢谢。\n0代表女，1代表男 首先，你可能会问，对于这样的问题还用想吗？不是都使用数字吗？0代表女，1代表男。\n其实，淘宝就是这么做的：\nhtml代码是这样的：\n这时，我会问如果这个用户没有填写性别信息呢？那你可能将原来的实现改成0代表空，1代表男，2代表女。我提醒你，当你开发的是一个大型网站时，你要将原来的“0代表女”改成“0代表空”，不会那么容易。历史数据要处理。你还需要修改所有用到0，1的代码，即使你使用的是常量代替而不是魔法数字，也不会容易到哪里去。\n有经验的程序员 是的，有经验的程序员写代码时，一开始就会想到这个问题，所以一开始就设计“0代表空，1代表男，2代表女”。从前端到后端都统一使用数字。比如：\nclass User{ final static int GENDER_NULL = 0 final static int GENDER_MALE = 1 final static int GENDER_FEMALE = 2 int gender } class UserController{ void saveUser(int gender) } \u0026lt;div\u0026gt;gender: #if($user.gender == 1)男#elseif($user.gender == 2)女 blabla….\u0026lt;/div\u0026gt; 当然前端这样写有些难看，那我们使用宏来代替，比如\u0026lt;div\u0026gt;#displayGender($user.gender)\u0026lt;/div\u0026gt;。这里我想留一个疑问：如果想国际化呢？你的displayGender怎么实现的？\n实习生来了 某天公司招来了一个实习生要实现一个活动申请表页面。领导觉得这个功能应该不难，所以就将这个任务分配给他。他为了表现自己，哐啷哐啷很快就写完了，还得到了领导的表扬。但实习生根本没有参照前面有经验的程序员的写法（有时不是他的问题，可能是没有人告诉他需要参照某个功能的写法来实现）。有意识一些的实习生还知道将gender的值写成常量，没有意识的，可能你只有去到前端页面看源码才能知道0, 1分别代表什么。\nclass ActivityApply{ final static int GENDER_MALE = 0 final static int GENDER_FEMALE = 1 int gender } 如果他没有参照前面有经验程序员的写法，我不确定他是否会重用那个前端宏。所以，讲到这里，你应该明白，有时你设计好的“重用”，并不一定会被重用。为什么呢？:P\n这里不是故意贬低所有实习生，只是情节需要。\n0和1到底放在哪里？ 也许你意识到了（通常不包括架构师），我们需要统一将gender常量的值放在某个地方。那位有经验的程序员将其放到了User类中。这样，所有使用的gender的地方都应该变成User.GENDER_MALE blabla，如 activityApply.gender = User.GENDER_MALE。\n也许有人想到了，建立一个Gender的类，或者枚举不就行了。比如：\npublic enum Gender { UNKNOWN(0), MALE(1), FEMALE(2); private final int value; Gender(int value) { this.value = value; } public int getValue() { return value; } } 然后使用的时候就变成了：\nuser.gender = Gender.UNKNOWN activityApply.gender = Gender.MALE 问题是不是解决了？就算是实习生来了，也能保证大家的gender的值是一致的。前提是他要知道关于gender的值我们取的都Gender枚举里的值。不论是入职时老员工跟他说，还是他自己发现的。\n问题解决了？并没有。当前端发来了个gender参数时，我们如何校验这个参数呢？比如前文提到的淘宝表单里，我们看到：\n_fm._0.g就是gender参数。\n校验时，我们的controller里，有人可能会写成：\nif(gender == 2){ user.gender = Gender.FEMALE }else if(gender == 1){ user.gender = Gender.MALE }else { user.gender = Gender.UNKNOWN } 高明一些人的会在gender枚举中加入一个静态方法：\npublic static Gender genderOf(int aGenderValue){ for (Gender gender : Gender.values()) { if (gender.value == aGenderValue) { return gender; } } return Gender.UNKNOWN; } 然后校验时，\nuser.gender = Gender.genderOf(genderParameter).value Gender以数字值存到数据库中，真是最好的方法？\n以上，我们的思路看似没有问题。只是，我们没有看到其中的假设。以上思路的假设是：\n代码使用者知道有Gender这个枚举类，然后再使用Gender枚举赋值给User.gender字段。\n对于数据库中的0、1、2，只有我们的程序进行解释，其它程序里可能使用的是10、11、12\n因为是性别可能的值不多，所以，前端代码写成if elseif elseif else，没所谓。但要知道，我们服务不仅仅输出html，还会输出json等其它格式。当然，你可以将这部分逻辑封装起来，这样别人就可以重用了。但是你这里又假设了“TA人知道你的重用”存在，然后正确使用。\n首先，你可能认为这些不是问题。我猜想你给出的理由是：\n关于1，一进入公司，我们就培训他，gender就使用这个类。或者写一个开发文档。\n关于2，我们不需要其它程序解释这个数据库里的值，其他程序都是通过调用我们程序的。\n关于3，性别来来回回就那几个，不会扩展到哪里去。\n问题不在于Gender而在于别处\n我觉得你这些理由是有道理的，但是不是最好的。\n关于1：我们的代码应该设计得尽量可靠，可靠到连代码使用者都不会使用错。而User、ActivityApply的gender是int和String这类基础数据类型，不管你培训还是写开发文档，都会给代码使用者有写错的机会。更好的办法是什么，使用枚举类型。这样就可以由编译器来给我们检查代码使用者有没有调用错了。而且，这也解放了代码使用者的大脑。当你在写user.setGender(value)时，如果setGender接收的是一个枚举，IDE自然会提示你，gender有哪些值。\n这很像地铁上的门写着“请勿倚靠”，你就认为真没有人倚靠？在合理成本内，地铁门应该设计成就算10个200斤的人倚靠都不怕。\n这时，你会提问，如果User gender使用的枚举，那么我们怎么持久化到数据库中呢？如果使用ORM框架，它会给你解决。如果不使用ORM，也可以有技巧解决的。这个问题留你自己思考。但是，有一点需要提下，你的业务代码不应该和数据库这样具体技术耦合！可以看看我写的《耦合的本质》\n关于2：如果养成了所有有限值内的字段都使用数字来存到数据库中的习惯，问题就没有使用gender这么简单了。在未来的几年，你的代码会充满魔法数字，最终架构腐化。\n关于3：和2是同一个问题。\n小结\n回答本文标题的问题：出现像gender这样有限值的字段，我会优先使用枚举包装起来，持久化时，我会优先使用人看得懂的字符串。\n深度一些的思考：\n本文的标题是个问题吗？ 设计模式能解决本文标题的问题吗？呵呵 为什么人们趋向于使用数字而不是字符串？ 为什么架构会腐化？ 架构师不写代码？ ","permalink":"https://showme.codes/zh-cn/2016-4-15-how-to-save-gender-in-database/","summary":"\u003cp\u003e在我和同事结对时，发现数据库中多个表中，分别都会有gender这个字段。比如A表，B表，C表。这三个表中，gender字段都是int类型。但是同一性别，在各个表中的值是不同的。比如A表中，1代表男，在B表中却代表了女，在C表中代表未知。\u003c/p\u003e\n\u003cp\u003e我突然意识到这背后存在更大的问题。从而引发我对“性别字段存储时应该使用的字符串，还是数字？”这个问题思考。也许已经有前辈思考过这个问题并写在某本书的某页，如果有，请告知。谢谢。\u003c/p\u003e\n\u003ch4 id=\"0代表女1代表男\"\u003e0代表女，1代表男\u003c/h4\u003e\n\u003cp\u003e首先，你可能会问，对于这样的问题还用想吗？不是都使用数字吗？0代表女，1代表男。\u003c/p\u003e\n\u003cp\u003e其实，淘宝就是这么做的：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"性别字段存储时应该使用的字符串，还是数字？01\" loading=\"lazy\" src=\"/assets/images/2016-4-01.png\"\u003e\u003c/p\u003e\n\u003cp\u003ehtml代码是这样的：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"性别字段存储时应该使用的字符串，还是数字？02\" loading=\"lazy\" src=\"/assets/images/2016-4-02.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这时，我会问如果这个用户没有填写性别信息呢？那你可能将原来的实现改成0代表空，1代表男，2代表女。我提醒你，当你开发的是一个大型网站时，你要将原来的“0代表女”改成“0代表空”，不会那么容易。历史数据要处理。你还需要修改所有用到0，1的代码，即使你使用的是常量代替而不是魔法数字，也不会容易到哪里去。\u003c/p\u003e\n\u003ch4 id=\"有经验的程序员\"\u003e有经验的程序员\u003c/h4\u003e\n\u003cp\u003e是的，有经验的程序员写代码时，一开始就会想到这个问题，所以一开始就设计“0代表空，1代表男，2代表女”。从前端到后端都统一使用数字。比如：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eclass User{\n\n   final static int GENDER_NULL = 0\n\n   final static int GENDER_MALE = 1\n\n   final static int GENDER_FEMALE = 2\n\n   int gender\n\n}\n\nclass UserController{\n\tvoid saveUser(int gender)\n}\n\u0026lt;div\u0026gt;gender: #if($user.gender == 1)男#elseif($user.gender == 2)女 blabla….\u0026lt;/div\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e当然前端这样写有些难看，那我们使用宏来代替，比如\u003ccode\u003e\u0026lt;div\u0026gt;#displayGender($user.gender)\u0026lt;/div\u0026gt;\u003c/code\u003e。这里我想留一个疑问：如果想国际化呢？你的displayGender怎么实现的？\u003c/p\u003e\n\u003ch4 id=\"实习生来了\"\u003e实习生来了\u003c/h4\u003e\n\u003cp\u003e某天公司招来了一个实习生要实现一个活动申请表页面。领导觉得这个功能应该不难，所以就将这个任务分配给他。他为了表现自己，哐啷哐啷很快就写完了，还得到了领导的表扬。但实习生根本没有参照前面有经验的程序员的写法（有时不是他的问题，可能是没有人告诉他需要参照某个功能的写法来实现）。有意识一些的实习生还知道将gender的值写成常量，没有意识的，可能你只有去到前端页面看源码才能知道0, 1分别代表什么。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e class ActivityApply{\n     final static int GENDER_MALE = 0\n     final static int GENDER_FEMALE = 1\n     int gender\n }\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e如果他没有参照前面有经验程序员的写法，我不确定他是否会重用那个前端宏。所以，讲到这里，你应该明白，有时你设计好的“重用”，并不一定会被重用。为什么呢？:P\u003c/p\u003e","title":"性别字段存储时应该使用的字符串，还是数字？"},{"content":"在我来到开源中国之后，尝试将每日站会、代码审查、结对编程这三种编程实践带入团队。而这个过程，我个人觉得是一项非常宝贵的体验。我觉得可以拿出来和大家分享。\n先介绍下目前我们团队的结构：3名Java开发，1名前端，2名实习。\n以下我不会详细介绍它们分别是什么，也无意讨论它们有什么好处坏处，本文侧重分享在实践它们的过程可能遇到的问题，以及我们是如何处理的。\n每日站会 每日站会 （Stand-up ）：是每天进行的会议旨在在组队成员之间进行状态更新。\u0026lsquo;半实时\u0026rsquo;的状态允许参与者了解到潜在的挑战以及用于处理一个困难或者耗时的问题的协调精力。它在一些敏捷软件开发过程中有着特定的价值，譬如Scrum，但是同样可以在任何开发方法论中被使用。术语 “站” 衍生于通过保持与会人员站立的状态（长时间站立会导致不适）从而帮助控制会议的时间的实践。\n我们每天会早上花十几分钟（具体时长看团队大小），大家一起站（是站）在卡墙前过卡。卡墙其实就是Team中的任务看板。就这样，我们从“已验收”列到“待办中”列，从上往下，一张卡一张卡的过。这里的卡是指定义了一个小功能需求的卡片。 站会不过是向领导汇报 我在实践每日站会的时候，发现不少人把每日站会当成一种“向领导汇报”的过程。比如他们会习惯地汇报：我昨天做了1，2，3 blabla。一大串，仿佛说得少就是做的少。所以这个过程，我不断地指正，你们不是在向领导汇报，我们只需要对这件事情负责，说到你的卡时，你就说你的卡的当前状态就好了。慢慢地团队里就养成了对事不对人的文化。为什么呢？每日站会就是提醒我们每日的工作就是对这些“事”负责。\n随着时间迁移，我们的团队就慢慢习惯了这种站会。也会在站会上开一些开玩笑了。不要认为这是浪费时间，这是团队文化中很重要的一部分。\n站会时间把控问题 站会还可能会遇到的问题是站会时间的把控。所以，我们每日站会会有一个主持人。如果大家说偏题了，主持人就必须指正，让相关人在站会后自己讨论。如果大家讨论的这个问题是个大问题，那么，也是在站完会后再讨论。另外，主持人还要是轮换的，这样就可以将团队所有成员带入项目。\n站会上的新人问题 每日站会常常遇到的问题是过卡时，这个人说得太细了，把功能的具体实现细节都说出来了。这时，我们不应该立即打断他。出现这样的情况，说明他一定是新人。我们应该选择在站会后单独找他重申一次每日站会的目的和内容。当然，一开始实践每日站会时，团队里除了你每个人都不懂时，你就有必要马上指正了。\n代码审查 代码审查（Code review）是指对计算机源代码系统化地审查，常用软件同行评审的方式进行，其目的是在找出及修正在软件开发初期未发现的错误，提升软件质量及开发者的技术。代码审查常以不同的形式进行，例如结对编程、非正式的看过整个代码，或是正式的软件检查。 我今天没有什么好说的 一开始，我实践时，遇到的最大问题是：团队成员喜欢说，我今天没有什么好说的。这句话听起来冷漠，其实背后的原因是大家不完全理解代码审查是什么，而不是因为他们真的没什么好说的。\n这时，我会说：只要你今天做了的事情，你都可以说。然后，他们常常不知从何说起，接着，一上来就给我们讲代码细节。\n遇到这种情况，我们需要再强调一遍代码审查需要说什么：上下文、你是如何解决问题的、解决过程遇到什么问题……有时被审查的人可能说的不够明白，我就会帮助补充。\n这个过程，你可能会发现有些人在表达能力上的不足，导致听的人一头雾水。我的做法是理解他说的，然后尝试帮助他更好表达出来。这样，提升他的表达能力的同时，让他在团队里也更有归属感。\n说得太多了 有时，有些成员可能会说的非常非常细。多人这样了，就会导致代码审查的时间过长。发生这种情况，将表达能力的问题排除外，大概就是这个人没考虑哪些应该是自己应该重点说出来的。这时leader就要站出来指正了。\n没写代码怎么审查？ 其实，我们实践的代码审查并不是十分严格。因为有时，我们一天下来没有写代码，而是做调研工作。遇到这种情况，被审查人也需要主动分享他今天的习得。有时，他说出来某个问题，也许其他成员也遇到过同样的问题，并解决了。这样就为团队节约时间。\n结对编程 结对编程（Pair programming）是一种敏捷软件开发的方法，两个程序员在一个计算机上共同工作。一个人输入代码，而另一个人审查他输入的每一行代码。输入代码的人称作驾驶员，审查代码的人称作观察员（或导航员）。两个程序员经常互换角色。 如何将结对编程带入团队？ 我们的做法由一个懂得结对的人分别和团队里每一个人进行结对。结对前，详细说明结对可能遇到的情况，比如双方有争执，一直都是一个人写的情况，双方都遇到不懂的情况……然后，结对时穿插结对编程的知识。\n团队成员中的两个都没有结对编程的经验，怎么办？\n实际情况是我们遇到更麻烦的事情。\n因为我在前端不擅长，所以我决定让两名前端结对。问题来了，这两个人都不会结对，在沟通方面也不是非常擅长。让他们结对后，我发现他们一起结对的时间非常少，一天下来基本就是各做各的。这时，我发现不对劲。我就分别找他们谈。为什么要分别呢？是希望他们大胆说自己的感受。\n在和他们谈了之后，发现根本原因是他们没有完全理解结对编程的目的。这时，找到他们俩再重申一遍结对编程是为了什么，以及如何结对。\n新人对结对编程常常有的疑惑？\n他在写代码，我看着有什么用？\n软件开发是一项集体脑力活，知识的流动在这项活动中非常关键。结对编程是促进知识流动的行为。\n看别人写代码的人，我们称为“观察者”。写代码的人，我们称为“驾驶员”。观察者的职责是对写代码的人的代码进行审查。其实除了这点，我更看重的是这个分享思维方式的过程，会加速双方的成长。这个过程还能营造一种相互学习的文化。\n我觉得我一个人一下子就写完了\n说这句话的人的能力不会差。其实有这样的想法很正常。这时，我们就鼓励他多分享。当我深究下去时，他说写那些东西根本不需要动脑。还很得意的样子。不知道你们有没有发现其中的问题：他在做体力活。更大的问题是：他还不知道自己在做体力活。这时，我会说：当你在做体力活时，和机器没有区别，说明你在退步，这时，你应该跳出来，挑战自己，比如coach别人，或找到一种避免这种体力活的方法。\n如果你有什么疑惑，可在本文评论留言。\n小结 每日站会，代码审查，结对编程实践的先后顺序的？\n本质上是没有先后顺序的。但是如果你是一位新来的leader，你就需要考虑你加入的团队的情况了。我们是先施行每日站会，代码审查。最近一个月才开始实施结对编程。为什么呢？因为对这些编程实践，如果强硬推行，可能会受到排斥。你需要时间让团队成员消化。\n给人留下“什么都管”的印象\n由于我带来了这些新的实践，看到团队成员实践过程的一些问题就会指出，所以经常给人“什么都管”的印象。\n当leader什么都管时，leader要问自己为什么什么要管，而团队成员也要反问自己为什么什么都要被管。排除leader的性格问题外，大多数时，是因为团队还处于比较初级的阶段。你问问自己，团队里有多少人可以自己做leader的就知道了。leader应该跟大家说清楚这点。这样大家就理解你了。但是这个“初级”的阶段要多长时间？就要看你什么时候培养出另一个leader了。\n你会发现我在本文没有谈什么M捷或者精Y，是因为我想就事论事，不想谈理论，只想解决实际问题。\n问题来了，你发现团队中没有人会结对，你作为leader不懂得如何结对编程时，怎么将结对编程带入团队中呢？\n这时就需要请外援了。好听一些，请咨询顾问。如果你觉得看了我的文章，觉得我还行，也可以找我。我在开源中国众包发布了一个专家服务： 将每日站会、代码审查、结对编程带入团队\n","permalink":"https://showme.codes/zh-cn/2016-4-1-standup-codereview-pair-in-oschina/","summary":"\u003cp\u003e在我来到开源中国之后，尝试将每日站会、代码审查、结对编程这三种编程实践带入团队。而这个过程，我个人觉得是一项非常宝贵的体验。我觉得可以拿出来和大家分享。\u003c/p\u003e\n\u003cp\u003e先介绍下目前我们团队的结构：3名Java开发，1名前端，2名实习。\u003c/p\u003e\n\u003cp\u003e以下我不会详细介绍它们分别是什么，也无意讨论它们有什么好处坏处，本文侧重分享在实践它们的过程可能遇到的问题，以及我们是如何处理的。\u003c/p\u003e\n\u003ch4 id=\"每日站会\"\u003e每日站会\u003c/h4\u003e\n\u003cp\u003e每日站会 （Stand-up ）：是每天进行的会议旨在在组队成员之间进行状态更新。\u0026lsquo;半实时\u0026rsquo;的状态允许参与者了解到潜在的挑战以及用于处理一个困难或者耗时的问题的协调精力。它在一些敏捷软件开发过程中有着特定的价值，譬如Scrum，但是同样可以在任何开发方法论中被使用。术语 “站” 衍生于通过保持与会人员站立的状态（长时间站立会导致不适）从而帮助控制会议的时间的实践。\u003c/p\u003e\n\u003cp\u003e我们每天会早上花十几分钟（具体时长看团队大小），大家一起站（是站）在卡墙前过卡。卡墙其实就是Team中的任务看板。就这样，我们从“已验收”列到“待办中”列，从上往下，一张卡一张卡的过。这里的卡是指定义了一个小功能需求的卡片。 \u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"每日站会、代码审查、结对编程 之开源中国实践01\" loading=\"lazy\" src=\"/assets/images/2016-4-team.jpg\"\u003e\u003c/p\u003e\n\u003ch5 id=\"站会不过是向领导汇报\"\u003e站会不过是向领导汇报\u003c/h5\u003e\n\u003cp\u003e我在实践每日站会的时候，发现不少人把每日站会当成一种“向领导汇报”的过程。比如他们会习惯地汇报：我昨天做了1，2，3 blabla。一大串，仿佛说得少就是做的少。所以这个过程，我不断地指正，你们不是在向领导汇报，我们只需要对这件事情负责，说到你的卡时，你就说你的卡的当前状态就好了。慢慢地团队里就养成了对事不对人的文化。为什么呢？每日站会就是提醒我们每日的工作就是对这些“事”负责。\u003c/p\u003e\n\u003cp\u003e随着时间迁移，我们的团队就慢慢习惯了这种站会。也会在站会上开一些开玩笑了。不要认为这是浪费时间，这是团队文化中很重要的一部分。\u003c/p\u003e\n\u003ch5 id=\"站会时间把控问题\"\u003e站会时间把控问题\u003c/h5\u003e\n\u003cp\u003e站会还可能会遇到的问题是站会时间的把控。所以，我们每日站会会有一个主持人。如果大家说偏题了，主持人就必须指正，让相关人在站会后自己讨论。如果大家讨论的这个问题是个大问题，那么，也是在站完会后再讨论。另外，主持人还要是轮换的，这样就可以将团队所有成员带入项目。\u003c/p\u003e\n\u003ch5 id=\"站会上的新人问题\"\u003e站会上的新人问题\u003c/h5\u003e\n\u003cp\u003e每日站会常常遇到的问题是过卡时，这个人说得太细了，把功能的具体实现细节都说出来了。这时，我们不应该立即打断他。出现这样的情况，说明他一定是新人。我们应该选择在站会后单独找他重申一次每日站会的目的和内容。当然，一开始实践每日站会时，团队里除了你每个人都不懂时，你就有必要马上指正了。\u003c/p\u003e\n\u003ch4 id=\"代码审查\"\u003e代码审查\u003c/h4\u003e\n\u003cp\u003e代码审查（Code review）是指对计算机源代码系统化地审查，常用软件同行评审的方式进行，其目的是在找出及修正在软件开发初期未发现的错误，提升软件质量及开发者的技术。代码审查常以不同的形式进行，例如结对编程、非正式的看过整个代码，或是正式的软件检查。 \u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"每日站会、代码审查、结对编程 之开源中国实践02\" loading=\"lazy\" src=\"/assets/images/2016-4-monkey.jpg\"\u003e\u003c/p\u003e\n\u003ch5 id=\"我今天没有什么好说的\"\u003e我今天没有什么好说的\u003c/h5\u003e\n\u003cp\u003e一开始，我实践时，遇到的最大问题是：团队成员喜欢说，我今天没有什么好说的。这句话听起来冷漠，其实背后的原因是大家不完全理解代码审查是什么，而不是因为他们真的没什么好说的。\u003c/p\u003e\n\u003cp\u003e这时，我会说：只要你今天做了的事情，你都可以说。然后，他们常常不知从何说起，接着，一上来就给我们讲代码细节。\u003c/p\u003e\n\u003cp\u003e遇到这种情况，我们需要再强调一遍代码审查需要说什么：上下文、你是如何解决问题的、解决过程遇到什么问题……有时被审查的人可能说的不够明白，我就会帮助补充。\u003c/p\u003e\n\u003cp\u003e这个过程，你可能会发现有些人在表达能力上的不足，导致听的人一头雾水。我的做法是理解他说的，然后尝试帮助他更好表达出来。这样，提升他的表达能力的同时，让他在团队里也更有归属感。\u003c/p\u003e\n\u003ch5 id=\"说得太多了\"\u003e说得太多了\u003c/h5\u003e\n\u003cp\u003e有时，有些成员可能会说的非常非常细。多人这样了，就会导致代码审查的时间过长。发生这种情况，将表达能力的问题排除外，大概就是这个人没考虑哪些应该是自己应该重点说出来的。这时leader就要站出来指正了。\u003c/p\u003e\n\u003ch5 id=\"没写代码怎么审查\"\u003e没写代码怎么审查？\u003c/h5\u003e\n\u003cp\u003e其实，我们实践的代码审查并不是十分严格。因为有时，我们一天下来没有写代码，而是做调研工作。遇到这种情况，被审查人也需要主动分享他今天的习得。有时，他说出来某个问题，也许其他成员也遇到过同样的问题，并解决了。这样就为团队节约时间。\u003c/p\u003e\n\u003ch4 id=\"结对编程\"\u003e结对编程\u003c/h4\u003e\n\u003cp\u003e结对编程（Pair programming）是一种敏捷软件开发的方法，两个程序员在一个计算机上共同工作。一个人输入代码，而另一个人审查他输入的每一行代码。输入代码的人称作驾驶员，审查代码的人称作观察员（或导航员）。两个程序员经常互换角色。 \u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"每日站会、代码审查、结对编程 之开源中国实践03\" loading=\"lazy\" src=\"/assets/images/2016-4-pair.jpg\"\u003e\u003c/p\u003e\n\u003ch5 id=\"如何将结对编程带入团队\"\u003e如何将结对编程带入团队？\u003c/h5\u003e\n\u003cp\u003e我们的做法由一个懂得结对的人分别和团队里每一个人进行结对。结对前，详细说明结对可能遇到的情况，比如双方有争执，一直都是一个人写的情况，双方都遇到不懂的情况……然后，结对时穿插结对编程的知识。\u003c/p\u003e\n\u003cp\u003e团队成员中的两个都没有结对编程的经验，怎么办？\u003c/p\u003e\n\u003cp\u003e实际情况是我们遇到更麻烦的事情。\u003c/p\u003e\n\u003cp\u003e因为我在前端不擅长，所以我决定让两名前端结对。问题来了，这两个人都不会结对，在沟通方面也不是非常擅长。让他们结对后，我发现他们一起结对的时间非常少，一天下来基本就是各做各的。这时，我发现不对劲。我就分别找他们谈。为什么要分别呢？是希望他们大胆说自己的感受。\u003c/p\u003e\n\u003cp\u003e在和他们谈了之后，发现根本原因是他们没有完全理解结对编程的目的。这时，找到他们俩再重申一遍结对编程是为了什么，以及如何结对。\u003c/p\u003e\n\u003cp\u003e新人对结对编程常常有的疑惑？\u003c/p\u003e\n\u003cp\u003e他在写代码，我看着有什么用？\u003c/p\u003e\n\u003cp\u003e软件开发是一项集体脑力活，知识的流动在这项活动中非常关键。结对编程是促进知识流动的行为。\u003c/p\u003e\n\u003cp\u003e看别人写代码的人，我们称为“观察者”。写代码的人，我们称为“驾驶员”。观察者的职责是对写代码的人的代码进行审查。其实除了这点，我更看重的是这个分享思维方式的过程，会加速双方的成长。这个过程还能营造一种相互学习的文化。\u003c/p\u003e\n\u003cp\u003e我觉得我一个人一下子就写完了\u003c/p\u003e\n\u003cp\u003e说这句话的人的能力不会差。其实有这样的想法很正常。这时，我们就鼓励他多分享。当我深究下去时，他说写那些东西根本不需要动脑。还很得意的样子。不知道你们有没有发现其中的问题：他在做体力活。更大的问题是：他还不知道自己在做体力活。这时，我会说：当你在做体力活时，和机器没有区别，说明你在退步，这时，你应该跳出来，挑战自己，比如coach别人，或找到一种避免这种体力活的方法。\u003c/p\u003e\n\u003cp\u003e如果你有什么疑惑，可在本文评论留言。\u003c/p\u003e\n\u003ch4 id=\"小结\"\u003e小结\u003c/h4\u003e\n\u003cp\u003e每日站会，代码审查，结对编程实践的先后顺序的？\u003c/p\u003e\n\u003cp\u003e本质上是没有先后顺序的。但是如果你是一位新来的leader，你就需要考虑你加入的团队的情况了。我们是先施行每日站会，代码审查。最近一个月才开始实施结对编程。为什么呢？因为对这些编程实践，如果强硬推行，可能会受到排斥。你需要时间让团队成员消化。\u003c/p\u003e\n\u003cp\u003e给人留下“什么都管”的印象\u003c/p\u003e\n\u003cp\u003e由于我带来了这些新的实践，看到团队成员实践过程的一些问题就会指出，所以经常给人“什么都管”的印象。\u003c/p\u003e\n\u003cp\u003e当leader什么都管时，leader要问自己为什么什么要管，而团队成员也要反问自己为什么什么都要被管。排除leader的性格问题外，大多数时，是因为团队还处于比较初级的阶段。你问问自己，团队里有多少人可以自己做leader的就知道了。leader应该跟大家说清楚这点。这样大家就理解你了。但是这个“初级”的阶段要多长时间？就要看你什么时候培养出另一个leader了。\u003c/p\u003e\n\u003cp\u003e你会发现我在本文没有谈什么M捷或者精Y，是因为我想就事论事，不想谈理论，只想解决实际问题。\u003c/p\u003e\n\u003cp\u003e问题来了，你发现团队中没有人会结对，你作为leader不懂得如何结对编程时，怎么将结对编程带入团队中呢？\u003c/p\u003e\n\u003cp\u003e这时就需要请外援了。好听一些，请咨询顾问。如果你觉得看了我的文章，觉得我还行，也可以找我。我在开源中国众包发布了一个专家服务： 将每日站会、代码审查、结对编程带入团队\u003c/p\u003e","title":"每日站会、代码审查、结对编程 之开源中国实践"},{"content":"\n前年，接触到了《丰田生产方式》，就对大野耐一这个人十分感兴趣，就专门找他的书来看。\n同时，我一直都有一种“感觉”：我们软件工程的管理方式都是从传统工业借鉴的。比如被吹上天的“精益”概念及“看板”概念。然而，这些概念里，少有人说明这样地借鉴的理由及借鉴了哪些，放弃了哪些。想回答这个问题就必须分别弄清楚传统工业和软件工程的本质。\n我尝试在这本书了解一些关于传统工业的管理概念。以下是书摘：\n####“精益”的概念的产生\n1990年，美国麻省理工学院的詹姆斯 沃麦克等多位教授，在《改变世界的机器》一书中，首次以“精益生产”（learn production）为核心介绍丰田生产方式，自此，欧美的一些企业才开始把丰田生产方式作为全球化以及提高生产率的标准和尺度。\n领导说服力：坦诚即代表强劲的说服力 要想说服别人或是得到理解，若没有什么根据或道理是行不通的。\n不要总是认为自己的言行没有错误，意识到错之后就应该爽快地说出来。如果有了这种胸怀，指挥现场以及下属不就变得轻而易举了吗？\n犯了错误之后，应该不吝于向他人甚至的下属道歉，怀着这样坦诚的态度，怎么会没有说服力呢？\n为了形成强劲的说服力，重要条件之一就是管理者自身应该怀着谦虚的胸襟。\n现实生活中，人们似乎随着知识越来越丰富，产生错觉的可能性反而越大。\n传统工业也会遇到我们软件工程一样的问题：面对同样一份工作，存在不同的意见。传统工业时里的体现可能是组装配件有两种方式，哪个效率更高；软件工程里的体现是实现某个功能，怎么实现更快（不要忘了，我们还要考虑可维护性，可读性）。当存在不同的意见时，双方容易在无用的争论上浪费时间，大野耐一是这么处理的：\n在产生两种意见的情况下，各给双方一个工作日的时间，让他们按照自己的意见试着做一做，最后比较结果，直到让大家彻底理解、赞同为止。\n其实，这样处理的最大好处是将**“实践出真理”**的文化慢慢带入企业。\n####要提高生产效率，“意识革命”是首要问题\n从上层管理者到中层管理者甚至工作在生产一线的作业员们，由于大家都是普通人，所以都有可能被禁锢在错觉之中，认为现行的做法是最科学的；或者说即使不认为是最好的，也觉得别无选择，这就是被常识化了。\n我们软件行业更是如此，特别是一毕业就只在一家公司待很久的人。很容易被“常识化”，认为软件开发就只有一种方式：手工将jar包下载回来，导入到IDE中，然后写代码；部署软件也就是ssh上服务器，然后stop,start。\n大野耐一说：\n若是不改变从管理顶层到一线作业员以及工会的意识、观念和想法，那么怎么可能探索出做好工作的新方法呢？组织上的改革或许相对容易，但是“意识革命”应该会更加困难一些吧。\n再比如很多人认为写单元测试会导致进度被拖慢。其实，关于单元测试是否加快进度，需要更多的数据支撑。所以，需要软件项目管理工具为我们提供更全面的统计工具，来收集这些数据。这也说明了软件行业和传统工业的一个很大的不同。传统工业中很多工作是重复的（产品通常是批量生产），可以快速实验，快速看效果。而软件行业中，根本没有批量生产某一软件的说法。\n####无效率的动作不是工作\n身为生产现场的管理者和主管，必须具有分辨“动”和“働”的慧眼，也就是说，必须能够分辨清楚哪些动作是无效率的，哪些动作与工作是无关的。\n这里，对于这个“无效率”的定义会有争议。我是这样认为的，如果不能帮助我写出可读、可维护、用户可用的软件的动作都是无效率的。比如手动去管理软件依赖、手动部署、需求沟通需要等上一个星期、新加入团队的成员需要花两天的时间搭建开发环境、重复手工测试、单元测试写在main方法里、写代码过程分心看微博，动弹……\n作为leader，发现这些无效率动作，然后找到改善办法是一项重中重的工作。\n####改善应该按顺序进行\n所谓作业改善，就是能够让现有的设备更好地发挥作用。在改善过程中，首先需要考虑的不是购买设备，而是最佳的工作方法。\n我认为首先需要进行的是作业改善，之后才依次为设备改善、工序改善，也就是改善应该有先后顺序。\n软件行业中，顺序应该是软件开发流程改善，之后依次是实现技术改善、软件开发工具改善。原因是软件开发流程的成本收益率比实现技术改善、软件开发工具改善更高。这只是我的片面之词。希望有数据的同学能帮我证实。\n####产品质量问题\n如果某个零件\b比较容易在前几道工序的时候出现问题，是不是应该考虑将检验工作提前呢？提前发现并剔除不良品，总比让它们一直往下走要好得好多。\n品质融于生产过程中，因此，如果能在必要的地方做好检验工作，那么就不必等到工程的最后才发现不良品，或者说到工序的最后阶段只需要重点检验某些部分即可。\n产品质量融于软件开发过程中，将风险高的软件模块提前开发。QA在功能开发前就参与需求的讨论，并提出验收条件AC。\n####降低成本\n如果有人问我，为什么要拼命减少库存、降低成本，我会告诉他，是为了让资金周转更加轻松。\n然而，只要提到降低成本，大家就会觉得这是财务人员的责任，其实不然，财务人员根本无法促使成本降低，这只能通过集体的努力实现。\n因此，所谓的降低成本，唯有依靠生产现场来进行，现场的降低成本的意识要做到比鬼还要精才行啊。\n减少浪费也可以降低成本。关键是我们如何看待浪费。在《丰田生产方式》中定义的浪费：\n1.过量生产的无效劳动：软件行业中指在不合适的时候开发多余的功能 2.窝工的时间浪费 3.搬运的无效劳动：需求沟通不完整，导致重复沟通 4.加工本身的无效劳动和浪费 5.库存的浪费 6.动作上的无效劳动：花费过多的时候搭建开发环境 7.制造次品的无效劳动和浪费：品质没有融于生产过程中 从这里就可以看出光靠架构师是不能彻底杜绝浪费，更不可能靠财务了。\n####小结 这本书给我最大的启发是：高高在上不接地气的技术管理是无法管理好团队的。高高在上意味着他无法或很难及时、准确得知开发现场的情况的。如果你连“施工现场”的情况都不了解，谈何改善？\n同时，我也发现软件行业的代码审查、每日站会就很好的体现了大野耐一的现场管理思想。通过这两个实践，我们的leader才有更多机会靠近“现场”。这才是代码审查、每日站会背后更深层的原因。\n半年后再次重读这本小书，又知新。\n老翟书摘说明 书摘内容完全来自原书，如果原书的作者或出版商觉得我侵权了。请联系我。\n老翟书摘旨在通过一种书摘的方式让大家花最少的时间了解一本书，从而决定要不要继续读下去。书摘的每一本书都是本人亲自读过并理解的。\n","permalink":"https://showme.codes/zh-cn/2016-2-15-software-dev-in-site-management-view/","summary":"\u003cp\u003e\u003cimg alt=\"大野耐一的现场管理\" loading=\"lazy\" src=\"/assets/images/2016-2-15-toyota.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e前年，接触到了\u003ca href=\"http://my.oschina.net/zjzhai/blog/522423\"\u003e《丰田生产方式》\u003c/a\u003e，就对大野耐一这个人十分感兴趣，就专门找他的书来看。\u003c/p\u003e\n\u003cp\u003e同时，我一直都有一种“感觉”：我们软件工程的管理方式都是从传统工业借鉴的。比如被吹上天的“精益”概念及“看板”概念。然而，这些概念里，少有人说明这样地借鉴的理由及借鉴了哪些，放弃了哪些。想回答这个问题就必须分别弄清楚传统工业和软件工程的本质。\u003c/p\u003e\n\u003cp\u003e我尝试在这本书了解一些关于传统工业的管理概念。以下是书摘：\u003c/p\u003e\n\u003cp\u003e####“精益”的概念的产生\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e1990年，美国麻省理工学院的詹姆斯 沃麦克等多位教授，在《改变世界的机器》一书中，首次以“精益生产”（learn production）为核心介绍丰田生产方式，自此，欧美的一些企业才开始把丰田生产方式作为全球化以及提高生产率的标准和尺度。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch4 id=\"领导说服力坦诚即代表强劲的说服力\"\u003e领导说服力：坦诚即代表强劲的说服力\u003c/h4\u003e\n\u003cblockquote\u003e\n\u003cp\u003e要想说服别人或是得到理解，若没有什么根据或道理是行不通的。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e不要总是认为自己的言行没有错误，意识到错之后就应该爽快地说出来。如果有了这种胸怀，指挥现场以及下属不就变得轻而易举了吗？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e犯了错误之后，应该不吝于向他人甚至的下属道歉，怀着这样坦诚的态度，怎么会没有说服力呢？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为了形成强劲的说服力，重要条件之一就是管理者自身应该怀着谦虚的胸襟。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e现实生活中，人们似乎随着知识越来越丰富，产生错觉的可能性反而越大。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e传统工业也会遇到我们软件工程一样的问题：面对同样一份工作，存在不同的意见。传统工业时里的体现可能是组装配件有两种方式，哪个效率更高；软件工程里的体现是实现某个功能，怎么实现更快（不要忘了，我们还要考虑可维护性，可读性）。当存在不同的意见时，双方容易在无用的争论上浪费时间，大野耐一是这么处理的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在产生两种意见的情况下，各给双方一个工作日的时间，让他们按照自己的意见试着做一做，最后比较结果，直到让大家彻底理解、赞同为止。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e其实，这样处理的最大好处是将**“实践出真理”**的文化慢慢带入企业。\u003c/p\u003e\n\u003cp\u003e####要提高生产效率，“意识革命”是首要问题\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e从上层管理者到中层管理者甚至工作在生产一线的作业员们，由于大家都是普通人，所以都有可能被禁锢在错觉之中，认为现行的做法是最科学的；或者说即使不认为是最好的，也觉得别无选择，这就是被常识化了。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我们软件行业更是如此，特别是一毕业就只在一家公司待很久的人。很容易被“常识化”，认为软件开发就只有一种方式：手工将jar包下载回来，导入到IDE中，然后写代码；部署软件也就是ssh上服务器，然后stop,start。\u003c/p\u003e\n\u003cp\u003e大野耐一说：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e若是不改变从管理顶层到一线作业员以及工会的意识、观念和想法，那么怎么可能探索出做好工作的新方法呢？组织上的改革或许相对容易，但是“意识革命”应该会更加困难一些吧。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e再比如很多人认为写单元测试会导致进度被拖慢。其实，关于单元测试是否加快进度，需要更多的数据支撑。所以，需要软件项目管理工具为我们提供更全面的统计工具，来收集这些数据。这也说明了软件行业和传统工业的一个很大的不同。传统工业中很多工作是重复的（产品通常是批量生产），可以快速实验，快速看效果。而软件行业中，根本没有批量生产某一软件的说法。\u003c/p\u003e\n\u003cp\u003e####无效率的动作不是工作\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e身为生产现场的管理者和主管，必须具有分辨“动”和“働”的慧眼，也就是说，必须能够分辨清楚哪些动作是无效率的，哪些动作与工作是无关的。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这里，对于这个“无效率”的定义会有争议。我是这样认为的，\u003cstrong\u003e如果不能帮助我写出可读、可维护、用户可用的软件的动作都是无效率的\u003c/strong\u003e。比如手动去管理软件依赖、手动部署、需求沟通需要等上一个星期、新加入团队的成员需要花两天的时间搭建开发环境、重复手工测试、单元测试写在main方法里、写代码过程分心看微博，动弹……\u003c/p\u003e\n\u003cp\u003e作为leader，发现这些无效率动作，然后找到改善办法是一项重中重的工作。\u003c/p\u003e\n\u003cp\u003e####改善应该按顺序进行\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e所谓作业改善，就是能够让现有的设备更好地发挥作用。在改善过程中，首先需要考虑的不是购买设备，而是最佳的工作方法。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我认为首先需要进行的是作业改善，之后才依次为设备改善、工序改善，也就是改善应该有先后顺序。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e软件行业中，顺序应该是软件开发流程改善，之后依次是实现技术改善、软件开发工具改善。原因是软件开发流程的成本收益率比实现技术改善、软件开发工具改善更高。这只是我的片面之词。希望有数据的同学能帮我证实。\u003c/p\u003e\n\u003cp\u003e####产品质量问题\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e如果某个零件\b比较容易在前几道工序的时候出现问题，是不是应该考虑将检验工作提前呢？提前发现并剔除不良品，总比让它们一直往下走要好得好多。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e品质融于生产过程中，因此，如果能在必要的地方做好检验工作，那么就不必等到工程的最后才发现不良品，或者说到工序的最后阶段只需要重点检验某些部分即可。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e产品质量融于软件开发过程中，将风险高的软件模块提前开发。QA在功能开发前就参与需求的讨论，并提出验收条件AC。\u003c/p\u003e\n\u003cp\u003e####降低成本\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e如果有人问我，为什么要拼命减少库存、降低成本，我会告诉他，是为了让资金周转更加轻松。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e然而，只要提到降低成本，大家就会觉得这是财务人员的责任，其实不然，财务人员根本无法促使成本降低，这只能通过集体的努力实现。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e因此，所谓的降低成本，唯有依靠生产现场来进行，现场的降低成本的意识要做到比鬼还要精才行啊。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e减少\u003cstrong\u003e浪费\u003c/strong\u003e也可以降低成本。关键是我们如何看待浪费。在《丰田生产方式》中定义的浪费：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e1.过量生产的无效劳动：软件行业中指在不合适的时候开发多余的功能\n2.窝工的时间浪费\n3.搬运的无效劳动：需求沟通不完整，导致重复沟通\n4.加工本身的无效劳动和浪费\n5.库存的浪费\n6.动作上的无效劳动：花费过多的时候搭建开发环境\n7.制造次品的无效劳动和浪费：品质没有融于生产过程中\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e从这里就可以看出光靠架构师是不能彻底杜绝浪费，更不可能靠财务了。\u003c/p\u003e\n\u003cp\u003e####小结\n这本书给我最大的启发是：高高在上不接地气的技术管理是无法管理好团队的。高高在上意味着他无法或很难及时、准确得知开发现场的情况的。如果你连“施工现场”的情况都不了解，谈何改善？\u003c/p\u003e\n\u003cp\u003e同时，我也发现软件行业的代码审查、每日站会就很好的体现了大野耐一的现场管理思想。通过这两个实践，我们的leader才有更多机会靠近“现场”。这才是代码审查、每日站会背后更深层的原因。\u003c/p\u003e\n\u003cp\u003e半年后再次重读这本小书，又知新。\u003c/p\u003e\n\u003ch4 id=\"老翟书摘说明\"\u003e老翟书摘说明\u003c/h4\u003e\n\u003cp\u003e书摘内容完全来自原书，如果原书的作者或出版商觉得我侵权了。请联系我。\u003c/p\u003e\n\u003cp\u003e老翟书摘旨在通过一种书摘的方式让大家花最少的时间了解一本书，从而决定要不要继续读下去。书摘的每一本书都是本人亲自读过并理解的。\u003c/p\u003e","title":"老翟书摘：从《大野耐一的现场管理》看软件工程管理"},{"content":"本文试图找到类似Puppet、Chef、Ansible这样自动化配置管理工具的共性，以不至于迷失在杂乱的尘世中。总会有各种人为各种目的造概念，来让世界更复杂。\n本文同样适用于没有运维经验的人。因为我就是一个没有运维经验的人。欢迎斧正。\n与这仨之间的历史 本人接触自动化运维的时间比较晚，也就一年前才知道Puppet及自动化运维（只限于知道），而Chef、Ansible就更晚了。然而在学习它们之前，我对运维要做哪些事情并没有概念。这就对我学习Puppet，Chef和Ansible造成的障碍。因为不知道这三个工具在运维领域的位置，解决运维过程中的哪些问题。我对这三个工具的最初印象就是有了它们，不用我手工的SSH上服务器，然后一条条命令去执行安装软件，不用SCP war包上服务器等，对服务器的操作都可以自动化了。\n这个最初印象也就是我跟它们的历史。为什么要说这些呢？就是因为这个最初印象，让我觉得它们是有共性。所谓共性就是存在一些共通的概念或原理之类的东西，掌握这些“东西”，我就可以站在一个更高的高度去思考。Puppet, Chef, Ansible都是工具，对于工具来说，共性指的是它们共同要解决的问题。\n但是当我翻了不少文章后依然没有结果。所以，我决定自己去找它们的共性，并记录下来。\n自制一个自动化运维工具 但是要从多个相似的东西中找到共性，似乎需要同时很熟悉它们。但是我没有那么多时间。所以，我选择了另一种方法：在大脑中预想自己去实现一套自动化运维工具。但问题是，我都不知道“自动化运维工具要实现哪些功能。\n那我就先把目标降低一些，把问题简化一下：将我最初的“印象”实现自动化了。我能想到的就是写一个bash脚本：\nssh .... apt-get install -y java apt-get -y nginx scp .. 好，现在将问题难度加大：对多台服务器进行同样的操作。我能到想就是将所有的服务器的IP放在一个数组里，然后用for循环执行。问题来了，如果我对服务器已经执行了一次命令时可能会失败，我再想执行第二次怎么办呢？这时，我们可以在bash脚本里加上if语句，如果安装了java就不安装第二次了。\n显然现实中还会有很多问题，如：\n反向配置问题，这时，我们应该另写一个bash脚本来解决这个反向的问题？如果采用这种方案，我们bash脚本数量上升到一定程度，如何管理这些脚本及它们之间的关系，这个方案带来的新的复杂性将会成为我们的新问题； 服务器上操作系统的兼容性问题：不同的操作系统，我们的bash命令会不同； 软件的版本升级或降级问题等等。 面对这些问题，我们是可以每次都用bash解决，但是这样始终不是个办法。因为，bash对于建立一个自动化运维工具过于底层。说到这句话，你应该很容易就想到设计一种DSL。到这个点，我觉得我们的方向已经明朗。Puppet, Chef和Ansible都分别采用不同的DSL。而这种DSL是需要编译成服务器可执行的东西的。什么东西是可执行的？目前，我们假设这个东西是bash脚本。\n但是这个DSL是放在哪里编译呢？放在受控机器端，还是主控机器上？所以，我认为所有的自动化运维工具都会遇到这个问题。什么是受控机器与主控机器？你就理解成一台机器只发命令，另一台机器只执行命令。\n我们刚刚谈的是设计DSL。但是，要设计一个完整的自动化运维工具，我们最先应该考虑是主控机器如何与受控机器通信。这个问题让我很长时间感到很无力，因为无从下手。后来醒悟，原来你不能单独考虑这个问题，通信方式还与你设计的DSL及编译DSL的方式有关。同时，受控机器的执行结果这个问题，也影响着我们设计DSL。\n上面我们似乎忘记了一个事实：自动化运维工具应对的往往不是一台机器，而是很多台机器。当面对多台机器时，就会产生一个新的问题：如何组织它们？因为不同的机器的职责不同，所对应的配置也就不同。\n我想我们已经知道自动化运维工具都要解决的问题了：\n如何与受控机器通信 如何组织成百上千台机器 DSL的设计与编译 如何得到执行结果 我不确定我们的思路与Puppet，Chef和Ansible的作者一样。也不一定完全正确。但是至少，我们大概知道自动化运维工具要解决哪些问题了。而它们是不是自动化运维工具的共性？我不确定。\n但是没有关系，我们假设它们就是所有配置管理工具都要解决的问题，它们的共性，接下来我们来看看它们分别是怎么解决这些问题的。\n这仨的背景 在进入学习之前，我们先看看它们的背景，然后详细了解它们是如何解决问题的。\nPuppet 如何与受控机器通信 Puppet的主控机器(Server)称为Puppet master，受控机器(client)称为Puppet agent。它们使用HTTPS进行通信。在安装puppet之前，需要分别在主控机器和受控上设置的hostname。\n因为是C/S架构的，意味着你需要在主控机器上安装Puppet master，（安装的过程或翻阅其它教程的时候，请注意教程所使用的Puppet的版本，Puppet版本之间是有差异的）我们以Ubuntu为例：\nsudo apt-get install -y puppetmaster\n在受控机器上安装Puppet agent:\nsudo apt-get install -y puppet\n安装完成后，受控机器需要设置在/etc/puppet/puppet.conf文件的[main]节点下加入server=\u0026lt;master’s hostname\u0026gt;，同时运行sudo puppet agent —test向主控机器申请认证。主控机器执行sudo puppet cert sign \u0026lt;agent’s hostname\u0026gt;认证。\n在生产环境并不一定采取这样的手工认证。\nDSL的设计与编译1 Puppet的DSL称为Manifest ，以.pp为文件扩展名。像其它编程语言一样，它也有一个程序的入口，它默认放在：/etc/puppet/manifests/site.pp ：\n#vim /etc/puppet/manifests/site.pp node \u0026#39;mysqlserver.example.com\u0026#39; { $mysql_password = 123456 package { [\u0026#39;mysql-common\u0026#39;, \u0026#39;mysql-client\u0026#39;, \u0026#39;mysql-server\u0026#39;]: ensure =\u0026gt; present } service { \u0026#39;mysql\u0026#39;: enable =\u0026gt; true, ensure =\u0026gt; running, require =\u0026gt; Package[\u0026#39;mysql-server\u0026#39;] } exec { \u0026#39;set-root-password\u0026#39;: path =\u0026gt; \u0026#34;/usr/bin:/usr/sbin:/bin\u0026#34;, subscribe =\u0026gt; [Package[\u0026#39;mysql-common\u0026#39;], Package[\u0026#39;mysql-client\u0026#39;], Package[\u0026#39;mysql-server\u0026#39;]], refreshonly =\u0026gt; true, unless =\u0026gt; \u0026#34;mysqladmin -uroot -p$mysql_password status\u0026#34;, command =\u0026gt; \u0026#34;mysqladmin -uroot password $mysql_password\u0026#34;, } file { \u0026#39;/etc/mysql/my.cnf\u0026#39;: content =\u0026gt; template(\u0026#39;my.cnf.erb\u0026#39;), require =\u0026gt; Package[\u0026#39;mysql-server\u0026#39;], notify =\u0026gt; Service[\u0026#39;mysql\u0026#39;] } } Manifest的基本格式：\nnode NODENAME{ RESOURCE { NAME: ATTRIBUTE =\u0026gt; VALUE, ... } } Nodename实际上就是节点的hostname。这里有个提问：为什么不直接使用IP呢？Resource代表的是资源，也就是Puppet将节点内所有的东西都当作资源。具体细节，我们稍后会讲到。\n如何组织成百上千台机器 如果我要控制1000台机器是不是意味着我要在site.pp中写1000个 节点 node NODENAME{…}呢？答案是肯定的。\nDSL的设计与编译2 我们注意到node节点中所包含的所有内容，本质上都是在描述这个node的状态，如：\nservice { \u0026#39;mysql\u0026#39;: enable =\u0026gt; true, ensure =\u0026gt; running } 指的是service mysql这个Resource的状态是可用的且正在运行的。Puppet中内建了不少Resource供给我们使用，如：file，exec, package等。但是当Puppet内建的Resource不够用了呢？所以，它应该支持resource的扩展。以此类推，所有的自动化运维工具都应该支持此类扩展。\n同时，Puppet提供了模板机制。Puppet的模板模式使用的是Ruby的ERB。你将.erb的文件放在/etc/puppet/templates后，就可以在puppet的代码中使用template('my.cnf.erb’)：\n#vim /etc/puppet/templates/my.cnf.erb [mysql] ... ... 既然有模板，怎么少得了变量？变量的定义：$VAR_NAME = VALUE。有变量的地方，一定会引用作用域概念，不论它是静态作用域还是全局作用域。可以告诉你Puppet是静态作用域。猜猜Puppet的变量的作用域分几级？实际上，Puppet，Chef，Ansible的变量都是静态作用域概念，它们的变量作用域分又都分成几级？多问一句：这也是它们的共性吗？\n现在我们了解了Manifest及其基本格式、node、resource概念、模板和变量。同时，我们并不应该把所有的内容都写到一个site.pp文件中，这就像我们不应该把所有的逻辑都写在C语言中的main函数中一样。那Puppet的DSL是如何解决这个问题的：如何让开发者更方便的组织代码?\n现在我们来看相对模块化一些的Puppet代码：\n#vim /etc/puppet/manifests/site.pp class mysql($mysql_password = \u0026#34;123456\u0026#34;) { package { [\u0026#39;mysql-common\u0026#39;, \u0026#39;mysql-client\u0026#39;, \u0026#39;mysql-server\u0026#39;]: ensure =\u0026gt; present } service { \u0026#39;mysql\u0026#39;: enable =\u0026gt; true, ensure =\u0026gt; running, require =\u0026gt; Package[\u0026#39;mysql-server\u0026#39;] } exec { \u0026#39;set-root-password\u0026#39;: path =\u0026gt; \u0026#34;/usr/bin:/usr/sbin:/bin\u0026#34;, subscribe =\u0026gt; [Package[\u0026#39;mysql-common\u0026#39;], Package[\u0026#39;mysql-client\u0026#39;], Package[\u0026#39;mysql-server\u0026#39;]], refreshonly =\u0026gt; true, unless =\u0026gt; \u0026#34;mysqladmin -uroot -p$mysql_password status\u0026#34;, command =\u0026gt; \u0026#34;mysqladmin -uroot password $mysql_password\u0026#34;, } file { \u0026#39;/etc/mysql/my.cnf\u0026#39;: content =\u0026gt; template(\u0026#39;my.cnf.erb\u0026#39;), require =\u0026gt; Package[\u0026#39;mysql-server\u0026#39;], notify =\u0026gt; Service[\u0026#39;mysql\u0026#39;] } } node \u0026#39;agent\u0026#39; { include mysql class { \u0026#39;mysql\u0026#39;: mysql_password =\u0026gt; \u0026#39;456789\u0026#39; } } 这个版本的site.pp我们用到了class概念。你可以定义了一个class，然后将职责相同的逻辑放在其中。最后在其它地方引用。但是引用的时候要分情况，这个class有没有带参数， 将影响使用这个class的方式。\n但是就算这样，我们的site.pp的代码可维护性一样不高。所以，Puppet还提供一种module概念。实际上，用了你就知道，puppet就是将class从site.pp移出去了的另一种说法。我们来看使用了module的第三版：\n我们将mysql class抽到mysql module中：\n#vim /etc/puppet/modules/mysql/manifests/init.pp class mysql($mysql_password = \u0026#34;123456\u0026#34;) { package { [\u0026#39;mysql-common\u0026#39;, \u0026#39;mysql-client\u0026#39;, \u0026#39;mysql-server\u0026#39;]: ensure =\u0026gt; present } service { \u0026#39;mysql\u0026#39;: enable =\u0026gt; true, ensure =\u0026gt; running, require =\u0026gt; Package[\u0026#39;mysql-server\u0026#39;] } exec { \u0026#39;set-root-password\u0026#39;: path =\u0026gt; \u0026#34;/usr/bin:/usr/sbin:/bin\u0026#34;, subscribe =\u0026gt; [Package[\u0026#39;mysql-common\u0026#39;], Package[\u0026#39;mysql-client\u0026#39;], Package[\u0026#39;mysql-server\u0026#39;]], refreshonly =\u0026gt; true, unless =\u0026gt; \u0026#34;mysqladmin -uroot -p$mysql_password status\u0026#34;, command =\u0026gt; \u0026#34;mysqladmin -uroot password $mysql_password\u0026#34;, } file { \u0026#39;/etc/mysql/my.cnf\u0026#39;: content =\u0026gt; template(\u0026#39;mysql/my.cnf.erb\u0026#39;), require =\u0026gt; Package[\u0026#39;mysql-server\u0026#39;], notify =\u0026gt; Service[\u0026#39;mysql\u0026#39;] } } 在使用module之前，我们的文件结构是这样的：\n|-- auth.conf |-- environments |-- files |-- manifests | `-- site.pp |-- puppet.conf |-- templates | `—my.cnf.erb 使用module后：\n|-- auth.conf |-- environments |-- files |-- manifests | `-- site.pp |-- modules | `-- mysql | |-- manifests | | `-- init.pp | `-- templates | `-- my.cnf.erb |-- puppet.conf `-- templates 定义了module，那怎么用呢？\n#vim /etc/puppet/manifests/site.pp node \u0026#39;agent\u0026#39; { include mysql } 到这里，我相信你知道大概怎么写Puppet，但是我觉得是不够的。我不理解它为什么这样设计。我们为什么不是： node \u0026#39;agent_hostname\u0026#39;{ install package [‘mysql’,’mysqlserver’] start service ‘mysql\u0026#39; create template mysql.cnf } 我的意思是为什么是以形容词导向的描述性语言，而不是以动词为导向。然后，我就去找《配置管理最佳实践》。醒悟了，原来配置管理不是一两个工具就可以搞定。它是一个系统，包括六个核心职能：\n源代码管理 构建工程 环境配置 变更控制 发布工程 部署 所以，我以前一直不理解自动化运维和自动化配置管理之间的区别。同时我看到了一些文章里：配置管理实际上就是状态管理，不论是服务器状态还是软件状态。\n这下终于感觉明白了，也难怪Puppet，Chef，Ansible的DSL都是描述性的语言。\n但是，Puppet如何编译，在哪里编译我们写好的manifest呢？在主控机器与受控机器认证成功后，受控机器会每隔一段时间就向主控机器发请求，这个请求将会把自己（受控机器）的信息告诉主控机器。主控机器拿到这些信息后与manifest链接编译，最后生成一份受控机器（puppet客户端）可执行的catalog。受控机器在执行的过程中，将执行情况反馈给主控主机。这就是Puppet中主控机器如何得到受控机器的命令执行结果的。\n到此，我们看到了Puppet已经回答了我们之前的四个问题。\n小结 如何与受控机器通信 采用C/S架构，使用HTTPS，agent向master申请证书。 如何组织成百上千台机器 在manifest中使用node关键字定义。 DSL的设计与编译 组织代码的方式 Puppet在manifest文件中定义node（受控节点），将所有node中的构件抽象为resource，我们可以给这个resource的attribute设置值。node下可以包含多个resource，这些resource共同构成了这个node的状态。但是不可能将所有的resource都写在一个文件中，再说一个manifest文件通常不止一个node。所以，所以，Puppet提供一种module和class机制，让你能将一些共同起到同一职责的resource打包到一起。class与module有什么不同呢？class可以直接写到manifest文件中，而module必须另外新建一个目录结构。这就是Puppet组织代码的方式。\n深入学习：如果处理resource之间的关系问题，它们很有可能有依赖关系。class及module也会有同样的问题。if else及for呢？ 变量定义： $VAR_NAME = VALUE\n深入学习：了解变量的作用域 模板：使用ruby的erb文件\n如何得到执行结果 受控机器主动将执行结果发送给主控机器。 它真的一定要有master才能用吗？不是的。Puppet提供了单机版的使用方法。具体请google: puppet apply。\nChef Chef的中文意思是厨师。所以它将所有的受控机器看作“菜肴”。但是如果我们不给告诉它菜谱（Cookbook），它是不会给我们做菜的。菜谱上都写着什么呢？是配方（Recipe）。所以，我们把recipe一个个的写到Cookbook中，最后交给Chef。\nChef同样是C/S架构，C与S也是使用HTTPS进行通信的。同样的，正因为这样，我们可能重用学习Puppet的pattern来学习Chef。但是因为Chef的C/S模式的投资回报率太低了，所以，我坚持一段时间后，就放弃了。和Puppet一样，Chef也提供了单机版：Chef-solo。\nAnsible Ansible说是agentless（去客户端）的。但是实际上，它要求受控机器上装有SSH及Python，并且装了python-simplejson包。实际上，我们现在的机器基本上默认都已经安装这些。所以，在使用Ansible时，你不需要特意准备一台机器做为主控服务器。只要你想，任何机器随时都可以变成主控机器。\n关于Ansible的安装看文档就好了。与Chef和Puppet不同的是，Ansible组织受控机器的那部分逻辑抽来单独放，叫Inventory。它是一个ini格式的文件，如hosts：\n[web] 192.168.33.10 [db] 192.168.33.11 文件名和路径都任意，但是建议使用表意的名字及合适的路径。\nPuppet和Chef都自己做了一套DSL，然后再自己写编译器，但是Ansible使用的是yaml格式。我觉得这是非常聪明的设计：一是大家都熟悉yaml格式比熟悉自定义的DSL来得简单，二是不需要自己设计DSL了，三是不用自己写编译器了。所以，我个人学习过程中，发现它是相对Puppet，Chef简单很多。\n了解yaml文件格式后，接着就是理解Ansible的隐喻了。Ansible是导演，将所有的受控机器理解为演员。而我们开发者则是编剧。我们只要把剧本(playbook)写好，Ansible拿剧本再与Invenstory一对上号，演员只会按照剧本上的如实发挥，不会有任何的个人发挥。\n好，我们就来写第一版本的playbook.yml（路径和名字都可自定义）：\n--- - hosts: web tasks: - name: install nginx apt: name=nginx state=latest - hosts: db vars: mysql_password: \u0026#39;123465\u0026#39; tasks: - name: install mysql yum: name={{item}} with_items: - \u0026#39;mysql-common\u0026#39; - \u0026#39;mysql-client\u0026#39; - \u0026#39;mysql-server\u0026#39; - name: configurate mysql-server template: src=my.cnf.j2 dest=/etc/mysql/my.cnf - name: start service service: name=mysql state=started #vim templates/my.cnf.j2 [mysql] ... passowrd={{mysql_password}} 我们的剧本包括两个演员：web，db。它们都对应哪些受控机器呢？看Invenstory文件就知道了。那这些演员都要做哪些事情呢？看tasks，它下面跟的是一个列表。像yum，apt，template，service，被称为module，类似于Puppet的resource和Chef的recipe。Ansible本身提供了不少module，但是想都不用想，一定不能满足所有项目的需求，所以，你可以开发自己的module。\n同样的，Ansible提供变量和模板（Jinja2）机制。问题来了，Ansible的作用域分为几级呢？\n同样的，我们可以不能容忍把所有的task都写在一个文件里。Ansible是如何组织代码的呢？Ansible提出role的概念。是的，扮演共同职责的task，我们把它们归到同一个role中。所以，我们文件结构也变了，由原来的只有两个文本文件，到现在需要新建目录了：\n├── hosts ├── playbook.yml └── roles └── mysql ├── tasks │ └── main.yml └── templcates └── my.cnf.j2 这时，playbook.yml：\n--- - hosts: web tasks: - name: install nginx apt: name=nginx state=latest - hosts: db var: mysql_password: \u0026#39;123456\u0026#39; roles: - mysql 而main.yml:\n- name: install mysql yum: name={{item}} with_items: - \u0026#39;mysql-common\u0026#39; - \u0026#39;mysql-client\u0026#39; - \u0026#39;mysql-server\u0026#39; - name: configurate mysql-server template: src=my.cnf.j2 dest=/etc/mysql/my.cnf - name: start service service: name=mysql state=started 就是task和role之间的关系。那task之间的关系，role之间的关系呢？\nAnsible的DSL就是这样组织代码的。最后一个问题如何得到执行结果呢？这就要说到Ansible的原理：Ansible将本地的yml文件编译成python代码，然后传到受控机器，受控机器执行结果以Json格式返回。\nAnsible的入门非常简单。\n小结 如何与受控机器通信: 只要主控机器与受控机器双方将有SSH 如何组织成百上千台机器: 使用Invenstory管理 DSL的设计与编译 组织代码的方式 Ansible Language的入口就是playbook。你可以直接在playbook里加tasks。很自然，我们想到的这个tasks里是一批小task。事实的确是这样，但是在Ansible中，它叫module。但是，我们不希望所有的task写在同一个文件中，这时，Ansible的role机制就起作用了。你可以把完成同一职责的一批放在一个role中就好了。 深入学习：module之间的依赖问题，if-else问题 变量定义：不同的级别有不同的定义方法 深入学习：了解变量的作用域 模板：使用Jinja2 如何得到执行结果 受控机器主动将执行结果发送给主控机器。 总结 面对层出不穷的新编程语言、新框架、新概念，我们程序员总是学不完。诚然，我假设大家都爱学习，但是，我们更需要问：学到的这些东西，到底属于解决域还是问题域。所以，来了新东西，我总是要问这东西解决了什么问题？为什么它能解决，依据是什么？我们需要的是问题的本质和问题的解决模型，而不是别人葫芦里的药。\n说远了。实际上，除了上述的仨自动化配置管理工具，市面还有很多别的。总的来，我觉得都可以用以上思路去学习。回到我们最开始的问题：Puppet，Chef，Ansible的共性是什么？ 我能不能说那四个问题就是它们的共性，答案是我也不知道。\n最后，我申明：除了上述工具，我没有将“思路”应用到其它工具。希望应用到的同学给我反馈。谢谢。\n","permalink":"https://showme.codes/zh-cn/2016-1-2-the-nature-of-ansible-puppet-chef/","summary":"\u003cp\u003e本文试图找到类似Puppet、Chef、Ansible这样自动化配置管理工具的共性，以不至于迷失在杂乱的尘世中。总会有各种人为各种目的造概念，来让世界更复杂。\u003c/p\u003e\n\u003cp\u003e本文同样适用于没有运维经验的人。因为我就是一个没有运维经验的人。欢迎斧正。\u003c/p\u003e\n\u003ch2 id=\"与这仨之间的历史\"\u003e与这仨之间的历史\u003c/h2\u003e\n\u003cp\u003e本人接触自动化运维的时间比较晚，也就一年前才知道Puppet及自动化运维（只限于知道），而Chef、Ansible就更晚了。然而在学习它们之前，我对运维要做哪些事情并没有概念。这就对我学习Puppet，Chef和Ansible造成的障碍。因为不知道这三个工具在运维领域的位置，解决运维过程中的哪些问题。我对这三个工具的最初印象就是有了它们，不用我手工的SSH上服务器，然后一条条命令去执行安装软件，不用SCP war包上服务器等，对服务器的操作都可以自动化了。\u003c/p\u003e\n\u003cp\u003e这个最初印象也就是我跟它们的历史。为什么要说这些呢？就是因为这个最初印象，让我觉得它们是有共性。所谓共性就是存在一些共通的概念或原理之类的东西，掌握这些“东西”，我就可以站在一个更高的高度去思考。Puppet, Chef, Ansible都是工具，对于工具来说，共性指的是它们共同要解决的问题。\u003c/p\u003e\n\u003cp\u003e但是当我翻了不少文章后依然没有结果。所以，我决定自己去找它们的共性，并记录下来。\u003c/p\u003e\n\u003ch2 id=\"自制一个自动化运维工具\"\u003e自制一个自动化运维工具\u003c/h2\u003e\n\u003cp\u003e但是要从多个相似的东西中找到共性，似乎需要同时很熟悉它们。但是我没有那么多时间。所以，我选择了另一种方法：在大脑中预想自己去实现一套自动化运维工具。但问题是，我都不知道“自动化运维工具要实现哪些功能。\u003c/p\u003e\n\u003cp\u003e那我就先把目标降低一些，把问题简化一下：将我最初的“印象”实现自动化了。我能想到的就是写一个bash脚本：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003essh ....\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapt-get install -y java\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapt-get -y nginx\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003escp ..\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e好，现在将问题难度加大：对多台服务器进行同样的操作。我能到想就是将所有的服务器的IP放在一个数组里，然后用for循环执行。问题来了，如果我对服务器已经执行了一次命令时可能会失败，我再想执行第二次怎么办呢？这时，我们可以在bash脚本里加上if语句，如果安装了java就不安装第二次了。\u003c/p\u003e\n\u003cp\u003e显然现实中还会有很多问题，如：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e反向配置问题，这时，我们应该另写一个bash脚本来解决这个反向的问题？如果采用这种方案，我们bash脚本数量上升到一定程度，如何管理这些脚本及它们之间的关系，这个方案带来的新的复杂性将会成为我们的新问题；\u003c/li\u003e\n\u003cli\u003e服务器上操作系统的兼容性问题：不同的操作系统，我们的bash命令会不同；\u003c/li\u003e\n\u003cli\u003e软件的版本升级或降级问题等等。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e面对这些问题，我们是可以每次都用bash解决，但是这样始终不是个办法。因为，bash对于建立一个自动化运维工具过于底层。说到这句话，你应该很容易就想到设计一种DSL。到这个点，我觉得我们的方向已经明朗。Puppet, Chef和Ansible都分别采用不同的DSL。而这种DSL是需要编译成服务器可执行的东西的。什么东西是可执行的？目前，我们假设这个东西是bash脚本。\u003c/p\u003e\n\u003cp\u003e但是这个DSL是放在哪里编译呢？放在受控机器端，还是主控机器上？所以，我认为所有的自动化运维工具都会遇到这个问题。什么是受控机器与主控机器？你就理解成一台机器只发命令，另一台机器只执行命令。\u003c/p\u003e\n\u003cp\u003e我们刚刚谈的是设计DSL。但是，要设计一个完整的自动化运维工具，我们最先应该考虑是主控机器如何与受控机器通信。这个问题让我很长时间感到很无力，因为无从下手。后来醒悟，原来你不能单独考虑这个问题，通信方式还与你设计的DSL及编译DSL的方式有关。同时，受控机器的执行结果这个问题，也影响着我们设计DSL。\u003c/p\u003e\n\u003cp\u003e上面我们似乎忘记了一个事实：自动化运维工具应对的往往不是一台机器，而是很多台机器。当面对多台机器时，就会产生一个新的问题：如何组织它们？因为不同的机器的职责不同，所对应的配置也就不同。\u003c/p\u003e\n\u003cp\u003e我想我们已经知道自动化运维工具都要解决的问题了：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e如何与受控机器通信\u003c/li\u003e\n\u003cli\u003e如何组织成百上千台机器\u003c/li\u003e\n\u003cli\u003eDSL的设计与编译\u003c/li\u003e\n\u003cli\u003e如何得到执行结果\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我不确定我们的思路与Puppet，Chef和Ansible的作者一样。也不一定完全正确。但是至少，我们大概知道自动化运维工具要解决哪些问题了。而它们是不是自动化运维工具的共性？我不确定。\u003c/p\u003e\n\u003cp\u003e但是没有关系，我们假设它们就是所有配置管理工具都要解决的问题，它们的共性，接下来我们来看看它们分别是怎么解决这些问题的。\u003c/p\u003e\n\u003ch2 id=\"这仨的背景\"\u003e这仨的背景\u003c/h2\u003e\n\u003cp\u003e在进入学习之前，我们先看看它们的背景，然后详细了解它们是如何解决问题的。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"puppet-chef-ansible\" loading=\"lazy\" src=\"/assets/images/200245_Z4A1_181141.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"puppet\"\u003ePuppet\u003c/h2\u003e\n\u003ch3 id=\"如何与受控机器通信\"\u003e如何与受控机器通信\u003c/h3\u003e\n\u003cp\u003ePuppet的主控机器(Server)称为Puppet master，受控机器(client)称为Puppet agent。它们使用HTTPS进行通信。在安装puppet之前，需要分别在主控机器和受控上设置的hostname。\u003c/p\u003e\n\u003cp\u003e因为是C/S架构的，意味着你需要在主控机器上安装Puppet master，（安装的过程或翻阅其它教程的时候，请注意教程所使用的Puppet的版本，Puppet版本之间是有差异的）我们以Ubuntu为例：\u003c/p\u003e\n\u003cp\u003e    sudo apt-get install -y puppetmaster\u003c/p\u003e\n\u003cp\u003e在受控机器上安装Puppet agent:\u003c/p\u003e\n\u003cp\u003e    sudo apt-get install -y puppet\u003c/p\u003e\n\u003cp\u003e安装完成后，受控机器需要设置在/etc/puppet/puppet.conf文件的[main]节点下加入server=\u0026lt;master’s hostname\u0026gt;，同时运行sudo puppet agent —test向主控机器申请认证。主控机器执行sudo puppet cert sign \u0026lt;agent’s hostname\u0026gt;认证。\u003c/p\u003e","title":"Puppet，Chef，Ansible的共性"},{"content":"本文使用Scala2.10.6，sbt。请自行提前装好。\n设置SSH，本地免密码登录 因为Spark master需要ssh到Spark worker中执行命令，所以，需要免密码登录。\ncat ~/.ssh/id_rsa.pub \u0026gt; ~/.ssh/authorized_keys\n执行ssh localhost确认一下，如果不需要密码登录就说明OK了。\nTips： Mac下可能ssh不到本地，请检查你sharing设置：\n下载Spark http://spark.apache.org/downloads.html\n我选择的是spark-1.6.0-bin-cdh4.tgz 。看到cdh4(Hadoop的一个分发版本)，别以为它是要你装Hadoop。其实不然，要看你自己的开发需求。因为我不需要，所以，我只装Spark。\n配置你的Spark slave 我很好奇，worker和slave这个名称有什么不同？还是因为历史原因，导致本质上一个东西但是两种叫法？\n在你的Spark HOME路径下\ncp ./conf/slaves.template ./conf/slaves\nslaves文件中有一行localhost代表在本地启动一个Spark worker。\n启动Spark伪分布式 \u0026lt;SPARK_HOME\u0026gt;/sbin/start-all.sh\n执行JPS验证Spark启动成功\n➜ jps 83141 Worker 83178 Jps 83020 Master 打开你的Spark界面 http://localhost:8080\n下载Spark项目骨架 为方便我自己开发，我自己创建了一个Spark应用开发的项目骨架。\n下载项目骨架： http://git.oschina.net/zacker330/spark-skeleton\n项目路径中执行：sbt package 编译打包你的spark应用程序。\n将你的spark应用程序提交给spark master执行 \u0026lt;SPARK_HOME\u0026gt;/bin/spark-submit \\ --class \u0026quot;SimpleApp\u0026quot; \\ --master spark://Jacks-MBP.workgroup:7077 \\ target/scala-2.10/spark-skeleton_2.10-1.0.jar 这个“spark://Jacks-MBP.workgroup:7077”是你在 http://localhost:8080 中看到的URL的值\n可以看到打印出: hello world\n","permalink":"https://showme.codes/zh-cn/2016-1-1-setup-spark-env-for-dev/","summary":"\u003cp\u003e本文使用Scala2.10.6，sbt。请自行提前装好。\u003c/p\u003e\n\u003ch4 id=\"设置ssh本地免密码登录\"\u003e设置SSH，本地免密码登录\u003c/h4\u003e\n\u003cp\u003e因为Spark master需要ssh到Spark worker中执行命令，所以，需要免密码登录。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003ecat ~/.ssh/id_rsa.pub \u0026gt; ~/.ssh/authorized_keys\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e执行\u003ccode\u003essh localhost\u003c/code\u003e确认一下，如果不需要密码登录就说明OK了。\u003c/p\u003e\n\u003cp\u003eTips：\nMac下可能ssh不到本地，请检查你sharing设置：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Spark本地开发环境搭建\" loading=\"lazy\" src=\"/assets/images/2016-1-spark-network.png\"\u003e\u003c/p\u003e\n\u003ch4 id=\"下载spark\"\u003e下载Spark\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"http://spark.apache.org/downloads.html\"\u003ehttp://spark.apache.org/downloads.html\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e我选择的是spark-1.6.0-bin-cdh4.tgz 。看到cdh4(Hadoop的一个分发版本)，别以为它是要你装Hadoop。其实不然，要看你自己的开发需求。因为我不需要，所以，我只装Spark。\u003c/p\u003e\n\u003ch4 id=\"配置你的spark-slave\"\u003e配置你的Spark slave\u003c/h4\u003e\n\u003cp\u003e我很好奇，worker和slave这个名称有什么不同？还是因为历史原因，导致本质上一个东西但是两种叫法？\u003c/p\u003e\n\u003cp\u003e在你的Spark HOME路径下\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003ecp ./conf/slaves.template ./conf/slaves\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003ccode\u003eslaves\u003c/code\u003e文件中有一行\u003ccode\u003elocalhost\u003c/code\u003e代表在本地启动一个Spark worker。\u003c/p\u003e\n\u003ch4 id=\"启动spark伪分布式\"\u003e启动Spark伪分布式\u003c/h4\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026lt;SPARK_HOME\u0026gt;/sbin/start-all.sh\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e执行JPS验证Spark启动成功\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e➜ jps\n83141 Worker\n83178 Jps\n83020 Master\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch4 id=\"打开你的spark界面\"\u003e打开你的Spark界面\u003c/h4\u003e\n\u003cp\u003ehttp://localhost:8080\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Spark本地开发环境搭建\" loading=\"lazy\" src=\"/assets/images/2016-1-spark-webview.png\"\u003e\u003c/p\u003e\n\u003ch4 id=\"下载spark项目骨架\"\u003e下载Spark项目骨架\u003c/h4\u003e\n\u003cp\u003e为方便我自己开发，我自己创建了一个Spark应用开发的项目骨架。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e下载项目骨架： \u003ca href=\"http://git.oschina.net/zacker330/spark-skeleton\"\u003ehttp://git.oschina.net/zacker330/spark-skeleton\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e项目路径中执行：\u003ccode\u003esbt package\u003c/code\u003e 编译打包你的spark应用程序。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"将你的spark应用程序提交给spark-master执行\"\u003e将你的spark应用程序提交给spark master执行\u003c/h4\u003e\n\u003cpre\u003e\u003ccode\u003e    \u0026lt;SPARK_HOME\u0026gt;/bin/spark-submit \\ \n          --class \u0026quot;SimpleApp\u0026quot; \\\n          --master spark://Jacks-MBP.workgroup:7077 \\\n              target/scala-2.10/spark-skeleton_2.10-1.0.jar\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e这个“spark://Jacks-MBP.workgroup:7077”是你在 http://localhost:8080 中看到的\u003ccode\u003eURL\u003c/code\u003e的值\u003c/p\u003e\n\u003cp\u003e可以看到打印出: hello world\u003c/p\u003e","title":"Spark本地开发环境搭建"},{"content":"\n我一直希望能了解那些富人一步步富起来的过程，更重要的是了解他们是如何思考的。但是很多传记写的就像李开复的《世界因你而不同》那样，除了鸡汤还是鸡汤。我不是说鸡汤没有价值，而是我更想看到真实世界到底是怎么样的。\n而这本《创富课》很是落地，解答了不少我一直以来的疑惑。\n下面从我的疑惑出发来对这本书进行书摘。\n从工作到现在，我很多时候都会担任“反调”的角色。也就是领导说出什么决定。我思考后经常会提出另一个与之相反的论调。先不谈我的论调是否正确。不少人对于这个“反调”要么觉得你就一个小兵，执行就好；要么觉得很烦。我就疑惑，有不一样的声音不好吗？但是说不上背后的机理。来看看老雕怎么说的：\n一方面，要做到团队统一理念。另一方面，又要鼓励团队中的反对声音。这一点儿不矛盾，换句话说，就是在“战略”上，大家要彼此达到共识（那是团队建设的核心），而在“战术”上，则必须反复认证，也就是怂恿争吵、鞭策博弈。\n大家早就知道一个人再高明，也无法面面俱到。却少有人知道，其实一群人更容易陷入“集体无意识”。尤其你是最高决策人时，你发表的意见，大多数下属无条件接受。（别忘了大部分中国人都喜欢察言观色，随时保留意见）。所以这时候，如果一个声音喊道：“老板，我认为你说的不对！我倒认为，从反方面来看……” 你千万别把脸憋成番茄，或者拍着桌子打断他，而应该像偷猎者见到大熊猫般欣喜若狂。\n说回来，我也很理解“领导”比常人更要“面子”。说到这里，我常常听人说要注意说话方式。这是另一个高深的话题，即使我总提醒自己：见人说人话，见鬼说鬼话。但是知易行难啊。\n很久以前就听说“办公室政治”，苦于阅历尚浅，始终理解不过来。老雕年青时也和我一样有相同的疑惑：\n没过多少日子，我所在部门的小领导，一个笨蛋，就找我谈话了，他苦口婆心劝我，教导我，“表哥，你这么拼，有问题的啊，你这么干，把很多同事反衬得不用功不努力似的，这会遭众怒的，明白吗？”\n噢，原来这就是“办公室政治”了吧？虽然老雕我当年年纪小，但也体会到他所说话语的含义了。\n关于如何做生意，老雕是这样的：\n第一步，分析你的企业所在的产业链。\n第二步，分析你的企业的“价值链”\n第三步，从价值链里找到核心竞争力。\n看任何企业问题，都首先站在产业链看价值链，然后站在价值链看核心竞争力，站在核心竞争力的高度上，看品牌、营销、成本控制……一系列的“药材”。\n咋一看很虚，但是他虚得是有道理的。因为他告诉我们的是一种思考方式。说实在的，我不知道有多少企业是采用这种思考方式。但是，我尝试了，感觉还真有用。老雕文章还有不少例子，有兴趣的同学可以看看。\n做生意，一定会有竞争对手。针对“我的竞争对手是谁”这个问题。老雕给了我一个没想到的答案：\n可口可乐的竞争对手，是所有抢“喉咙份额”的家伙们。比如，茶、酒、矿泉水、果汁……\n原来，我们在分析竞争对手时，视野不能那么狭隘，认为可口可乐的竞争对手就只是百事可乐。\n小结： 不要认为我把这些东西摘抄下来，就代表我完全赞同，就算我现在赞同部分观点，也不代表我将来赞同。\n总的来说，还是能从这本书得到不少启示。\n老翟书摘说明 书摘内容完全来自原书，如果原书的作者或出版商觉得我侵权了。请联系我。\n老翟书摘旨在通过一种书摘的方式让大家花最少的时间了解一本书，从而决定要不要继续读下去。书摘的每一本书都是本人亲自读过并理解的。\n","permalink":"https://showme.codes/zh-cn/2015-12-30-mba-wealth/","summary":"\u003cp\u003e\u003cimg alt=\"老翟书摘：《MBA教不了的创富课》\" loading=\"lazy\" src=\"/assets/images/2015-12-30-mba.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e我一直希望能了解那些富人一步步富起来的过程，更重要的是了解他们是如何思考的。但是很多传记写的就像李开复的《世界因你而不同》那样，除了鸡汤还是鸡汤。我不是说鸡汤没有价值，而是我更想看到真实世界到底是怎么样的。\u003c/p\u003e\n\u003cp\u003e而这本《创富课》很是落地，解答了不少我一直以来的疑惑。\u003c/p\u003e\n\u003cp\u003e下面从我的疑惑出发来对这本书进行书摘。\u003c/p\u003e\n\u003cp\u003e从工作到现在，我很多时候都会担任“反调”的角色。也就是领导说出什么决定。我思考后经常会提出另一个与之相反的论调。先不谈我的论调是否正确。不少人对于这个“反调”要么觉得你就一个小兵，执行就好；要么觉得很烦。我就疑惑，有不一样的声音不好吗？但是说不上背后的机理。来看看老雕怎么说的：\u003c/p\u003e\n\u003cp\u003e一方面，要做到团队统一理念。另一方面，又要鼓励团队中的反对声音。这一点儿不矛盾，换句话说，就是在“战略”上，大家要彼此达到共识（那是团队建设的核心），而在“战术”上，则必须反复认证，也就是怂恿争吵、鞭策博弈。\u003c/p\u003e\n\u003cp\u003e大家早就知道一个人再高明，也无法面面俱到。却少有人知道，其实一群人更容易陷入“集体无意识”。尤其你是最高决策人时，你发表的意见，大多数下属无条件接受。（别忘了大部分中国人都喜欢察言观色，随时保留意见）。所以这时候，如果一个声音喊道：“老板，我认为你说的不对！我倒认为，从反方面来看……” 你千万别把脸憋成番茄，或者拍着桌子打断他，而应该像偷猎者见到大熊猫般欣喜若狂。\u003c/p\u003e\n\u003cp\u003e说回来，我也很理解“领导”比常人更要“面子”。说到这里，我常常听人说要注意说话方式。这是另一个高深的话题，即使我总提醒自己：见人说人话，见鬼说鬼话。但是知易行难啊。\u003c/p\u003e\n\u003cp\u003e很久以前就听说“办公室政治”，苦于阅历尚浅，始终理解不过来。老雕年青时也和我一样有相同的疑惑：\u003c/p\u003e\n\u003cp\u003e没过多少日子，我所在部门的小领导，一个笨蛋，就找我谈话了，他苦口婆心劝我，教导我，“表哥，你这么拼，有问题的啊，你这么干，把很多同事反衬得不用功不努力似的，这会遭众怒的，明白吗？”\u003c/p\u003e\n\u003cp\u003e噢，原来这就是“办公室政治”了吧？虽然老雕我当年年纪小，但也体会到他所说话语的含义了。\u003c/p\u003e\n\u003cp\u003e关于如何做生意，老雕是这样的：\u003c/p\u003e\n\u003cp\u003e第一步，分析你的企业所在的产业链。\u003c/p\u003e\n\u003cp\u003e第二步，分析你的企业的“价值链”\u003c/p\u003e\n\u003cp\u003e第三步，从价值链里找到核心竞争力。\u003c/p\u003e\n\u003cp\u003e看任何企业问题，都首先站在产业链看价值链，然后站在价值链看核心竞争力，站在核心竞争力的高度上，看品牌、营销、成本控制……一系列的“药材”。\u003c/p\u003e\n\u003cp\u003e咋一看很虚，但是他虚得是有道理的。因为他告诉我们的是一种思考方式。说实在的，我不知道有多少企业是采用这种思考方式。但是，我尝试了，感觉还真有用。老雕文章还有不少例子，有兴趣的同学可以看看。\u003c/p\u003e\n\u003cp\u003e做生意，一定会有竞争对手。针对“我的竞争对手是谁”这个问题。老雕给了我一个没想到的答案：\u003c/p\u003e\n\u003cp\u003e可口可乐的竞争对手，是所有抢“喉咙份额”的家伙们。比如，茶、酒、矿泉水、果汁……\u003c/p\u003e\n\u003cp\u003e原来，我们在分析竞争对手时，视野不能那么狭隘，认为可口可乐的竞争对手就只是百事可乐。\u003c/p\u003e\n\u003ch4 id=\"小结\"\u003e小结：\u003c/h4\u003e\n\u003cp\u003e不要认为我把这些东西摘抄下来，就代表我完全赞同，就算我现在赞同部分观点，也不代表我将来赞同。\u003c/p\u003e\n\u003cp\u003e总的来说，还是能从这本书得到不少启示。\u003c/p\u003e\n\u003ch4 id=\"老翟书摘说明\"\u003e老翟书摘说明\u003c/h4\u003e\n\u003cp\u003e书摘内容完全来自原书，如果原书的作者或出版商觉得我侵权了。请联系我。\u003c/p\u003e\n\u003cp\u003e老翟书摘旨在通过一种书摘的方式让大家花最少的时间了解一本书，从而决定要不要继续读下去。书摘的每一本书都是本人亲自读过并理解的。\u003c/p\u003e","title":"老翟书摘：《MBA教不了的创富课》"},{"content":"这本书的作者是大野耐一，原丰田汽车工业公司副社长。里面写了大多是一些生产过程中的原则和看法。但是，这些原则和看问题的角度是通用的，即使我们是软件行业。同时，在看完这本书之后，你会对市面上的那些”精益”理论，会有不一样的看法，也不至于盲从。\n序 我们的初衷是找出一条适合于日本经济环境的独特的方式，但又不想让别家公司，特别是不想让先进国家轻易地了解它，甚至不让他们留下一个完整的概念，而一直推行和强调“看板方式”或“包括人的因素的自働化”。因此，人们难以理解它，也是很自然的。\n第一章：丰田生产方式的诞生\n我认为只要杜绝浪费，生产效率就有可能提高10倍。这种想法，正是现在丰田生产方式的出发点。\n“彻底杜绝浪费”是丰田生产方式的基本思想，而贯穿其中的两大支柱就是：\n（1）准时化\n（2）自働化\n所谓“准时化”，就是在通过流水作业装配一辆汽车的过程中，所需要的零件在需要的时刻，以需要的数量，不多不少地送到生产线的旁边。\n究竟怎样才能做到“准时化”？\n我们进行了各种试验，最后总结出以下做法：以生产工序的最后一道总装配线为起点，开始给装配线提出生产计划；而装配线上用的零部件的运送方法，也从过去由前一道工序向后一道工序运送的方式，改为由后一道工序在需要的时刻到前一道工序去领取，而前一道工序只按后一道工序的数量生产。\n“看板”方式则是顺利推行丰田方式的手段。\n丰田生产方式的另一个支柱是“自働化”，但不是单纯的机械“自动化”，而是包括人的因素的“自働化”。\n丰田公司的“包括人的因素的自动机器”就是指“带自动停止装置的机器”。\n因为当机器正常运转的时候用不到人，人只是在机器发生异常情况、停止运转的时候去处理就可以了。\n“自働化”的作用主是，杜绝生产现场中过量制造的无效劳动，防止生产不合格品。\n第二章：丰田生产方式的精髓 “为什么会出现生产过量的浪费呢？”针对这个问题，会得出因为“没有控制过量生产机能”的答案，据此展开便生产“目视化管理”的设想，进而导出“看板”的构思。\n彻底杜绝浪费，最重要的是充分掌握下述两点：\n第一，提高效率只有同降低成本结合起来才有意义。为此，必须朝着以最少量的人员、只生产所需要数量的产品这一方向努力。\n第二，关于效率，必须从每一个操作人员以及他们组织起来的生产线，进而以生产线为中心从整个工厂着眼，每个环节都要提高，才能收到效果。\n以运用丰田生产方式为前提，需要彻底找出无效劳动和浪费现象：\n1. 过量生产的无效劳动 2. 窝工的时间浪费 3. 搬运的无效劳动 4. 加工本身的无效劳动和浪费 5. 库存的浪费 6. 动作上的无效劳动 7. 制造次品的无效劳动和浪费 我是彻底的现场主义者。这是因为我从年轻时起就是在生产现场的不断磨炼中长大的。当了负责经营的管理者以后，就更不开企业的主要数据来源的生产现场了。\n我们曾反复提及“准时化”和“自働化”是丰田生产方式的两大支柱，把这一体系的运作工具称为“看板”。现在，让我谈谈它的由来。\n实际上，“看板方式”是从美国的自选超市得来的启示。\n前面我们已经谈到，这是从自选超市中得到启发的。自选超市使用“看板”后会出现什么情况呢？\n在计价器将顾客购买的许多商品计价以后，要把记载着销售出去的商品的各类和数量的卡片（相当于“看板”）送到采购部。这样采购部便可以迅速地补充商品。这种卡片，拿丰田生产方式来说，就相当于“取货看板”。自选超市陈列的商品，就相当于生产现场的工序贮备。\n丰田生产方式通过“看板”便可以完全杜绝“过量生产”的现象，不需要超出需求量的库存。不需要仓库，也不需要仓库管理人员，而且，也不需要散发许多单据、传票之类的东西了。\n“看板”是“准时化”的一种手段。也就是它是以实现“准时化”为目的的。“看板”是生产线的反射神经，生产现场的作业人员可以根据“看板”开始作业，并判断所需加班时间的长短。\n“看板”也能使用管理者、监督者的职责明确化。\n“看板”的使用规则第一条是“后一道工序要到前一道工序去领取产品”。\n“看板”的第二条使用规则是“前一道工序只生产后一道工序所需要数量的零部件”。\n实际上，如果不遵守这些规则而只引进“看板”，既发挥不了“看板”本来的作用，也不能降低成本。这种孤立使用“看板”的做法是有百害而无一利的。\n企业越大越需要要具备很好的反射神经。对于计划的微小改动，要做到无需大脑发令也能采取相应的行动。就是说，如果生产管理部不发指令或者不发计划变更通知便不能改变作业，不能采取行动的话，企业就不能避免受创伤、遇大害，并且还会贻误大好时机。只有企业具备无意识适应变化的微调机能，才可以说真正装上了反射神经。我确信：通过“目视化管理”和“准时化”、“自働化”丰田生产方式这两大支柱，将会更好地锻炼这种反射神经。\n第四章：丰田生产方式与福特生产方式\n丰田生产方式同福特生产方式一样，基本形式是流水作业。索伦森在放置零部件的仓库上颇费一番苦心，而丰田生产方式却不需要仓库。\n把同一品种和同一型号的零部件凑在一起，即把批量加大，不换冲模，尽量多次连续冲压进行大批量生产的作法，现在仍是生产现场的常识。福特生产方式大批量体系的关键就在这一点上。美国汽车企业一直证明，有计划地进行大批量生产对降低成本最有成效。\n丰田生产方式与此相反，而是“尽量缩小批量，迅速变换模具”。\n福特生产方式要加大批量来提高产量，所以，在各处都要有工序间的库存贮备。丰田生产方式则不同，其考虑方法是把这些库存可能导致的生产过剩的无效劳动和浪费，以及管理这些库存的人员、土地建筑的负担完全清除。\n福特生产方式和丰田生产方式任何一方都有自身的优点，而且两者都在日日求新与改革，无法下结论说哪一个更优秀，但是我个人深信在低增长的时代，以丰田生产方式较为适合。\n老翟评书 市面上有一本有名的书叫《看板方法》的书。说实话，这本书，我没看完。是我看不下去（你可以想象背后的原因），所以我不好评论。为什么要提《看板方法》这本书呢？因为我见过不少把《看板方法》里面的“看板”与丰田生产方式中的“看板”看作是一样东西，包括我自己。所以，我建议大家先看这本大野耐一的《丰田生产方式》，再看《看板方法》。大野耐一说了，他的看板是从美国自选超市得到的启示，是为了实现丰田生产方式的支柱之一：准时化。所以，我也希望大家能从《看板方法》里找出它说的“看板”的根源。\n不知道有没有注意到作者在序中说的：但又不想让别家公司，特别是不想让先进国家轻易地了解它，甚至不让他们留下一个完整的概念。这点时刻提醒着我自己，有时我们看不懂别人的理论，并不是我们读者的问题。而是有可能作者有意，或者他自己根本不懂，所以无法表达清楚。\n老翟书摘说明 书摘内容完全来自原书，如果原书的作者或出版商觉得我侵权了。请通过开源中国 @翟志军 联系我。\n老翟书摘旨在通过一种书摘的方式让大家花最少的时间了解一本书，从而决定要不要继续读下去。书摘的每一本书都是本人亲自读过并理解的。\n","permalink":"https://showme.codes/zh-cn/2015-12-29-tps/","summary":"\u003cp\u003e这本书的作者是大野耐一，原丰田汽车工业公司副社长。里面写了大多是一些生产过程中的原则和看法。但是，这些原则和看问题的角度是通用的，即使我们是软件行业。同时，在看完这本书之后，你会对市面上的那些”精益”理论，会有不一样的看法，也不至于盲从。\u003c/p\u003e\n\u003ch4 id=\"序\"\u003e序\u003c/h4\u003e\n\u003cp\u003e我们的初衷是找出一条适合于日本经济环境的独特的方式，但又不想让别家公司，特别是不想让先进国家轻易地了解它，甚至不让他们留下一个完整的概念，而一直推行和强调“看板方式”或“包括人的因素的自働化”。因此，人们难以理解它，也是很自然的。\u003c/p\u003e\n\u003cp\u003e第一章：丰田生产方式的诞生\u003c/p\u003e\n\u003cp\u003e我认为只要杜绝浪费，生产效率就有可能提高10倍。这种想法，正是现在丰田生产方式的出发点。\u003c/p\u003e\n\u003cp\u003e“彻底杜绝浪费”是丰田生产方式的基本思想，而贯穿其中的两大支柱就是：\u003c/p\u003e\n\u003cp\u003e（1）准时化\u003c/p\u003e\n\u003cp\u003e（2）自働化\u003c/p\u003e\n\u003cp\u003e所谓“准时化”，就是在通过流水作业装配一辆汽车的过程中，所需要的零件在需要的时刻，以需要的数量，不多不少地送到生产线的旁边。\u003c/p\u003e\n\u003cp\u003e究竟怎样才能做到“准时化”？\u003c/p\u003e\n\u003cp\u003e我们进行了各种试验，最后总结出以下做法：以生产工序的最后一道总装配线为起点，开始给装配线提出生产计划；而装配线上用的零部件的运送方法，也从过去由前一道工序向后一道工序运送的方式，改为由后一道工序在需要的时刻到前一道工序去领取，而前一道工序只按后一道工序的数量生产。\u003c/p\u003e\n\u003cp\u003e“看板”方式则是顺利推行丰田方式的手段。\u003c/p\u003e\n\u003cp\u003e丰田生产方式的另一个支柱是“自働化”，但不是单纯的机械“自动化”，而是包括人的因素的“自働化”。\u003c/p\u003e\n\u003cp\u003e丰田公司的“包括人的因素的自动机器”就是指“带自动停止装置的机器”。\u003c/p\u003e\n\u003cp\u003e因为当机器正常运转的时候用不到人，人只是在机器发生异常情况、停止运转的时候去处理就可以了。\u003c/p\u003e\n\u003cp\u003e“自働化”的作用主是，杜绝生产现场中过量制造的无效劳动，防止生产不合格品。\u003c/p\u003e\n\u003ch4 id=\"第二章丰田生产方式的精髓\"\u003e第二章：丰田生产方式的精髓\u003c/h4\u003e\n\u003cp\u003e“为什么会出现生产过量的浪费呢？”针对这个问题，会得出因为“没有控制过量生产机能”的答案，据此展开便生产“目视化管理”的设想，进而导出“看板”的构思。\u003c/p\u003e\n\u003cp\u003e彻底杜绝浪费，最重要的是充分掌握下述两点：\u003c/p\u003e\n\u003cp\u003e第一，提高效率只有同降低成本结合起来才有意义。为此，必须朝着以最少量的人员、只生产所需要数量的产品这一方向努力。\u003c/p\u003e\n\u003cp\u003e第二，关于效率，必须从每一个操作人员以及他们组织起来的生产线，进而以生产线为中心从整个工厂着眼，每个环节都要提高，才能收到效果。\u003c/p\u003e\n\u003cp\u003e以运用丰田生产方式为前提，需要彻底找出无效劳动和浪费现象：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e1. 过量生产的无效劳动\n\n2. 窝工的时间浪费\n\n3. 搬运的无效劳动\n\n4. 加工本身的无效劳动和浪费\n\n5. 库存的浪费\n\n6. 动作上的无效劳动\n\n7. 制造次品的无效劳动和浪费\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e我是彻底的现场主义者。这是因为我从年轻时起就是在生产现场的不断磨炼中长大的。当了负责经营的管理者以后，就更不开企业的主要数据来源的生产现场了。\u003c/p\u003e\n\u003cp\u003e我们曾反复提及“准时化”和“自働化”是丰田生产方式的两大支柱，把这一体系的运作工具称为“看板”。现在，让我谈谈它的由来。\u003c/p\u003e\n\u003cp\u003e实际上，“看板方式”是从美国的自选超市得来的启示。\u003c/p\u003e\n\u003cp\u003e前面我们已经谈到，这是从自选超市中得到启发的。自选超市使用“看板”后会出现什么情况呢？\u003c/p\u003e\n\u003cp\u003e在计价器将顾客购买的许多商品计价以后，要把记载着销售出去的商品的各类和数量的卡片（相当于“看板”）送到采购部。这样采购部便可以迅速地补充商品。这种卡片，拿丰田生产方式来说，就相当于“取货看板”。自选超市陈列的商品，就相当于生产现场的工序贮备。\u003c/p\u003e\n\u003cp\u003e丰田生产方式通过“看板”便可以完全杜绝“过量生产”的现象，不需要超出需求量的库存。不需要仓库，也不需要仓库管理人员，而且，也不需要散发许多单据、传票之类的东西了。\u003c/p\u003e\n\u003cp\u003e“看板”是“准时化”的一种手段。也就是它是以实现“准时化”为目的的。“看板”是生产线的反射神经，生产现场的作业人员可以根据“看板”开始作业，并判断所需加班时间的长短。\u003c/p\u003e\n\u003cp\u003e“看板”也能使用管理者、监督者的职责明确化。\u003c/p\u003e\n\u003cp\u003e“看板”的使用规则第一条是“后一道工序要到前一道工序去领取产品”。\u003c/p\u003e\n\u003cp\u003e“看板”的第二条使用规则是“前一道工序只生产后一道工序所需要数量的零部件”。\u003c/p\u003e\n\u003cp\u003e实际上，如果不遵守这些规则而只引进“看板”，既发挥不了“看板”本来的作用，也不能降低成本。这种孤立使用“看板”的做法是有百害而无一利的。\u003c/p\u003e\n\u003cp\u003e企业越大越需要要具备很好的反射神经。对于计划的微小改动，要做到无需大脑发令也能采取相应的行动。就是说，如果生产管理部不发指令或者不发计划变更通知便不能改变作业，不能采取行动的话，企业就不能避免受创伤、遇大害，并且还会贻误大好时机。只有企业具备无意识适应变化的微调机能，才可以说真正装上了反射神经。我确信：通过“目视化管理”和“准时化”、“自働化”丰田生产方式这两大支柱，将会更好地锻炼这种反射神经。\u003c/p\u003e\n\u003cp\u003e第四章：丰田生产方式与福特生产方式\u003c/p\u003e\n\u003cp\u003e丰田生产方式同福特生产方式一样，基本形式是流水作业。索伦森在放置零部件的仓库上颇费一番苦心，而丰田生产方式却不需要仓库。\u003c/p\u003e\n\u003cp\u003e把同一品种和同一型号的零部件凑在一起，即把批量加大，不换冲模，尽量多次连续冲压进行大批量生产的作法，现在仍是生产现场的常识。福特生产方式大批量体系的关键就在这一点上。美国汽车企业一直证明，有计划地进行大批量生产对降低成本最有成效。\u003c/p\u003e\n\u003cp\u003e丰田生产方式与此相反，而是“尽量缩小批量，迅速变换模具”。\u003c/p\u003e\n\u003cp\u003e福特生产方式要加大批量来提高产量，所以，在各处都要有工序间的库存贮备。丰田生产方式则不同，其考虑方法是把这些库存可能导致的生产过剩的无效劳动和浪费，以及管理这些库存的人员、土地建筑的负担完全清除。\u003c/p\u003e\n\u003cp\u003e福特生产方式和丰田生产方式任何一方都有自身的优点，而且两者都在日日求新与改革，无法下结论说哪一个更优秀，但是我个人深信在低增长的时代，以丰田生产方式较为适合。\u003c/p\u003e\n\u003ch4 id=\"老翟评书\"\u003e老翟评书\u003c/h4\u003e\n\u003cp\u003e市面上有一本有名的书叫《看板方法》的书。说实话，这本书，我没看完。是我看不下去（你可以想象背后的原因），所以我不好评论。为什么要提《看板方法》这本书呢？因为我见过不少把《看板方法》里面的“看板”与丰田生产方式中的“看板”看作是一样东西，包括我自己。所以，我建议大家先看这本大野耐一的《丰田生产方式》，再看《看板方法》。大野耐一说了，他的看板是从美国自选超市得到的启示，是为了实现丰田生产方式的支柱之一：准时化。所以，我也希望大家能从《看板方法》里找出它说的“看板”的根源。\u003c/p\u003e\n\u003cp\u003e不知道有没有注意到作者在序中说的：但又不想让别家公司，特别是不想让先进国家轻易地了解它，甚至不让他们留下一个完整的概念。这点时刻提醒着我自己，有时我们看不懂别人的理论，并不是我们读者的问题。而是有可能作者有意，或者他自己根本不懂，所以无法表达清楚。\u003c/p\u003e\n\u003ch4 id=\"老翟书摘说明\"\u003e老翟书摘说明\u003c/h4\u003e\n\u003cp\u003e书摘内容完全来自原书，如果原书的作者或出版商觉得我侵权了。请通过开源中国 @翟志军  联系我。\u003c/p\u003e\n\u003cp\u003e老翟书摘旨在通过一种书摘的方式让大家花最少的时间了解一本书，从而决定要不要继续读下去。书摘的每一本书都是本人亲自读过并理解的。\u003c/p\u003e","title":"老翟书摘：《丰田生产方式》"},{"content":"耦合(coupling)的定义 耦合是对coupling的中文翻译。而coupling是couple的变形，指a connection (like a clamp or vise) between two things so they move together。我相信这就是coupling最朴实的定义。请允许我将其翻译成中文：存在一种连接在两事物之间，以至于这两事物相互影响。\n在本文中，耦合可以是一个名词——耦合度的同义词，也可以作为形容词——耦合性的同义词。\n软件行业中，耦合从何而来？ 至于中文书籍什么时候将coupling翻译成耦合，已经不那么重要了。因为耦合不是从“翻译”而来的。从coupling的定义出发，我们看出不论在哪个层次，不存在绝对不耦合的软件。\n软件与业务是需要耦合的，如果不耦合，软件就失去了意义。库存系统是需要和电子商务前端（销售）系统耦合的，否则前端就无法确定是否能供货给用户。如果我们的应用需要使用commons-lang库的StringUtils类，那么我们的应用就要commons-lang耦合的，否则我们没法使用StringUtils中的方法。在函数式编程中，我们的确可以将所有的逻辑（不论技术逻辑还是业务逻辑）封装在没有副作用的一个个函数中，但是我相信这些函数之间还是需要在某个时间点进行耦合。\n进而我认为：耦合是“天生”的。\n那是不是说我们就可以随意耦合了？显然不是，上述的耦合的例子只是表明耦合的存在性，并没有说明耦合的程度在软件开发过程所起的作用。\n所以，和复杂性[1]一样，从根本上来说，我们可以掌握这种耦合，但不能消除这种耦合。\n不同级别的耦合 软件是由不同级别的概念层组成，不同级别的概念层具有不同的职责，不同级别的概念层中存在不同的耦合：有方法级别、类级别、包级别、协议级别、语言级别、数据流级别、数据库级别、业务级别……\n方法级别：这不用多说了。\n类级别：如果你了解“组合优于继承”原则，你就应该理解什么叫类级别的耦合。这里并不是说继承不好，而面对不同的问题，你需要权衡是“组合”还是“继承”与问题模型更匹配。\n语言级别：我们的业务必须使用某各种语言来表达，无论是DSL还是通用编程语言Java、C#。\n数据流级别：数据流是指一种我们处理问题的方式：输入数据，然后数据在一条充满处理环节的链上流动，直到完全最后一个处理环节输出处理结果。这很像我们的面向切面编程。\n如果P_A要求输入的数据中的日期格式是: YYYY-MM。某天一个新手不小心把在P1中把日期格式改了，那么P_A就会出错。而现实中，我就遇到过一个数据处理流中有将近20多个处理环节，目前几乎没人敢动那块逻辑。另一个例子就是项目使用了Spring的切面编程，由于“切”得太多，到后来调试起来，我不得不遍历所有的切面的逻辑，以确定数据流到哪个环节出的错。\n数据库级别：如果你的应用使用到了数据库Oracle特有的特性，那么你的应用就是和Oracle数据库耦合的。想像一下阿里去O的过程。也可以看下这篇文章：http://tech.it168.com/a2015/0417/1720/000001720950.shtml。ORM框架的好处在这方面尤为突出。\n通信协议级别：如果通信的双方只依靠协议通信，而不关心实现这个协议的是Ruby程序还是Java程序。这也正是基于HTTP的 RESTFul风格的架构的关键所在。而业界提的微服务思路，其实就是期望各个应用之间只在协议级别耦合，从而与语言、操作系统解耦。通信协议级别上，我们也需要考虑解耦，比如，你的应用能否轻松从HTTP协议迁移到HTTPS。\n操作系统级别：文件的路径的写法在各个系统下的不同，所以Java才会有： File.separator 这个常量，在Windows环境下返回\\，而在*nix环境下返回/。如果不用此常量来拼文件路径，那么，那部分就是与操作系统耦合的。像：\nfor Windows: C:\\windows\\system32\\cmd.exe\nfor Linux: /var/log/ansible.log\n有些人觉得这不如挂齿，看看这条新闻：坚持用 XP 的代价：美国海军付给微软上千万。\nPS：使用Ruby的某些gem时就要小心，因为某些gem用到了操作系统的库。 如Nokogiri (鋸) 业务级别：业务级别上的耦合度越高，软件的风险系数越高。假如A公司的领导在职时采用代码行数来KPI程序员，代码行数越多，KPI越好。如果在为这家公司设计HR系统时，你就必须考虑如果A公司换领导了或者原领导反省了，换采用360度评估进行KPI。你的系统能否以相当小的代价实现？又比如政府的某些老系统将一代身份证的身份证号作为自然人在数据库中的ID。可是某天，二代身份证出了，身份证号变成了18位了。那么如果自然人要求更新系统中的身份证号，你怎么办？\n比较好玩的是，有时我们的程序有时会和系统用户耦合。\napache-commons-io 下的设置文件的可读/可写权限时，如果使用的是root用户执行此程序会有问题，因为root可对所有的文件可读。\nFile tmpFile = File.createTempFile(getClass().getSimpleName(), \u0026ldquo;localities.xml\u0026rdquo;);\ntmpFile.deleteOnExit();\ntmpFile.setReadable(false);\nAssert.assertFalse(tmpFile.canRead()); // It would be failed\n这个assert将会失败，因为不论该文件是否可读，以root用户执行此程序时，tmpFile始终是可读的。\n了解“不同级别的耦合”有什么用？ 在现实中，我们开发软件时需要做很多的权衡。在权衡过程中，对于不同级别的耦合度的思考为我们提供了很好的参考。例如在选择使用什么内容来做数据库人员表的ID时，如果你知道身份证ID可能会变，你绝对不会使用身份证ID作为数据库表ID。但是你可能知道它会变吗？你不知道。所以，最后答案是使用与业务、具体技术没有任何关系的UUID或者数据分配的自增int值。\n低耦合代表什么 不少书籍都告诉我们要追求高内聚（High Cohesion）及低耦合（Low Coupling）的软件？但是为什么？我见到答案无非就是：高内聚和低耦合可以给我们软件开发人员带来可读性、复用性、可维护性和易变更性。\n但是这里存在一个本质问题：为什么高内聚低耦合能给我们带来这“四性”？以及两个伴生问题：高内聚和低耦合在软件中分别达到什么比例才能达到四性、内聚要高到什么程度和耦合要低什么程度才能达到四性。要回答这个问题，我们首先必须分别给这四个“性”进行定义。\n可是什么是可读性，怎么样的代码才算可读？什么才叫得复用？怎么样才算可维护？易重构？易加需求？……诚然弄清楚这些问题比耦合这个topic更重要，但不是本文讨论的范围。\n所以，我不打算从别人的结论进行反问以得到答案。我们从另一个角度反问：高耦合给我们带来了什么？不幸的是，我们至今仍然没有权威的统计数据告诉我们高耦合给我们带来了什么。如果你发现，请告诉我。\n是的，这里引出我们软件行业中各种概念、理念、方法学的一个通病：没有数据支撑。\n但是，我们这样就算了吗？不。我们尝试从耦合原始定义出发，耦合意味两事物相互连接，并相互影响。如果这两事物之间的连接越多，则相互影响的机率就越高，我们认为这就是高耦合。否则就是低耦合。\n我们将“两事物”特化为两个软件系统如企业中的财务系统和HR系统，高耦合意味着我们对HR系统的变更，影响到财务系统的机率高。夸张点说就是，我们以为只是对HR系统进行一次小小的修改，但是要对财务系统进行不少修改。再换句话说就是两个系统不能独立演进。低耦合则反之。\n低耦合代表的是在软件不同级别的概念上只依赖它需要依赖的，从而达到它本身的修改不至于造成其它系统的非必要影响，反之亦然。\n但是又回到老问题：什么叫做“只依赖它需要依赖的”、“非必要影响”？这个问题就像你问你的男/女朋友：你有多爱我？我没有办法准确回答。但是，我会告诉你我是如何思考耦合度的，在我做决策时。也就是我会告诉你，我是怎样爱你的。你觉得那样算不算爱你，答案只有你知道。\n耦合的本质是什么？ 事实上，在软件开发过程中，我认为耦合的本质是假设。\n在设计软件过程，在业务级别上，对与其交互的业务的假设是什么？它需要使用哪些具体技术，可否将这些具体技术隔离出去，以至于我可以低成本的更换实现，也就是减少对具体技术的假设。在写代码时，方法级别上它是不是对其它方法的处理结果进行假设了？类级别的设计有什么假设？等等。\n总的来说，就是找到软件开发过程中每个环节可能的假设，并问：如果这个假设并打破了，系统会受到什么影响？\n这是一种思考方式。所以，它是普世的。\n在团队管理过程中，你可以假设某个人离职了，会产生什么影响？如果这个人很重要（关键人物），那么你就要考虑如何push他去share knowledge了。在公司运营中，假设某项业务占了公司总体的99%，那么就你要考虑什么样的可能性会将这个假设打破？因为打破这个假设，这个公司99%就跨了。如果你知道软件系统的假设，那么在测试过程，你就更知道如何有效测试了——打破这些测试，看发生什么。\n总结 耦合是天生的，不存在绝对不耦合。耦合的本质是假设，假设越多，被打破的机率就越高，所以软件的可靠性就越低。低耦合代表的是在软件不同级别的概念上只依赖它需要依赖的，从而达到它本身的修改不至于造成其它系统的非必要影响，反之亦然。\n在软件开发过程中，我们应该管理这些耦合，不论在哪个级别上。应该经常考虑如果这些假设被打破了，会给系统带来哪些风险。\n但是低耦合并不代表没有成本。ORM框架使我们与具体数据库解耦，但是在性能上，我们可能就有所失。我们必须权衡这些利弊。有没有好的办法帮助我们做权衡呢？可惜。我能看到的全靠这个做决策的人的经验。\n[1]《面向对象分析与设计》Grady Booch. P7\n部分图片来自网络，如有侵权，请联系我。\n","permalink":"https://showme.codes/zh-cn/2015-10-10-the-nature-of-coupling/","summary":"\u003ch3 id=\"耦合coupling的定义\"\u003e耦合(coupling)的定义\u003c/h3\u003e\n\u003cp\u003e耦合是对coupling的中文翻译。而coupling是couple的变形，指a connection (like a clamp or vise) between two things so they move together。我相信这就是coupling最朴实的定义。请允许我将其翻译成中文：存在一种连接在两事物之间，以至于这两事物相互影响。\u003c/p\u003e\n\u003cp\u003e在本文中，耦合可以是一个名词——\u003cstrong\u003e耦合度\u003c/strong\u003e的同义词，也可以作为形容词——耦合性的同义词。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"coupling\" loading=\"lazy\" src=\"/assets/images/23165657_OIfV.jpg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"软件行业中耦合从何而来\"\u003e软件行业中，耦合从何而来？\u003c/h3\u003e\n\u003cp\u003e至于中文书籍什么时候将coupling翻译成耦合，已经不那么重要了。因为耦合不是从“翻译”而来的。从coupling的定义出发，我们看出不论在哪个层次，不存在绝对不耦合的软件。\u003c/p\u003e\n\u003cp\u003e软件与业务是需要耦合的，如果不耦合，软件就失去了意义。库存系统是需要和电子商务前端（销售）系统耦合的，否则前端就无法确定是否能供货给用户。如果我们的应用需要使用commons-lang库的StringUtils类，那么我们的应用就要commons-lang耦合的，否则我们没法使用StringUtils中的方法。在函数式编程中，我们的确可以将所有的逻辑（不论技术逻辑还是业务逻辑）封装在没有副作用的一个个函数中，但是我相信这些函数之间还是需要在某个时间点进行耦合。\u003c/p\u003e\n\u003cp\u003e进而我认为：耦合是“天生”的。\u003c/p\u003e\n\u003cp\u003e那是不是说我们就可以随意耦合了？显然不是，上述的耦合的例子只是表明耦合的存在性，并没有说明耦合的程度在软件开发过程所起的作用。\u003c/p\u003e\n\u003cp\u003e所以，和复杂性[1]一样，从根本上来说，我们可以掌握这种耦合，但不能消除这种耦合。\u003c/p\u003e\n\u003ch3 id=\"不同级别的耦合\"\u003e不同级别的耦合\u003c/h3\u003e\n\u003cp\u003e软件是由不同级别的概念层组成，不同级别的概念层具有不同的职责，不同级别的概念层中存在不同的耦合：有方法级别、类级别、包级别、协议级别、语言级别、数据流级别、数据库级别、业务级别……\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e方法级别\u003c/strong\u003e：这不用多说了。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e类级别\u003c/strong\u003e：如果你了解“组合优于继承”原则，你就应该理解什么叫类级别的耦合。这里并不是说继承不好，而面对不同的问题，你需要权衡是“组合”还是“继承”与问题模型更匹配。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e语言级别\u003c/strong\u003e：我们的业务必须使用某各种语言来表达，无论是DSL还是通用编程语言Java、C#。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e数据流级别\u003c/strong\u003e：数据流是指一种我们处理问题的方式：输入数据，然后数据在一条充满处理环节的链上流动，直到完全最后一个处理环节输出处理结果。这很像我们的面向切面编程。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"数据流级别耦合\" loading=\"lazy\" src=\"/assets/images/165341_0n7P_181141.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如果P_A要求输入的数据中的日期格式是: YYYY-MM。某天一个新手不小心把在P1中把日期格式改了，那么P_A就会出错。而现实中，我就遇到过一个数据处理流中有将近20多个处理环节，目前几乎没人敢动那块逻辑。另一个例子就是项目使用了Spring的切面编程，由于“切”得太多，到后来调试起来，我不得不遍历所有的切面的逻辑，以确定数据流到哪个环节出的错。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e数据库级别\u003c/strong\u003e：如果你的应用使用到了数据库Oracle特有的特性，那么你的应用就是和Oracle数据库耦合的。想像一下阿里去O的过程。也可以看下这篇文章：http://tech.it168.com/a2015/0417/1720/000001720950.shtml。ORM框架的好处在这方面尤为突出。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e通信协议级别\u003c/strong\u003e：如果通信的双方只依靠协议通信，而不关心实现这个协议的是Ruby程序还是Java程序。这也正是基于HTTP的 RESTFul风格的架构的关键所在。而业界提的微服务思路，其实就是期望各个应用之间只在协议级别耦合，从而与语言、操作系统解耦。通信协议级别上，我们也需要考虑解耦，比如，你的应用能否轻松从HTTP协议迁移到HTTPS。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"通信协议级别耦合\" loading=\"lazy\" src=\"/assets/images/165244_0uji_181141.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e操作系统级别\u003c/strong\u003e：文件的路径的写法在各个系统下的不同，所以Java才会有： File.separator 这个常量，在Windows环境下返回\u003ccode\u003e\\\u003c/code\u003e，而在*nix环境下返回\u003ccode\u003e/\u003c/code\u003e。如果不用此常量来拼文件路径，那么，那部分就是与操作系统耦合的。像：\u003c/p\u003e\n\u003cp\u003e     for Windows: C:\\windows\\system32\\cmd.exe\u003c/p\u003e\n\u003cp\u003e     for Linux: /var/log/ansible.log\u003c/p\u003e\n\u003cp\u003e有些人觉得这不如挂齿，看看这条新闻：\u003ca href=\"https://www.oschina.net/news/63614/navy-re-ups-with-microsoft-for-more-windows-xp-support\"\u003e坚持用 XP 的代价：美国海军付给微软上千万\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"操作系统级别耦合\" loading=\"lazy\" src=\"/assets/images/165314_EFNZ_181141.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003ePS：使用Ruby的某些gem时就要小心，因为某些gem用到了操作系统的库。 如Nokogiri (鋸) \u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e业务级别\u003c/strong\u003e：业务级别上的耦合度越高，软件的风险系数越高。假如A公司的领导在职时采用代码行数来KPI程序员，代码行数越多，KPI越好。如果在为这家公司设计HR系统时，你就必须考虑如果A公司换领导了或者原领导反省了，换采用360度评估进行KPI。你的系统能否以相当小的代价实现？又比如政府的某些老系统将一代身份证的身份证号作为自然人在数据库中的ID。可是某天，二代身份证出了，身份证号变成了18位了。那么如果自然人要求更新系统中的身份证号，你怎么办？\u003c/p\u003e\n\u003cp\u003e比较好玩的是，有时我们的程序有时会和系统用户耦合。\u003c/p\u003e\n\u003cp\u003eapache-commons-io 下的设置文件的可读/可写权限时，如果使用的是root用户执行此程序会有问题，因为root可对所有的文件可读。\u003c/p\u003e\n\u003cp\u003e        File tmpFile = File.createTempFile(getClass().getSimpleName(), \u0026ldquo;localities.xml\u0026rdquo;);\u003c/p\u003e\n\u003cp\u003e        tmpFile.deleteOnExit();\u003c/p\u003e\n\u003cp\u003e        tmpFile.setReadable(false);\u003c/p\u003e\n\u003cp\u003e        Assert.assertFalse(tmpFile.canRead()); // It would be failed\u003c/p\u003e\n\u003cp\u003e这个assert将会失败，因为不论该文件是否可读，以root用户执行此程序时，tmpFile始终是可读的。\u003c/p\u003e","title":"耦合的本质"},{"content":"English Version 5 years of Java backend development experience, applying Domain-Driven Design (DDD) in real-world work; followed by 5 years of hands-on DevOps practice. Led a 10-person team from zero to one in adopting and coaching Agile practices — daily stand-ups, unit testing, Kanban, and retrospectives. Led a 10-person team from zero to one in building software stability and reliability. Strong writing skills: authored \u0026ldquo;Jenkins 2.x in Practice\u0026rdquo; (published 2018, 5000+ copies sold). 中文版本 有5年Java开发经验，并能在工作中应用DDD。拥有5年DevOps实践经验； 能带领10人团队从零到一落地并培训敏捷实践，包括：站会、单元测试、看板、回顾会议等； 能带领10人团队从零到一进行软件稳定性建设； 良好的写作能力：18年出版《Jenkinx2.x实践指南》。 ","permalink":"https://showme.codes/about-me/","summary":"\u003ch2 id=\"english-version\"\u003eEnglish Version\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e5 years of Java backend development experience, applying Domain-Driven Design (DDD) in real-world work; followed by 5 years of hands-on DevOps practice.\u003c/li\u003e\n\u003cli\u003eLed a 10-person team from zero to one in adopting and coaching Agile practices — daily stand-ups, unit testing, Kanban, and retrospectives.\u003c/li\u003e\n\u003cli\u003eLed a 10-person team from zero to one in building software stability and reliability.\u003c/li\u003e\n\u003cli\u003eStrong writing skills: authored \u003ca href=\"https://item.jd.com/46026668231.html\"\u003e\u0026ldquo;Jenkins 2.x in Practice\u0026rdquo;\u003c/a\u003e (published 2018, 5000+ copies sold).\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"中文版本\"\u003e中文版本\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e有5年Java开发经验，并能在工作中应用DDD。拥有5年DevOps实践经验；\u003c/li\u003e\n\u003cli\u003e能带领10人团队从零到一落地并培训敏捷实践，包括：站会、单元测试、看板、回顾会议等；\u003c/li\u003e\n\u003cli\u003e能带领10人团队从零到一进行软件稳定性建设；\u003c/li\u003e\n\u003cli\u003e良好的写作能力：18年出版《Jenkinx2.x实践指南》。\u003c/li\u003e\n\u003c/ul\u003e","title":""}]