<?xml version="1.0" encoding="utf-8"?>
<search>
  <entry>
    <title>ClawdBot（OpenClaw）试用记录</title>
    <url>/dev-clawdbot-start/</url>
    <content><![CDATA[1. ClawdBot炸圈了！最近，GitHub上一个名为ClawdBot（先更名为Moltbot，又更名为OpenClaw）的开源项目在AI圈刷屏！
这个项目的核心理念很简单：ClawdBot是一个运行在本地的AI智能体，可以操控本地电脑，包括但不限于文件、终端、浏览器等，然后我们通过常用的聊天软件让它干活。
不简单的是这件事做成了，并且效果非常好！还能这么玩？Aha ？！还能这么玩！！！
今天有些时间，必须试用一下！
相关文档：

github - openclaw/openclaw
OpenClaw官网
Introducing OpenClaw
OpenClaw官方文档
DeepWiki OpenClaw
《转发Discord消息到飞书》
Discord Developer Platform
Clawdbot 的真正创新是网关：一篇了解架构、权限和风险
深度拆解 Clawdbot（OpenClaw）架构与实现



2. ClawdBot架构工作原理：
WhatsApp / Telegram / Discord / iMessage (+ plugins)        │        ▼  ┌───────────────────────────┐  │          Gateway          │  ws://127.0.0.1:18789 (loopback-only)  │     (single source)       │  │                           │  http://&lt;gateway-host&gt;:18793  │                           │    /__openclaw__/canvas/ (Canvas host)  └───────────┬───────────────┘              │              ├─ Pi agent (RPC)              ├─ CLI (openclaw …)              ├─ Chat UI (SwiftUI)              ├─ macOS app (OpenClaw.app)              ├─ iOS node via Gateway WS + pairing              └─ Android node via Gateway WS + pairing

架构图（摘自架构师公众号）：
3. 基于云平台使用ClawdBot“ClawdBot带动了Mac Mini价格大涨！” 看到这条消息，可能让人误以为ClawdBot最适合运行在Mac Mini上，实际上需要综合考虑。从数据隐私和成本方面考虑，本地设备跑ClawdBot很适合，Mac Mini和普通的废旧笔记本都很好。从简单和稳定方面考虑，云主机跑ClawdBot更合适，目前各大云厂商都推出了ClawdBot的轻量应用云主机或者云镜像。
本文中，我们选择使用云主机跑ClawdBot。各大厂商都有，自行检索教程购买即可，这里不再赘述购买流程。
4. ClawdBot初始化
登录到clawdbot主机
ssh root@your-host-ip
执行clawdbot初始化
clawdbot --versionclawdbot onboard --install-daemon

接下来的配置就需要一些大模型基础知识了，下面给出我的配置供参考。
风险确认。
选择调用的大模型，并配置API Key。按需配置，个人选择了Openrouter。
选择默认的模型：
选择用于和clawdbot交流的应用（Telegram、Discord、Slack等）。按需配置，个人选择了Discord，比较熟悉。
选择Discord后，界面上会出现Discord的配置说明，按照说明操作配置Discord Bot。访问Discord Developer Portal，具体操作可以参考下面的图。
给clawdbot安装一些能力工具。
按需配置一些大模型API Key，可以不配置。
自动安装Gateway service。
自动安装Control UI。
选择 hatch your bot 的方式为推荐的TUI。
clawdbot个性化设定，给它取一个名字。
根据交互，提供更多信息，比如身份设定是一个discord助手。不想设置更多信息，就让clawdbot自由发挥。
至此，clawdbot就配置好了。
5. ClawdBot初始化日志记录ClawdBot初始化日志记录：init.log
6. 测试ClawdBot在discord中给clawdbot发送几条消息，看看效果。
如上图，clawdbot可以调用大模型回答问题，也可以操作所在的主机，比如获取磁盘用量信息。
以上，clawdbot配置测试完成，接下来就是自行摸索更有趣玩法的时间了！
7. 后记ClawdBot的设计思路简单，但是成熟度做到这么高确实niubility！
ClawdBot、Claude Cowork、QoderWork等项目和产品的出现，标志着AI正从“回答问题”走向“代替执行”，从“偶尔使用”走向“持续在线”，从“应用”走向“系统”。
个人计算设备可能会越来越像一台随时可被消息唤醒的“家庭服务器”，而我们和它的交互，可能是聊天窗口，也可能是自然语言对话，就像老板给下属交代任务一样。一个有趣的比喻是，牛马也有了自己的牛马。
我们正在经历这个时代最大的变革之一，何其有幸，未来已来。
]]></content>
      <categories>
        <category>engineering</category>
        <category>aigc</category>
        <category>agent</category>
      </categories>
      <tags>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>discord</tag>
        <tag>agent</tag>
      </tags>
  </entry>
  <entry>
    <title>编程辅助工具 Codex 入门篇</title>
    <url>/dev-codex-start/</url>
    <content><![CDATA[1. Codex 简介Codex 是一个辅助编程的命令行工具，借助GPT系列模型提高编码的速度和质量。Codex的安装配置方法和Claude Code很像（几乎一模一样），本文会对Codex的基础配置和用法进行说明，方便大家入手使用。
相关文档：

Codex官网
github - openai/codex



2. 安装Codex2.1. 安装node.js 18+
安装nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
安装node.js 20
nvm install v20.19.4nvm use v20.19.4nvm alias default v20.19.4

2.2. 安装Codexnpm install -g @openai/codex


2.3. 初始化Codexcodex

出现提示：
  Welcome to Codex, OpenAI&#x27;s command-line coding agent  Sign in with ChatGPT to use Codex as part of your paid plan  or connect an API key for usage-based billing&gt; 1. Sign in with ChatGPT     Usage included with Plus, Pro, and Team plans  2. Provide your own API key     Pay for what you use  Press Enter to continue

这里不用再继续配置，直接 ctrl+C 退出。
3. 安装配置OpenAI API代理Codex依赖OpenAI API，但是因为国内无法访问OpenAI，所以我们需要一些访问OpenAI API的替代方案。推荐使用NewAPI，它是OneAPI停止更新后的更强继承者。
4. 配置Codex
创建codex配置文件vim ~/.codex/config.toml

假设使用的Claude API代理是NewAPI，那么填入如下内容：
model = &quot;gpt-5&quot;model_provider = &quot;new-api&quot;model_reasoning_effort = &quot;medium&quot;[model_providers.new-api]name = &quot;New Api OpenAI&quot;base_url = &quot;https://newapi.voidking.com/v1&quot;env_key = &quot;NEW_OPENAI_API_KEY&quot;wire_api = &quot;chat&quot;

特别注意的是不能将 api_key 作为字符串直接传递给 env_key。 env_key 必须指向环境变量。

配置key环境变量echo &#x27;export NEW_OPENAI_API_KEY=&quot;xxx&quot;&#x27; &gt;&gt; ~/.zshrcsource ~/.zshrc#echo &#x27;export NEW_OPENAI_API_KEY=&quot;xxx&quot;&#x27; &gt;&gt; ~/.bash_profile#source ~/.bash_profile

5. 命令行使用Codexcd your-projectcodex

6. VSCode/Cursor使用Codex
安装插件 Codex – OpenAI’s coding agent插件安装完成，VSCode右上角会出现OpenAI图标

点击VSCode右上角会OpenAI图标，启动Codex首次启动会比较慢（半分钟左右），请耐心等待。

输入需求，辅助编程


7. Codex基础用法参考文档：

Codex CLI 快速入门指南
Codex &amp; Codex CLI 国内使用教程，手把手教你如何支付、安装 Codex！
再见 Claude Code！玩转 CodeX CLI 的 16 个实用小技巧，效率拉满！！

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>golang</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>golang</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>vscode</tag>
        <tag>编辑器</tag>
        <tag>git</tag>
        <tag>java</tag>
      </tags>
  </entry>
  <entry>
    <title>编程辅助工具 Claude Code 入门篇</title>
    <url>/dev-claude-code-start/</url>
    <content><![CDATA[1. Claude Code 简介Claude Code 是一个辅助编程的命令行工具，借助Claude模型提高编码的速度和质量。当前 Claude Code 风评远高于 Cursor。本文会对Claude Code的基础配置和用法进行说明，方便大家入手使用。
相关文档：

Claude Code官网
Claude Code文档
用了 ClaudeCode 开发后，我再也不想碰 Cursor 了



2. 安装Claude Code2.1. 安装node.js 18+
安装nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
安装node.js 20
nvm install v20.19.4nvm use v20.19.4nvm alias default v20.19.4

2.2. 安装Claude Codenpm install -g @anthropic-ai/claude-code


2.3. 初始化Claude Codecd your-projectclaude
这里不用再继续配置，直接 ctrl+C 退出。
3. 安装配置Claude API代理Claude Code依赖Claude API，但是因为国内无法访问 Claude，所以我们需要一些访问Claude API的替代方案。

方案一：OneAPI + litellm
方案二：NewAPI
方案三：claude-relay-service

4. 配置Claude Code创建claude code配置文件
vim ~/.claude/settings.json

假设使用的Claude API代理是NewAPI，那么填入如下内容：
&#123;  &quot;env&quot;: &#123;    &quot;ANTHROPIC_BASE_URL&quot;: &quot;https://newapi.voidking.com&quot;,    &quot;ANTHROPIC_AUTH_TOKEN&quot;: &quot;sk-xxx&quot;,    &quot;ANTHROPIC_MODEL&quot;: &quot;claude-sonnet-4-20250514&quot;  &#125;&#125;

5. 命令行使用Claude Codecd your-projectclaude

6. VSCode/Cursor使用Claude Code
安装插件 Claude Code for VSCode插件安装完成，VSCode右上角会出现claude图标

点击VSCode右上角会claude图标，启动claude code

输入需求，辅助编程


7. Claude Code基础用法参考文档：

Claude Code完全指南：使用教程详解，看这一篇就够了！
Claude Code 实用教程——使用方法详解
最强Coding Agent: Claude Code权威实践指南
Claude Code 用法全面拆解！26 项核心功能 + 实战技巧

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>golang</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>golang</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>vscode</tag>
        <tag>编辑器</tag>
        <tag>git</tag>
        <tag>java</tag>
      </tags>
  </entry>
  <entry>
    <title>Claude API 中转服务 Claude Relay Service</title>
    <url>/dev-claude-relay/</url>
    <content><![CDATA[1. Claude Relay Service简介Claude Relay Service 是一个 Claude API中转服务，支持多账户管理。试用场景：

找朋友拼车: 三五好友一起分摊Claude Code Max订阅，Opus爽用
隐私敏感: 不想让第三方镜像看到你的对话内容
技术折腾: 有基本的技术基础，愿意自己搭建和维护
稳定需求: 需要长期稳定的Claude访问，不想受制于镜像站
地区受限: 无法直接访问Claude官方服务

相关文档：

github - Wei-Shaw/claude-relay-service



2. 部署Claude Relay ServiceClaude Relay Service 提供了多种部署方式，包括脚本部署、手动部署、Docker部署等，本文中选择Docker部署方式。
前置条件：安装配置好Docker和Docker Compose。
1、准备项目目录
mkdir -p /data/claude-relaycd /data/claude-relay

2、创建 docker-compose 配置文件 docker-compose.yml
version: &#x27;3.8&#x27;services:  claude-relay:    image: weishaw/claude-relay-service:latest    container_name: claude-relay-service    restart: unless-stopped    ports:      - &quot;3000:3000&quot;    environment:      - JWT_SECRET=$&#123;JWT_SECRET&#125;      - ENCRYPTION_KEY=$&#123;ENCRYPTION_KEY&#125;      - REDIS_HOST=redis      - ADMIN_USERNAME=$&#123;ADMIN_USERNAME:-&#125;      - ADMIN_PASSWORD=$&#123;ADMIN_PASSWORD:-&#125;    volumes:      - ./logs:/app/logs      - ./data:/app/data    depends_on:      - redis  redis:    image: redis:7-alpine    container_name: claude-relay-redis    restart: unless-stopped    volumes:      - redis_data:/datavolumes:  redis_data:

3、创建配置文件 .env内容如下：
# 必填：安全密钥（请修改为随机值）JWT_SECRET=your-random-secret-key-at-least-32-charsENCRYPTION_KEY=your-32-character-encryption-key# 可选：管理员凭据ADMIN_USERNAME=cr_adminADMIN_PASSWORD=your-secure-password

4、拉取镜像
docker-compose pull

5、启动claude-relay
docker-compose up -d

6、查看claude-relay状态
docker-compose psdocker logs -f claude-relay-service

3. 配置域名（可选）1、配置DNS域名解析
2、配置Nginx
server &#123;    listen 80;    server_name cc.voidking.com;    charset utf-8;    location / &#123;        client_body_timeout 3600s;        keepalive_timeout 3600s;        client_max_body_size  64m;        proxy_http_version 1.1;        proxy_set_header Host $host;        proxy_set_header X-Forwarded-For $remote_addr;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_cache_bypass $http_upgrade;        proxy_set_header Accept-Encoding gzip;        proxy_read_timeout 3600s;          proxy_pass http://127.0.0.1:3000;    &#125;&#125;

4. 购买claude code账号1、访问claude codehttps://www.anthropic.com/claude-code
2、采购max 200刀的套餐
3、获取到key
5. 配置Claude Relay Service5.1. 登录Claude Relay Service浏览器访问 claude relay service，http://你的服务器IP:3000/web点击页面右上角“管理后台”，输入安装时配置的管理员账密登录。
5.2. 配置Claude账户1、点击账户管理，添加账户
2、账户配置

平台选择 Claude
添加方式选择 Setup Token
账户名称 随意
其他配置项保持默认

3、配置授权（1）下一步，进入授权配置页面（2）点击 生成授权链接，会打开一个新页面，在新页面完成Claude登录和授权（3）复制返回的Authorization Code，粘贴到页面完成添加
5.3. 创建API Key给每个使用者分配一个Key：
1、点击 API Keys
2、点击 创建新Key
3、给Key起个名字，比如「张三的Key」
4、设置使用限制（可选）

速率限制: 限制每个时间窗口的请求次数和Token使用量
并发限制: 限制同时处理的请求数
模型限制: 限制可访问的模型列表
客户端限制: 限制只允许特定客户端使用（如ClaudeCode、Gemini-CLI等）

5、保存，记下生成的Key
6. Claude Code 使用 Claude Relay ServiceClaude Code 设置环境变量：
export ANTHROPIC_BASE_URL=&quot;http://127.0.0.1:3000/api/&quot; # 根据实际填写你服务器的ip地址或者域名export ANTHROPIC_AUTH_TOKEN=&quot;后台创建的API密钥&quot;

或者修改 Claude Code 配置文件：
vim ~/.claude/settings.json

填入如下内容：
&#123;  &quot;env&quot;: &#123;    &quot;ANTHROPIC_BASE_URL&quot;: &quot;http://127.0.0.1:3000/api/&quot;,    &quot;ANTHROPIC_AUTH_TOKEN&quot;: &quot;后台创建的API密钥&quot;,    &quot;ANTHROPIC_MODEL&quot;: &quot;claude-sonnet-4-20250514&quot;  &#125;&#125;

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>newapi</tag>
      </tags>
  </entry>
  <entry>
    <title>LLM 接口管理和分发系统 New API</title>
    <url>/dev-newapi-start/</url>
    <content><![CDATA[1. New API简介New API是一个LLM代理系统，支持LLM接口管理和消息分发。使得用户可以通过标准的 OpenAI API 格式访问所有的大模型，开箱即用。
New API是在One API基础上二次开发的项目，因为 One API 已经不再维护，所以更推荐使用 New API。
相关文档：

github - QuantumNous/new-api



2. New API核心功能One API具备的功能，New API都有，并且比One API支持更多大模型、界面更友好。
New API核心功能如下：

支持多种大模型：OpenAI、Claude、Gemini、Mistral、豆包、文心一言、通义千问、腾讯混元、SiliconCloud等等等等。
支持 stream 模式，可以通过流式传输实现打字机效果
支持绘图接口
支持失败自动重试
支持多机部署，提高并发
支持渠道管理，批量创建渠道，支持渠道设置模型列表，支持通过负载均衡访问多个渠道
支持多用户管理，支持用户额度配置，支持充值，支持用户分组以及渠道分组，支持为不同分组设置不同的倍率
支持令牌管理，设置令牌的过期时间、额度、允许的 IP 范围以及允许的模型访问

3. 安装配置 New API3.1. 安装配置 New API 概述New API 提供了多种安装部署方式，推荐Docker部署或者Docker Compose部署。本文中选择使用Docker部署，数据库选择MySQL，不使用Redis。
3.2. 安装MySQL（可选）1、安装MySQL8，参考文档好好学MySQL：使用Docker安装配置MySQL
2、创建一个数据库，名称为 newapi
3.3. 安装 New API1、下载 new-api 镜像
docker pull calciumion/new-api:v0.9.0-alpha.16

2、运行 new-api 容器使用SQLite数据库：
docker run --name new-api -d \  --restart always \  -p 3000:3000 \  -e TZ=Asia/Shanghai \  -v /opt/data/new-api:/data \  calciumion/new-api:v0.9.0-alpha.16

使用MySQL数据库：
docker run --name new-api -d \  --restart always \  -p 3000:3000 \  -e SQL_DSN=&quot;root:123456@tcp(localhost:3306)/newapi&quot; \  -e TZ=Asia/Shanghai \  -v /opt/data/new-api:/data \  calciumion/calciumion/new-api:v0.9.0-alpha.16

其中，SQL_DSN的值要改成自己的MySQL配置。
3、查看容器状态
docker ps | grep new-apidocker logs new-api

3.4. 配置Nginx参考配置：
server&#123;  server_name newapi.voidking.com;  # 请根据实际情况修改你的域名  location / &#123;    client_max_body_size  64m;    proxy_http_version 1.1;    proxy_pass http://localhost:3000;  # 请根据实际情况修改你的端口    proxy_set_header Host $host;    proxy_set_header X-Forwarded-For $remote_addr;    proxy_cache_bypass $http_upgrade;    proxy_set_header Accept-Encoding gzip;    proxy_read_timeout 300s;  # GPT-4 需要较长的超时时间，请自行调整  &#125;&#125;

4. 使用 New API4.1. 登录 New API使用 IP+端口 或者 域名 访问 New API 首页
首次访问时，会自动引导到初始化页面。按照页面指引手动设置管理员账号和密码（仅首次安装需要），完成后即可使用新设置的管理员账号登录系统。
4.2. 配置与使用因为New API基于One API二次开发，核心功能基本相同，所以配置和使用方法可以参考《LLM 接口管理和分发系统 One API》。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>newapi</tag>
      </tags>
  </entry>
  <entry>
    <title>编程辅助工具Cursor入门篇</title>
    <url>/dev-cursor-start/</url>
    <content><![CDATA[1. Cursor简介Cursor 是一款AI代码编辑器（IDE），目标是借助AI提高编码的速度和质量。
相关文档：

Cursor官网
Cursor官方文档
Migration from VS Code
《VSCode常用快捷键和配置》



2. Cursor基础知识2.1. Cursor与VSCodeCursor 是基于 VSCode 构建的编辑器，包含 VSCode 的全部功能。对于习惯使用 VSCode 的开发者，从 VSCode 迁移到 Cursor，几乎没有什么成本。
VSCode的活动栏默认是垂直的，Cursor 的活动栏是水平的，这是为了使AI聊天界面空间更大。如果想要和VSCode一样使用垂直的活动栏，那么可以在 settings.json 中添加配置：
&#123;  &quot;workbench.activityBar.orientation&quot;: &quot;vertical&quot;&#125;

2.2. Cursor核心功能Cursor的核心功能包括：

代码补全：理解代码库并提供基于上下文的代码补全建议
部分代码编辑：选中部分代码，让大模型按照需求在文件级别小范围增删改代码
聊天编程：在统一的AI对话界面，通过聊天让大模型在仓库级别增删改代码

3. 聊天模式AI对话界面提供了三种聊天模式：Agent、Ask和Manual

Agent：允许Cursor自主学习代码库，并代表用户进行代码库范围的更改
Ask：获取有关代码库的解释和答案，并使用AI规划功能
Manual：仅使用用户提供的上下文进行有针对性的编辑

4. 上下文说明AI对话界面可以自行选择上下文：

@Files：引用整个文件作为上下文。支持文件路径预览和分块处理。适用于需要引用特定文件内容时，如代码文件、文档等。
@Folders：引用整个文件夹作为上下文。适用于需要提供大量文件作为上下文，例如项目目录。
@Code：引用特定代码片段作为上下文。适用于需要针对特定代码片段进行查询或操作。
@Docs：引用预设的第三方文档或自定义文档。支持添加自定义文档。适用于需要引用外部文档或自定义知识库。
@Git：扫描 Git 提交、差异或拉取请求，帮助查找问题。适用于需要分析 Git 相关信息，如提交记录、代码差异等。
@Past Chats：将历史聊天消息添加为上下文。适用于在聊天中需要引用之前的对话内容。
@Cursor Rules：引用项目规则作为上下文。规则文件存储在 .cursor/rules 目录下，支持 MDC 格式，可以包含元数据和内容。适用于需要为项目提供持久化指导、编码规范或工作流程时。
@Terminals：引用终端输出作为上下文。适用于需要分析命令执行结果、日志输出或调试信息时。
@Web：搜索网络信息作为附加上下文。适用于需要获取最新网络信息或外部资源。
@Recent Changes：引用最近的文件变更作为上下文。适用于需要了解最近的代码修改、文件更新或项目变动时。

5. 安装Cursor
访问 cursor.com 并点击“下载”按钮

运行安装程序并等待安装完成

通过桌面快捷方式或从应用程序菜单启动 Cursor


6. 登录Cursor
首次打开Cursor，会提示登录Cursor账号

根据提示，注册一个Cursor账号并登录


7. 使用CursorCursor的核心功能可以通过快捷键调用：

代码补全：按下 Tab ，使用智能代码补全
代码编辑：按下 CMD+K，使用内联代码编辑
聊天编程：按下 CMD+L，打开AI对话界面

7.1. 代码补全代码补全基础用法：

鼠标放在代码中想要代码补全的位置
输入或者不输入字符，等待Cursor提示
当出现满意的提示时，按下Tab

代码补全TODO引导：

在代码中使用注释描述清楚TODO
鼠标放在注释的下方，等待Cursor提示
当出现满意的提示时，按下Tab

7.2. 部分代码编辑部分代码编辑：

鼠标光标选中一部分代码
按下CMD+K 打开 Prompt Bars（提示词弹窗），输入代码需求

命令行编辑：

鼠标光标位于Terminal
按下CMD+K 打开 Prompt Bars（提示词弹窗），输入命令行需求

7.3. 聊天编程聊天编程创建新文件：

新建一个空的代码文件
按下 CMD+L 打开AI对话界面
对话界面中，输入需求
接受或者拒绝代码修改

需求示例：请编写一段python代码，连接mysql并执行 select id,username from users
聊天编程修改老文件：

打开一个代码文件
按下 CMD+L 打开AI对话界面
对话界面中，选择本次修改涉及到的所有文件
对话界面中，输入需求
接受或者拒绝修改代码

需求示例：请改为使用pymysql
8. Cursor安全注意事项
在使用Cursor开发的代码仓库中，不要使用明文账密，避免账密被携带提交给大模型
使用npm软件包时注意安全，比如 sw-cur 、sw-cur1 和 aiide-cur 这三个包都是被标记的恶意软件。参考文档3200+ Cursor 用户被恶意“劫持”！贪图“便宜API”却惨遭收割， AI 开发者们要小心了

9. Cursor远程连接Linux主机Cursor连接Linux主机，与VSCode方法相同。如果报错：无法与 “xxx” 建立连接: 未能下载VSCode服务器(Server returned 404).解决办法：安装使用 1.0.0 版本的Anysphere Remote SSH插件。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>golang</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>golang</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>vscode</tag>
        <tag>编辑器</tag>
        <tag>git</tag>
        <tag>java</tag>
      </tags>
  </entry>
  <entry>
    <title>FastGPT入门篇</title>
    <url>/dev-fastgpt-start/</url>
    <content><![CDATA[1. FastGPT简介FastGPT 是一个基于 LLM 大语言模型的知识库问答系统，提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排，从而实现复杂的问答场景！
本文中，我们学习FastGPT的部署和基础用法。
参考文档：

github - labring/FastGPT
快速了解 FastGPT
Docker Compose 快速部署
FastGPT 在线使用



2. FastGPT部署架构图
组件说明：

MongoDB：用于存储除了向量外的各类数据
PostgreSQL/Milvus：存储向量数据
OneAPI: 聚合各类 AI API，支持多模型调用 （任何模型问题，先自行通过 OneAPI 测试校验）

3. FastGPT工作流程3.1. 准备知识库1、准备知识内容（比如一段文本）
2、知识内容通过 embedding model （索引模型）进行向量化
3、知识内容和向量，都存入数据库
3.2. 与大模型对话1、用户提出一个问题
2、通过 embedding model 查询向量数据库，匹配到对应的知识内容
3、用户的问题，加上知识内容，一起发送给文本理解模型
4、文本理解模型解析用户问题和知识内容，给出结果
参考文档：

【一文读懂】RAG的重要组成-Embedding Model
什么是机器学习中的嵌入？

4. 准备Docker环境# 安装 Dockercurl -fsSL https://get.docker.com | bash -s docker --mirror Aliyunsystemctl enable --now docker# 安装 docker-composecurl -L https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-composechmod +x /usr/local/bin/docker-compose# 验证安装docker -vdocker-compose -v

5. 部署FastGPT本节中，我们部署FastGPT，选择 PostgreSQL 作为向量存储数据库。
1、准备配置文件
mkdir -p /opt/fastgpt &amp;&amp; cd /opt/fastgptcurl -O https://raw.githubusercontent.com/labring/FastGPT/main/projects/app/data/config.jsoncurl -o docker-compose.yml https://raw.githubusercontent.com/labring/FastGPT/main/files/docker/docker-compose-pgvector.yml

2、修改配置
vim docker-compose.yml

修改 FE_DOMAIN 为真实的前端地址，例如 http://192.168.0.1:3000
3、启动容器
# 启动容器docker-compose up -d# 等待10s，OneAPI第一次总是要重启几次才能连上Mysqlsleep 10# 重启一次oneapi(由于OneAPI的默认Key有点问题，不重启的话会提示找不到渠道，临时手动重启一次解决，等待作者修复)docker restart oneapi

PS：重启容器
docker-compose downdocker-compose up -d

6. 使用FastGPT6.1. 访问OneAPI可以通过 ip:3001 访问OneAPI，默认账号为 root ，密码为 123456 。
在OneApi中添加合适的AI模型渠道。点击查看相关教程
注意：这里必须要添加至少一个索引模型和文本理解模型。
索引模型：可以将自然语言转成向量，用于进行语义检索。文本理解模型：用于增强索引和QA生成。
6.2. 访问FastGPT可以通过 ip:3000 访问FastGPT，默认账号为 root，密码为 1234（docker-compose.yml 环境变量里设置的 DEFAULT_ROOT_PSW）。
6.3. 创建知识库参考文档：FastGPT - 快速上手
1、点击知识库，新建，通用知识库

填入知识库名称
选择索引模型，不同索引模型无法一起使用，选择完索引模型后将无法修改
选择文本理解模型

2、点击新创建的知识库，新建/导入，手动数据集
3、点击插入，手动输入一段知识内容
上传内容这里有四种模式：

手动输入：手动输入问答对，是最精准的数据
QA 拆分：选择文本文件，让AI自动生成问答对
直接分段：选择文本文件，直接将其按分段进行处理
CSV 导入：批量导入问答对

6.4. 创建应用1、点击工作台，新建，简易应用

填入应用名称
模板选择 知识库+对话引导

2、关联知识库，选择我们自己创建的知识库
3、API配置

选择AI模型
修改提示词，例如：你是一个知识库助手，请严格根据知识库的内容回答问题。
修改开场白对话

4、测试应用在调试预览对话框中，输入问题
7. 后记FastGPT文档站，不止提供了FastGPT的安装配置方法，还提供了访问国外大模型API的方案，感兴趣的同学可以参考代理方案和使用 One API 接入 Azure、ChatGLM 和本地模型。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>aigc</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git Submodule详解</title>
    <url>/dev-git-submodule/</url>
    <content><![CDATA[1. 什么是Git Submodule？Git Submodule（子模块）是Git版本控制系统中的一个功能，它允许我们将一个Git仓库作为另一个Git仓库的子目录。这在我们需要将一个项目作为另一个项目的依赖项，同时又希望保持它们作为独立项目时非常有用。
子模块的主要特点包括：

保持子项目的独立版本控制
允许主项目和子项目独立开发
可以跟踪子项目的特定提交，而不是分支



2. 使用场景Git Submodule通常适用于以下场景：

项目依赖第三方库，且需要对该库进行定制修改
大型项目被拆分为多个独立部分，由不同团队维护
需要在多个项目中共享通用组件或库
需要精确控制依赖项的版本

3. 基本操作3.1. 添加子模块git submodule add &lt;repository_url&gt; &lt;path&gt;

例如：
git submodule add https://github.com/example/lib.git libs/external

这会在当前仓库中添加一个名为libs/external的子模块，指向指定的仓库。
3.2. 克隆包含子模块的项目当克隆一个包含子模块的项目时，子模块的目录会是空的。我们需要执行以下命令来初始化和更新子模块：
git clone &lt;repository_url&gt;git submodule initgit submodule update

或者使用组合命令：
git clone --recurse-submodules &lt;repository_url&gt;

3.3. 更新子模块要更新子模块到其远程仓库的最新提交：
git submodule update --remote

3.4. 提交子模块变更如果我们在子模块中做了修改并提交，需要在父仓库中记录这些变更：
cd submodule_directorygit commit -am &quot;Update submodule&quot;cd ..git add submodule_directorygit commit -m &quot;Update submodule reference&quot;

4. 高级用法4.1. 遍历所有子模块git submodule foreach &#x27;&lt;command&gt;&#x27;

例如：
git submodule foreach &#x27;git checkout main&#x27;

4.2. 查看子模块状态git submodule status

4.3. 删除子模块删除子模块需要几个步骤：

删除子模块目录：
git rm --cached &lt;submodule_path&gt;rm -rf &lt;submodule_path&gt;
删除.gitmodules文件中的相关部分

删除.git/config中的相关配置

提交这些变更


5. 注意事项
权限问题：确保我们对子模块仓库有适当的访问权限
递归子模块：一些项目可能有嵌套的子模块，使用--recursive选项可以处理这种情况
分支管理：子模块默认不跟踪分支，而是指向特定提交。要跟踪分支，需要在子模块目录中显式检出分支
协作问题：团队成员需要知道如何初始化和更新子模块
路径问题：移动包含子模块的项目时要小心，可能需要更新相关路径

6. 替代方案虽然子模块很有用，但在某些情况下，我们可能需要考虑其他方案：

Git Subtree：将外部项目合并到主项目中，作为普通目录
包管理器：如npm、pip等语言特定的依赖管理工具
Monorepo：将所有相关项目放在一个大的仓库中

7. 最佳实践
为子模块使用明确的版本（提交哈希）
在文档中记录子模块的使用方法
定期更新子模块以获取安全修复和新功能
考虑使用.gitmodules文件来配置子模块的默认分支
在CI/CD流程中加入子模块初始化步骤

8. 总结Git Submodule是一个强大的工具，可以帮助我们管理项目依赖关系，同时保持各个组件的独立性，可以极大地提高多项目协作的效率。
]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Golang问题记录</title>
    <url>/dev-golang-problem/</url>
    <content><![CDATA[1. 前言本文记录Golang开发时遇到的问题，备忘。
相关文档：

Golang文档汇总
《好好学Golang：Golang入门篇》
《好好学Golang：VSCode配置Golang Debug》



2. 编译报错问题2.1. 问题描述macos运行命令 go run main.go，报错：
# internal/godebugscompile: version &quot;go1.23.8&quot; does not match go tool version &quot;go1.24.2&quot;# internal/byteordercompile: version &quot;go1.23.8&quot; does not match go tool version &quot;go1.24.2&quot;# internal/gooscompile: version &quot;go1.23.8&quot; does not match go tool version &quot;go1.24.2&quot;

2.2. 解决办法参考文档：compile: version goX does not match go tool version goY
可能原因：大概率是因为brew安装软件时，软件依赖包含golang，部分覆盖了原本的glang版本。解决办法：卸载brew安装的golang，重新安装原本的golang版本。

卸载brew安装的golangbrew uninstall golanggo versino

执行完步骤1，如果已经恢复，就不再需要步骤2。

下载golang软件包，并重新安装golang下载链接：All releases

]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>vscode</tag>
      </tags>
  </entry>
  <entry>
    <title>One API配置自定义渠道</title>
    <url>/dev-one-api-custom-channel/</url>
    <content><![CDATA[1. 前言《LLM 接口管理和分发系统 One API》一文中，我们学习了One API的安装和基础配置。
随着One API项目不断更新，支持的渠道也越来越多。但是，有些新的渠道暂未支持或者不会支持，这时我们可以使用自定义渠道。本文中，我们来学习一下One API自定义渠道的配置方法。
相关文档：

github - one-api



2. 配置并测试LLM API以配置并测试 UCloud DeepSeek 为例，整个流程如下。
1、申请UCloud DeepSeek的使用权限
2、获取模型名称和Key
3、调用测试参考文档：模型服务平台 UModelVerse API说明
curl --location &#x27;https://deepseek.modelverse.cn/v1/chat/completions&#x27; \--header &#x27;Authorization: Bearer &lt;你的API Key&gt;&#x27; \--header &#x27;Content-Type: application/json&#x27; \--data &#x27;&#123;    &quot;reasoning_effort&quot;: &quot;low&quot;,    &quot;stream&quot;: true,    &quot;model&quot;: &quot;models/DeepSeek-V3&quot;,    &quot;messages&quot;: [        &#123;            &quot;role&quot;: &quot;user&quot;,            &quot;content&quot;: &quot;say hello to ucloud&quot;        &#125;    ]&#125;&#x27;

3. 配置自定义渠道配置自定义渠道，与配置其他渠道最主要的差别，在于 代理（老版本中是 Base URL）需要填写自己渠道的 URL。
以配置 UCloud DeepSeek 渠道为例，整个配置如下：

类型：自定义渠道
名称：ucloud-deepseek
分组：default
代理：https://deepseek.modelverse.cn
模型：deepseek-chat
模型重定向：&#123;&quot;deepseek-chat&quot;: &quot;models/DeepSeek-V3&quot;&#125;
密钥：自己的密钥

配置完成后，点击测试，验证渠道是否可以正常调用。
4. 调用自定义渠道使用curl命令测试调用自定义的 deepseek-chat 模型：
curl &quot;https://oneapi.voidking.com/v1/chat/completions&quot; \    -H &quot;Content-Type: application/json&quot; \    -H &quot;Authorization: Bearer &lt;ONE_API_KEY&gt;&quot; \    -d &#x27;&#123;        &quot;model&quot;: &quot;deepseek-chat&quot;,        &quot;messages&quot;: [            &#123;                &quot;role&quot;: &quot;user&quot;,                &quot;content&quot;: &quot;hello!&quot;            &#125;        ]    &#125;&#x27;

如果想要使用 python 测试调用自定义的 deepseek-chat 模型，那么可以参考文档《LLM 接口管理和分发系统 One API》
5. 自定义渠道 + Nginx代理在配置 UCloud DeepSeek时，比较简单，代理直接填写 UCloud 官方给的域名就可以了。这是因为UCloud官方的 LLM API 路径刚好和 OneAPI 的规范匹配，是 /v1/chat/completions如果某个渠道的 LLM API 路径不是 /v1/chat/completions，那么就需要配合 Nginx代理 来进行配置了。
已知潞晨云的LLM API URL为：
https://cloud.luchentech.com/api/maas/chat/completions

那么在OneAPI中自定义渠道配置代理时，是不能直接填入 https://cloud.luchentech.com 这个地址的，需要借助Nginx。
原理：通过Nginx代理，将 /api/maas/chat/completions 转换成 /v1/chat/completions
Nginx配置示例：
server &#123;  listen 80;  listen 443 ssl;  server_name luchen.voidking.com;  charset utf-8;  ssl_certificate /etc/letsencrypt/live/voidking.com/fullchain.pem;  ssl_certificate_key  /etc/letsencrypt/live/voidking.com/privkey.pem;  ssl_session_timeout  5m;  ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;  ssl_ciphers  HIGH:!ADH:!EXPORT56:RC4+RSA:+MEDIUM;  ssl_prefer_server_ciphers on;  if ($ssl_protocol = &quot;&quot;) &#123;    return 301 https://$host$request_uri;  &#125;  location /v1/chat/completions &#123;    proxy_pass https://cloud.luchentech.com/api/maas/chat/completions;    proxy_set_header Host cloud.luchentech.com;    proxy_set_header X-Real-IP $remote_addr;    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_set_header X-Forwarded-Proto $scheme;    proxy_connect_timeout 60s;    proxy_send_timeout 60s;    proxy_read_timeout 60s;    # WebSocket support    proxy_http_version 1.1;    proxy_set_header Upgrade $http_upgrade;    proxy_set_header Connection &quot;upgrade&quot;;  &#125;&#125;

配置完成后，在OneAPI中自定义渠道配置代理时，填入 https://luchen.voidking.com
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>deepseek</tag>
        <tag>one-api</tag>
      </tags>
  </entry>
  <entry>
    <title>FastAPI入门篇</title>
    <url>/dev-fastapi-start/</url>
    <content><![CDATA[1. FastAPI简介FastAPI 是一个现代、快速（高性能）的 Python Web 框架，用于构建 API。它基于 Python 3.6 及以上版本，并且利用了 Python 类型提示的功能。FastAPI 旨在帮助开发者快速构建出高性能的 API，并且具有自动生成文档的功能。
参考文档：

FastAPI官方文档
FastAPI源码



2. FastAPI的特性FastAPI的关键特性包括：

快速：可与 NodeJS 和 Go 并肩的极高性能，是最快的 Python Web 框架之一。高性能归功于 Starlette 和 Pydantic，Starlette 提供了底层的异步功能，而 Pydantic 则处理数据解析和验证。
高效编码：提高功能开发速度约 200％ 至 300％。
更少 bug：减少约 40％ 的人为（开发者）导致错误。
智能：极佳的编辑器支持。处处皆可自动补全，减少调试时间。
简单：设计的易于使用和学习，阅读文档的时间更短。
简短：使代码重复最小化。通过不同的参数声明实现丰富功能。bug 更少。
健壮：生产可用级别的代码。还有自动生成的交互式文档。
标准化：基于（并完全兼容）API 的相关开放标准：OpenAPI (以前被称为 Swagger) 和 JSON Schema。

3. FastAPI与ASGIDjango是一个Python Web框架，同时也可以作为Web Server，只不过性能差一些。我们通常会在Django的上层配置一个WSGI或ASGI服务器，用来提升性能。
而FastAPI这个Python Web框架，本身是不可以作为Web Server的，因此必须要配置一个ASGI服务器。
ASGI（Asynchronous Server Gateway Interface）是一种异步服务器网关接口，它是WSGI（Web Server Gateway Interface）的扩展和进化。ASGI旨在为Python Web服务、框架和应用之间提供一个标准的异步接口，以适应现代Web开发中对异步处理和多种协议支持的需求。
ASGI的主要特点包括：

异步处理能力：ASGI支持异步编程，允许同时处理多个请求，通过使用事件循环和协程实现高并发性能，从而更好地应对大量并发请求。
多协议支持：ASGI不仅支持传统的HTTP请求，还支持WebSocket、HTTP/2以及未来的协议，使其能够适应不断发展的Web技术。
性能提升：由于ASGI的异步特性，它能够更高效地处理异步请求，提升性能和响应速度。
框架支持：现代的Python Web框架，如FastAPI、Tornado以及Django 3.1以后的版本，都支持ASGI，这使得开发者可以利用ASGI的优势构建高性能的Web应用程序。

ASGI服务器，如Uvicorn、Hypercorn和Daphne，实现了这种异步的网关接口，允许开发者构建异步Web应用程序。这些服务器能够处理来自客户端的请求，并将其传递给ASGI应用程序，同时ASGI应用程序可以异步地处理这些请求并返回响应。
总的来说，ASGI是构建现代、高性能、异步Web应用程序的关键技术，它通过支持异步处理和多协议，为Python Web开发提供了强大的支持。
4. 安装使用FastAPI4.1. 安装FastAPI和Uvicorn1、安装fastapi
pip install fastapi

2、安装uvicorn
pip install uvicorn

Uvicorn是一个轻量级的 ASGI 服务器，用于运行 FastAPI 应用。生产环境的 ASGI 服务器通常使用 Uvicorn 或者 Hypercorn。
4.2. Hello World1、编写代码 main.py
from typing import Unionfrom fastapi import FastAPIapp = FastAPI()@app.get(&quot;/&quot;)def read_root():    return &#123;&quot;Hello&quot;: &quot;World&quot;&#125;@app.get(&quot;/items/&#123;item_id&#125;&quot;)def read_item(item_id: int, q: Union[str, None] = None):    return &#123;&quot;item_id&quot;: item_id, &quot;q&quot;: q&#125;

2、运行代码
uvicorn main:app --reload

--reload 参数使得服务器在代码改变时自动重载，适用于开发阶段。
3、测试访问
curl http://127.0.0.1:8000curl http://127.0.0.1:8000/items/5?q=somequery

4、查看接口文档浏览器访问 http://127.0.0.1:8000/docs
我们会看到自动生成的交互式 API 文档（由 Swagger UI 生成）。
4.3. 启动命令常规化uvicorn main:app --reload 这种启动命令，不方便记忆。本节中，我们将启动命令改为：python main.py 
实现方法：main.py 脚本中调用 uvicorn.run() 
if __name__ == &quot;__main__&quot;:    import uvicorn    uvicorn.run(&quot;main:app&quot;, host=&quot;0.0.0.0&quot;, port=8000, reload=True)

5. FastAPI基础5.1. 路径操作和参数FastAPI 允许定义路径操作装饰器（如 @app.get, @app.post 等），并直接在函数参数中声明预期的请求数据类型。
@app.get(&quot;/items/&#123;item_id&#125;&quot;)async def read_item(item_id: int, q: str = None):    return &#123;&quot;item_id&quot;: item_id, &quot;q&quot;: q&#125;

5.2. 请求体和JSON对于需要请求体的接口，我们可以使用 Pydantic 模型来定义请求体的结构。
from pydantic import BaseModelclass Item(BaseModel):    name: str    description: str = None    price: float    tax: float = None@app.post(&quot;/items/&quot;)async def create_item(item: Item):    return &#123;&quot;name&quot;: item.name, &quot;price&quot;: item.price&#125;

5.3. 数据验证FastAPI 会自动验证传入的数据是否符合预期的类型和结构，并在不符合时返回错误信息。
5.4. 文档和交互式API访问 http://127.0.0.1:8000/docs 或 http://127.0.0.1:8000/redoc 可以查看自动生成的 API 文档。
这些文档是交互式的，可以直接在浏览器中测试 API。
5.5. 进阶使用FastAPI 还支持依赖注入、中间件、安全性（如 OAuth2）、数据库集成等高级功能。
6. FastAPI项目结构规划参考文档：

FastAPI全栈模板
github - fastapi/full-stack-fastapi-template
Python 项目工程化开发指南 - 项目结构
github - pypa/sampleproject
FastAPI 项目结构：打造模块化清晰的大型应用架构
使用FastAPI来开发项目，项目的目录结构如何规划

6.1. 推荐项目结构一sampleproject├── LICENSE.txt├── noxfile.py├── pyproject.toml├── README.md├── src│   └── sample│       ├── __init__.py│       ├── package_data.dat│       └── simple.py└── tests    ├── __init__.py    └── test_simple.py

6.2. 推荐项目结构二my_fastapi_project/├── app/│   ├── __init__.py│   ├── main.py            # 入口文件│   ├── core/│   │   ├── __init__.py│   │   ├── config.py      # 配置文件│   │   ├── security.py    # 安全相关│   │   └── ...            # 其他核心功能│   ├── api/│   │   ├── __init__.py│   │   ├── v1/│   │   │   ├── __init__.py│   │   │   ├── endpoints/│   │   │   │   ├── __init__.py│   │   │   │   ├── users.py     # 用户相关接口│   │   │   │   ├── items.py     # 其他接口│   │   │   │   └── ...│   │   │   └── ...              # 其他版本的API│   ├── models/│   │   ├── __init__.py│   │   ├── user.py         # 用户模型│   │   ├── item.py         # 其他模型│   │   └── ...│   ├── schemas/│   │   ├── __init__.py│   │   ├── user.py         # 用户数据模型│   │   ├── item.py         # 其他数据模型│   │   └── ...│   ├── crud/│   │   ├── __init__.py│   │   ├── user.py         # 用户CRUD操作│   │   ├── item.py         # 其他CRUD操作│   │   └── ...│   ├── db/│   │   ├── __init__.py│   │   ├── base.py         # 数据库基础设置│   │   ├── session.py      # 数据库会话│   │   └── ...│   ├── tests/│   │   ├── __init__.py│   │   ├── test_main.py    # 测试主文件│   │   ├── test_users.py   # 用户相关测试│   │   └── ...│   └── utils/│       ├── __init__.py│       ├── utils.py        # 工具函数│       └── ...├── .env                    # 环境变量文件├── alembic/                # 数据库迁移工具目录│   ├── env.py│   ├── script.py.mako│   └── versions/│       └── ...├── alembic.ini             # Alembic 配置文件├── requirements.txt        # 项目依赖├── Dockerfile              # Docker 配置文件└── README.md               # 项目说明文件

7. 小结FastAPI 是一个强大的 Python Web 框架，特别适合用于构建 API。通过上面的内容，我们可以快速开始使用 FastAPI 来构建我们的应用。FastAPI 的文档和社区资源非常丰富，可以帮助我们深入了解和解决更复杂的问题。推荐阅读官方文档以获取更多高级特性和最佳实践。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>fastapi</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置AList</title>
    <url>/dev-docker-alist/</url>
    <content><![CDATA[1. AList简介
AList是一个支持多种存储介质的文件列表程序，基于 Gin 和 Solidjs 实现。

相关文档：

github - AlistGo/alist
AList Docs
《好好学Docker：使用Docker安装配置FileBrowser》



2. AList优点
部署方便，开箱即用；支持Docker部署
支持多种存储介质，包括：本地存储、FTP、WebDAV、S3、阿里云盘、夸克网盘、百度网盘、天翼云盘等等。
支持作为WebDav服务器
支持LDAP
文件预览（PDF、markdown、代码、纯文本等）
画廊模式下的图像预览
视频和音频预览，支持歌词和字幕
Office 文档预览（docx、pptx、xlsx等）
受保护的路由（密码保护和身份验证）
文件永久链接复制和直接文件下载
文件/文件夹打包下载
网页上传(可以允许访客上传)，删除，新建文件夹，重命名，移动，复制
跨存储复制文件
离线下载
单线程下载/串流的多线程下载加速

3. 安装AList参考文档：AList Install - Use Docker
1、下载 alist 镜像
docker pull xhofe/alist:v3.40.0-ffmpeg

2、启动 alist 容器
docker run --name alist -d \  --restart=unless-stopped \  -v /opt/alist/data:/opt/alist/data \  -v /opt/alist/root:/opt/alist/root \  -p 5244:5244 \  -e PUID=0 \  -e PGID=0 \  -e UMASK=022 \  -e &quot;TZ=Asia/Shanghai&quot; \  xhofe/alist:v3.40.0-ffmpeg 

参数说明：

/opt/alist/data 目录，用于存储alist程序本身的数据
/opt/alist/root 目录，用于对外共享数据

3、检查运行状态
docker ps | grep alistdocker logs alist

4. 使用AList4.1. 设置密码3.25.0以上版本将密码改成加密方式存储的hash值，无法直接反算出密码，如果忘记了密码只能通过重新 随机生成 或者 手动设置
# 随机生成一个密码docker exec -it alist ./alist admin random# 手动设置一个密码,`NEW_PASSWORD`是指你需要设置的密码docker exec -it alist ./alist admin set NEW_PASSWORD

4.2. 访问alist1、浏览器访问 alist假设主机IP为 192.168.56.101，那么浏览器访问 http://192.168.56.101:5244
2、登录 alist用户名 admin，密码是上一节中设置的密码。
4.3. 配置存储介质1、点击管理，存储，添加
2、添加本机存储

驱动选择本机存储
根文件夹路径填入 /opt/alist/root/
挂载路径填入 /
启用缩略图
其他选项保持默认

3、查看文件浏览器访问 http://192.168.56.101:5244
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：容器指标查看工具ctop</title>
    <url>/dev-docker-ctop/</url>
    <content><![CDATA[1. ctop简介ctop 是一个类似于 top 的容器指标查看工具。ctop 提供了查询多个容器实时指标的视图，以及查询特定容器实时指标的视图。ctop 内置对 Docker 和 runC 的支持；未来版本计划支持其他容器和集群系统的连接器。
相关文档：

github - bcicen/ctop
容器的top工具：ctop



2. 安装ctop推荐使用二进制安装：
wget https://github.com/bcicen/ctop/releases/download/v0.7.3/ctop-0.7.3-linux-amd64 -O /usr/local/bin/ctopchmod +x /usr/local/bin/ctop

3. 使用ctop3.1. 查看全部容器指标1、启动 ctop，默认界面查看全部容器指标
ctop

2、退出 ctop按下 q，退出 ctop
3.2. 容器排序ctop界面中，按下 s ，选择排序字段。支持的字段包括：cpu，io，mem，mem %，name，pids，net，state
3.3. 查看单个容器指标ctop界面中，选中容器后，按下 o
3.4. 查看单个容器日志ctop页面中，选中容器后，按下 l
3.5. 进入容器内部ctop页面中，选中容器后，按下 e
3.6. 查找容器查找：ctop界面中，按下 f，输入关键词筛选容器，回车退出查找：ctop界面中，按下 f，删除所有关键词，回车
3.7. 更多操作ctop界面中，选中容器后回车，出现菜单此时可以选择查看单个容器指标、查看单个容器日志，还可以停止、暂停、重启、进入容器内部。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：自建RustDesk Server</title>
    <url>/dev-docker-install-rustdesk-server/</url>
    <content><![CDATA[1. RustDesk简介RustDesk 是一款开源的远程桌面软件，是 TeamViewer、向日葵的替代软件，提供了高效、安全的远程访问解决方案。
相关文档：

github - rustdesk/rustdesk
RustDesk官网
RustDesk官方文档
RustDesk 自建服务器部署和使用教程
远程桌面软件 RustDesk



2. 安装RustDesk Client2.1. Ubuntu22中安装RustDesk Client本节中，我们在 Ubuntu22 中安装支持 Wayland 的 RustDesk Client。
1、下载 RustDesk Client 安装包
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-1.3.3-x86_64.deb

2、安装 RustDesk Client
apt install -fy ./rustdesk-1.3.3-x86_64.deb

3、启动 RustDesk Client在图形化界面中，启动RustDesk Client，记录下ID和密码用于远程控制。建议设置一个固定的密码，并设置开机自动启动。
2.2. MacOS中安装 RustDesk Client1、下载 RustDesk Client 安装包
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-1.3.3-x86_64.dmg

2、安装 RustDesk Client双击 dmg 文件，拖拽安装
3、启动 RustDesk Client
3. 使用RustDesk进行远程操作在两台设备上安装并启动 RustDesk Client，然后就可以像Teamviewer一样，输入对方的ID和密码，进行远程控制了。
RustDesk首次建立连接后会提示 请选择要分享的画面（对端操作） ，也就是说必须要求对端操作才能开始远程。对端选择屏幕并记住选择后，后续再建立连接就不需要对端操作了。
注意：使用RustDesk远程时，哪怕对端已经锁屏息屏，也可以唤醒并进行远程，相当于本地操作远程屏幕。
重启主机后再次连接，会提示 不支持使用 Wayland 登录界面 ，说明RustDesk对 Wayland 的支持并不好。详情参考：Login screen using Wayland is not supported yet.
4. 自建RustDesk Server4.1. 为什么需要自建RustDesk Server？以上，我们已经可以使用RustDesk Client，为什么还需要自建RustDesk Server？两个原因：

由于被诈骗分子频繁使用，该项目现已暂停国内服务。
使用默认的RustDesk Server，稳定性和隐私性不能保证。

因此，自建RustDesk Server还是有必要的。
4.2. 基于Docker搭建RustDesk Server参考文档：

RustDesk 自建服务器
Install your own server with Docker

1、准备软件目录
mkdir -p /opt/rustdesk/data

2、准备 docker-compose.yml
cd /opt/rustdesk/vim docker-compose.yml

写入如下内容：
services:  hbbs:    container_name: hbbs    image: rustdesk/rustdesk-server:1.1.12    command: hbbs    volumes:      - /opt/rustdesk/data:/root    network_mode: &quot;host&quot;    depends_on:      - hbbr    restart: unless-stopped  hbbr:    container_name: hbbr    image: rustdesk/rustdesk-server:1.1.12    command: hbbr    volumes:      - /opt/rustdesk/data:/root    network_mode: &quot;host&quot;    restart: unless-stopped


3、启动 RustDesk Server
docker-compose up -d

4、查看 RustDesk Server 状态
docker-compose psdocker ps | grep rustdeskdocker logs hbbsdocker logs hbbr

5、获取服务器Key
docker logs hbbs

可以在日志中看到一个Key，记录下来。
4.3. 开放端口hbbs默认监听端口：

21114（TCP）：用于 Web 控制台，仅在 Pro 版本中可用
21115（TCP）：用于 NAT 类型测试
21116（TCP/UDP）：UDP 用于 ID 注册和心跳服务，TCP 用于 TCP 打孔和连接服务
21118（TCP）：用于支持Web客户端，可选禁用

hbbr默认监听端口：

21117（TCP）：用于 Relay 服务
21119（TCP）：用于支持Web客户端，可选禁用

5. 配置使用Rust Server假设 Rust Server 的IP地址为 8.136.12.13
1、打开Rust Client设置页面
2、网络，解锁网络设置

ID服务器：填入 8.136.13.58:21116
中继服务器：填入 8.136.13.58:21117
Key：填入【基于Docker搭建RustDesk Server】一节中获取到的Key。

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Ubuntu18 升级到 Ubuntu22</title>
    <url>/dev-upgrade-ubuntu18-to-ubuntu22/</url>
    <content><![CDATA[1. 为什么要升级Ubuntu？Ubuntu18 的常规支持已于2023年4月结束，而 Ubuntu22 有更长的常规支持周期，这意味着在未来几年内，用户将持续获得包括功能更新、安全更新和错误修复在内的全面支持，确保系统的稳定性和安全性。
Ubuntu22 采用了较新的 Linux 内核版本，新内核通常会带来更好的硬件支持、性能优化和安全性增强。
此外，最重要的是，很多Ubuntu中的软件仅支持最新版本的Ubuntu，比如Waydroid，比如麟卓卓懿。
因此，虽然升级Ubuntu比较麻烦，并且存在一定的风险，但是是值得的。
参考文档：

Upgrade Ubuntu desktop
18.04 -&gt; 22.04 upgrade
Upgrade ubuntu 18.04 to 22.04
ubuntu18.04如何更新到22.04



2. Ubuntu18升级Ubuntu20Ubuntu不能跨版本升级，只能逐个版本升级，或者逐个LTS版本升级。因此需要先从Ubuntu18升级到Ubuntu20，再从Ubuntu20升级到Ubuntu22。
2.1. 升级前准备0、关闭一些软件（可选）
systemctl stop dockersystemctl disable dockersystemctl stop v2rayasystemctl disable v2rayavncserver -kill :1

1、修改软件源配置
vim /etc/apt/sources.list

添加如下配置：
deb http://cn.archive.ubuntu.com/ubuntu/ bionic main restricteddeb http://cn.archive.ubuntu.com/ubuntu/ bionic-updates main restricteddeb http://cn.archive.ubuntu.com/ubuntu/ bionic-security main restricted

2、更新软件包
apt updateapt upgradeapt dist-upgrade

3、删除无用软件包（可选）
apt autoremove

4、安装 update-manager-core
apt install update-manager-core

5、重启系统
reboot

2.2. 执行升级1、配置使用lts升级
vim /etc/update-manager/release-upgrades

如下修改：
Prompt=lts

注意：如果配置了 Prompt=never ，那么说明禁用了Ubuntu更新。更新时会提示：
Checking for a new Ubuntu releaseIn /etc/update-manager/release-upgrades Promptis set to never so upgrading is not possible.

参考文档：Can’t upgrade from 18.04 server to 20.04
2、执行升级
do-release-upgrade

升级过程会有交互提示，根据提示选择即可。
整个升级过程大约1-2小时。升级完成需要重启一次系统。
3、查看当前版本
lsb_release -a

3. Ubuntu20升级到Ubuntu22不用再次进行升级前准备，直接执行升级命令即可。
1、执行升级
do-release-upgrade

升级过程会有交互提示，根据提示选择即可。
Ubuntu20升级Ubuntu22过程中，在 Upgrade to the firefox snap 时会卡住半个小时以上，请耐心等待。详情参考Upgrade Ubuntu 20.04 to 22.04 stuck on ‘’installing the firefox snap’’
2、查看当前版本
lsb_release -a


]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置FileBrowser</title>
    <url>/dev-docker-filebrowser/</url>
    <content><![CDATA[1. FileBrowser简介FileBrowser 提供了一个在指定目录内的文件管理界面，功能包括上传、删除、预览、重命名和编辑文件。它允许创建多个用户，每个用户可以拥有自己的目录。
相关文档：

github - filebrowser/filebrowser
FileBrowser Installation
《好好学Docker：使用Docker安装配置AList》



2. 安装FileBrowser1、下载 filebrowser 镜像
docker pull filebrowser/filebrowser:v2.31.2-s6

2、准备持久化目录和文件
docker run --name tmp -d \    filebrowser/filebrowser:v2.31.2-s6mkdir -p /opt/filebrowser/docker cp tmp:/database/filebrowser.db /opt/filebrowser/docker cp tmp:/config/settings.json /opt/filebrowser/docker rm tmp -f

3、启动 filebrowser 容器
docker run --name filebrowser -d \    -v /opt/filebrowser/srv:/srv \    -v /opt/filebrowser/filebrowser.db:/database/filebrowser.db \    -v /opt/filebrowser/settings.json:/config/settings.json \    -e PUID=$(id -u) \    -e PGID=$(id -g) \    -p 8889:80 \    filebrowser/filebrowser:v2.31.2-s6

4、检查运行状态
docker psdocker logs filebrowser

3. 使用FileBrowser1、浏览器访问 filebrowser假设主机IP为 192.168.56.101，那么浏览器访问 http://192.168.56.101:8889
2、登录 filebrowser默认用户名和密码都是 admin，首次登录后建议修改密码。
4. 后记FileBrowser配合Jupyter，绝了！让主机变得触手可及。FileBrowser负责界面管理文件，Jupyter负责界面执行脚本，无线可能。比如笔记系统，比如影音系统，比如加解密系统。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>邮箱配置中的SPF、DKIM、DMARC记录</title>
    <url>/dev-mail-spf-dkim-dmarc/</url>
    <content><![CDATA[1. 前言在现代电子邮件系统中，确保邮件的安全性和可靠性至关重要。SPF、DKIM和DMARC是三种主要的电子邮件身份验证机制，它们共同工作以防止垃圾邮件、钓鱼攻击和其他形式的邮件欺诈。
SPF、DKIM和DMARC机制在配置DNS时，记录类型都是TXT类型，在配置记录值时请注意DNS平台是否需要双引号。
本文将学习这三种机制及其在邮箱配置中的作用。
参考文档：

详解 SPF、DKIM、DMARC 记录与工作原理
550-5.7.26 Gmail Error: Email Blocked Because Sender is Unauthenticated
POWERDMARC - 什么是DNS TXT记录？
CloudFlare - 什么是 DNS TXT 记录？



2. SPF2.1. SPF简介SPF全称为发件人策略框架，SPF是一种用于验证发送电子邮件的服务器是否被授权代表域名发送邮件的机制。通过在DNS中添加SPF记录，域名所有者可以指定哪些IP地址可以发送该域名的邮件。
2.2. SPF工作原理当接收邮件服务器收到一封来自某个域名的邮件时，它会查找该域名的SPF记录，验证发件服务器的IP地址是否在授权列表中。如果匹配，邮件将被接受；如果不匹配，邮件可能会被标记为垃圾邮件或拒绝。
2.3. SPF记录示例主机记录：@记录类型：TXT记录值：&quot;v=spf1 ip4:192.0.2.0/24 include:_spf.example.com -all&quot;

记录值字段说明：

v=spf1：表示使用SPF版本1。
ip4:192.0.2.0/24：允许该IP地址范围发送邮件。
include:_spf.example.com：允许其他域名的SPF记录。
-all：表示不允许其他IP地址发送邮件。

腾讯企业邮推荐记录值：&quot;v=spf1 include:spf.mail.qq.com ~all&quot; 
参考文档：

550 SPF check failed
如何设置企业邮箱的SPF记录值？

2.4. SPF语法解释SPF记录定义了多种不同的 mechanism，这些 mechanism 规定了哪些 IP 是被允许的，哪些 IP 是被禁止的。
这一些mechanism包含以下几类：
all | ip4 | ip6 | a | mx | ptr | exists | include同时还支持以下四种前缀：&quot;+&quot; -&gt; Pass (通过/同时这也是默认值) -&gt; 发件IP合法，接收&quot;-&quot; -&gt; Fail (拒绝) -&gt; 发件IP非法，退信&quot;~&quot; -&gt; Soft Fail (软拒绝) -&gt; 发件IP非法，接收&quot;?&quot; -&gt; Neutral (中立) -&gt; SPF记录中没有该IP信息，接收

all 表示命中所有的 IP，通常放在 SPF 记录的尾部作为 fallback，结合上面提到的前缀举几个例子：
&quot;v=spf1 -all&quot; 拒绝所有&quot;v=spf1 +all&quot; 接受所有

ip4/ip6 的格式相近，具体的配置举例如下：
&quot;v=spf1 ip4:192.168.0.1/16 -all&quot;只允许在 192.168.0.1 ~ 192.168.255.255 范围内的 IP&quot;v=spf1 ip6:1080::8:800:200C:417A/96 -all&quot;只允许在 1080::8:800:0000:0000 ~ 1080::8:800:FFFF:FFFF 范围内的 IP如果没有指定前缀长度，则默认只表示记录中所填写的IP（1个IP）

a/mx 这两个 mechanism 会命中相应域名的 a 记录和 mx 记录，a语法mx的语法一致，如果没有填写域名则默认值为当前域名：
&quot;v=spf1 mx -all&quot; 允许当前域名的 mx 记录对应的 IP 地址。&quot;v=spf1 mx mx:deferrals.example.com -all&quot; 允许当前域名和 deferrals.example.com 的 mx 记录对应的 IP 地址。&quot;v=spf1 a/24 -all&quot; 这个配置允许一个地址段

include 用于引入某个域名下的SPF记录，例如：
&quot;v=spf1 include:example.com -all&quot; 采用和 example.com 完全一样的 SPF 记录

3. DKIM3.1. DKIM简介DKIM全称为域名密钥识别邮件，DKIM是一种电子邮件身份验证方法，它通过数字签名确保邮件内容在传输过程中未被篡改。发件人使用私钥对邮件进行签名，而接收方则使用公钥进行验证。
3.2. DKIM工作原理当接收服务器收到一封带有DKIM签名的邮件时，它会提取签名并查找相应的公钥。如果公钥验证成功，表明邮件未被篡改且确实来自声称的发件人。
3.3. DKIM记录示例主机记录：&lt;selector&gt;._domainkey记录类型：TXT记录值：&quot;v=DKIM1; k=rsa; p=MIGfMA0GCS...&quot;记录值中的k=rsa

记录值字段说明：

v=DKIM1：表示使用DKIM版本1。
k=rsa：表示公钥是以RSA方式进行加密的。
p=MIGfMA0GCS...：公钥部分。

主机记录和记录值通常由电子邮件服务商提供，比如使用腾讯企业邮箱或网易企业邮箱，就登录管理员查看DKIM配置。
4. DMARC4.1. DMARC简介DMARC全称为基于域名的消息身份验证、报告和一致性，DMARC是建立在SPF和DKIM之上的一种机制，允许域名所有者指定如何处理未通过身份验证的邮件，并提供报告功能。
4.2. DMARC工作原理接收服务器根据DMARC策略处理未通过SPF或DKIM验证的邮件。根据设置，它可以选择拒绝、隔离或无操作。同时，发送报告给域名所有者，以便他们监控和分析身份验证结果。
4.3. DMARC记录示例主机记录：_dmarc记录类型：TXT记录值：&quot;v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com&quot;

记录字段说明：

v=DMARC1：表示使用DMARC版本1。
p=reject：指示接收方拒绝未通过身份验证的邮件。
rua=mailto:dmarc-reports@example.com：指定接收报告的电子邮件地址。

腾讯企业邮推荐记录值：&quot;v=DMARC1; p=none; rua=mailto:mailauth-reports@qq.com&quot; 
参考文档：企业邮箱如何设置DMARC（TXT记录）
5. 发送邮件给Gmail报错问题5.1. 问题描述发送邮件给Gmail邮箱时，报错：
550-5.7.26 Your email has been blocked because the sender is unauthenticated. 550-5.7.26 Gmail requires all senders to authenticate with either SPF or DKIM. 550-5.7.26550-5.7.26 Authentication results: 550-5.7.26 DKIM = did not pass550-5.7.26 SPF [fuzhi.ai] with ip: [128.x.x.x] = did not pass

5.2. 解决办法报错很清晰，发送邮件的服务器被Gmail拒绝了，Gmail怀疑这是伪造的发件人，因为邮件没能通过SPF或DKIM验证。
解决办法：检查并配置SPF和DKIM。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>One API 开发环境配置</title>
    <url>/dev-one-api-develop/</url>
    <content><![CDATA[1. 前言有些情况下，我们需要基于 One API 进行二次开发，以满足特定的需求。
本文中，我们会学习 One API 的开发环境配置。
相关文档：

github - one-api
《LLM 接口管理和分发系统 One API》
《好好学Golang：Gin框架入门篇》



2. 获取 One API 代码2.1. 方法一：直接从源仓库获取代码git clone https://github.com/songquanpeng/one-api.git

2.2. 方法二：fork后再获取代码（推荐）fork后再获取代码的优点，是更加容易维护。每次修改后提交到自己的仓库进行存档，还可以往源仓库中提交PR。

fork oneapi 代码浏览器访问 github - one-api ，fork oneapi 到自己的 github 仓库。

克隆 oneapi 代码
git clone https://github.com/voidking/one-api.git
配置多个远程仓库（可选）
cd one-apigit remote add upstream https://github.com/songquanpeng/one-api.git

3. 本地运行 One API3.1. 安装依赖
安装前端依赖（node版本 v20.10.0）
cd one-api/web/defaultnpm installnpm run build
安装后端依赖
cd one-api# export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890go mod tidy

3.2. 运行代码
运行代码
go run main.go
运行成功的话，控制台会输出：
[INFO] 2025/04/08 - 17:36:38 | main.go:32 [main] One API v0.0.0 started[INFO] 2025/04/08 - 17:36:38 | model/main.go:103 [openSQLite] SQL_DSN not set, using SQLite as database[INFO] 2025/04/08 - 17:36:38 | model/main.go:129 [InitDB] database migration started[INFO] 2025/04/08 - 17:36:38 | model/main.go:134 [InitDB] database migrated2025/04/08 17:36:38 /path/to/one-api/model/main.go:27 record not found[0.437ms] [rows:0] SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1[INFO] 2025/04/08 - 17:36:38 | model/main.go:28 [CreateRootAccountIfNeed] no user exists, creating a root user for you: username is root, password is 123456[INFO] 2025/04/08 - 17:36:39 | common/redis.go:20 [InitRedisClient] REDIS_CONN_STRING not set, Redis is not enabled[INFO] 2025/04/08 - 17:36:39 | main.go:65 [main] using theme default[INFO] 2025/04/08 - 17:36:39 | relay/adaptor/openai/token.go:23 [InitTokenEncoders] initializing token encoders[INFO] 2025/04/08 - 17:36:40 | relay/adaptor/openai/token.go:49 [InitTokenEncoders] token encoders initialized[INFO] 2025/04/08 - 17:36:40 | main.go:119 [main] server started on http://localhost:3000
测试访问浏览器访问 http://localhost:3000测试登录，默认用户名为root，密码为 123456。


若登录成功，则证明One API本地运行环境配置完成了。
4. VSCode中配置调试环境参考文档：《好好学Golang：VSCode配置Golang Debug》
5. One API核心逻辑One-API 的核心功能是将客户端的 OpenAI 格式请求转发到不同的大模型服务商，其处理流程如下：

客户端请求：客户端发送符合 OpenAI API 格式的请求到 One-API 端点

认证与鉴权：通过中间件验证 API Key 的有效性和权限

模型映射：根据请求中的模型名称确定要使用的渠道

请求转换：将 OpenAI 格式的请求转换为目标服务商的API格式

转发请求：将转换后的请求发送到目标服务商

响应转换：将服务商的响应转换回 OpenAI 格式

返回客户端：将标准化响应返回给客户端


6. 支持新的大模型6.1. 实现思路新出现的大模型，One API并不能保证可以在第一时间支持。如果比较着急使用，那么我们可以自己进行二次开发。
实现思路如下：

获取模型的API Key，并且通过curl验证可以正常访问该模型的API

One API 新增渠道，配置该模型的API和Key

测试新增的渠道，根据报错搜索代码，定位到报错的位置

报错的代码，打断点；请求和响应的代码，打断点；可疑的代码，打断点

测试新增的渠道，触发调试模式，跳转到断点，单步调试

修改代码以支持新的大模型，同时输出更多日志

重复4-6，直到调通整个流程


6.2. 支持新的Gemini模型以支持新的 gemini-2.5-pro-exp-03-25 模型为例，整个实现过程遵循实现思路，需要特别说明的是curl验证方法和获取关键信息的方法。
6.2.1. curl验证One API 渠道可用，最基础的前提是模型的API可用，gemini-2.5-pro-exp-03-25 模型的API验证方法如下：
curl &quot;https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-exp-03-25:generateContent?key=xxx&quot; \-H &#x27;Content-Type: application/json&#x27; \-X POST \-d &#x27;&#123;  &quot;contents&quot;: [&#123;    &quot;parts&quot;:[&#123;&quot;text&quot;: &quot;hello&quot;&#125;]    &#125;]   &#125;&#x27;

6.2.2. 获取关键信息以发送请求并获得响应为分界点，整个系统可以分为三个部分：请求头和请求体处理，发送请求并获得响应，响应内容处理。
如果有了请求相关的关键信息，那么就可以定位解决很多问题了。举个例子，假设获取到的响应不符合预期，那么我们可以直接定位到请求头和请求体处理部分有问题。因此，在开发调试过程中，必须要获取到四个请求相关的关键信息：请求的URL、请求头、请求体、响应的内容。
经过单步调试，我们会发现，在 relay/adaptor/common.go 中的DoRequestHelper 和 DoRequest 函数中，我们可以获取到请求相关的关键信息。
打印请求体的方法：
// req 类型为 http.RequestrequestBody, err := io.ReadAll(req.Body)fmt.Println(&quot;Request Body:&quot;, string(requestBody))// 读取Body会消耗掉Body，如果需要后续使用，需要重新赋值req.Body = io.NopCloser(bytes.NewBuffer(body))

打印响应内容的方法：
// resp 类型为 http.Response responseBody, err := io.ReadAll(resp.Body)fmt.Println(&quot;Response Body:&quot;, string(responseBody))

获取到请求的URL、请求头、请求体后，就可以通过curl直接发送请求了。正确的curl与不正确的curl进行对比，就可以定位到问题出现在系统的哪一部分。
同样的道理，One API系统的三个部分还可以根据其他关键信息进一步细分，这样就可以达到类似于二分法定位问题的效果。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>one-api</tag>
      </tags>
  </entry>
  <entry>
    <title>LLM 接口管理和分发系统 One API</title>
    <url>/dev-one-api-start/</url>
    <content><![CDATA[1. One API简介One API是一个LLM代理系统，支持LLM接口管理和消息分发。使得用户可以通过标准的 OpenAI API 格式访问所有的大模型，开箱即用。
One API 比 openai-forword 更强大，因为 One API 除了作为LLM代理，还可以作为LLM一个中间商系统。
相关文档：

github - one-api
《好好学Docker：使用Docker安装配置OPANAI API代理》



2. One API核心功能
支持多种大模型：OpenAI、Claude、Gemini、Mistral、豆包、文心一言、通义千问、腾讯混元、SiliconCloud等等等等。
支持 stream 模式，可以通过流式传输实现打字机效果
支持绘图接口
支持失败自动重试
支持多机部署，提高并发
支持渠道管理，批量创建渠道，支持渠道设置模型列表，支持通过负载均衡访问多个渠道
支持多用户管理，支持用户额度配置，支持充值，支持用户分组以及渠道分组，支持为不同分组设置不同的倍率
支持令牌管理，设置令牌的过期时间、额度、允许的 IP 范围以及允许的模型访问

3. 安装配置 One API3.1. 安装配置 One API 概述One API 提供了多种安装部署方式，包括Docker部署、裸机编译部署、多机部署、宝塔部署、第三方平台一键部署等。本文中选择使用Docker部署，数据库选择MySQL，不使用Redis。
3.2. 安装MySQL（可选）1、安装MySQL8，参考文档好好学MySQL：使用Docker安装配置MySQL
2、创建一个数据库，名称为 oneapi
3.3. 安装 One API1、下载 one-api 镜像
docker pull justsong/one-api:v0.6.10-alpha.1

2、运行 one-api 容器使用SQLite数据库：
docker run --name one-api -d \  --restart always \  -p 3000:3000 \  -e TZ=Asia/Shanghai \  -v /opt/one-api/data:/data \  justsong/one-api:v0.6.11-alpha.8

备用镜像地址：ghcr.io/songquanpeng/one-api:v0.6.11-alpha.8
使用MySQL数据库：
docker run --name one-api -d \  --restart always \  -p 3000:3000 \  -e SQL_DSN=&quot;root:123456@tcp(localhost:3306)/oneapi&quot; \  -e TZ=Asia/Shanghai \  -v /opt/one-api/data:/data \  justsong/one-api:v0.6.11-alpha.8

其中，SQL_DSN的值要改成自己的MySQL配置。
3、查看容器状态
docker ps | grep one-apidocker logs one-api

3.4. 配置Nginx参考配置：
server&#123;  server_name oneapi.voidking.com;  # 请根据实际情况修改你的域名  location / &#123;    client_max_body_size  64m;    proxy_http_version 1.1;    proxy_pass http://localhost:3000;  # 请根据实际情况修改你的端口    proxy_set_header Host $host;    proxy_set_header X-Forwarded-For $remote_addr;    proxy_cache_bypass $http_upgrade;    proxy_set_header Accept-Encoding gzip;    proxy_read_timeout 300s;  # GPT-4 需要较长的超时时间，请自行调整  &#125;&#125;

4. 使用 One API4.1. 登录 One API1、使用 IP+端口 或者 域名 访问 One API 首页
2、点击页面右上角 登录，登录进入管理页面默认用户名为 root ，密码为 123456，登录后请先修改密码
4.2. 系统配置4.2.1. 配置服务器地址1、点击设置
2、配置服务器地址该地址要和配置的域名一致。
4.2.2. 配置用户登录和注册方式1、点击设置
2、配置登录注册部分，选择登录和注册方式
3、配置邮箱域名白名单
4.3. 配置渠道1、点击渠道，添加新的渠道
2、填入渠道信息

选择类型
填入名称
选择分组
选择模型或者填入模型
填入密钥
部分类型需要填入 base_url
填入模型重定向（可选）

填入的模型，其实是oneapi中的模型名，默认与真实模型名相同。如果oneapi中的模型名，与真实模型名不同，那么可以配置模型重定向。例如：oneapi中模型名是 gpt3.5-test，真实模型名是 gpt-3.5-turbo-16k ，那么模型重定向配置如下
&#123;  &quot;gpt3.5-test&quot;: &quot;gpt-3.5-turbo-16k&quot;&#125;

3、提交，并点击测试
注意：根据个人经验，添加渠道并且测试成功后，并不能立刻通过API调用。如果立刻调用，会报错：
openai.InternalServerError: Error code: 503 - &#123;&#x27;error&#x27;: &#123;&#x27;message&#x27;: &#x27;当前分组 default 下对于模型 xxx 无可用渠道 (request id: xxx)&#x27;, &#x27;type&#x27;: &#x27;one_api_error&#x27;&#125;&#125;
需要等待2到3分钟，再进行API调用才会正常，也许是oneapi的什么机制。
4.4. 配置额度1、点击用户，选择某一个用户，编辑
2、剩余额度填入 token 数，会自动显示等价金额100000 token，等价金额 0.2 刀
3、提交
4.5. 配置令牌1、用户登录 One API
2、点击令牌，添加新的令牌
3、填入令牌信息

名称
模型范围
IP限制
过期时间
额度

4、提交
5、复制 token找到新创建的令牌，点击复制，就可以获取到 token注意：该 token 对应的 base_url 是我们配置的域名 https://oneapi.voidking.com/v1
6、聊天找到新创建的令牌，点击聊天，会自动跳转到 NextChat，同时提示自动填入 base_url 和 token，点击确定，就可以使用 One API 代理进行聊天了。
5. 模型价格与倍率参考文档：

one-api 常见问题
oneapi倍率讨论
倍率计算器

模型倍率和补全倍率算法：
基准价格：0.002 $ /1k tokens模型倍率 = 输入价格 / 基准价格补全倍率 = 输出价格 / 输入价格

假设X模型的价格为 $0.18/M input tokens 和 $0.72/M output tokens，那么它的模型倍率和补全倍率如下：
模型倍率 = 输入价格 / 基准价格 = (0.18/1000) / 0.002 = 0.09补全倍率 = 输出价格 / 输入价格 = 0.72 / 0.18 = 4

在oneapi代码中，在计算费用时，涉及到变量包括：

model_name：模型名（针对模型名配置模型倍率和补全倍率）
base_price：基准价格，0.002 $ /1k tokens
prompt_tokens：提示词token数
completion_tokens：返回token数
model_ratio：模型倍率
completion_ratio：补全倍率
group_ratio：分组倍率（针对不同用户类型，控制打折或溢价）
quota：某个大模型的提示词token数和返回token数，换算成基准价格token数

变量关系如下：
quota = ((prompt_tokens * model_ratio) + (completion_ratio * model_ratio * completion_ratio)) * group_ratio

最终费用为：
cost = quota * base_price = quota * (0.002/1000)

6. API 调用 One API参考文档：

OpenAI - Developer quickstart
OpenRouter - Quickstart

6.1. curl调用测试curl &quot;https://oneapi.voidking.com/v1/chat/completions&quot; \    -H &quot;Content-Type: application/json&quot; \    -H &quot;Authorization: Bearer &lt;ONE_API_KEY&gt;&quot; \    -d &#x27;&#123;        &quot;model&quot;: &quot;gpt-3.5-turbo-16k&quot;,        &quot;messages&quot;: [            &#123;                &quot;role&quot;: &quot;user&quot;,                &quot;content&quot;: &quot;hello!&quot;            &#125;        ]    &#125;&#x27;

6.2. python调用测试1、编写测试脚本 test.py
from openai import OpenAIclient = OpenAI(  base_url=&quot;https://oneapi.voidking.com/v1&quot;,  api_key=&quot;&lt;ONE_API_KEY&gt;&quot;,)completion = client.chat.completions.create(  model=&quot;gpt-3.5-turbo-16k&quot;,  messages=[    &#123;      &quot;role&quot;: &quot;user&quot;,      &quot;content&quot;: &quot;hello!&quot;    &#125;  ])print(completion.choices[0].message.content)


2、运行脚本
python test.py



]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
        <tag>one-api</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中的Leader Election机制</title>
    <url>/dev-k8s-leader-election/</url>
    <content><![CDATA[1. 需求描述假设有一个基于client-go的程序，叫做watcher，会监听k8s集群中pod被删除的消息，当pod被删除时，会触发执行一个动作。当watcher只有一个副本时，程序运行符合预期。但是当watcher有多个副本时，多个watcher副本都监听到pod被删除的消息，都会触发执行一个动作。而这个动作，我们希望只执行一次。有什么办法，可以让多个watcher具备多个副本，但是当监听到pod被删除时，只会触发一次执行动作？


2. 实现思路要实现这个需求，有三个思路：

思路一：基于K8S ValidatingAdmissionWebhook机制。实现一个ValidatingAdmissionWebhook，限制watcher服务只能有一个副本。watcher服务只有一个副本，自然就只会执行一次动作，但是，与需求不相符，不能高可用。
思路二：基于分布式锁。常见的实现方式包括使用 redis、zookeeper 或 etcd 等工具。这种实现方式简单，但是需要额外维护一个中间件，不够优雅。
思路三：基于K8S Leader Election机制。K8S 提供了 Leader Election 机制，可以通过 client-go 库实现，只有被选为 Leader 的 watcher 才会处理 pod 删除事件。

本文中选择思路三（K8S Leader Election机制）来实现需求：服务多个副本但是只执行一次动作。
参考文档：

kubernetes源码-控制器 leader 选举机制
k8s(kubernetes) 使用leader election实现选举

3. Leader Election机制简介3.1. Leader Election原理Leader Election 是一种分布式系统中的机制，旨在确保在多个候选者中选出一个“领导者”进程，以负责执行特定的操作。
1、候选者识别：在 Leader Election 中，首先需要识别出一组候选者，这些候选者可能是运行在同一集群中的多个实例（如 Pods）。这些候选者会竞争成为领导者。
2、竞选过程：候选者通过某种方式（如心跳信号）宣告自己为领导者。通常，所有候选者会尝试同时声明自己为领导者。其中一个候选者成功地获得领导权，而其他候选者则进入待命状态，准备在当前领导者失效时进行新的竞选。
3、心跳机制：一旦某个实例成为领导者，它会定期发送心跳信号以维持其领导地位。如果领导者未能在预定时间内发送心跳信号，其他候选者将启动新的竞选过程，以确保始终有一个活跃的领导者。
4、故障恢复：当当前领导者发生故障或被终止时，其他候选者会迅速重新进行竞选，以确定新的领导者。这种机制保证了系统的高可用性。
4. K8S 中的 Leader Election 实现Kubernetes 提供了一种简化的方式来实现 Leader Election，相关概念包括资源锁和Lease API。

资源锁：Kubernetes 使用 ConfigMap 或 Lease 等资源作为锁来管理领导权。每个候选者尝试更新这个资源以声明自己为领导者。例如，通过更新 ConfigMap 中的某个字段来表示当前的领导者。
Lease API：Kubernetes 从 v1.14 开始引入了 coordination.k8s.io API，允许更高效地管理领导权。使用 Lease 对象可以减少对 API 的调用频率，并避免过多的事件通知。

选举过程：1、候选者创建或获取 Lease 对象，并尝试更新其内容以表明其身份。2、只有第一个成功更新 Lease 的实例能够获得领导权。3、其他实例在发现 Lease 被更新后，将停止尝试并进入待命状态。
选举机制保证了控制器的高可用，同时只有一个控制器为主，其他为从，防止同个事件被多次重复监听，重复执行相关的业务逻辑。
5. Leader Election示例代码示例代码地址：examples - leader-election
1、创建示例项目
mkdir leader-election-demo &amp;&amp; cd leader-election-demogo mod init leader-election-demo

2、粘贴示例代码创建文件 main.go ，并且把示例代码粘贴进去。
/*Copyright 2018 The Kubernetes Authors.Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);you may not use this file except in compliance with the License.You may obtain a copy of the License at    http://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an &quot;AS IS&quot; BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.*/package mainimport (    &quot;context&quot;    &quot;flag&quot;    &quot;os&quot;    &quot;os/signal&quot;    &quot;syscall&quot;    &quot;time&quot;    &quot;github.com/google/uuid&quot;    metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;    clientset &quot;k8s.io/client-go/kubernetes&quot;    &quot;k8s.io/client-go/rest&quot;    &quot;k8s.io/client-go/tools/clientcmd&quot;    &quot;k8s.io/client-go/tools/leaderelection&quot;    &quot;k8s.io/client-go/tools/leaderelection/resourcelock&quot;    &quot;k8s.io/klog/v2&quot;)func buildConfig(kubeconfig string) (*rest.Config, error) &#123;    if kubeconfig != &quot;&quot; &#123;        cfg, err := clientcmd.BuildConfigFromFlags(&quot;&quot;, kubeconfig)        if err != nil &#123;            return nil, err        &#125;        return cfg, nil    &#125;    cfg, err := rest.InClusterConfig()    if err != nil &#123;        return nil, err    &#125;    return cfg, nil&#125;func main() &#123;    klog.InitFlags(nil)    var kubeconfig string    var leaseLockName string    var leaseLockNamespace string    var id string    flag.StringVar(&amp;kubeconfig, &quot;kubeconfig&quot;, &quot;&quot;, &quot;absolute path to the kubeconfig file&quot;)    flag.StringVar(&amp;id, &quot;id&quot;, uuid.New().String(), &quot;the holder identity name&quot;)    flag.StringVar(&amp;leaseLockName, &quot;lease-lock-name&quot;, &quot;&quot;, &quot;the lease lock resource name&quot;)    flag.StringVar(&amp;leaseLockNamespace, &quot;lease-lock-namespace&quot;, &quot;&quot;, &quot;the lease lock resource namespace&quot;)    flag.Parse()    if leaseLockName == &quot;&quot; &#123;        klog.Fatal(&quot;unable to get lease lock resource name (missing lease-lock-name flag).&quot;)    &#125;    if leaseLockNamespace == &quot;&quot; &#123;        klog.Fatal(&quot;unable to get lease lock resource namespace (missing lease-lock-namespace flag).&quot;)    &#125;    // leader election uses the Kubernetes API by writing to a    // lock object, which can be a LeaseLock object (preferred),    // a ConfigMap, or an Endpoints (deprecated) object.    // Conflicting writes are detected and each client handles those actions    // independently.    config, err := buildConfig(kubeconfig)    if err != nil &#123;        klog.Fatal(err)    &#125;    client := clientset.NewForConfigOrDie(config)    run := func(ctx context.Context) &#123;        // complete your controller loop here        klog.Info(&quot;Controller loop...&quot;)        select &#123;&#125;    &#125;    // use a Go context so we can tell the leaderelection code when we    // want to step down    ctx, cancel := context.WithCancel(context.Background())    defer cancel()    // listen for interrupts or the Linux SIGTERM signal and cancel    // our context, which the leader election code will observe and    // step down    ch := make(chan os.Signal, 1)    signal.Notify(ch, os.Interrupt, syscall.SIGTERM)    go func() &#123;        &lt;-ch        klog.Info(&quot;Received termination, signaling shutdown&quot;)        cancel()    &#125;()    // we use the Lease lock type since edits to Leases are less common    // and fewer objects in the cluster watch &quot;all Leases&quot;.    lock := &amp;resourcelock.LeaseLock&#123;        LeaseMeta: metav1.ObjectMeta&#123;            Name:      leaseLockName,            Namespace: leaseLockNamespace,        &#125;,        Client: client.CoordinationV1(),        LockConfig: resourcelock.ResourceLockConfig&#123;            Identity: id,        &#125;,    &#125;    // start the leader election code loop    leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig&#123;        Lock: lock,        // IMPORTANT: you MUST ensure that any code you have that        // is protected by the lease must terminate **before**        // you call cancel. Otherwise, you could have a background        // loop still running and another process could        // get elected before your background loop finished, violating        // the stated goal of the lease.        ReleaseOnCancel: true,        LeaseDuration:   60 * time.Second,        RenewDeadline:   15 * time.Second,        RetryPeriod:     5 * time.Second,        Callbacks: leaderelection.LeaderCallbacks&#123;            OnStartedLeading: func(ctx context.Context) &#123;                // we&#x27;re notified when we start - this is where you would                // usually put your code                run(ctx)            &#125;,            OnStoppedLeading: func() &#123;                // we can do cleanup here                klog.Infof(&quot;leader lost: %s&quot;, id)                os.Exit(0)            &#125;,            OnNewLeader: func(identity string) &#123;                // we&#x27;re notified when new leader elected                if identity == id &#123;                    // I just got the lock                    return                &#125;                klog.Infof(&quot;new leader elected: %s&quot;, identity)            &#125;,        &#125;,    &#125;)&#125;

3、安装依赖
go mod tidy

4、运行代码
# first terminalgo run main.go -kubeconfig=/path/to/kubeconfig -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=1# second terminalgo run main.go -kubeconfig=/path/to/kubeconfig -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=2# third terminalgo run main.go -kubeconfig=/path/to/kubeconfig -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=3

5、查看lease
kubectl get lease

看到如下内容：
NAME      HOLDER   AGEexample   1        104s

6、结束1号进程，再次查看lease看到如下内容：
NAME      HOLDER   AGEexample   3        3m12s

上面的示例，可以证明Leader Election机制已经生效了。当1号进程不可用时，另外的两个进程其中之一会成为领导者。
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>client-go</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Viper库</title>
    <url>/dev-golang-viper/</url>
    <content><![CDATA[1. Viper库简介Viper是一个功能强大的Go语言配置管理库，旨在简化应用程序的配置处理。它支持多种配置文件格式，并能够从多种来源读取配置，适合现代应用程序的需求。Viper使得开发者能够专注于业务逻辑，而不必过多担心配置管理的问题。
参考文档：

github - spf13/viper
配置解析神器viper使用详解
Go语言功能齐全的配置管理库-Viper



2. Viper的特性
多种配置格式支持: Viper可以读取JSON、TOML、YAML、HCL、INI、envfile和Java properties等格式的配置文件。
环境变量支持: Viper可以从环境变量中读取配置。
命令行参数: 通过与pflag等库结合，Viper能够处理命令行参数，并优先覆盖其他配置来源。
实时监控: Viper支持实时监控配置文件的变动，并能在文件变化时自动重新加载配置。
远程配置系统: Viper可以从远程配置系统（如etcd或Consul）读取和监控配置变化。
默认值设置: 可以为不同的配置选项设置默认值，以确保应用在没有提供特定配置时仍能正常运行。
别名系统: Viper允许为参数设置别名，以便在不破坏现有代码的情况下重命名参数。

3. Viper读取配置的优先级Viper支持从多个数据源读取配置值，因此当同一个配置key在多个数据源有值时，Viper读取配置的优先级从高到低如下：

set：显示使用Set函数设置值
flag：命令行参数
env：环境变量
config：配置文件
key/value store：key/value存储系统，如 etcd
default：默认值

4. Viper读取环境变量Viper读取环境变量，和读取配置文件中的变量，有很大不同：

Viper的key不区分大小写，内部会统一转为小写
配置文件中的变量名，对应到环境变量中，全部大写
环境变量不方便表示层级，因此在层级key和环境变量对应时，通常把点和中划线替换为下划线

5. 安装Vipergo get github.com/spf13/viper

6. Viper简单示例以下是一个简单的示例，展示如何使用Viper加载YAML配置文件并从环境变量中覆盖某些值。
1、初始化一个golang项目
mkdir viper-demo &amp;&amp; cd viper-demogo mod init viper-demogo get github.com/spf13/viper

2、创建配置文件 config.yaml
addr: 0.0.0.0port: 8080

3、编写代码 main.go
package mainimport (    &quot;fmt&quot;    &quot;github.com/spf13/viper&quot;)type Config struct &#123;    Addr string     Port int   &#125;func main() &#123;    // 设置读取的配置文件名和路径    viper.SetConfigName(&quot;config&quot;) // 配置文件名，不需要扩展名    viper.SetConfigType(&quot;yaml&quot;)    // 配置文件类型    viper.AddConfigPath(&quot;.&quot;)       // 配置文件路径    // 读取配置    if err := viper.ReadInConfig(); err != nil &#123;        panic(fmt.Errorf(&quot;无法读取配置文件: %s&quot;, err))    &#125;    // 启用环境变量支持，从环境变量中覆盖配置    viper.AutomaticEnv()    // 获取最终的配置信息    config := Config&#123;        Addr: viper.GetString(&quot;addr&quot;),        Port: viper.GetInt(&quot;port&quot;),    &#125;    fmt.Printf(&quot;地址: %s, 端口: %d\n&quot;, config.Addr, config.Port)&#125;

4、运行 main.go
go run main.go

5、环境变量覆盖配置
export ADDR=127.0.0.1 # export addr=127.0.0.1 # 小写是错误的go run main.go

7. Viper层级配置示例1、修改配置文件 config.yaml
http:  addr: 0.0.0.0  port: 8080

2、编写代码 main.go
package mainimport (    &quot;fmt&quot;    &quot;strings&quot;    &quot;github.com/spf13/viper&quot;)type Config struct &#123;    Addr string     Port int   &#125;func main() &#123;    // 设置读取的配置文件名和路径    viper.SetConfigName(&quot;config&quot;) // 配置文件名，不需要扩展名    viper.SetConfigType(&quot;yaml&quot;)    // 配置文件类型    viper.AddConfigPath(&quot;.&quot;)       // 配置文件路径    // 读取配置    if err := viper.ReadInConfig(); err != nil &#123;        panic(fmt.Errorf(&quot;无法读取配置文件: %s&quot;, err))    &#125;    // 启用环境变量支持，从环境变量中覆盖配置    viper.AutomaticEnv()    // 匹配环境变量时，将配置Key中的点号(.)和横杠(-)替换为下划线(_)    viper.SetEnvKeyReplacer(strings.NewReplacer(&quot;.&quot;, &quot;_&quot;, &quot;-&quot;, &quot;_&quot;))    // 获取最终的配置信息    config := Config&#123;        Addr: viper.GetString(&quot;http.addr&quot;),        Port: viper.GetInt(&quot;http.port&quot;),    &#125;    fmt.Printf(&quot;地址: %s, 端口: %d\n&quot;, config.Addr, config.Port)&#125;

3、运行 main.go
go run main.go

4、环境变量覆盖配置
export HTTP_ADDR=127.0.0.1 go run main.go

8. Viper层级配置示例进阶package mainimport (    &quot;fmt&quot;    &quot;strings&quot;    &quot;github.com/spf13/viper&quot;)type Config struct &#123;    HTTP struct &#123;        Addr string         Port int       &#125;&#125;func main() &#123;    // 设置读取的配置文件名和路径    viper.SetConfigName(&quot;config&quot;) // 配置文件名，不需要扩展名    viper.SetConfigType(&quot;yaml&quot;)    // 配置文件类型    viper.AddConfigPath(&quot;.&quot;)       // 配置文件路径    // 读取配置    if err := viper.ReadInConfig(); err != nil &#123;        panic(fmt.Errorf(&quot;无法读取配置文件: %s&quot;, err))    &#125;    // 从环境变量中覆盖配置    viper.AutomaticEnv() // 启用环境变量支持    // 匹配环境变量时，将配置Key中的点号(.)和横杠(-)替换为下划线(_)    viper.SetEnvKeyReplacer(strings.NewReplacer(&quot;.&quot;, &quot;_&quot;, &quot;-&quot;, &quot;_&quot;))    // 将配置文件反序列化为 config 对象    var config *Config    if err := viper.Unmarshal(&amp;config); err != nil &#123;        panic(err)    &#125;    fmt.Printf(&quot;地址: %s, 端口: %d\n&quot;, config.HTTP.Addr, config.HTTP.Port)&#125;


]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：Pod网络限速</title>
    <url>/dev-k8s-pod-network-speed-limit/</url>
    <content><![CDATA[1. 前言为避免Pod打满带宽，有时需要对Pod进行限速。本文中，我们就来学习一下Pod中的网络限速方法。
相关文档：

为Pod配置带宽限制
Limit the inbound and outbound bandwidth of pods
《好好学K8S：K8S中的网络策略》
《好好学K8S：Pod网络限速》



2. Pod网络限速思路1、CNI插件需要支持带宽限速2、Pod中配置注解
注意：这种限速方式，对内网和外网都有效。
3. CNI插件启用带宽限速3.1. Flannel1、编辑flannel插件的配置
kubectl edit cm -n kube-system kube-flannel-cfg

plugins 配置中，增加 bandwidth 配置：
apiVersion: v1data:  cni-conf.json: |    &#123;      &quot;name&quot;: &quot;cb0&quot;,      &quot;cniVersion&quot;:&quot;0.3.1&quot;,      &quot;plugins&quot;: [        &#123;          &quot;type&quot;: &quot;bandwidth&quot;,          &quot;capabilities&quot;: &#123;&quot;bandwidth&quot;: true&#125;        &#125;      ]    &#125;

2、重建flannel pod
kubectl -n kube-system delete pod -l app=flannel

3.2. Calico1、编辑calico插件的配置
vim /etc/cni/net.d/10-calico.conflist

plugins 配置中，增加 bandwidth 配置：
&#123;  &quot;name&quot;: &quot;k8s-pod-network&quot;,  &quot;cniVersion&quot;: &quot;0.3.1&quot;,  &quot;plugins&quot;: [    &#123;      &quot;type&quot;: &quot;bandwidth&quot;,      &quot;capabilities&quot;: &#123;&quot;bandwidth&quot;: true&#125;    &#125;  ]&#125;

2、重建calico pod
kubectl -n calico-system delete pod -l k8s-app=calico-node

4. Pod中配置注解1、pod中添加限速注解
apiVersion: v1kind: Podmetadata:  creationTimestamp: null  annotations:    kubernetes.io/ingress-bandwidth: &quot;1M&quot;  # 限制入站带宽为 1 Mbps，单位bit    kubernetes.io/egress-bandwidth: &quot;3M&quot;   # 限制出站带宽为 3 Mbps，单位bit  labels:    run: testpod  name: testpodspec:  containers:  - image: debian:bookworm-slim    name: testpod    resources: &#123;&#125;    command: [&quot;/bin/sh&quot;, &quot;-c&quot;]    args:    - tail -f /dev/nullstatus: &#123;&#125;

2、登录进入pod中
kubectl exec -it testpod -- /bin/bash

3、测试外网速度
apt updateapt install -y wget python3wget https://raw.github.com/sivel/speedtest-cli/master/speedtest.pypython3 speedtest.py

4、测试内网速度创建两个pod，其中一个启动 iperf server，另外一个启动 iperf client（1）pod1启动iperf server：
apt install -y iperfiperf -s
（2）pod2启动iperf client：
apt install -y iperfiperf -c $POD1_IP -i 3

5. CNI插件带宽限速原理解析本节以CNI插件Calico为例，学习Pod带宽限速的原理。
5.1. 限速原理Calico 本身不直接处理带宽注解，而是依赖以下组件协同工作：
Kubernetes 网络组件:

kubelet 会读取这些注解
通过 CNI 插件配置传递给底层网络

Linux Traffic Control (tc):

实际带宽限制是通过 Linux 的流量控制子系统实现的
使用 Hierarchical Token Bucket (HTB) 队列规则（Queueing Discipline）

CNI 插件:

Calico 会确保这些限制被应用到正确的网络接口

5.2. 出口限制实现1、当 Pod 创建时，kubelet 检测到 egress-bandwidth 注解
2、通过 CNI 调用，在 Pod 的网络命名空间中设置规则
tc qdisc add dev eth0 root handle 1: htb default 10tc class add dev eth0 parent 1: classid 1:1 htb rate 1mbit

所有从 Pod 发出的流量都会受到这个限制。
5.3. 入口限制实现入口限制的实现更复杂，通常通过设置一个虚拟的 ifb (Intermediate Functional Block) 设备
# 创建 ifb 设备ip link add ifb0 type ifbip link set dev ifb0 up# 将入口流量重定向到 ifbtc qdisc add dev eth0 handle ffff: ingresstc filter add dev eth0 parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb0# 在 ifb 上应用限制tc qdisc add dev ifb0 root handle 1: htb default 10tc class add dev ifb0 parent 1: classid 1:1 htb rate 1mbit

5.4. 注解与底层限速规则的关联已知pod中的限速注解为：
apiVersion: v1kind: Podmetadata:  annotations:    kubernetes.io/ingress-bandwidth: &quot;1M&quot;  # 限制入站带宽为 1 Mbps，单位bit    kubernetes.io/egress-bandwidth: &quot;3M&quot;   # 限制出站带宽为 3 Mbps，单位bit

创建pod后，查看pod在宿主机中的底层限速配置：
tc qdisc show

看到的内容格式如下：
qdisc tbf 1: dev cali47528e10e0c root refcnt 2 rate 1Mbit burst 27917286b lat 1924.2sqdisc ingress ffff: dev cali47528e10e0c parent ffff:fff1 ----------------qdisc tbf 1: dev bwpc6db82aef347 root refcnt 2 rate 3Mbit burst 62277024b lat 549.8s

出口限制内容解析：

设备名：bwpc6db82aef347（Kubernetes 为带宽限制创建的虚拟接口）
类型：tbf（Token Bucket Filter）
速率：rate 3Mbit → 对应 egress-bandwidth: 3M
作用方向：root 表示这是出口限制
突发流量限制：burst是为了应对突发流量，避免丢包，burst 62277024b，b表示bytes，允许短时间内突发最多传输 59.4MB 数据，这部分数据可以超过rate限制

入口限制内容解析：

设备名：cali47528e10e0c（Calico 的 Pod 接口）
类型：tbf + ingress 队列
速率：rate 1Mbit → 对应 ingress-bandwidth: 1M
特殊实现：Kubernetes 通过将入口流量 重定向到虚拟接口 的方式实现入口限制；实际生效的是 tbf 规则（虽然显示在 root 位置，但因重定向而作用于入口）
突发流量限制：burst 27917286b，b表示bytes，允许短时间内突发最多传输 26.6MB 数据，这部分数据可以超过rate限制

查看tc规则详情：
tc -s qdisc show dev bwpc6db82aef347tc -s qdisc show dev cali47528e10e0c

6. 后记实际测试发现，Pod中配置注解的方式确实可以实现限制带宽，测速时的 带宽量级 会随着 限速量级 的变化而变化。但是，限速效果不太好，测出的 带宽数值 与设置的 限速数值 有较大差距，并且多次测试结果波动很大。整体来说，Pod中配置注解的方式可以限速，但是不可以精确限速。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>Web安全之开源WAF</title>
    <url>/dev-web-security-waf/</url>
    <content><![CDATA[1. WAF简介WAF（Web Application Firewall）是保护Web应用程序免受各种网络攻击的重要工具。区别于传统防火墙，WAF 工作在应用层，对基于 HTTP/HTTPS 协议的 Web 系统有着更好的防护效果，使其免于受到黑客的攻击。
随着网络安全威胁的不断演变，开源WAF因其灵活性、可定制性和成本效益而受到越来越多企业的青睐。本文中，将盘点几款流行的开源WAF。
相关文档：

2024 值得推荐的免费开源 WAF
盘点2023年十大免费/开源WAF
开源Waf安全防护解决方案
CentOS 7.7上利用OpenResty实现WAF应用防火墙
WAF：保护我们的网络服务
The Best Open Source Web Application Firewalls
Top 10 Free WAFs (Web Application Firewalls) for 2024



2. ModeSecurityModSecurity是一个老牌的开源WAF，最初为Apache HTTP服务器开发，后来扩展到Nginx和IIS等其他平台。
准确来说 ModSecurity 并不是一款 “WAF”，而是一个 “WAF 规则集”，ModSecurity 是绝大部分 WAF 的底层基石，它并不包含其他 WAF 常见的网站管理、日志管理等功能，甚至完全没有界面，ModSecurity 有的只是防护规则。
ModSecurity 不适合直接使用，安装配置复杂，需要配合二次开发做一定的定制才能用起来，上手门槛高。
项目地址：ModeSecurity，star数 8.1k
3. SafeLineSafeLine，中文名 “雷池”，是一款简单好用，效果突出的 Web 应用防火墙(WAF)，可以保护 Web 服务不受黑客攻击。
雷池通过过滤和监控 Web 应用与互联网之间的 HTTP 流量来保护 Web 服务。可以保护 Web 服务免受 SQL 注入、XSS、 代码注入、命令注入、CRLF 注入、ldap 注入、xpath 注入、RCE、XXE、SSRF、路径遍历、后门、暴力破解、CC、爬虫 等攻击。
雷池的社区认可度很高，优势在于性能好、防护能力强、安装配置简单。
项目地址：SafeLine，star数 12.2k
4. BunkerWebBunkerWeb 号称是下一代开源 Web 应用程序防火墙（WAF）。BunkerWeb 作为一个功能齐全的 Web 服务器（在后台基于 NGINX），负责保护 Web 服务，使其“默认安全”。BunkerWeb 可以无缝集成到我们现有的环境（Linux、Docker、Swarm、Kubernetes 等）中，并且是完全可配置的（不要惊慌，如果我们不喜欢 CLI，有一个很棒的 Web UI）以满足我们自己的用例。换句话说，网络安全不再是一件麻烦事。BunkerWeb 包含作为核心一部分的主要安全功能，还可以通过插件系统轻松扩展其他功能。
项目地址：BunkerWeb，star数 6.3k
5. CorazaCoraza 是一个开源的企业级高性能 Web 应用防火墙 (WAF)。它是用 Go 编写的，支持 ModSecurity SecLang 规则集，并且与 OWASP 核心规则集 v4 100% 兼容。
项目地址：Coraza，star数 2.2k
6. open-appsecopen-appsec (openappsec.io) 基于机器学习提供针对 OWASP 前十名和零日攻击的预防性 Web 应用程序和 API 威胁保护。它可以作为 Kubernetes Ingress、NGINX、Envoy（即将推出）和 API 网关的附加组件进行部署。
open-appsec 引擎学习用户如何与您的 Web 应用程序正常交互。然后，它利用这些信息自动检测超出正常操作范围的请求，并进行进一步分析以决定该请求是否恶意。
项目地址：open-appsec，star数 0.8k
7. OpenRestyOpenResty是一个基于Nginx和LuaJIT的Web平台，它提供了强大的功能和灵活性，使得我们可以轻松地实现各种Web应用的需求。在OpenResty中，我们可以利用Lua脚本编写WAF规则，对进入的HTTP请求进行检测和过滤。
项目地址：OpenResty，star数 12.6k
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>安全</tag>
        <tag>黑客</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker创建安卓容器</title>
    <url>/dev-docker-android/</url>
    <content><![CDATA[1. 前言以前，我们使用VirtualBox等虚拟化工具创建安卓虚拟机。现在，我们使用Docker可以创建安卓容器了，更加简单便捷。本文中，我们就学习一下使用Docker创建安卓容器的方法，该方法基于 budtmo/docker-android 项目，目前仅支持Ubuntu系统作为宿主机。


2. 检查宿主机是否支持虚拟化apt install cpu-checkerkvm-ok

如果输出下面的内容，表明没问题
INFO: /dev/kvm existsKVM acceleration can be used

3. 启动安卓容器1、启动安卓容器
docker run --name android -d \  -p 6080:6080 \  -e EMULATOR_DEVICE=&quot;Samsung Galaxy S10&quot; \  -e WEB_VNC=true \  --device /dev/kvm \  budtmo/docker-android:emulator_11.0

2、查看容器状态
docker logs androiddocker exec -it android cat device_status

4. 使用安卓容器使用安卓容器的方法：通过浏览器访问使用。假设宿主机IP为：192.168.56.101 ，那么浏览器访问 http://192.168.56.101:6080然后，就可以愉快地操作安卓系统了。
不过，这种使用安卓系统的方式，是没有声音的，适合APP测试，不适合作为娱乐工具。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>android</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：基于shell实现主机存活探测</title>
    <url>/dev-shell-liveness-probe/</url>
    <content><![CDATA[1. 需求描述当前有三个K8S集群，每个集群内部都配置了监控和告警。但是，如果某个集群整个都挂掉了，那么是收不到告警的。为了解决这个问题，需要配置一个外部的探活脚本，探测集群是否存活。探测时，选择集群中的任意两台主机的ssh端口进行探测。


2. 探活脚本实现1、探活脚本为 probe.sh
#!/bin/bashdev_feishu_url=&quot;https://open.feishu.cn/open-apis/bot/v2/hook/aaa&quot;dev_idc=&quot;开发机房&quot;dev_error=falsetest_feishu_url=&quot;https://open.feishu.cn/open-apis/bot/v2/hook/bbb&quot;test_idc=&quot;测试机房&quot;test_error=falseprod_feishu_url=&quot;https://open.feishu.cn/open-apis/bot/v2/hook/ccc&quot;prod_idc=&quot;生产机房&quot;prod_error=falsesource /root/.bashrcnow=`date +%Y%m%d-%H:%M:%S`host=$(&lt; /path/to/host.txt)linenum=$(echo &quot;$&#123;host&#125;&quot; | wc -l)for index in `seq 1 $&#123;linenum&#125;`;do  #echo $&#123;index&#125;  line=$(echo &quot;$&#123;host&#125;&quot; | sed -n &quot;$&#123;index&#125;p&quot;)  idc=$(echo $line | awk &#x27;&#123;print $1&#125;&#x27;)  ip=$(echo $line | awk &#x27;&#123;print $2&#125;&#x27;)  port=$(echo $line | awk &#x27;&#123;print $3&#125;&#x27;)  value=$(timeout 5s nc -zv $ip $port 2&gt;&amp;1)  echo &quot;$value&quot;  if [[ &quot;$value&quot; =~ &quot;succeeded&quot; ]];then    continue  fi  if [[ &quot;$idc&quot; == &quot;dev&quot; ]];then    dev_error=true  fi  if [[ &quot;$idc&quot; == &quot;test&quot; ]];then    test_error=true  fi  if [[ &quot;$idc&quot; == &quot;prod&quot; ]];then    prod_error=true  fidone#echo &quot;$dev_error&quot;if $dev_errorthen  echo &quot;dev_error&quot;  curl -X POST &quot;$&#123;dev_feishu_url&#125;&quot; \    -H &quot;Content-Type: application/json&quot; \    -d &quot;&#123;\&quot;msg_type\&quot;:\&quot;text\&quot;,\&quot;content\&quot;:&#123;\&quot;text\&quot;:\&quot;$&#123;dev_idc&#125; \n$&#123;now&#125; \ndev cluster error!\&quot;&#125;&#125;&quot;else  echo &quot;dev_working&quot;fiif $test_errorthen  echo &quot;test_error&quot;  curl -X POST &quot;$&#123;test_feishu_url&#125;&quot; \    -H &quot;Content-Type: application/json&quot; \    -d &quot;&#123;\&quot;msg_type\&quot;:\&quot;text\&quot;,\&quot;content\&quot;:&#123;\&quot;text\&quot;:\&quot;$&#123;test_idc&#125; \n$&#123;now&#125; \ntest cluster error!\&quot;&#125;&#125;&quot;else  echo &quot;test_working&quot;fiif $prod_errorthen  echo &quot;prod_error&quot;  curl -X POST &quot;$&#123;prod_feishu_url&#125;&quot; \    -H &quot;Content-Type: application/json&quot; \    -d &quot;&#123;\&quot;msg_type\&quot;:\&quot;text\&quot;,\&quot;content\&quot;:&#123;\&quot;text\&quot;:\&quot;$&#123;prod_idc&#125; \n$&#123;now&#125; \nprod cluster error!\&quot;&#125;&#125;&quot;else  echo &quot;prod_working&quot;fi

2、准备一个机房和主机信息文件 host.txt
dev 192.168.56.101 22dev 192.168.56.102 22test 172.16.0.101 222test 172.16.0.102 222prod 172.16.10.101 2222prod 172.16.10.102 2222

3. 配置定时探活每隔5分钟探活一次：
*/5 * * * * /path/to/probe.sh



]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
        <category>k8s</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>监控</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：基于 xfs_quota 限制目录大小</title>
    <url>/dev-xfs-quota/</url>
    <content><![CDATA[1. xfs_quota简介xfs_quota 是一个管理XFS文件系统配额的工具，它提供了一套丰富的命令行操作来配置项目、用户和组的磁盘配额。比如某些情况下，我们需要限制Linux系统中一些目录的大小，此时就可以使用 xfs_quota 来实现。
参考文档：

《好好学Linux：Linux挂载文件系统》
Linux有什么办法可以限制目录的大小？



2. 格式化磁盘和分区参考《好好学Linux：Linux挂载文件系统》，磁盘分区并格式化为xfs格式。
3. 挂载 xfs 分区因为想要使用 xfs_quota 限制目录大小，所以挂载命令和普通的挂载会有不同。单次挂载命令为：
mkdir -p /limitdatamount -o pquota,uquota,gquota /dev/sdb1 /limitdata

挂载命令中，加入了 pquota,uquota,gquota，意思是启用项目配额限制、用户配额限制和组配额限制。
自动挂载命令为：
mkdir -p /limitdataecho &quot;/dev/sdb1 /limitdata xfs defaults,pquota,uquota,gquota 0 0&quot; &gt;&gt; /etc/fstabmount -a

4. xfs_quota常用命令4.1. 查看配额使用情况要查看用户或组的配额使用情况，可以使用 report 命令：
xfs_quota -x -c &#x27;report -h&#x27; /limitdata
report -h 参数会显示用户和组的磁盘使用情况，-h 为人类可读格式（如KB、MB、GB）。
初始配额使用情况为：
User quota on /limitdata (/dev/sdb1)                        BlocksUser ID      Used   Soft   Hard Warn/Grace---------- ---------------------------------root            0      0      0  00 [------]Group quota on /limitdata (/dev/sdb1)                        BlocksGroup ID     Used   Soft   Hard Warn/Grace---------- ---------------------------------root            0      0      0  00 [------]Project quota on /limitdata (/dev/sdb1)                        BlocksProject ID   Used   Soft   Hard Warn/Grace---------- ---------------------------------#0              0      0      0  00 [------]

4.2. 进入交互shellxfs_quota 可以通过一个交互式的shell界面来操作，或者通过命令行参数执行单个命令。如果想要使用交互式shell界面，可以运行：
xfs_quota -x /limitdata

这里 /limitdata 是XFS文件系统挂载点。
4.3. 设置用户配额xfs_quota -x -c &#x27;limit bsoft=10G bhard=20G username&#x27; /limitdata

在这个例子中，bsoft 是软限制（当用户达到这个限制时，系统会发出警告但仍允许一定时间内超额使用），bhard 是硬限制（用户不能超过这个限制），username 是要设置配额的用户名，10G 和 20G 分别是对应的大小限制。
4.4. 设置组配额sudo xfs_quota -x -c &#x27;limit bsoft=10G bhard=20G -g groupname&#x27; /limitdata

-g 参数指明接下来的限制是针对组的。groupname 是要设置配额的组名。
4.5. 删除用户和组配额xfs_quota -x -c &#x27;limit -u bsoft=0 bhard=0 username&#x27; /limitdata

同理，删除组配额，用 -g 参数替换 -u 并提供组名。
4.6. 设置项目配额4.6.1. 定义项目1、编辑项目定义文件
vim /etc/projid

添加：
project100:100
其中，冒号前的项目名，冒号后的是项目ID。
2、编辑项目和路径映射文件
vim /etc/projects

添加：
100:/limitdata/dir100
其中，冒号前的是项目ID，冒号后的是项目路径。意思是 /limitdata/dir100 归属 100 这个项目。
4.6.2. 初始化项目配额在配置配额之前，需要初始化目录的项目配额记录。
mkdir -p /limitdata/dir100xfs_quota -x -c &#x27;project -s project100&#x27; /limitdata

-s 选项会扫描目录树，查找所有属于该项目的文件，并初始化项目的配额记录。
Setting up project project100 (path /limitdata/dir100)...Processed 1 (/etc/projects and cmdline) paths for project project100 with recursion depth infinite (-1).

4.6.3. 设置项目配额xfs_quota -x -c &#x27;limit -p bhard=2g project100&#x27; /limitdata

这个命令设置 project100 这个项目的硬磁盘空间限制为2GB。
4.6.4. 调整项目配额xfs_quota -x -c &#x27;limit -p bhard=1g project100&#x27; /limitdata

4.6.5. 检查项目配额xfs_quota -x -c &#x27;report -p&#x27; /limitdata

这个命令会显示所有项目的配额和使用情况。
4.6.6. 测试写入内容dd if=/dev/zero of=/limitdata/dir100/testfile bs=1G count=2

正常应该会报错：
dd: 写入&quot;/limitdata/dir100/testfile&quot; 出错: 设备上没有空间记录了2+0 的读入记录了1+0 的写出1073741824字节(1.1 GB)已复制，3.31217 秒，324 MB/秒

5. 限制目录的大小限制目录的大小，对应 xfs_quota 就是设置项目配额。
name=p101id=101echo &quot;$&#123;id&#125;:/limitdata/dir$&#123;id&#125;&quot; &gt;&gt; /etc/projectsecho &quot;$&#123;name&#125;:$&#123;id&#125;&quot; &gt;&gt; /etc/projidxfs_quota -x -c &quot;project -s $&#123;name&#125;&quot; /limitdataxfs_quota -x -c &quot;limit -p bhard=1G $&#123;name&#125;&quot; /limitdata

6. xfs_quota + nfs注意： xfs_quota 管理的目录，开启了nfs共享目录，依然可以被限制。比如 /limitdata 目录作为nfs共享目录对外开放，从其他主机上往 /limitdata/dir100 目录中写入数据，依然会受到 xfs_quota 的限制。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>storage</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>nftables入门篇</title>
    <url>/dev-nftables-start/</url>
    <content><![CDATA[1. 什么是nftables？nftables是Linux内核的一个网络包过滤工具（框架），用于替代传统的iptables/ip6tables/arptables/ebtables范式。nftables代表了对经典的iptables的进化与革新，它提供了一个简单、统一和高效的方式来管理网络数据包的流动，包括过滤和路由。
本文中，我们会学习nftables的基础知识，了解nftables的常用命令。
相关文档：

《iptables入门篇》



2. iptables vs nftablesiptables和nftables（nf表示netfilter）都是Linux系统中用于管理网络流量规则的工具，它们用于配置内核的网络包过滤、转发以及网络地址转换(NAT)等功能。
2.1. iptables特点
iptables是在较早的Linux内核版本中引入的网络包过滤框架。
它主要和Netfilter框架交互，负责IPv4流量。
iptables有自己的语法规则，通过命令行工具来管理。
它有多个表(table)，比如filter, nat, mangle, raw，每个表包含一组链(chain)，例如INPUT, FORWARD, OUTPUT。
命令行语法可能对新手来说比较复杂且难以理解。
iptables没有直接的支持IPV6，需要通过ip6tables来进行配置。

2.2. nftables特点
nftables是iptables的现代替代品，随Linux内核3.13及以后版本发布。
它是为了解决iptables命令复杂和性能不佳等问题而设计的。
nftables使用新的nft命令行工具，提供了一种更简洁的的语法，结构更为统一，并且可以同时处理IPv4、IPv6和其他协议，从而简化了规则集的管理和配置。
nftables提供了一个内置的数据包分类引擎，用来代替iptables中的多个存在重叠功能的模块。
nftables使用单个框架来取代iptables中的多个工具(iptables, ip6tables, arptables, ebtables)。

2.3. 区别
语法：nftables有更为简单和现代的语法。
性能：nftables旨在提供更好的性能。
易用性：nftables提供更为统一的处理方式，减少了之前版本的复杂度。
功能整合：nftables合并了iptables的多个实用工具和表，简化了管理过程。
协议支持：nftables可以同时支持多个协议。

2.4. 联系
目的：它们都旨在管理Linux内核的包过滤规则。
基础：nftables建立在iptables的基础上，尝试克服iptables现存的不足。
Netfilter：它们都使用Netfilter框架来实现网络包的过滤和处理。

2.5. 小结总的来说，虽然nftables是作为iptables的后续者被设计和构建的，但两者在很长一段时间内是并存的，而现在Linux社区鼓励大家使用nftables以取代被认为过时的iptables。不过，许多老系统和应用可能仍然依赖于iptables。
3. nftables关键组成
nftables子系统：这是Linux内核的一部分，是nftables操作的核心。它通过Netfilter提供的钩子（hooks）处理数据包。
nft命令行工具：用户通过nft命令行工具与nftables子系统进行交互。这个工具用于设置和更新规则集。
规则集：规则集（rulesets）是nftables配置的核心。它包含了基于各种条件对数据包进行处理的规则。

4. 安装nftables在现代的Linux发行版上，默认情况下可能已经安装了nftables。如果没有安装，我们可以通过发行版的包管理工具来安装：
# Debian/Ubuntusudo apt install nftables# CentOS/RHELsudo yum install nftables# Fedorasudo dnf install nftables

5. nftables基础5.1. 表、链和规则在nftables中，表（tables）是存储链（chains）的容器，而链则是规则的集合。一个规则可以决定接受、拒绝、转发还是处理一个数据包。
5.2. nft命令基础5.2.1. 创建表nft add table ip my_table

其中 ip 指定了正在创建的表将用于IPv4流量。
5.2.2. 创建链到表中nft add chain ip my_table my_chain &#123; type filter hook input priority 0 \; &#125;

5.2.3. 创建规则到链中nft add rule ip my_table my_chain ip saddr 192.168.1.0/24 accept

这个命令会在my_chain中添加一个规则，允许来自子网192.168.1.0/24的所有进入数据包。
5.2.4. 查看表、链和规则nft list tablesnft list chainsnft list ruleset

输出结果为：
[root@gateway ~]# nft list tablestable ip my_table[root@gateway ~]# nft list chainstable ip my_table &#123;    chain my_chain &#123;        type filter hook input priority 0; policy accept;    &#125;&#125;[root@gateway ~]# nft list rulesettable ip my_table &#123;    chain my_chain &#123;        type filter hook input priority 0; policy accept;        ip saddr 192.168.1.0/24 accept    &#125;&#125;

5.2.5. 删除规则查看当前的规则集：
nft list ruleset

删除某个特定的规则：
nft delete rule ip my_table my_chain handle 1

5.2.6. 保存和恢复规则集当主机重启后，表、链和规则会全部清空，因此需要保存和恢复规则集。
保存当前的规则集到文件：
nft list ruleset &gt; /etc/nftables.conf

从文件恢复规则集：
nft -f /etc/nftables.conf

6. 示例：基本的端口过滤假设我们想要允许SSH（端口22）和HTTP（端口80）流量，但拒绝其他所有的入站流量。
1、新建一个表和链，默认允许所有流量
nft add table ip filternft add chain ip filter input &#123; type filter hook input priority 0 \; policy accept \; &#125;

2、添加规则允许SSH和HTTP流量
nft add rule ip filter input tcp dport &#123;22, 80&#125; accept

3、除了允许的规则外，拒绝所有流量
nft add rule ip filter input counter dropnft list chain ip filter input
通过插入一个计数器规则，丢弃并记录没有匹配到accept规则的流量。
或者：
nft chain ip filter input &#123; type filter hook input priority 0 \; policy drop \; &#125;nft list chain ip filter input
通过修改链的默认策略为drop，丢弃没有匹配到accept规则的流量。
以上命令建立了一个默认拒绝所有进入流量的策略，并明确设定了接受端口为22和80的TCP连接的规则。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>iptables</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：基于Docker buildx构建多平台镜像</title>
    <url>/dev-docker-buildx/</url>
    <content><![CDATA[1. Docker buildx 是什么？Docker buildx 是 Docker 官方维护的一个 CLI 插件，它基于 BuildKit 引擎，提供了不同于 docker build 的更多功能，buildx的一个关键特性是可以同时为不同的硬件架构构建并输出镜像，使得我们可以很容易地交叉构建镜像。比如，我们可以在 AMD64 的机器上构建 ARM64 架构的镜像，这对于软件的跨平台支持非常有用。
Docker buildx 支持的平台主要包括以下几种：

Linux：包括多种 CPU 架构和操作系统，如 x86_64、ARM、IBM Power、IBM Z 等。
Windows：包括多种 CPU 架构和操作系统，如 x86_64、ARM64 等。
macOS：支持 Intel、Apple M1 架构。

在本文中，我们将学习 Docker buildx 工具，并基于它构建多平台镜像。
参考文档：

github - docker/buildx
Docker 多平台构建指南：构建 WebAssembly 镜像
Docker container build driver
利用 docker buildx 构建多平台镜像



2. Docker buildx builder是什么？单纯的 Docker builder，一般是指执行 Docker 镜像构建任务的一个实体或者进程。
在 Docker buildx 工具中，builder 指的是进行镜像构建的一个实例或环境。
builder 实例的特性：

它可以被配置为支持跨多种平台的构建，如 linux/amd64、linux/arm64 等。
每个 builder 实例可以使用不同的驱动程序，Docker buildx 默认支持两种类型的驱动：docker 和 docker-container。
它允许我们定义构建环境的不同方面，比如资源限制、构建缓存存储位置等。
builder 实例可以是本地的，也可以被配置为分布式，由多个节点组成，以便分布式地执行构建任务。

builder 实例常用命令：

docker buildx ls：查看 builer 实例列表
docker buildx inspect：检查当前 builder 实例的详细信息，如果想查看特定的 builder，可以将其名称作为参数传入命令
docker buildx use：切换到一个指定的 builder 实例，让后续的 docker buildx 命令使用该实例
docker buildx create：创建新的 builder 实例
docker buildx update：修改 builder 实例配置
docker buildx rm：删除一个 builder 实例

builder 常用配置：

平台：指定 builder 可以为哪些操作系统和架构对组合来构建镜像。例如 linux/amd64、linux/arm64、windows/amd64 等。
环境变量：为 builder 实例设定环境变量，这些变量在构建过程中可用。
构建选项：例如构建缓存的位置、构建输出的位置、是否使用 BuildKit 功能等。
节点：在使用 docker-container 驱动时，我们可以添加多个节点，这些节点可以扩展 builder 的构建能力。
驱动选项：指定构建使用的驱动和配置，例如 image 驱动或者 docker-container 驱动的细节配置，包括使用的网络模式等。

3. 配置 Docker buildx 环境1、确保 Docker 版本不低于 19.03，才能使用 buildx
docker --version

2、检查 buildx 插件
docker buildx version

通过apt或者yum等包管理方式安装的Docker，默认是带有 buildx 插件的。
如果我们使用的 Docker 版本中没有包含 buildx，或者想要安装最新版本的 buildx，可以访问 github - docker/buildx 获取安装指南。
3、安装 buildx 插件
wget https://github.com/docker/buildx/releases/download/v0.14.0/buildx-v0.14.0.linux-amd64chmod a+x buildx-v0.14.0.linux-amd64mkdir -p /usr/libexec/docker/cli-pluginsmv buildx-v0.14.0.linux-amd64 /usr/libexec/docker/cli-plugins/docker-buildx
更多系统适用的 buildx 版本，可以访问github - buildx releases页面获取。
4、启用实验性功能为了构建多平台镜像，需要确保 Docker 的实验性功能被启用，因为 buildx 依赖这些功能。我们可以通过修改 Docker 配置文件（通常位于 ~/.docker/config.json）来启用实验功能。
&#123;  &quot;experimental&quot;: &quot;enabled&quot;&#125;

4. 支持多平台构建的 builder 实例buildx 有一个默认的 builder，但是这个builder不支持多平台构建。因此，我们需要创建一个支持多平台构建的 builder 实例。
1、查看 builder
docker buildx ls

2、创建一个支持多平台构建的 builder 实例，并切换到新的builder
docker buildx create --name mybuilder --driver docker-container --use

注意：使用docker-container driver才支持多平台构建。
3、查看 builder 详情并初始化
docker buildx inspect --bootstrap

4、切换builder
docker buildx use defaultdocker buildx use mybuilder

5、查看builder详情
docker buildx inspect mybuilder

5. 构建多平台的镜像1、查看基础镜像的多平台镜像
docker buildx imagetools inspect busybox:1.34

2、编写 Dockerfile
FROM busybox:1.34

3、构建多平台镜像
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7  -t voidking/busybox:1.34 .

出现警告：WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use –push or to load image into docker use –load
这个命令将会为 amd64、arm64 和 arm/v7 架构构建镜像，但是，构建后的镜像通过 docker images 是看不到的，只会存在BuildKit 的构建缓存中，也就是说-t参数是无效的。
想要看到构建出的镜像，有两个办法：方法一：使用--push参数直接上传到镜像仓库
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7  -t voidking/busybox:1.34 --push .docker buildx imagetools inspect voidking/busybox:1.34

方法二：使用--load参数，一次只构建一个平台的镜像
docker buildx build --platform linux/arm64  -t voidking/busybox:1.34 --load .docker inspect voidking/busybox:1.34

6. 后记构建出的镜像，并不确保一定可用。可以使用像 QEMU 这样的模拟器，或者实际的硬件设备来测试构建的容器镜像。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置WebIDE VSCode</title>
    <url>/dev-docker-webide-vscode/</url>
    <content><![CDATA[1. WebIDE简介WebIDE是一个现代化的开发工具，它可以让我们直接在浏览器中编写、运行和调试代码。不需要安装任何软件，只需要一个现代化的浏览器和网络连接就可以进行开发。这是一种全新的开发方式，并迅速得到了广大开发者的欢迎。
参考文档：

WebIDE技术方案调研
2022 年最有前景的 5 个 Web IDE
如何在团队内快速落地WebIDE



2. 主流WebIDE对比2.1. VSCode流派VSCode流派基于VSCode，提供了通过浏览器访问VSCode IDE的功能。支持VSCode插件，并提供了与VSCode相同的编辑器功能。
代表：Codespaces、coder/code-server、CodeSandbox
2.2. Projector流派JetBrains Projector流派基于JetBrains，提供了通过浏览器远程访问JetBrains IDE的功能。支持多个编程语言和多种插件，并提供了与JetBrains IDE相同的编辑器功能。
代表：JetBrains Projector
2.3. Theia流派Theia流派基于Eclipse Theia，部分功能复用VSCode，但是在扩展能力上，远超VSCode。
代表：Gitpod、阿里云OpenSumi、华为云CodeArtsIDE、Google云IDE
2.4. StackBlitzStackBlitz是一款专为Web应用程序开发设计的WebIDE，适合前端开发。
3. code-server工作原理通过浏览器或者客户端连接远程的IDE（code-server），远程IDE负责执行，结果实时返回。如果产生数据，那么数据存储在远程磁盘上。远程IDE可以运行在VM上，也可以运行在容器中。
4. 安装配置code-server参考文档：Coder Docs - Install by Docker
1、安装code-server
# This will start a code-server container and expose it at http://127.0.0.1:8080.# It will also mount your current directory into the container as `/home/coder/project`# and forward your UID/GID so that all file system operations occur as your user outside# the container.## Your $HOME/.config is mounted at $HOME/.config within the container to ensure you can# easily access/modify your code-server config in $HOME/.config/code-server/config.json# outside the container.mkdir -p ~/.configdocker run -it --name code-server -d \  -p 8080:8080 \  -v &quot;$HOME/.local:/home/coder/.local&quot; \  -v &quot;$HOME/.config:/home/coder/.config&quot; \  -v &quot;$PWD:/home/coder/project&quot; \  -u &quot;$(id -u):$(id -g)&quot; \  -e &quot;DOCKER_USER=$USER&quot; \  codercom/code-server:latest

2、查看code-server状态
docker logs code-server

5. 使用code-server1、浏览器访问code-server服务假设主机IP为 192.168.50.101，那么浏览器访问 http://192.168.50.101:8080/login
2、获取密码
docker exec -it code-server cat /root/.config/code-server/config.yaml

获取到密码后，在浏览器页面中输入密码，即可进入code-server。
3、使用code-server使用方法和vscode基本没有差别。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>vscode</tag>
        <tag>编辑器</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置Chrome</title>
    <url>/dev-docker-chrome/</url>
    <content><![CDATA[1. 前言
Kasm Workspaces is a docker container streaming platform for delivering browser-based access to desktops, applications, and web services.

Kasm Workspaces是一个Docker容器流媒体平台，用于提供基于浏览器的访问桌面、应用程序和Web服务。
Kasm Workspaces提供了一个可通过浏览器访问的 Google Chrome 版本。本文中，我们就来部署一下这个Docker Chrome。
参考文档：

github - kasmtech/KasmVNC
dockerhub - kasmweb/chrome



2. 安装Docker Chrome1、安装Docker Chrome
docker run --name chrome -d \  --restart always \  --shm-size=1024m \  -p 6901:6901 \  -e VNC_PW=password123 \  -e LANG=zh_CN.UTF-8 \  -e TZ=Asia/Shanghai \  -u root \  kasmweb/chrome:1.14.0

2、查看运行状态
docker ps | grep chromedocker logs chrome


3. 使用Docker Chrome1、浏览器访问Chrome假设宿主机地址为 192.168.56.101 ，那么访问地址为：https://192.168.56.101:6901注意是https，不是http。
2、登录KasmVNC根据提示，输入用户名和密码，登录KasmVNC用户名为 kasm_user ，密码为 password123
3、使用Chrome和本地Chrome基本没有差别，使用起来很方便。
Please note that some functionality, such as audio, uploads, downloads, and microphone pass-through, is only available when using Kasm Workspaces for orchestration.
请注意，某些功能只有在使用Kasm工作区进行编排时才可用，比如音频、上传、下载和麦克风传递。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：Shell脚本问题记录</title>
    <url>/dev-shell-problem/</url>
    <content><![CDATA[1. 前言本文记录在编写Shell脚本时遇到的问题，备忘。


2. Docker输出取值问题2.1. 问题描述#!/bin/bashvalue=$(docker exec -it test echo &#x27;&quot;10&quot;&#x27;)# echo &quot;$value&quot;value=$&#123;value//\&quot;/&#125;if [[ $value =~ ^[0-9]+$ ]]then    echo &quot;running&quot;else    echo &quot;failed&quot;fi

这段代码的期望输出为 running，实际输出为 failed ，是为什么？其中test容器是任意一个docker容器，比如busybox。
2.2. 问题排查解决在docker命令执行的结果中，可能包含一些看不见的控制字符。
排查方法：使用od命令查看不可见的控制字符
echo &quot;$value&quot; | od -c

可以看到输出为：
0000000   &quot;   1   0   &quot;  \r  \n0000006

由此可以发现控制字符为 \r 和 \n。
解决办法：进一步处理value变量，去除控制字符。
value=$(echo -n &quot;$value&quot; | tr -d &#x27;\r\n&#x27;)

2.3. 扩展学习：od命令od命令用于将指定文件内容以八进制、十进制、十六进制、浮点格式或ASCII编码字符方式显示。通常用于显示或查看文件中不能直接显示在终端的字符。od命令系统默认的显示方式是八进制，名称源于Octal Dump。
常见的文件为文本文件和二进制文件。od命令主要用来查看保存在二进制文件中的值，按照指定格式解释文件中的数据并输出，不管是IEEE754格式的浮点数还是ASCII码，od命令都能按照需求输出它们的值。
od -c: same as -t c,  select printable characters or backslash escapes
3. 直接运行和定时任务运行结果不一致问题3.1. 问题描述#!/bin/bashvalue=$(docker exec -it test echo &#x27;&quot;10&quot;&#x27;)value=$(echo -n &quot;$value&quot; | tr -d &#x27;\r\n\&quot;&#x27;)if [[ $value =~ ^[0-9]+$ ]]then    echo &quot;running&quot; | tee -a /tmp/shell.logelse    echo &quot;failed&quot; | tee -a /tmp/shell.logfi

上面这段代码，直接运行时结果为 running ，contab运行结果为 failed ，表现不一致，为什么？
3.2. 问题排查解决经测试，发现 docker exec -it test echo &#39;&quot;10&quot;&#39; 命令中去掉 -it ，问题可以解决。
推测问题原因是cron作业不能添加交互式终端参数。
4. tail报错Too many open files4.1. 问题描述执行tail命令，报错：
tail: inotify cannot be used, reverting to polling: Too many open files

4.2. 解决办法参考文档：

tail: inotify cannot be used, reverting to polling: Too many open files
How to fix: “inotify cannot be used, reverting to polling: Too many open files”

1、修改 /etc/sysctl.conf 配置
vim /etc/sysctl.conf

写入如下内容：
fs.inotify.max_user_watches=1048576fs.inotify.max_user_instances=1048576

2、重载配置文件
sysctl -p




]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>troubleshooting</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>shell</tag>
        <tag>crontab</tag>
        <tag>好好学Shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：kubectl增强工具和替代工具</title>
    <url>/dev-kubectl-enhancements-and-alternatives/</url>
    <content><![CDATA[1. 前言kubectl 在 Kubernetes 环境中发挥了关键的管理作用，但随着 Kubernetes 的复杂性增加，管理者可能需要更直观、更便捷的工具。
本文介绍一些可替代或补充 kubectl 的工具，可以分为命令行工具、GUI工具、GUI平台三类。其中命令行工具和GUI工具是指适合个人使用的Kubernetes管理工具，GUI平台是指适合多人同时使用的Kubernetes管理系统。

命令行工具：kube-shell、k9s、stern
GUI工具：k8slens、octant
GUI平台：kubesphere、rancher



2. 命令行工具2.1. kube-shellkube-shell 是一个增强的 Kubernetes shell，基于kubectl，增加了自动完成、内联文档和其他有用的功能，使得在交互式环境中更友好地使用 Kubernetes。
2.2. k9sk9s 提供了一个终端 UI 界面，以交互的方式对 Kubernetes 集群进行操作。它的操作方式类似 vim，可以快速浏览各种资源、实时查看日志等。
Linux中安装方法：
wget https://github.com/derailed/k9s/releases/download/v0.32.4/k9s_Linux_amd64.tar.gztar -xzvf k9s_Linux_amd64.tar.gz k9s &amp;&amp; chmod +x k9s &amp;&amp; mv k9s /usr/bin 

使用k9s：
k9s # 使用默认 kubeconfigk9s --context ~/.kube/config # 指定 kubeconfig

刚打开k9s界面，点击enter可能会提示 no connection to cached dial ，需要稍等一会再次点击enter。
使用方法参考文档：

K9s简易教程
k9s - Commands

2.3. sternstern 是一个 Kubernetes 的日志跟踪工具，它可以同时跟踪一个或多个 pod 的日志，支持颜色高亮、自定义模板、自动刷新等功能。
3. GUI工具3.1. k8slensk8slens（Lens） 是一个具有丰富功能的 Kubernetes IDE，它可以直接运行在桌面环境中，为用户提供直观的图形化界面对 Kubernetes 集群进行管理。
3.2. octantOctant 是由 VMware 开发的一款 Kubernetes 辅助工具，它提供了一个 Web 界面，可以帮助理解 Kubernetes 工作负载和集群，支持自定义插件，增强其功能。
目前已经停止维护。
4. GUI平台4.1. kubesphereKubeSphere 是一个以 Kubernetes 为基础的开源平台，提供更高级的多租户服务、更友好的 API 和 UI、应用市场、DevOps、微服务管理等功能。
4.2. rancherRancher 是一个全功能的 Kubernetes 管理平台，它可以在任何基础设施上运行 Kubernetes，同时提供集群管理、应用管理、监控、日志、CI/CD、网络、存储等功能。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux中拷贝硬盘</title>
    <url>/dev-linux-copy-disk/</url>
    <content><![CDATA[1. 需求描述Linux主机中原本有一块数据盘 /dev/sdb，挂载到了 /data 目录，存储了大量数据。现在新增了一块数据盘 /dev/sdc，想要把sdb的数据完全拷贝到sdc，然后用sdb替换sdc，这个需求该怎样处理？


2. 实现思路1、把sdb的数据拷贝到sdc2、检查sdc数据3、修改挂载配置，sdc替换sdb，重启主机
3. 拷贝硬盘全部数据3.1. 拷贝硬盘全部数据概述拷贝硬盘全部数据，有三种方法：

使用cp命令。不推荐，因为cp不能断点续传，所以拷贝大量文件时如果出错会浪费时间重传。
使用rsync命令。备选，支持断点续传，但是如果存在大量小文件时拷贝速度会很慢。
使用dd命令。首选，拷贝整个磁盘的数据，速度非常快且数据完整。

因此，这里选择使用dd命令来拷贝磁盘数据。
3.2. dd命令拷贝数据dd if=/dev/sdb of=/dev/sdc bs=64K conv=noerror,sync

参数说明：

if：input file，输入文件，这里就是要复制的源盘/dev/sdb。
of：output file，输出文件，这里就是要复制的目标盘/dev/sdc。
bs：block size，块大小，这里我们设置为64K。
conv：convert，转换选项，这里我们设置为noerror（读写出错不停止）和sync（输入输出不足块大小时，会用空字符补足）。

dd命令默认的块大小是512字节，这意味着默认情况下，dd命令每次读取和写入的数据大小是512字节。如果我们的磁盘支持更大的块大小，那么增加块大小可以帮助提高数据复制的速度。在这个例子中，我们设定块大小为64K，这意味着dd命令每次读取和写入的数据大小是64K字节。
注意：在执行这个命令之前，确保/dev/sdc 没有被挂载并且没有其他程序正在使用。此外，这个操作可能会需要一些时间，需要耐心等待。
3.3. 查看dd进度dd命令在复制数据完成后会自动结束。有时候，dd命令可能会运行非常长的时间，此时可以通过向dd命令发送SIGUSR1信号来查看它当前的进度。
kill -USR1 $(pgrep ^dd)
这条命令会找到dd命令的进程ID并向其发送SIGUSR1信号，这将导致dd命令打印出其当前的复制进度。
输出内容类似于：
6525616+0 records in6525616+0 records out427662770176 bytes (428 GB) copied, 3034.84 s, 141 MB/s

这个输出中，6525616 指的是已经复制的块数量，块大小为64K（默认512），6525616*64*1024 = 427662770176
3.4. dd断点续传dd命令本身并不支持断点续传。如果执行 dd 命令过程中出现了中断，那么重新执行dd命令会从头开始复制，比较浪费时间。但是，我们可以使用一些参数来模拟断点续传的功能。skip 参数可以让 dd 在复制数据前跳过源文件/设备的一定数量的块，seek 参数可以让 dd 在开始写入数据前跳过目标文件/设备的一定数量的块。
假设我们确认已知的dd进度为：
6525616+0 records in6525616+0 records out427662770176 bytes (428 GB) copied, 3034.84 s, 141 MB/s

那么，断点续传可以使用如下命令：
dd if=/dev/sdb of=/dev/sdc bs=64K skip=6525616 seek=6525616 conv=noerror,sync

3.5. 修改UUID使用dd拷贝一个设备到另一个设备，那么设备的UUID也被同时复制过去。这将导致两个设备拥有相同的UUID，这样在一些情况下可能会引发问题，比如在/boot/grub/grub.cfg文件或者/etc/fstab里面用UUID来标识设备。
要修改一个文件系统的UUID，需要使用对应文件系统的工具。
查看UUID命令：
blkid /dev/sdc

对于ext2，ext3，ext4文件系统，可以使用tune2fs命令，例如：
tune2fs -U random /dev/sdc

对于xfs文件系统，可以使用xfs_admin命令，例如：
xfs_admin -U generate /dev/sdc

对于btrfs文件系统，可以使用btrfstune命令，例如：
btrfstune -f -u /dev/sdc

注意：上述所有命令在执行之前，需要先确保该设备已经被卸载。
3.6. 修改UUID报错问题1、执行修改UUID的命令：
xfs_admin -U generate /dev/sdc

报错：
ERROR: The filesystem has valuable metadata changes in a log which needs tobe replayed.  Mount the filesystem to replay the log, and unmount it beforere-running xfs_admin.  If you are unable to mount the filesystem, then usethe xfs_repair -L option to destroy the log and attempt a repair.Note that destroying the log may cause corruption -- please attempt a mountof the filesystem before doing this.

2、根据报错提示挂载磁盘，然后卸载磁盘。
mkdir /sdcmount /dev/sdc /sdc

报错：
mount: wrong fs type, bad option, bad superblock on /dev/sdc,       missing codepage or helper program, or other error       In some cases useful info is found in syslog - try       dmesg | tail or so.

3、尝试修复磁盘问题
xfs_repair -L /dev/sdc

输出：
Phase 1 - find and verify superblock...Phase 2 - using internal log        - zero log......Phase 7 - verify and correct link counts...Maximum metadata LSN (12:673358) is ahead of log (1:2).Format log to cycle 15.done

4、再次尝试修改UUID
xfs_admin -U generate /dev/sdc

输出：
Clearing log and setting UUIDwriting all SBsnew UUID = 25b43fed-781f-419e-b5a9-273042c7f1e1

5、尝试挂载和卸载
mount /dev/sdc /sdcls -l /sdcumount /sdc

4. 检查sdc数据（可选）1、挂载 sdc
mkdir /sdcmount /dev/sdc /sdc

2、比较数据
rsync -P -avz /data/ /sdc

5. 修改挂载配置1、查看 sdc UUID
blkid /dev/sdc

2、修改 /etc/fstab 配置
UUID=xxx /data xfs  defaults 0 0

其中 xxx 修改为 sdc 的 UUID
3、重启主机 
reboot


]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>storage</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker容器磁盘IO限速</title>
    <url>/dev-docker-container-blkio-limit/</url>
    <content><![CDATA[1. 前言为避免Docker容器打满磁盘IO，有时需要对容器进行磁盘IO限速。
参考文档：

docker container run
Services top-level elements - blkio_config
Compose file version 2 reference - blkio_config
容器磁盘限速：我的容器里磁盘读写为什么不稳定?



2. 启动容器时限制容器磁盘IOdocker run --name test -d \  --device-read-bps=/dev/sda:40mb \  --device-write-bps=/dev/sda:20mb \  --blkio-weight=500 \  busybox:latest tail -f /dev/null

上面的命令中，限制了容器读写 /dev/sda 的速度：

读速度最高40MBps
写速度最高 20MBps
磁盘IO权重500（多个容器竞争IO时的相对权重，取值10到1000）

注意1：device对应的宿主机的磁盘设备，例如 /dev/sda、/dev/sdb1注意2：创建容器之后，无法通过 update 修改容器磁盘IO限制注意3：宿主机内核版本在3.13以上才支持blkio cgroups；Docker存储驱动也需要支持blkio cgroups，比如overlay2
3. Compose限制容器磁盘IOversion: &#x27;3.2&#x27;services:  your-service:    image: your-image:latest    blkio_config:      weight: 500      device_read_bps:        - path: /dev/sda          rate: 40mb      device_write_bps:        - path: /dev/sda          rate: 20mb

4. 查看容器磁盘IO限制docker inspect -f &#x27;&#123;&#123;.HostConfig.BlkioDeviceReadBps&#125;&#125;&#x27; &lt;container id&gt;docker inspect -f &#x27;&#123;&#123;.HostConfig.BlkioDeviceWriteBps&#125;&#125;&#x27; &lt;container id&gt;




]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置AdGuard Home</title>
    <url>/dev-docker-adguardhome/</url>
    <content><![CDATA[1. Adguard Home简介
Adguard Home: Free and open source, powerful network-wide ads &amp; trackers blocking DNS server.

AdGuard Home 是一款免费开源的用于拦截广告和跟踪的DNS服务器，是 AdGuard DNS 的开源版本。
参考文档：

AdGuard Home 主页
GitHub - AdguardTeam/AdguardHome
DockerHub - adguard/adguardhome
AdGuard DNS 主页



2. 安装AdGuard Home1、下载 adguardhome 镜像
docker pull adguard/adguardhome:v0.107.44

2、运行 adguardhome 容器
docker run --name adguardhome\    --restart unless-stopped\    -v /opt/adguardhome/workdir:/opt/adguardhome/work\    -v /opt/adguardhome/confdir:/opt/adguardhome/conf\    -p 53:53/tcp -p 53:53/udp\    -p 67:67/udp -p 68:68/udp\    -p 80:80/tcp -p 443:443/tcp -p 443:443/udp -p 3000:3000/tcp\    -p 853:853/tcp\    -p 784:784/udp -p 853:853/udp -p 8853:8853/udp\    -p 5443:5443/tcp -p 5443:5443/udp\    -d adguard/adguardhome:v0.107.44


端口说明：

-p 53:53/tcp -p 53:53/udp: plain DNS.
-p 67:67/udp -p 68:68/tcp -p 68:68/udp: add if you intend to use AdGuard Home as a DHCP server.
-p 80:80/tcp -p 443:443/tcp -p 443:443/udp -p 3000:3000/tcp: add if you are going to use AdGuard Home’s admin panel as well as run AdGuard Home as an HTTPS/DNS-over-HTTPS server.
-p 853:853/tcp: add if you are going to run AdGuard Home as a DNS-over-TLS server.
-p 784:784/udp -p 853:853/udp -p 8853:8853/udp: add if you are going to run AdGuard Home as a DNS-over-QUIC server. You may only leave one or two of these.
-p 5443:5443/tcp -p 5443:5443/udp: add if you are going to run AdGuard Home as a DNSCrypt server.

简化版命令：
docker run --name adguardhome\    --restart unless-stopped\    -v /opt/adguardhome/workdir:/opt/adguardhome/work\    -v /opt/adguardhome/confdir:/opt/adguardhome/conf\    -p 53:53/tcp -p 53:53/udp\    -p 80:80/tcp -p 3000:3000/tcp\    -d adguard/adguardhome:v0.107.44

3、查看运行状态
docker ps | grep adguardhomedocker logs adguardhome

3. systemd-resolved 端口冲突问题启动 adguardhome 容器时，如果使用的是ubuntu系统，那么可能出现 53 端口冲突问题，这是因为 ubuntu 系统中默认自带的 systemd-resolved 服务负责 DNS 解析。
解决办法一：关闭systemd-resolved
systemctl stop systemd-resolved.servicesystemctl disable systemd-resolved.service
可能的问题：禁用 systemd-resolved 可能会影响 VPN 连接的 DNS 解析。如果 VPN 客户端或 NetworkManager 的 VPN 插件依赖 systemd-resolved 来推送 DNS 设置，禁用后可能需要手动配置 VPN 连接的 DNS。
解决办法二（推荐）：禁用 systemd-resolved 的 DNS 存根监听器1、创建配置文件
mkdir -p /etc/systemd/resolved.conf.d/vim /etc/systemd/resolved.conf.d/no-stub-listener.conf

写入如下内容：
[Resolve]DNS=180.76.76.76 8.8.8.8FallbackDNS=1.1.1.1 1.0.0.1DNSStubListener=no

2、修改 /etc/resolv.conf 的链接
ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

3、重启 systemd-resolved
systemctl restart systemd-resolved.service

4、验证53端口占用情况
netstat -nlpt | grep 53

如果打算安装并使用其他的 DNS 解析器（如 dnsmasq、Unbound 或 AdGuard Home），或者希望完全手动控制 DNS 配置，那么完全禁用 systemd-resolved 可能是更干净的选择。
如果只是想释放 53 端口给其他应用程序使用，但同时还想利用 systemd-resolved 来管理上游 DNS 服务器（比如处理从网络连接中获取的 DNS 信息），那么仅禁用其存根监听器是更合适且对系统原有功能影响较小的方案。
4. 配置使用AdGuard Home4.1. 初始化配置1、访问网页配置界面假设宿主机IP为 192.168.56.101 ，那么 网页配置界面 为：http://192.168.56.101:3000/
2、初始化配置第一次登录时，AdGuard Home会引导我们对 网页管理界面 进行初始化配置。初始化配置完成之后，网页配置界面就不能访问了。
配置数据会保存到 /opt/adguardhome/confdir/AdGuardHome.yaml 文件中。
3、访问网页管理界面假设宿主机IP为 192.168.56.101 ，那么 网页管理界面 为：http://192.168.56.101:80/
4.2. AdGuard Home DNS配置点击设置，点击DNS设置，按需配置DNS。
常用配置：

上游 DNS 服务器
速率限制
DNS 缓存配置
允许的客户端
不允许的客户端
不允许的域名

配置数据会保存到 /opt/adguardhome/confdir/AdGuardHome.yaml 文件中。
4.3. 过滤器配置4.3.1. DNS黑名单点击过滤器，点击DNS黑名单。
DNS黑名单，可以阻止访问特定域名。默认黑名单列表为AdGuard DNS filter，可选的黑名单列表还有几十个。
4.3.2. DNS重写点击过滤器，点击DNS重写。
DNS重写，可以配置自定义域名解析。
4.3.3. 自定义过滤规则点击过滤器，点击自定义过滤规则。
自定义过滤规则，可以配置自定义域名解析，也可以阻止访问特定域名。
4.4. 客户机配置使用AdGuard Home假设宿主机IP为 192.168.56.101 ，那么 resolv.conf 配置为：
nameserver 192.168.56.101

具体操作方法参考文档：《DNS入门篇》
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>dns</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中的Request和Limit</title>
    <url>/dev-k8s-request-limit/</url>
    <content><![CDATA[1. Pod和容器的资源设置当我们定义 Pod 时，可以选择性地为每个容器设定所需要的资源数量。最常见的可设定资源是 CPU 和内存（RAM）大小，此外还有其他类型的资源。
当我们为 Pod 中的 Container 指定了资源 request（请求） 时， kube-scheduler 就利用该信息决定将 Pod 调度到哪个节点上。当我们为 Container 指定了资源 limit（限制） 时，kubelet 就可以确保运行的容器不会使用超出所设限制的资源。kubelet 还会为容器预留所 request（请求） 数量的系统资源，供其使用。
参考文档：

为 Pod 和容器管理资源
限制范围
资源配额
设置 Request 与 Limit



2. K8S中的资源单位2.1. CPU资源单位CPU 资源的限制和请求以 cpu 为单位，资源清单中关键字是 cpu 。在 Kubernetes 中，1个CPU核等于1个逻辑核，1个逻辑核 等于 1个物理CPU核 或者 1个虚拟核 ，取决于节点是一台物理主机还是运行在某物理主机上的虚拟机。
我们也可以表达带小数 CPU 的请求。 当我们定义一个容器，将其 spec.containers[].resources.requests.cpu 设置为 0.5 时， 我们所请求的 CPU 是我们请求 1.0 CPU 时的一半。 对于 CPU 资源单位，数量 表达式 0.1 等价于表达式 100m，可以看作 100 millicpu。 有些人说成是一百毫核，其实说的是同样的事情。
CPU 资源总是设置为资源的绝对数量而非相对数量值。 例如，无论容器运行在单核、双核或者 48-核的机器上，500m CPU 表示的是大约相同的计算能力。
2.2. 内存资源单位内存的限制和请求以字节为单位，资源清单中关键字是 memory 。我们可以使用普通的整数，或者带有以下 数量后缀 的定点数字来表示内存：E、P、T、G、M、k。我们也可以使用对应的 2 的幂数：Ei、Pi、Ti、Gi、Mi、Ki。例如，以下表达式所代表的是大致相同的值：
128974848、129e6、129M、128974848000m、123Mi

请注意后缀的大小写。如果我们请求 400m 临时存储，实际上所请求的是 0.4 字节。 如果有人这样设定资源请求或限制，可能他的实际想法是申请 400Mi 字节（400Mi） 或者 400M 字节。
2.3. PVC存储单位PVC存储单位和内存资源的的单位相同，资源清单中的关键字是 storage 。
2.4. 本地临时性存储单位本地临时性存储的单位和内存资源的单位相同，资源清单中关键字是 ephemeral-storage 。
本地临时性存储说明：本地临时性存储，以Pod中的用户角度来看，就是系统盘。节点可以具有本地的临时性存储，由本地挂接的可写入设备或者有时也用 RAM 来提供支持。临时（Ephemeral）意味着对所存储的数据不提供长期可用性的保证。
Pods 通常可以使用临时性本地存储来实现缓冲区、保存日志等功能。 kubelet 可以为使用本地临时存储的 Pods 提供这种存储空间，允许后者使用 emptyDir 类型的卷将其挂载到容器中。
kubelet 也使用此类存储来保存节点层面的容器日志、 容器镜像文件以及运行中容器的可写入层。
详情参考文档本地临时存储
2.5. GPU资源单位K8S中，在调度使用GPU资源时，使用不同的 Kubernets Device Plugin，GPU单位也是不同的。
参考文档：

《K8S使用GPU》
《K8S使用GPU Manager调度GPU》
《K8S使用GPU Share调度GPU》

3. 资源设置方式3.1. 资源设置方式概述资源设置有三种方式：1、在Container中设置在Container中设置资源需求是最常用的方式，其中request是容器可以使用到的最小资源，limit是宿主机资源充足的情况下容器可以使用到的最大资源
2、使用LimitRange限制使用 LimitRange 可以设置某个命名空间的 request 与 limit 默认值，也可设定 request 与 limit 的最大值与最小值。
3、使用ResourceQuota限制当多个用户或团队共享具有固定节点数目的集群时，人们会担心有人使用超过其基于公平原则所分配到的资源量。资源配额是帮助管理员解决这一问题的工具。使用 ResourceQuota 可以对某个命名空间的资源消耗总量提供限制。它可以限制命名空间中某种类型的对象的总数目上限，也可以限制命名空间中的 Pod 可以使用的计算资源的总上限。
3.2. 在Container中设置3.2.1. 设置CPU和内存在Container中设置CPU和内容，示例如下：
---apiVersion: v1kind: Podmetadata:  name: frontendspec:  containers:  - name: app    image: images.my-company.example/app:v4    resources:      requests:        memory: &quot;64Mi&quot;        cpu: &quot;250m&quot;      limits:        memory: &quot;128Mi&quot;        cpu: &quot;500m&quot;  - name: log-aggregator    image: images.my-company.example/log-aggregator:v6    resources:      requests:        memory: &quot;64Mi&quot;        cpu: &quot;250m&quot;      limits:        memory: &quot;128Mi&quot;        cpu: &quot;500m&quot;

上面的 Pod 有两个容器。每个容器的请求为 0.25 CPU 和 64MiB（226 字节）内存， 每个容器的资源限制为 0.5 CPU 和 128MiB 内存。 我们可以认为该 Pod 的资源请求为 0.5 CPU 和 128 MiB 内存，资源限制为 1 CPU 和 256MiB 内存。
3.2.2. 设置本地临时存储在Container中设置本地临时存储，示例如下：
apiVersion: v1kind: Podmetadata:  name: testpodspec:  containers:  - image: alpine:3.7.3    name: testpod    resources:      requests:        ephemeral-storage: 1Gi      limits:        ephemeral-storage: 1Gi    command: [&quot;/bin/sh&quot;, &quot;-c&quot;]    args:    - tail -f /dev/nullstatus: &#123;&#125;

3.3. 使用LimitRange限制使用LimitRange，设置Request和Limit默认值和范围，示例如下：
apiVersion: v1kind: LimitRangemetadata:  name: cpu-resource-constraint  namespace: defaultspec:  requests:  - default: # 此处定义默认限制值      cpu: 500m    defaultRequest: # 此处定义默认请求值      cpu: 500m    max: # max 和 min 定义限制范围      cpu: &quot;1&quot;    min:      cpu: 100m    type: Container  limits:  - default: # 此处定义默认限制值      cpu: 500m    defaultRequest: # 此处定义默认请求值      cpu: 500m    max: # max 和 min 定义限制范围      cpu: &quot;1&quot;    min:      cpu: 100m    type: Container

注意：使用LimiteRange，目前是不支持限制本地临时存储的。
3.4. 使用ResourceQuota限制3.4.1. 限制CPU和内存使用ResourceQuota，限制default namespace下所有pod的request cpu总和不超过1，request memory总和不超过2G，limit cpu总和不超过2，limit memory总和不超过2G，示例如下：
apiVersion: v1kind: ResourceQuotametadata:  name: cpu-memory-quota  namespace: defaultspec:  hard:    requests.cpu: &quot;1&quot;    requests.memory: 1Gi    limits.cpu: &quot;2&quot;    limits.memory: 2Gi

3.4.2. 限制本地临时存储使用ResourceQuota，也可以限制磁盘的用量。限制default namespace下所有pod的数据写入总和不能超过1G，示例如下：
apiVersion: v1kind: ResourceQuotametadata:  name: diskquota  namespace: defaultspec:  hard:    requests.ephemeral-storage: 1Gi    limits.ephemeral-storage: 1Gi

4. 怎样设置合理的Request和Limit？4.1. 建议设置RequestRequest 的值并不代表给容器实际分配的资源大小，而是用于提供给调度器。调度器会检测每个节点可用于分配的资源（节点可分配资源 = 节点资源总额 - 已调度到节点上的 Pod 内容器 request 之和），同时记录每个节点已经被分配的资源（节点上所有 Pod 中定义的容器 request 之和）。如发现节点剩余的可分配资源已小于当前需被调度的 Pod 的 request，则该 Pod 就不会被调度到此节点。反之，则会被调度到此节点。  
如果不配置 request，调度器就无法感知节点资源使用情况，无法做出合理的调度决策，可能会造成调度不合理，引起节点状态混乱。建议给所有容器设置 request，使调度器可感知节点资源情况，以便做出合理的调度决策。集群的节点资源能够被合理的分配使用，避免因资源分配不均而导致发生故障。 
4.2. 重要应用Request等于Limit在Kubernetes中，request和limit之间的差异决定了容器的QoS类别。

如果 request == limit，则容器属于Guaranteed QoS类别。
如果 request &lt; limit，则容器属于Burstable QoS类别。
如果没有设置request和limit，则容器属于BestEffort QoS类别。

不同的QoS类别有不同的调度和驱逐策略。节点资源不足时，会触发自动驱逐和删除，优先删除BestEffort容器，其次是Burstable容器，最后是Guaranteed容器。同时，request和limit之间的差值越大，Burstable类型的容器被驱逐的风险就越高。
对于生产环境中关键的服务，推荐设置 request == limit ，即Guaranteed QoS类别，以保证在节点故障时关键服务不易被驱逐导致线上业务受到影响。
4.3. 提高资源利用率如果应用设置了较高的 request 值，而实际占用资源远小于设定值，会导致节点整体的资源利用率较低。除对时延非常敏感的业务外，敏感的业务本身并不期望节点利用率过高，影响网络包收发速度。建议对非核心，并且资源非长期占用的应用，适当减少 request 以提高资源利用率。如果我们的服务支持水平扩容，那么除 CPU 密集型应用外，单副本的 request 值通常可设置为不大于1核。例如，coredns 设置为0.1核，即100m即可。  
4.4. 避免Request和Limit值过大如果我们的服务使用单副本或少量副本，且 request 及 limit 的值设置过大，使服务可分配到足够多的资源去支撑业务。那么某个副本发生故障时，可能会给业务带来较大影响。当 Pod 所在节点发生故障时，由于 request 值过大，且集群内资源分配的较为碎片化，其余节点无足够可分配资源满足该 Pod 的 request，那么该 Pod 无法实现漂移，无法自愈，会加重对业务的影响。建议尽量减小 request 及 limit，通过增加副本的方式对我们的服务支撑能力进行水平扩容，使系统更加灵活可靠。 
]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>架构</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>基于MetaGPT实现一个订阅智能体：开篇</title>
    <url>/dev-ai-agent-based-on-metagpt-0/</url>
    <content><![CDATA[1. 前言最近半年爆火的智能体是什么？多智能体是什么？MetaGPT是什么？MetaGPT和智能体是什么关系？怎样基于MetaGPT实现一个智能体？…
带着这些疑问开始学习《MetaGPT智能体开发入门》教程，并且会把学习过程中的笔记发布在《基于MetaGPT实现一个订阅智能体》系列文章中。希望能对智能体感兴趣的同学有一点儿启发。
整个系列全部笔记分成六章：

《基于MetaGPT实现一个订阅智能体：第一章 前期准备》
《基于MetaGPT实现一个订阅智能体：第二章 AI Agent知识体系结构》
《基于MetaGPT实现一个订阅智能体：第三章 MetaGPT框架组件介绍》
《基于MetaGPT实现一个订阅智能体：第四章 OSS订阅智能体》
《基于MetaGPT实现一个订阅智能体：第五章 ActionNode》
《基于MetaGPT实现一个订阅智能体：第六章 订阅智能体进阶》

参考文档：

《MetaGPT智能体开发入门》教程
《MetaGPT智能体开发入门》学习手册
《MetaGPT智能体开发入门》FAQ 常见问题
《MetaGPT智能体开发入门》学员打卡
MetaGPT文档站
《MetaGPT快速体验》



2. 《MetaGPT智能体开发入门》教程简介《MetaGPT智能体开发入门》是由MetaGPT教程组以及Datawhale的小伙伴共同推出的首个专注于Agent代码开发的入门级实践教程。尽管关于大型模型prompt的教程、对话知识库，以及GPTs的指南已广泛存在并且不断更新，但在中文互联网上，关于Agent开发的知识却相对零散，缺乏系统性。本教程旨在解决这一问题。通过深入解析大型模型API，指导从业者如何快速开发出具有个性化特征的Agent。它从代码层面进行剖析，使泛科技领域的从业者能够轻松进入源码世界，避免了解读晦涩难懂的学术论文或被自媒体过度包装的长篇文章，为他们开启Agent开发旅程的第一步。
3. 目标：实现一个订阅智能体MetaGPT提供了Agent的开发能力，Agent可以协助我们解决日常的一些问题，其中一个应用场景就是我们可以让Agent为我们关注某些想关注的信息，当有我们关注的事件发生时，Agent获取信息并进行处理，然后通过一些如邮件、微信、discord等通知渠道将处理后的信息发送给我们。我们将这类Agent称为订阅智能体，这类智能体可以结合MetaGPT的PubSub模块运行。
《基于MetaGPT实现一个订阅智能体》系列文章的目标，就是实现一个基于MetaGPT的订阅智能体。
4. 订阅智能体三要素订阅智能体的实现主要有3个要素，分别是Role、Trigger、Pusher，即智能体本身、触发器、数据推送。

我们首先按要实现一个OSSWatcher的角色，OSS即Open source software，我们对OSS智能体定位是，帮我们关注并分析热门的开源项目，当有相关信息时将信息推送给我们；
然后Trigger是指这个OSSWatcher的角色什么时候触发，我们可以是定时触发或者是某个网站有更新时触发；
最后，Pusher可以设置OSSWatcher角色运行生成的信息，我们可以将数据发送到微信或者discord

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>aigc</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>metagpt</tag>
      </tags>
  </entry>
  <entry>
    <title>基于MetaGPT实现一个订阅智能体：第一章 前期准备</title>
    <url>/dev-ai-agent-based-on-metagpt-1/</url>
    <content><![CDATA[1. 前言本章任务：

阅读教程
了解MetaGPT
准备Python环境
获取MetaGPT
配置MetaGPT



2. 阅读教程阅读《MetaGPT智能体开发入门》教程的第一章。
3. 了解MetaGPT观看视频【直播回放】MetaGPT作者深度解析直播回放
这个视频讲述了MetaGPT作者关于模型训练和优化的深入解析。作者分享了关于大语言模型发展路径的实际路径，以及ZeroShot的逻辑和效果应用，还涉及了智能体和环境的认知互动等。视频内容包括智能体架构、代码结构、环境配置等方面的讲解。 

0:01 🌍大语言模型的发展路径
9:09 💡BERT的有效性和训练成本
18:23 🎮强化学习在模型推理中的应用
27:33 🗣️直觉、语音交互和多智能体协作
36:46 🔍智能体的需求和发展前景
45:55 🤖MetaGPT：AI智能体框架
55:11 📊MMU榜单、GPT-4性能和商业机会
64:24 🌟多智能体框架的愿景和影响
73:34 💼创业公司和MetaGPT软件公司实例
82:47 🔧智能体的功能和设计理念
91:59 🔬Hugging GPT的工作原理和开源竞争

4. 准备Python环境方法一：使用pyenv安装，参考文档《Python版本管理器pyenv》方法二（推荐）：使用conda安装，参考文档《Python包管理工具Conda》方法三：从Python官网下载安装包，安装
5. 获取MetaGPTMetaGPT官方文档 - 安装中提供了pip、git、docker等安装方式。因为我们要进行二次开发，因此最好选择git的方式。
1、clone MetaGPT
git clone https://github.com/geekan/MetaGPT.git

2、切换到学习版本 v0.5.2
git checkout v0.5.2

3、安装依赖
cd MetaGPTpip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simplepip install -e.

6. 配置MetaGPT6.1. 获取 OpenAI API Key1、登录openai官网，注册并登录账号，选择API。
2、openai platform页面，点击API keys，Create new secret key，得到Openai API key
6.2. 配置 OpenAI API Key配置OpenAI API key有三种方法：配置文件法、系统环境变量法、代码环境变量法。
其中配置文件的优先级大于系统环境变量和代码环境变量，具体来说，MetaGPT将会按照以下优先级来读取配置：
$HOME/.metagpt/config.yaml &gt; config/key.yaml &gt; config/config.yaml &gt; environment variable

6.2.1. 配置文件法（推荐）默认配置文件为 config/config.yaml，建议拷贝出一个 config/key.yaml 用于自定义配置。
config/key.yaml 配置示例：
OPENAI_API_KEY: &quot;sk-...&quot; # your openai api keyOPENAI_API_MODEL: &quot;gpt-3.5-turbo-16k&quot; # your model

测试与openai官方api的连通性
curl https://api.openai.com/v1

如果不通的话，那么还需要配置一个openai api专用代理。
OPENAI_API_BASE: &quot;https://api.openai-forward.com/v1&quot;

注意：如果是使用 v0.6 以上的MetaGPT版本，那么需要使用 OPENAI_BASE_URL
6.2.2. 系统环境变量法配置系统环境变量
export OPENAI_API_KEY=&quot;sk-...&quot; # your openai api keyexport OPENAI_API_MODEL=&quot;gpt-3.5-turbo-16k&quot; # your modelexport OPENAI_API_BASE=&quot;https://api.openai-forward.com/v1&quot;

6.2.3. 代码环境变量法在代码中使用os库来将自己的OpenAI API key设定为环境变量
import osos.environ[&quot;OPENAI_API_KEY&quot;] = &quot;sk-...&quot;  # your openai api keyos.environ[&quot;OPENAI_API_MODEL&quot;] = &quot;gpt-3.5-turbo-16k&quot; # your modelos.environ[&quot;OPENAI_API_BASE&quot;] = &quot;https://api.openai-forward.com/v1&quot;

6.3. 获取&amp;配置其他LLM API当前支持的其他LLM：openai, azure_openai, gemini, spark, zhipuai, fireworks, open_llm, ollama, anthropic, metagpt
获取&amp;配置其他LLM的方法，可以参考文档适配其他国产模型
7. 创建一个多智能体团队7.1. 运行已有脚本python startup.py --idea &quot;write a cli blackjack game&quot;
其中 –idea 可以省略，运行后，智能体就会开始工作，最终workspace目录中会生成一个 blackjack game 项目。
7.2. 自己创建脚本1、创建一个脚本 demo.py
import asynciofrom metagpt.roles import (    Architect,    Engineer,    ProductManager,    ProjectManager,)from metagpt.team import Teamasync def startup(idea: str):    company = Team()    company.hire(        [            ProductManager(),            Architect(),            ProjectManager(),            Engineer(),        ]    )    company.invest(investment=3.0)    company.start_project(idea=idea)    await company.run(n_round=5)asyncio.run(startup(idea=&quot;write a cli blackjack game&quot;)) # blackjack: 二十一点

代码解释：首先，导入已实现的角色。然后，初始化公司团队，配置对应的智能体，设置对应的预算以及提供一个写一个小游戏的需求。最后，运行并得到生成的游戏代码！
2、运行脚本
python demo.py

8. 问题记录8.1. DEFAULT_PROVIDER无效问题agent学习群里其他同学发现了一个 DEFAULT_PROVIDER 无效问题：1、只要 self._is_valid_llm_key(self.OPENAI_API_KEY) and not self.OPENAI_API_TYPE and self.OPENAI_API_MODEL 为true，那么 DEFAULT_PROVIDER 就会失效。2、如果用户之前设置过 OPENAI_API_KEY ，那么想要换成其他大模型，那么必须注释掉 OPENAI_API_KEY
修复方法：https://github.com/geekan/MetaGPT/pull/776/files
但是，因此dev分支已经完全避免了这个问题，很快就会合入到main，所以这个PR先关掉了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>aigc</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>metagpt</tag>
      </tags>
  </entry>
  <entry>
    <title>基于MetaGPT实现一个订阅智能体：第二章 AI Agent知识体系结构</title>
    <url>/dev-ai-agent-based-on-metagpt-2/</url>
    <content><![CDATA[1. 前言本章任务：

了解智能体的定义
了解热门智能体案例
了解智能体的宏观机会
了解智能体与System1&amp;System2

本文中的内容，大都摘录自《MetaGPT智能体开发入门》教程，更多内容请阅读全文。


2. 智能体的定义代理（Agent）这个词来源于拉丁语“agere”，意为“行动”。现在可以表示在各个领域能够独立思考和行动的人或事物的概念。智能体（智能代理）是以智能方式行事的代理。Agent感知环境，自主采取行动以实现目标，并可以通过学习或获取知识来提高其性能。可以把单个Agent看成是某个方面的专家。

一个精简的Agent工作流程为：感知 -&gt; 规划 -&gt; 行动

感知（Perception）是指Agent从环境中收集信息并从中提取相关知识的能力。
规划（Planning）是指Agent为了某一目标而作出的决策过程。
行动（Action）是指基于环境和规划做出的动作。
其中，Policy是Agent做出Action的核心决策，而行动又通过观察（Observation）成为进一步Perception的前提和基础，形成自主地闭环学习过程。

MetaGPT中对于智能体（Agent）的定义，请参阅文档《基于MetaGPT实现一个订阅智能体：第三章 MetaGPT框架组件介绍》
参考文档：

基于大语言模型的AI Agents—Part 1
GPT Agents 是什么？
What are GPT Agents? A deep dive into the AI interface of the future

3. 热门智能体案例23年11月OpenAI推出ChatGPT自定义功能，用户可为特定目的创建自己版本的ChatGPT，被称为GPTs，以构成GPT商店（GPT Store）。GPTs可以单纯通过提示词创建，也可以配合调用工具实现更强大的功能。OpenAI的GPTs，可能是目前最广为人知的智能体。此外，国内的文心一言、通义千问、腾讯混元等大模型也在自己的APP上推出了能够完成特定任务的智能体，其中文心一言是支持自己定义智能体的。而MetaGPT，和以上LLM不同，它能够调用各种大模型的API，以编程辅助，实现更自由的智能体定制。
GPTs汇总：

github - ai-boost/Awesome-GPTs
github - linexjlin/GPTs
github - AgentOps-AI/BestGPTs

GPT Agent案例体验：

Chatgpt Code Interpreter ：代码解释器（Code Interpreter）可以让普通用户（非程序员）用自然语言完成以前需要写代码来完成的复杂工作。
AgentGPT：基于浏览器的 AutoGPT 实现，例如帮我写一个openai近期新闻的时间线
Tavily AI：基于浏览器的GPT Research实现
Perplexity：LLM驱动的agent搜索引擎
Cursor - The AI-first Code Editor：AI优先的代码编辑器。在专为与人工智能进行配对编程而设计的编辑器中更快地构建软件。

更多agent项目和产品，请参阅github - e2b-dev/awesome-ai-agents
4. 智能体的宏观机会99%的互联网入口将由App变为智能体：人类感知的字节数降低到极限
信息技术革命沿着，门户-&gt;搜索-&gt;推荐-&gt;Agent演变；本质是人类需要操作的字节数在迅速变少。比如一个调研需求，人类需要搜索、阅读100个页面再做总结，但Agent可以自己做完这个流程，人类阅读结果就可以。以此类推，订票、购物、搜索、IM等大部分流程都可以交给Agent做软件的需求依然会持续存在，但入口将被Agent接管。软件仅做为数字世界的一部分被感知、做为部分API被使用。
社会协作变革：虚拟员工进入企业，红杉预计 智能体数量 五年后和人类1比1
AI Agent是新的高级物种，为了实现类人智能，大语言模型实现了人脑的20%，我们需要构建AgentStack补齐剩余的80%补齐之后，Agent就可以直接供给生产力，并且同类问题的平均解决成本在人类的1%以内，效率平均在100倍以上

5. 智能体与System1&amp;System2Andrej Karpathy说：诺贝尔经济学奖得主丹尼尔卡尼曼在《思考快与慢》中提出，人的认知系统包含System1和System2两个子系统。System1主要靠直觉，而System2是逻辑分析系统。通俗来说，System1是一个快速自动生成的过程，而System2是经过深思熟虑的部分。
例如，如果我问你 2+2 等于多少，你实际上并没有做数学计算。你只是告诉我它等于4，因为这是可用的，已经存在于你的大脑中，是本能的。但是当我问你 17*24 等于多少时，你并没有准备好这个答案，所以你会启动你的大脑的另一部分（系统2），这部分更加理性，更加缓慢，进行复杂的决策，感觉更有意识。你不得不在脑海中解决这个问题，然后给出答案。另一个例子是，如果你们中的一些人可能下象棋，当你进行快速象棋比赛时，你没有时间思考，所以你只是根据直觉移动棋子。这时大部分由你的System1 来处理。但如果你在比赛中，有更多时间来思考，那么你会更多地参与到决策中，你会感到自己在建立可能性的树，这是一个更有意识、更费力的过程。你必须在脑海中解决问题并给出答案。
现在事实证明，LLM 目前只有System1。它们只有这个本能部分，它们不能思考并推理出各种可能性。所以很多人认为，创建一种思考的树状结构，赋予 LLM System2的能力让它们更深入地思考问题，反思和重新表述，LLM的答案会更好。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>aigc</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>metagpt</tag>
      </tags>
  </entry>
  <entry>
    <title>基于MetaGPT实现一个订阅智能体：第四章 OSS订阅智能体</title>
    <url>/dev-ai-agent-based-on-metagpt-4/</url>
    <content><![CDATA[1. 前言本章任务：

了解订阅智能体
了解爬虫
了解MetaGPT订阅模块
实现订阅智能体



2. 什么是订阅智能体？MetaGPT提供了Agent的开发能力，Agent可以协助我们解决日常的一些问题，其中一个应用场景就是我们可以让Agent为我们关注某些想关注的信息，当有我们关注的事件发生时，Agent获取信息并进行处理，然后通过一些如邮件、微信、discord等通知渠道将处理后的信息发送给我们，我们将这类Agent称为订阅智能体。
在这里，Agent的Role是以一个“资讯订阅员”的身份为你服务，而Agent中包含的Action则主要有两种：从外界信息源中搜集信息和对搜集得到的信息进行总结。
完成上述任务后，我们可以为这个Agent开发更多额外功能：定时运行的功能和发送到通知渠道的功能。
3. 什么是爬虫？本文中所说的爬虫是可以抓取网页内容的程序工具。想要让Agent获取网络中的信息，需要实现爬虫进行抓取。
实现爬虫时常用的 Python 工具：

aiohttp：使用 aiohttp ，发起网络请求
beautifulsoup：使用 beautifulsoup，从HTML中提取信息

4. MetaGPT订阅模块在MetaGPT中，metagpt.subscription模块提供了SubscriptionRunner类，与metagpt.team.Team类似，SubscriptionRunner类是提供了一个Role的运行方式，基于SubscriptionRunner类，我们可以定时触发运行一个Role，然后将Role的执行输出通知给用户，例如：
import asynciofrom metagpt.subscription import SubscriptionRunnerfrom metagpt.roles import Searcherfrom metagpt.schema import Messageasync def trigger():    while True:        yield Message(&quot;the latest news about OpenAI&quot;)        await asyncio.sleep(3600 * 24)async def callback(msg: Message):    print(msg.content)async def main():    pb = SubscriptionRunner()    await pb.subscribe(Searcher(), trigger(), callback)    await pb.run()asyncio.run(main())

从例子可以知道订阅智能体的实现主要有3个要素，分别是Role、Trigger、Pusher，即智能体本身、触发器、数据推送器。
这里的Role就是我们的Agent的实际代码实现，所以订阅智能体在代码实现上并没有什么特别的地方，可以看到上面的例子实际运行的就是MetaGPT中的Searcher Role，它早就存在了我们的MetaGPT仓库中。
不过需要注意的是，我们虽然不对订阅智能体的Role做限制，但是不是所有的Role都适合用来做订阅智能体，比如MetaGPT软件公司中的几个角色，例如产品经理、架构师、工程师等，因为当给这些Role一个需求输入时，它们的产出往往是类似的，并没有定时执行然后发送给我们的必要。所以从应用的角度出发，订阅智能体的输出应该具有实时性，相同的一个需求描述输入，输出的内容一般是会随着时间的变化而不同，例如新闻资讯、技术前沿进展、热门的开源项目等。
这里的Trigger是指Role运行的触发条件，可以是定时触发或者是某个网站有更新时触发。Trigger是个异步生成器，详情参考 https://peps.python.org/pep-0525/这里的Pusher用于处理Role生成的信息，我们可以将数据发送到微信或者discord。
5. 实现OSS订阅智能体5.1. 本节任务实现一个OSS（Open Source Software）订阅智能体，它要能够从github获取热门开源项目的信息，并进行汇总分析。
5.2. 需求拆解订阅智能体的实现主要有3个要素，分别是Role、Trigger、Pusher。因此，我们可以订阅智能体进行拆解：1、实现一个 OSSWatcher Role：OSS 即 Open source software，我们对OSS 智能体定位是，帮我们关注并分析热门的开源项目，当有相关信息时将信息推送给我们，这里需要确定让 OSS 从哪个网页获取信息2、实现Trigger：指这个OSSWatcher角色运行的触发条件，可以是定时触发或者是某个网站有更新时触发3、实现Pusher：处理OSSWatcher角色运行生成的信息，我们可以将数据发送到微信或者discord
5.3. 网络需求教程涉中需要访问到一些国外的网站，可能会遇到网络问题，因为 aiohttp 默认不走系统代理，所以需要代理配置。MetaGPT中已经提供了GLOBAL_PROXY参数用来表示全局代理配置，教程中遇到使用aiohttp进行请求的地方，都会将代理设置为GLOBAL_PROXY的值。通过在config/key.yaml配置文件中，添加自己代理服务器的配置，以解决网络问题，例如：
GLOBAL_PROXY: http://127.0.0.1:7890  # 改成自己的代理服务器地址

如果配置 GLOBAL_PROXY 无效，可以尝试使用全局代理。
export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890

注意：中国大陆地区无法访问Discord，使用代理也不行，暂未找到解决办法。详情参考how to connect a discord bot through proxy
5.4. 实现OSSWatcher Role实现OSSWatcher的Role前，需要先确定我们要 OSSWatcher 做哪些事情，即需要实现哪些 Action。OSSWatcher可以拆分出两个Action，爬取热门开源项目数据和分析热门开源项目数据。
5.4.1. 爬取数据Action爬虫部分的代码可以借助ChatGPT来辅助开发，询问ChatGPT之前，建议先对html代码进行精简。Prompt参考爬虫解析GitHub数据。
爬取热门开源项目数据 Action ，实现代码 crawl_data.py 
5.4.2. 分析数据Action网页爬取的Action实现后，我们就可以让LLM基于爬取的网页内容进行分析了，这个Action实现比较简单，主要是写提示词。在提示词中我们可以要求LLM从几个角度进行分析，并按照一定的格式进行输出，例如：

今天榜单的整体趋势，例如哪几个编程语言比较热门、最热门的项目是哪些、主要集中在哪些领域
榜单的仓库分类
推荐进一步关注哪些仓库，推荐原因是什么

分析热门开源项目数据 Action，代码文件 analyze_data.py 
5.4.3. 关注者Role热门开源项目关注者 Role，代码文件 oss_watcher.py
5.5. 实现TriggerTrigger即触发器，代表了OSSWatcher角色的执行时机，最简单的触发方式即定时触发。Github Trending不是实时更新的，参考 Do you know when trending repositories on GitHub are usually updated?，可以了解到Github Trending更新时间大约是在 10:00 AM UTC，实测更新时间不是很准时，所以我们可以一天触发一次，选取一个比较适合自己的推送时间即可，比如每天早上9点。
基于aiocron实现Trigger，代码文件 trigger.py
5.6. 实现PusherPusher就是定义了如何处理智能体生成的信息，本示例中将智能体产生的数据发送到Discord和微信。
5.6.1. 配置Discord在Discord开发者面板添加BOT，并将BOT添加到某个服务器中，并获取到Bot的Token，Bot的Token对应接下来代码中的 DISCORD_TOKEN。具体操作步骤参考Creating a Bot Account或者《转发Discord消息到飞书》。
5.6.2. 配置微信之前MetaGPT有尝试通过微信机器人同步不同社区群的消息，不过由于引起了过大的讨论，最终关闭了该功能，当时是使用wechaty实现的，但是使用该方式较为繁琐，而且由于我们的内容是markdown格式的，直接发送微信消息阅读体验较差，所以我们需要寻找合适的微信消息发送方式。公众号可以发送富本文消息，比较符合我们的场景，但是为了个推送的功能，开发个公众号的成本也是比较大，但是如果可以通过已有的一个公众号给我们发送消息，那实现将会非常简单。虽然微信官方没有提供类似的功能，但是已有许多的第三方公众号提供了消息推送的功能，例如server酱、wxpusher、Pushplus等，我们可以选择其中之一，例如wxpusher，它的代码是开源的，也有详细的文档，开发文档参考WxPusher微信推送服务。
参考文档获取appToken，appToken 对应接下来代码中的 WXPUSHER_TOKEN。
5.6.3. Discord和微信Pusher调用Discord API发送消息到Discord，调用WXPusher发送消息到微信，代码文件 pusher.py
5.7. 运行代码export DISCORD_TOKEN=xxx.xxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxexport DISCORD_CHANNEL_ID=xxxxxxexport WXPUSHER_TOKEN=AT_xxxxxxxxxxxxxxxxxxxxxxexport WXPUSHER_UIDS=UID_xxxxxxxxxxxxxxxxxxxxxxxxxpython main.py

5.8. 完整代码本节完整代码，请参阅ai-agent-based-on-metagpt/oss-agent关于代码的解释，请参阅《MetaGPT智能体开发入门》教程，写的非常详细了。
6. 本章作业根据上面教程的介绍，你已经学会了如何针对具体场景开发一个实用的资讯收集助手。现在，你可以试着完成一个能订阅自己感兴趣的资讯的Agent。

根据前面你所学习的爬虫基本知识（如果你对写爬虫代码感到不熟练，使用GPT帮助你），为你的Agent自定义两个获取资讯的Action类
Action 1：独立实现对Github Trending页面的爬取，并获取每一个项目的 名称、URL链接、描述
Action 2：独立完成对Huggingface Papers页面的爬取，先获取到每一篇Paper的链接（提示：标题元素中的href标签），并通过链接访问标题的描述页面（例如：https://huggingface.co/papers/2312.03818 ），在页面中获取一篇Paper的 标题、摘要


参考第三章【实现一个更复杂的Agent：技术文档助手】的内容，重写有关方法，使你的Agent能自动生成总结内容的目录，然后根据二级标题进行分块，每块内容做出对应的总结，形成一篇资讯文档
自定义Agent的SubscriptionRunner类，独立实现Trigger、Pusher的功能，让你的Agent定时为通知渠道发送以上总结的资讯文档（尝试实现邮箱发送的功能，这是加分项）

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>aigc</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>metagpt</tag>
        <tag>discord</tag>
      </tags>
  </entry>
  <entry>
    <title>基于MetaGPT实现一个订阅智能体：第五章 ActionNode</title>
    <url>/dev-ai-agent-based-on-metagpt-5/</url>
    <content><![CDATA[1. 为什么要采用ActionNode的数据结构？随着Agent需求的复杂化，SOP也会复杂化。动作的执行不再只是线性，需要支持CoT、ToT、GoT等技术，使用 ActionNode 可以实现统一抽象。
之前的Action，我们是用结构化的prompt形式，用markdown文本输入一个很长的文本信息，然后在action的run方法中解析LLM返回的信息，这种方式的灵活性是比较差的。
而对于LLM的请求，实际上都可以拆解为结构化的填槽：指定问题的上下文、需求、目的、输出格式。对于写出一个有 SOP  且完整的文档，人类的撰写方式实际上是分解文档的子标题，对子标题进行动作的编排以及填槽。而通俗的来看SOP应该在被规划进行逐一执行，而不是硬编码在prompt当中要求llm一次性进行返回。我们平常用llm写文章的时候就会发现生成的回复往往是各个章节均衡的。对于写代码而言，llm往往能够生成一个60-75行左右的详细代码，而对于复杂的系统级需求，它生成的代码往往只能提供一个结构和部分注释。
ActionNode，实现了结构化填槽，使LLM请求和返回都更加标准化。
参考文档：

为什么要采用Action node的数据结构
CoT、CoT-SC、ToT和GoT的简析



2. ActionNode基础知识在MG框架0.5版本中，新增加了ActionNode类，为Agent的动作执行提供更强的能力。
ActionNode可以被视为一组动作树，根据类内定义，一个动作树的父节点可以访问所有的子动作节点；也就是说，定义了一个完整的动作树之后，可以从父节点按树的结构顺序执行每一个子动作节点。因此，动作的执行也可以突破0.4版本框架中，需要在Role的_react内循环执行的限制，达到更好的CoT效果。
ActionNode仍然需要基于Action类进行构建，在定义一个ActionNode动作树之后，需要将该动作树作为参数赋予一个Action子类，在并将其输入到Role中作为其动作。在这个意义上，一个ActionNode动作树可以被视为一个内置CoT思考的Action。
同时，在ActionNode基类中，也配置了更多格式检查和格式规范工具，让CoT执行过程中，内容的传递更加结构化。这也服务于让MetaGPT框架生成更好、更长、更少Bug的代码这一目的。
3. ActionNode源码ActionNode具体实现，参见metagpt/actions/action_node.py
SIMPLE_TEMPLATE = &quot;&quot;&quot;## context&#123;context&#125;-----## format example&#123;example&#125;## nodes: &quot;&lt;node&gt;: &lt;type&gt;  # &lt;comment&gt;&quot;&#123;instruction&#125;## constraint&#123;constraint&#125;## actionFill in the above nodes based on the format example.&quot;&quot;&quot;class ActionNode:    &quot;&quot;&quot;ActionNode is a tree of nodes.&quot;&quot;&quot;    mode: str    # Action Context    context: str  # all the context, including all necessary info    llm: BaseGPTAPI  # LLM with aask interface    children: dict[str, &quot;ActionNode&quot;]    # Action Input    key: str  # Product Requirement / File list / Code    expected_type: Type  # such as str / int / float etc.    # context: str  # everything in the history.    instruction: str  # the instructions should be followed.    example: Any  # example for In Context-Learning.    # Action Output    content: str    instruct_content: BaseModel    def __init__(self, key: str, expected_type: Type, instruction: str, example: str, content: str = &quot;&quot;,                 children: dict[str, &quot;ActionNode&quot;] = None):        self.key = key        self.expected_type = expected_type        self.instruction = instruction        self.example = example        self.content = content        self.children = children if children is not None else &#123;&#125;    def compile(self, context, to=&quot;json&quot;, mode=&quot;children&quot;, template=SIMPLE_TEMPLATE) -&gt; str:        &quot;&quot;&quot;        mode: all/root/children            mode=&quot;children&quot;: 编译所有子节点为一个统一模板，包括instruction与example            mode=&quot;all&quot;: NotImplemented            mode=&quot;root&quot;: NotImplemented        &quot;&quot;&quot;        # FIXME: json instruction会带来格式问题，如：&quot;Project name&quot;: &quot;web_2048  # 项目名称使用下划线&quot;,        self.instruction = self.compile_instruction(to=&quot;markdown&quot;, mode=mode)        self.example = self.compile_example(to=to, tag=&quot;CONTENT&quot;, mode=mode)        prompt = template.format(            context=context, example=self.example, instruction=self.instruction, constraint=CONSTRAINT        )        return promptclass Action(ABC):    def __init__(self, name: str = &quot;&quot;, context=None, llm: LLM = None):        self.name: str = name        if llm is None:            llm = LLM()        self.llm = llm        self.context = context        self.prefix = &quot;&quot;  # aask*时会加上prefix，作为system_message        self.profile = &quot;&quot;  # FIXME: USELESS        self.desc = &quot;&quot;  # for skill manager        self.nodes = ...

这里重点学习一下ActionNode中的结构化填槽。SIMPLE_TEMPLATE 定义了一个结构化的包含槽位的模板：问题的上下文、需求、目的、输出格式。ActionNode初始化的时候，传入一些参数，之后调用compile方法的时候，再传入一些参数，生成完整的prompt。Action中的一个变量（nodes）是ActionNode，以此关联。也可以在Action直接调用ActionNode，不必作为Action的变量。
4. 本章作业《第三章 MetaGPT框架组件介绍》中基于Action实现了一个技术文档助手，本章作业是基于ActionNode实现一个功能相同的技术文档助手。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>aigc</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>metagpt</tag>
      </tags>
  </entry>
  <entry>
    <title>基于MetaGPT实现一个订阅智能体：第六章 订阅智能体进阶</title>
    <url>/dev-ai-agent-based-on-metagpt-6/</url>
    <content><![CDATA[1. OSS订阅智能体的思考与优化前面的教程中，沃恩实现了OSS订阅智能体，将Github Trending的信息由智能体总结之后，通过discord或者微信的渠道发送给了我们。
订阅智能体的实现过程，要分别完成SubscriptionRunner运行需要的三个要素：Role、Trigger、Pusher。Trigger是触发器，我们实现了一个定时触发器；Pusher是推送器，我们实现了将消息发送到discord/微信；Pusher，Trigger这二者比较固定，实现后可以复用。比较特殊的是Role的实现，订阅智能体设计思路基本就是源数据获取+信息提取+总结分析，我们的OSS订阅智能体就是：aiohttp爬取Github Trending -&gt; bs4解析html提取榜单信息 -&gt; LLM总结分析。
通过自己写智能体的方式虽然完成了订阅智能体的功能，但是这个智能体的作用是被局限在某一特定领域的，当我们需要订阅另外一个数据源的分析结果时，我们需要手动再写一个Role。基于上面的分析，我们知道，Role并不能像Pusher，Trigger这样通用，这就会让我们的订阅智能体实现起来需要较多的开发成本。那有没有什么办法让Role变得通用呢？可以有两个思路：思路一：我们实现一个智能体，它可以爬取我们要求的任意网站，然后进行数据的分析，最后再总结。思路二：实现一个可以写订阅智能体代码的智能体，这个智能体可以浏览我们需要爬取的网页，写爬虫和网页信息提取的代码，生成Role，甚至根据我们的订阅需求，直接完整调用SubscriptionRunner，实现我们的订阅需求。


2. 通用订阅智能体设计思路2.1. 思路一：爬取任意网站的智能体实现一个可以爬取我们要求的任意网站的智能体，首先要有一个通用的网页数据获取方式。
在前面的OSS订阅智能体中，我们使用了aiohttp成功爬取了Github Trending的数据，但如果对爬虫有进一步了解的话，就知道使用这种传统的爬取方式会有一定的限制，在面对取动态页面，需要执行JavaScript代码或绕过简单的反爬措施等需求时，都需要花费更高的成本。相比之下，我们可以使用浏览器自动化的方式来爬取网页，从而让我们的网页爬取实现更加简单，python中也有很多的浏览器自动化工具，例如Selenium、Playwright，浏览器自动化是通过编程方式控制和操作浏览器的过程，即通过浏览器直接打开网页，所以正常人可以浏览的页面，基本都可以通过这个方式进行爬取。
初步来看，浏览器自动化的爬取方式可以满足我们的通用爬取需求。aiohttp使用较简单，主打高性能场景，但是我们的订阅智能体爬取需求，并不需要太高的性能，因为一般订阅的触发时间间隔也比较长，一次运行也不需要爬取非常多数量的网页；浏览器自动化的爬取方式性能虽然较差，但是也完全能够满足我们的需求，同时又可以覆盖较多的爬虫场景，所以这边使用浏览器自动化的爬取方式是非常合适的。
根据上面的分析，我们可以有一种较为通用的爬取网页的方式。但是不幸的是，我们并不能找到一个通用的网页特定信息提取方式，比如Github Trending的数据爬取之后，在没人工和大模型的介入的前提下，我们并不能从网页中提取出今天的Github Trending列表。当然，我们也可以选择让大模型来帮我们先提取信息，但是，明显这会需要消耗很多token，因为一般情况下，网页中会有大量的与我们主题无关的信息，所以使用这种方式，很大部分的token都是浪费了的，特别地，因为订阅智能体是定时触发，那么这个成本是直线上升的。那有没有什么方式可以降低这个成本呢？有的，那就是我们的思路二：会写订阅智能体的智能体。
2.2. 思路二：会写订阅智能体的智能体我们在前面的OSS订阅智能体实现过程中，让chatgpt帮助我们生成了Github Trending的爬取代码，我们发现，当我们把html内容和提取需求提供给大模型，大模型是可以写出正确的爬虫代码的，所以让大模型帮我们写爬虫代码是可行的。另外需要做的主要是需要解决html内容过长的问题，一是减少token消耗，二是防止上下文过长导致请求失败。
因此，通过以上两种技术实现路线分析，我们需要通过思路二来实现我们的通用订阅智能体。
接下来，我们主要探讨一下思路二的实现路线。1、上述网页的爬取的结论仍然适用，使用浏览器自动化的方式爬取网页2、让大模型写从html中提取用户需要的数据，把获取到网页内容提取的代码和浏览器自动化的方式爬取网页的代码进行结合，就可以得到爬取指定网页信息的Action3、将提取后的数据，让大模型进行处理和分析，这就得到网页数据分析的Action4、将这两个Action组合，就可以得到一个特定网页的Watcher Role
当然，这里还有很多个问题没解决，例如，待爬取的网页URL、网页数据提取需求、分析需求等怎么获取，写爬虫代码时的html该如何简化，Role实现后该如何运行等，我们将在接下来一个个解决。
3. Role和Action设计为了实现通用订阅智能体，我们计划设计两个Role，分别是订阅助手和爬虫工程师。
整个工作流程如下：1、用户给出订阅某个站点信息的需求。2、订阅助手把用户需求转换为标准的订阅格式。对应Action ParseSubRequirement3、爬虫工程师基于Playwright获取网页，并且调用LLM编写解析网页的代码。对应Action WriteCrawlerCode4、订阅助手基于Playwright获取网页，调用爬虫工程师写的代码解析网页，解析后的内容调用LLM进行总结。对应Action ParseSubRequirement5、总结后的内容，推送到微信或Discord。
4. 完整代码本章完整代码，请参阅ai-agent-based-on-metagpt/universal-subscription-agent关于代码的解释，请参阅《MetaGPT智能体开发入门》教程，写的非常详细了。
注意需要安装playwright依赖：
pip install &#x27;metagpt[playwright]&#x27;

5. 本章作业1、根据“通用订阅智能体设计思路”中提供的思路一，尝试实现思路一，即使用llm提取出需要的信息而不是写爬虫代码。2、目前，订阅智能体是通过RunSubscription运行的，即RunSubscription这个action，不仅创建了订阅智能体代码，并启动了SubscriptionRunner，这会让我们的RunSubscription一直无法退出，请尝试将二者分离，即从RunSubscription分离出AddSubscriptionTask的action，并且让SubscriptionRunner单独运行（可以是同一个进程也可以是不同的进程）。
测试用例：
从36kr创投平台融资快报 - 36氪爬取所有初创企业融资的信息，获取标题，链接， 时间，总结今天的融资新闻，然后在14:55发送给我从https://huggingface.co/papers获取今天推荐的论文的标题和链接，整理成表格，分析这些论文主要主题，然后在下午5点26分发送给我从 https://www.qbitai.com/category/资讯 获取量子位今天推送的文章，总结今天的主要资讯，然后在每天下午5点34分发送给我从 https://www.jiqizhixin.com/ 获取机器之心今天推送的文章，总结今天的主要资讯，然后在每天下午5点38分发送给我


]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>aigc</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>metagpt</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：限制用户命令</title>
    <url>/dev-linux-limit-user-command/</url>
    <content><![CDATA[1. 前言在Linux系统中，为了增强安全性，避免不适当的使用，有时需要限制特定用户或用户组的命令行访问。
常用的限制方法包括 profile限制、authorized_keys限制、rbash限制，本文我们就来学习一下这些方法。
参考文档：

linux 限制用户命令方法
受限的 bash – rbash
rbash逃逸大全
ghantoos/lshell
《shell命令之sudo》



2. profile限制当一个shell登录时，它将读取 /etc/profile 文件中的全部环境变量，然后再执行 /etc/profile.d/ 目录中的所有 .sh 结尾的脚本。因此，我们可以定义一个 /etc/profile.d/login.sh 脚本，用来限制用户的PATH，以此限制用户命令。
下面是一个 login.sh 示例，限制用户只能使用 ssh 命令。
#!/bin/bashuser=`whoami`if [ &quot;$&#123;user&#125;&quot; != &quot;root&quot; ];then    echo -e &quot;** \e[01;33m current user is: \e[01;31m $LOGNAME\e[00m  ** \e[00m&quot;    mkdir -p $HOME/bin    rm -f $HOME/bin/*    ln -s /usr/bin/ssh $HOME/bin/    echo &#x27;PATH=$HOME/bin&#x27; &gt; $HOME/.restricted_profile    echo &#x27;export PATH&#x27; &gt;&gt; $HOME/.restricted_profile    chown $&#123;user&#125;:$&#123;user&#125; $HOME/.restricted_profile    exec bash --restricted --noprofile --rcfile $HOME/.restricted_profilefi

但是，这种方法只能限制用户登录后的命令，如果用户远程执行命令，就限制不住了。比如：
ssh -t work@192.168.55.100 &quot;bash&quot;

更多关于登录session和非登录session的内容，可以参考《bash_profile和bashrc的区别》
3. authorized_keys限制想要限制非登录session，一种可选的方案是使用 authorized_keys 。
1、authorized_keys 中配置 public key
command=&quot;/usr/bin/ssh $SSH_ORIGINAL_COMMAND@tiger.voidking.com -p 2222&quot; ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9...

2、用户使用ssh
ssh -t work@192.168.55.100 &quot;haojin&quot;

详情参考：《shell命令之ssh》
4. rbash限制4.1. rbash简介rbash（restricted bash）是 bash 的一个版本，实际上 rbash 是 bash 的一个软连接，能够限制用户的命令。
具体限制包括：

不能执行 cd 命令。只能在当前目录下工作，其它目录都无法进入。
不能修改 PATH、ENV、HOME、SHELL等环境变量的值。
不能执行包含 / 字符的程序。例如，我们无法执行 /usr/bin/uname 或 ./uname 命令或程序。但是，我们依然可以运行 uname 命令。也就是说，当前目录以外的程序或命令是无法被执行的。
不能使用 &gt; 和 &gt;&gt; 等重定向运算符进行重定向输出。
不能在脚本中或其它方式关闭 Restricted Shell 模式。
不能使用 set+r 或 set+o restricted 等方式退出 Restricted Shell 模式。

4.2. rbash限制只能执行ssh1、创建一个用户
# ls -s /bin/bash /bin/rbashuseradd -s /bin/rbash -m rusertouch /home/ruser/&#123;.bashrc,.bash_profile&#125;chown -R root:ruser /home/ruser/&#123;.bashrc,.bash_profile&#125;chmod 640 /home/ruser/&#123;.bashrc,.bash_profile&#125;mkdir -p /home/ruser/bin

2、限制用户只能执行ssh
echo &quot;export PATH=/home/ruser/bin&quot; &gt;&gt; /home/ruser/.bash_profileln -s /usr/bin/ssh /home/ruser/bin/ssh

但是，rbash有很多逃逸方法，比如一个简单的bash命令就可以逃逸，无论是session登录还是非session登录。
ssh -t work@192.168.55.100 &quot;bash&quot;

5. 限制用户命令实战5.1. 需求描述
jumpserver只能内网使用，但是有少量外网用户也需要使用jumpserver。
为避免增加了风险，jumpserver不能放通到外网。

5.2. 解决方案外网可以访问的主机A放通一个ssh命令用于登录，主机A可以连通到jumpserver主机。
5.3. Step1：制作并运行ssh镜像1、准备一个限制登录脚本 ssh.sh
#!/bin/bashecho &quot;`date +%Y%m%d%H%M%S` ssh script was executed by key: $SSH_KEY&quot; &gt;&gt; /opt/ssh.logssh -o StrictHostKeyChecking=no -o HostKeyAlgorithms=+ssh-rsa $SSH_ORIGINAL_COMMAND@jump.voidking.com -p 2222

chmod a+x ssh.sh

2、定义 Dockerfile
# Using debian as baseFROM debian:bookworm-slim# MaintainerMAINTAINER voidking &quot;voidking@qq.com&quot;# Update and install opensshRUN sed -i &#x27;s/deb.debian.org/mirrors.ustc.edu.cn/g&#x27; /etc/apt/sources.list.d/debian.sourcesRUN apt-get update &amp;&amp; \    apt-get install -y --no-install-recommends openssh-serverRUN cd /etc/ssh &amp;&amp; ssh-keygen -A &amp;&amp; mkdir -p /run/sshd# Disable SFTP and password loginRUN sed -i &#x27;/sftp/c\Subsystem sftp /bin/false&#x27; /etc/ssh/sshd_config &amp;&amp; \    sed -i &#x27;/PasswordAuthentication/c\PasswordAuthentication no&#x27; /etc/ssh/sshd_config &amp;&amp; \    echo &#x27;PermitUserEnvironment yes&#x27; &gt;&gt; /etc/ssh/sshd_configCOPY ssh.sh /opt/ssh.shRUN chmod 777 -R /opt# Add new user# RUN useradd -m work &amp;&amp; echo &quot;work:work123&quot; | chpasswdRUN useradd -m work &amp;&amp; \    mkdir -p /home/work/.ssh &amp;&amp; \    chown work:work /home/work/.ssh &amp;&amp; \    touch /home/work/.ssh/authorized_keys &amp;&amp; \    chown work:work /home/work/.ssh/authorized_keys &amp;&amp; \    chmod 600 /home/work/.ssh/authorized_keys# Set the entry pointENTRYPOINT [&quot;/usr/sbin/sshd&quot;, &quot;-D&quot;]

3、打包镜像
docker build -t voidking/ssh:debian-bookworm-slim .

4、运行镜像
docker run -d --name ssh \    -p 10022:22 \    -v /etc/timezone:/etc/timezone:ro \    -v /etc/localtime:/etc/localtime:ro    voidking/ssh:debian-bookworm-slim

5.4. Step2：配置public key配置外部用户的 public key 到ssh容器中 work用户的authorized_keys，以使得外部用户有权限执行 command 中指定的 ssh.sh 脚本。
docker exec -it ssh /bin/bashecho &#x27;environment=&quot;SSH_KEY=haojin&quot;,command=&quot;/opt/ssh.sh $SSH_ORIGINAL_COMMAND&quot; ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9...&#x27; &gt;&gt; /home/work/.ssh/authorized_keys

5.5. Step3：测试登录ssh -t work@192.168.55.100 -p 10022 &quot;haojin&quot;



]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>基于MetaGPT实现一个订阅智能体：第三章 MetaGPT框架组件介绍</title>
    <url>/dev-ai-agent-based-on-metagpt-3/</url>
    <content><![CDATA[1. 前言本章任务：

了解MetaGPT中Agent的概念
实现一个单动作Agent
实现一个多动作Agent
实现一个复杂Agent：技术文档助手



2. MetaGPT中Agent的概念MetaGPT中对于智能体的定义如下：

智能体（Agent） = 大语言模型（LLM）+ 观察 + 思考 + 行动 + 记忆

这个公式概括了智能体的功能本质。为了理解每个组成部分，让我们将其与人类进行类比：

大语言模型（LLM）：LLM作为智能体的“大脑”部分，使其能够处理信息，从交互中学习，做出决策并执行行动。
观察：这是智能体的感知机制，使其能够感知其环境。智能体可能会接收来自另一个智能体的文本消息、来自监视摄像头的视觉数据或来自客户服务录音的音频等一系列信号。这些观察构成了所有后续行动的基础。
思考：思考过程涉及分析观察结果和记忆内容并考虑可能的行动。这是智能体内部的决策过程，其可能由LLM进行驱动。
行动：这些是智能体对其思考和观察的显式响应。行动可以是利用 LLM 生成代码，或是手动预定义的操作，如阅读本地文件。此外，智能体还可以执行使用工具的操作，包括在互联网上搜索天气，使用计算器进行数学计算等。
记忆：智能体的记忆存储过去的经验。这对学习至关重要，因为它允许智能体参考先前的结果并据此调整未来的行动。

在MetaGPT中定义的一个agent运行示例如下：

一个agent在启动后他会观察自己能获取到的信息，加入自己的记忆中
下一步进行思考，决定下一步的行动，也就是从Action1，Action2，Action3中选择执行的Action
决定行动后，紧接着就执行对应行动，得到这个环节的结果

而在MetaGPT内 Role 类是智能体的逻辑抽象。一个 Role 能执行特定的 Action，拥有记忆、思考并采用各种策略行动。基本上，它充当一个将所有这些组件联系在一起的凝聚实体。
MetaGPT中对于多智能体的定义如下：

多智能体 = 智能体 + 环境 + SOP + 评审 + 路由 + 订阅 + 经济

3. 实现一个单动作Agent3.1. 本节任务利用MetaGPT框架实现一个生成代码的 Role，名为SimpleCoder。基于这个Role创建的Agent能够根据我们的需求来生成代码。
3.2. 需求分析要实现一个 SimpleCoder ，我们需要分析这个 Role 它需要哪些能力

能够接受用户输入的需求
记忆用户的需求
根据自己已知的信息和需求来编写代码

3.3. 实现Role的思路要自己实现一个最简单的Role，只需要重写Role基类的 _init_ 与 _act 方法
1、在 _init_ 方法中，声明 Role 的name（名称）profile（人设）2、使用 self._init_action 函数为Role配备期望的动作3、在_act方法中，我们需要编写智能体具体的行动逻辑，智能体将从最新的记忆中获取人类指令，运行配备的动作，最后返回一个完整的消息。
在 MetaGPT 中，类 Action 是动作的逻辑抽象。用户可以通过简单地调用 self._aask 函数 来获取 LLM 的回复。self._aask 函数如下：
async def _aask(self, prompt: str, system_msgs: Optional[list[str]] = None) -&gt; str:    &quot;&quot;&quot;Append default prefix&quot;&quot;&quot;    if not system_msgs:        system_msgs = []    system_msgs.append(self.prefix)    return await self.llm.aask(prompt, system_msgs)

它将调用你预设好的 LLM 来根据输出的提示词生成回答。
3.4. 实现SimpleWriteCode动作simple_write_code.py 文件中，实现SimpleWriteCode的动作
import refrom metagpt.actions import Actionclass SimpleWriteCode(Action):    PROMPT_TEMPLATE = &quot;&quot;&quot;    Write a python function that can &#123;instruction&#125; and provide two runnnable test cases.    Return ```python your_code_here ``` with NO other texts,    your code:    &quot;&quot;&quot;        def __init__(self, name=&quot;SimpleWriteCode&quot;, context=None, llm=None):        super().__init__(name, context, llm)    async def run(self, instruction: str):        prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)        rsp = await self._aask(prompt)        code_text = SimpleWriteCode.parse_code(rsp)        return code_text    @staticmethod    def parse_code(rsp):        pattern = r&#x27;```python(.*)```&#x27;        match = re.search(pattern, rsp, re.DOTALL)        code_text = match.group(1) if match else rsp        return code_text

SimpleWriteCode 继承自 Action，重写了__init__ 和 run 方法。__init__ 方法用来初始化这个Action，而run方法决定了我们对传入的内容到底要做什么样的处理。在__init__方法中，我们声明了这个这个动作的名称，行动前的一些前置知识context，要使用的llm。在run方法中，我们需要声明当采取这个行动时，我们要对传入的内容做什么样的处理。run运行完成，返回代码内容。
3.5. 实现SimpleCoder角色simple_coder.py 文件中，实现SimpleCoder角色
from metagpt.roles import Rolefrom metagpt.schema import Messagefrom metagpt.logs import loggerfrom simple_write_code import SimpleWriteCodeclass SimpleCoder(Role):    def __init__(        self,        name: str = &quot;Alice&quot;,        profile: str = &quot;SimpleCoder&quot;,        **kwargs,    ):        super().__init__(name, profile, **kwargs)        self._init_actions([SimpleWriteCode])    async def _act(self) -&gt; Message:        logger.info(f&quot;&#123;self._setting&#125;: ready to &#123;self._rc.todo&#125;&quot;)        todo = self._rc.todo # todo will be SimpleWriteCode()        msg = self.get_memories(k=1)[0] # find the most recent messages        code_text = await todo.run(msg.content)        msg = Message(content=code_text, role=self.profile, cause_by=type(todo))        return msg

SimpleCoder继承自 Role，重写了 __init__ 和 _act 方法。__init__ 方法用来初始化这个Role，而_act方法决定了当这个角色行动时它的具体行动逻辑在 __init__ 方法中声明了这个Role的name（名称），profile（人设），并且调用self._init_actions配备刚才写好的Action SimpleWriteCode调用self._init_actions之后，SimpleWriteCode就会被加入到代办self._rc.todo中。在_act方法中，智能体调用todo.run()方法，执行Action。
当调用Action时，我们需要获取用户的输入来作为instruction传递给Action，这里就涉及到我们该如何获取用户之前传递给agent的信息，在MetaGPT中，当用户与Agent交互时，所有的内容都会被存储在Agent自有的Memory中。在MetaGPT中，Memory类是智能体的记忆的抽象。当初始化时，Role初始化一个Memory对象作为self._rc.memory属性，它将在之后的_observe中存储每个Message，以便后续的检索。简而言之，Role的记忆是一个含有Message的列表。
当需要获取记忆时（获取LLM输入的上下文），我们可以使用self.get_memories。函数定义如下：
def get_memories(self, k=0) -&gt; list[Message]:    &quot;&quot;&quot;A wrapper to return the most recent k memories of this role, return all when k=0&quot;&quot;&quot;    return self._rc.memory.get(k=k)

在SimpleCoder中，我们只需要获取最近的一条记忆，也就是用户下达的需求，将它传递给Action即可。
然后我们调用LLM，拿到LLM的输出，最后我们将拿到的信息封装为MetaGPT中通信的基本格式 Message 返回。
3.6. 运行单动作Agent1、定义一个入口脚本 main.py
import asynciofrom metagpt.logs import loggerfrom simple_coder import SimpleCoderasync def main():    msg = &quot;write a function that calculates the sum of a list&quot;    role = SimpleCoder()    logger.info(msg)    result = await role.run(msg)    logger.info(result)asyncio.run(main())

2、配置好LLM（第一章中已完成）参考《基于MetaGPT实现一个订阅智能体：第一章 前期准备》，给MetaGPT配置一个LLM。
3、运行Agent
python main.py

以上，我们就实现了一个简单的单动作Agent。
4. 实现一个多动作Agent4.1. 为什么需要多动作Agent？我们注意到一个智能体能够执行一个动作，但如果只有这些，实际上我们并不需要一个智能体。通过直接运行动作本身（输入提示词调用LLM），我们可以得到相同的结果。智能体的力量，或者说Role抽象的惊人之处，在于动作的组合（以及其他组件，比如记忆，但我们将把它们留到后面的部分）。通过连接动作，我们可以构建一个工作流程，使智能体能够完成更复杂的任务。
4.2. 本节任务假设现在我们不仅希望用自然语言编写代码，而且还希望生成的代码立即执行，那么一个拥有多个动作的智能体可以满足我们的需求。让我们称之为RunnableCoder，一个既写代码又立即运行的Role。我们需要两个Action：SimpleWriteCode 和 SimpleRunCode
4.3. 实现SimpleWriteCode动作与上文中 simple_write_code.py 一致，不用变化。
4.4. 实现SimpleRunCode动作一个动作可以调用LLM，也可以不调用LLM。在SimpleRunCode的情况下，不涉及调用LLM。我们只需启动一个子进程来运行代码并获取结果。
在Python中，我们通过标准库中的subprocess包来fork一个子进程，并运行一个外部的程序。
simple_run_code.py 文件中，实现SimpleRunCode的动作
import subprocessfrom metagpt.actions import Actionfrom metagpt.logs import loggerclass SimpleRunCode(Action):    def __init__(self, name=&quot;SimpleRunCode&quot;, context=None, llm=None):        super().__init__(name, context, llm)    async def run(self, code_text: str):        result = subprocess.run([&quot;python3&quot;, &quot;-c&quot;, code_text], capture_output=True, text=True)        code_result = result.stdout        logger.info(f&quot;&#123;code_result=&#125;&quot;)        return code_result

4.5. 实现RunnableCoder角色runnable_coder.py 文件中，实现RunnableCoder角色
from metagpt.roles import Rolefrom metagpt.schema import Messagefrom metagpt.logs import loggerfrom simple_write_code import SimpleWriteCodefrom simple_run_code import SimpleRunCodeclass RunnableCoder(Role):    def __init__(        self,        name: str = &quot;Alice&quot;,        profile: str = &quot;RunnableCoder&quot;,        **kwargs,    ):        super().__init__(name, profile, **kwargs)        self._init_actions([SimpleWriteCode, SimpleRunCode])        self._set_react_mode(react_mode=&quot;by_order&quot;)    async def _act(self) -&gt; Message:        logger.info(f&quot;&#123;self._setting&#125;: 准备 &#123;self._rc.todo&#125;&quot;)        todo = self._rc.todo        msg = self.get_memories(k=1)[0]         result = await todo.run(msg.content)        msg = Message(content=result, role=self.profile, cause_by=type(todo))        self._rc.memory.add(msg)        return msg


用 self._init_actions 初始化所有 Action用 self._set_react_mode 方法来设定我们action执行的先后顺序。将 react_mode 设置为 “by_order”，这意味着 Role 将按照 self._init_actions 中指定的顺序执行 Action。_act 定义每个action执行前后的处理逻辑。
此外，RunnableCoder继承自Role的run方法也很关键，它使得多个action都能被调用。
async def run(self, with_message=None):    &quot;&quot;&quot;Observe, and think and act based on the results of the observation&quot;&quot;&quot;    if with_message:        msg = None        if isinstance(with_message, str):            msg = Message(with_message)        elif isinstance(with_message, Message):            msg = with_message        elif isinstance(with_message, list):            msg = Message(&quot;\n&quot;.join(with_message))        if not msg.cause_by:            msg.cause_by = UserRequirement        self.put_message(msg)    if not await self._observe():        # If there is no new information, suspend and wait        logger.debug(f&quot;&#123;self._setting&#125;: no news. waiting.&quot;)        return    rsp = await self.react()    # Reset the next action to be taken.    self._rc.todo = None    # Send the response message to the Environment object to have it relay the message to the subscribers.    self.publish_message(rsp)    return rsp

4.6. 运行多动作Agent1、定义一个入口脚本 main.py
import asynciofrom metagpt.logs import loggerfrom runnable_coder import RunnableCoderasync def main():    msg = &quot;write a function that calculates the sum of a list&quot;    role = RunnableCoder()    logger.info(msg)    result = await role.run(msg)    logger.info(result)asyncio.run(main())

2、配置好LLM（第一章中已完成）参考《基于MetaGPT实现一个订阅智能体：第一章 前期准备》，给MetaGPT配置一个LLM。
3、运行Agent
python main.py

以上，我们就实现了一个多动作Agent。本节的完整代码，请参阅ai-agent-based-on-metagpt/muliti-action-agent。
5. 实现一个复杂Agent：技术文档助手5.1. 本节任务怎么让大模型为我们写一篇技术文档？可能想到的是，我们告诉大模型：“请帮我生成关于Mysql的技术文档”，他可能很快地就能帮你完成这项任务，但是受限于大模型自身的token限制，我们无法实现让他一次性就输出我们希望的一个完整的技术文档。当然我们可以将我们的技术文档拆解成一个一个很小的需求，然后一个一个的提问，但是这样来说不仅费时，而且还需要人工一直去跟他交互，比较麻烦。而使用智能体，就可以解决这个问题。
5.2. 需求分析因为token限制的原因，我们先通过 LLM 大模型生成教程的目录，再对目录按照二级标题进行分块，对于每块目录按照标题生成详细内容，最后再将标题和内容进行拼接，解决 LLM 大模型长文本的限制问题。
5.3. 需求实现1、实现 WriteDirectory 动作，根据用户需求生成文章大纲2、实现 WriteContent 动作，根据传入的子标题来生成内容3、实现 TutorialAssistant 角色，重写_think、_act、_react方法，添加角色特有方法 _handle_directory
本节完整代码，请参阅ai-agent-based-on-metagpt/doc-agent。关于代码的解释，请参阅《MetaGPT智能体开发入门》教程，写的非常详细了。
6. 本章作业经过上面的学习，我想你已经对 MetaGPT 的框架有了基本了解，现在我希望你能够自己编写这样一个 agent

这个 Agent 拥有三个动作 打印1 打印2 打印3（初始化时 init_action([print,print,print])）
重写有关方法（请不要使用act_by_order，我希望你能独立实现）使得 Agent 顺序执行上面三个动作
当上述三个动作执行完毕后，为 Agent 生成新的动作 打印4 打印5 打印6 并顺序执行，（之前我们初始化了三个 print 动作，执行完毕后，重新 init_action([…,…,…])，然后顺序执行这个新生成的动作列表)

如果完成上面的任务，那这次作业已经可以算完成了，这个作业可以用多种思路去解决，比如我可以直接写死一整套的逻辑，甚至都不需要引入llm来完成这个工作，我希望大家通过这个作业来感受 MetaGPT 中 Agent 的行动逻辑，run-&gt;react-&gt;think-&gt;act 的这样一个过程，但你也可以试着在中间的某个环节中加入llm的交互，来尝试减少硬编码的工作，就像【实现一个更复杂的Agent：技术文档助手】中，我们实际上是让llm帮我们设计了 action 列表的内容。你也可以在此基础上做出更多的尝试，关于这个 Agent 我们还有更多可以思考的地方。
目前为止我们设计的所有思考模式都可以总结为是链式的思考（chain of thought），能否利用 MetaGPT 框架实现树结构的思考（tree of thought）图结构的思考（graph of thought）？试着实现让 ai 生成树结构的动作列表，并按照树的遍历方式执行他们，如果你实现，这将是加分项。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>aigc</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>metagpt</tag>
      </tags>
  </entry>
  <entry>
    <title>GitHub Actions实现PyPI发版</title>
    <url>/dev-github-actions-pypi-release/</url>
    <content><![CDATA[1. 前言本文中，我们学习在GitHub Actions中实现PyPI发版。具体来说，就是在GitHub Actions中实现打包Python软件包，并且上传软件包到 pypi.org 站点。
相关文档：

《Python软件仓库PyPI》
《使用Docker安装配置PyPI Server》



2. GitHub Secret配置GitHub 项目仓库中，添加一个名为 PYPI_API_TOKEN 的 secret ，它的值是从 pypi.org 获取到的API TOKEN。这个secret，使得 workflow 有权限向 pypi.org 上传Python软件包。
3. GitHub Actions新建一个workflow，例如 .github/workflows/build-package.yaml
内容为：
name: Build and upload python packageon:  release:    types: [created]jobs:  deploy:    runs-on: ubuntu-latest    steps:    - uses: actions/checkout@v4    - name: Set up Python      uses: actions/setup-python@v4      with:        python-version: &#x27;3.9&#x27;        cache: &#x27;pip&#x27;    - name: Install dependencies      run: |        python -m pip install --upgrade pip        pip install -r requirements.txt        pip install -e.        pip install setuptools wheel twine    - name: Set package version      run: |        export VERSION=&quot;$&#123;GITHUB_REF#refs/tags/v&#125;&quot;        sed -i &quot;s/version=.*/version=\&quot;$&#123;VERSION&#125;\&quot;,/&quot; setup.py    - name: Build and publish      env:        TWINE_USERNAME: __token__        TWINE_PASSWORD: $&#123;&#123; secrets.PYPI_API_TOKEN &#125;&#125;      run: |        python setup.py bdist_wheel sdist        twine upload dist/*

4. 使用方法示例1、打tag
git tag v0.6.4git push origin v0.6.4

2、github发版github项目页面，点击左侧的 Releases，点击Draft a new release选择tag，Release title 输入版本号 v0.6.4，点击 Publish release
3、pypi自动发版github发版后，会触发 workflow ，自动打包并发版到 pypi.org
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>git</tag>
        <tag>github</tag>
      </tags>
  </entry>
  <entry>
    <title>MacOS安装配置Mermaid</title>
    <url>/dev-macos-mermaid/</url>
    <content><![CDATA[1. Mermaid简介
Mermaid: JavaScript based diagramming and charting tool that renders Markdown-inspired text definitions to create and modify diagrams dynamically.

参考文档：

Mermaid官网
github - mermaid
github - mermaid-cli



2. Mermaid与PuppeteerMermaid库本身并不依赖于Puppeteer，Mermaid可以直接在浏览器或其他Javascript环境下运行，通过Mermaid的API可以生成SVG格式的图表。
&lt;script src=&quot;https://unpkg.com/mermaid/dist/mermaid.min.js&quot;&gt;&lt;/script&gt;&lt;script&gt;mermaid.mermaidAPI.initialize(&#123;    startOnLoad:true&#125;);&lt;/script&gt;

通过这种方式使用Mermaid就不需要安装或配置Puppeteer或Chromium。
但是，Mermaid提供的命令行工具mermaid-cli（mmdc）是依赖Puppeteer的。mermaid-cli使用Puppeteer（一个Node.js库）启动一个Chromium实例来渲染图表的SVG并将其保存为文件。
3. 安装Node1、安装nvmnvm安装方法参考文档《MacOS上软件配置》
2、安装v20版本node
nvm install v20.10.0
node版本要大于v14，更低版本的node会报错 SyntaxError ，详情参考 Client does not start
3、使用v20版本node
nvm use v20.10.0nvm alias default v20.10.0nvm list

4. 安装Mermaidexport PUPPETEER_SKIP_DOWNLOAD=&quot;true&quot; npm install -g @mermaid-js/mermaid-cli

5. 安装Chromium（可选）1、下载chromium
2、放置chromium到指定路径
unzip chrome-mac.zipmv chrome-mac $HOME/.cache/puppeteer/chrome

6. 使用mmdc6.1. mmdc简介mmdc是Mermaid CLI（命令行接口）的核心命令，用于把Mermaid图表代码转换为图像文件（如SVG、PNG、PDF）。
mmdc语法：
mmdc -i /path/to/input_file -o /path/to/output_file -p puppeteer-config.json

参数说明：

-i 参数后接输入文件路径，对应的是包含 Mermaid 代码的文件。
-o 参数后接输出文件路径，对应的是转换后的图像文件。
-p 参数用于指定Puppeteer配置文件的路径，Puppeteer是一个Node库，提供了控制Chromium、Chrome或Firefox的高级API。
-w 和 -H 控制浏览器视窗的尺寸（不是生成的图像的尺寸）
-t 参数用于指定主题样式，比如default、forest、neutral等。

6.2. mmdc使用示例1、创建 pupeteer 配置文件 puppeteer-config.json
&#123;    &quot;executablePath&quot;: &quot;/Users/vk/.cache/puppeteer/chrome/chrome-mac/Chromium.app/Contents/MacOS/Chromium&quot;&#125;

如果使用Chrome的话，配置内容如下
&#123;    &quot;executablePath&quot;: &quot;/Applications/Google Chrome.app/Contents/MacOS/Google Chrome&quot;&#125;

2、创建一个 mmd 文件 flowchart.mmd
graph TD;    A--&gt;B;    A--&gt;C;    B--&gt;D;    C--&gt;D;

3、生成图表
mmdc -i flowchart.mmd -o flowchart.png -p puppeteer-config.json

7. Python调用Mermaid参考代码：MetaGPT - metagpt/utils/mermaid.py
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>frontend</category>
        <category>shell</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>macos</tag>
        <tag>shell</tag>
        <tag>mermaid</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python单元测试框架之pytest</title>
    <url>/dev-python-pytest/</url>
    <content><![CDATA[1. pytest简介
The pytest framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries.

pytest是一种卓越的Python测试框架，它提供了简单、高效的方式来编写可以扩展的测试用例。
本文学习pytest的测试用例写法，内容整理自 chatgpt gpt-4-0613 。
相关文档：

pytest官网
《Python单元测试》



2. pytest基本测试2.1. 准备一个模块准备一个mathlib模块，包含一个求和函数add()：
# mathlib.pydef add(a, b):    return a + b

2.2. 编写测试用例Python中编写测试用例，一种常见的方式是创建一个以test_为前缀的函数，这个函数应包含测试用例的名字和内部执行逻辑。
我们想要创建一个测试用例来检验 add 函数，我们可以创建一个 test_mathlib.py 文件：
# test_mathlib.pyimport mathlibdef test_add():    res = mathlib.add(5, 3)    assert res == 8

2.3. 执行测试1、运行单测文件中的所有函数
pytest test_mathlib.py

2、运行单测文件中的指定函数
pytest -v test_mathlib.py::test_add

2.4. 丰富测试用例为了使测试更丰富，我们可以使用 @pytest.mark.parametrize 装饰器为函数参数提供多组值：
# test_mathlib.pyimport mathlibimport pytest@pytest.mark.parametrize(&quot;a, b, expected&quot;, [    (5, 3, 8),     (10, 20, 30),     (1, -1, 0)])def test_add(a, b, expected):    res = mathlib.add(a, b)    assert res == expected

3. pytest进阶测试3.1. FixtureFixture（预置） 是 pytest 提供的一种特殊的函数，它将测试前的准备工作和解除步骤打包成一个函数，以供测试用例调用。这样做可以提高测试的可重用性和代码的可读性。
例如，我们要为数据库相关的测试都创建一个测试数据库，并在测试结束后清理掉，可以写个fixture如下：
@pytest.fixturedef db():    db = setup_database()    yield db    teardown_database(db)

然后在测试函数中使用这个 db fixture：
def test_database(db):    # Now you can interact with the db object, which is set up     # at the beginning of the test and will be torn down at the end.

3.2. MockingMocking（模拟） 是一种强大的技术，它可以模拟我们在测试中所依赖的部分的行为。例如，我们的函数可能依赖于一个第三方服务或者数据库，在测试环境中，很可能无法启动这些服务。这时，就可以用 Mock 对象替代这些服务或者数据库。pytest 提供了 unittest.mock 作为内置的 mocking 框架。
下面是一个简单的 mocking 例子，假设我们有一个打印函数 print_content()，我们并不希望在测试的时候真的打印出来：
from unittest.mock import MagicMockdef test_print_content():    print_content = MagicMock()    print_content(&quot;hello&quot;)     print_content.assert_called_with(&quot;hello&quot;)


3.3. Marker在 pytest 中，Marker（标记）用于给测试用例添加元数据。比如最常见的 skip 和 xfail，它们会让 pytest 跳过或者期望失败的测试用例。
import pytest@pytest.mark.skip(reason=&quot;No way to test this&quot;)def test_the_unknown():    ...@pytest.mark.xfaildef test_feature_x():    ...

@pytest.mark.asyncio 也是一个常用的内置装饰器，用于标记需要异步执行的函数。
Python3.5 版本引入了 async / await 关键字支持原生协程，它允许我们编写异步的代码，而不需要依赖于特定的包或者复杂的回调链。然而，pytest 是同步的，它不能原生支持 async / await 式的测试。这就是 pytest.mark.asyncio 装饰器的作用: 它让我们能够以同步的方式来测试异步的代码。
我们也可以自定义 marker。然后我们可以在运行 pytest 的时候指定参数 -m 来只运行有特定标记的测试用例，从而帮助我们更好地管理和运行我们的测试。
@pytest.mark.my_markerdef test_function():    ...# Run the testpytest -m my_marker





]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>testing</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>好好学Python</tag>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git LFS</title>
    <url>/dev-git-lfs/</url>
    <content><![CDATA[1. Git LFS 简介
Git LFS: An open source Git extension for versioning large filesGit Large File Storage (LFS) replaces large files such as audio samples, videos, datasets, and graphics with text pointers inside Git, while storing the file contents on a remote server like GitHub.com or GitHub Enterprise.

Git LFS (Large File Storage）是一个开源的Git大文件版本控制的解决方案和工具集，工具自身是基于Golang进行实现，并在Github上开源。原理上是通过对Git客户端进行扩展的方式，从而集成并兼容了原生的Git客户端。Git LFS良好的设计，让用户感觉在对大文件处理过程中，与普通Git的使用方式上没有任何差别，也就是说，在使用方式上Git LFS对用户是完全透明的。
参考文档：

GIT Large File Storage
github - git-lfs/git-lfs
关于 Git Large File Storage
什么是Git LFS大文件存储?
如何使用 Git LFS



2. 下载和安装Git LFS2.1. 官方脚本安装1、下载适合系统的 git-lfs/releases
2、解压安装解压后，执行 install.sh 脚本，这个脚本会做两个事情：

在$PATH中安装Git LFS的二进制可执行文件
执行git lfs install命令，让当前环境支持全局的LFS配置

安装成功，会输出：
Git LFS initialized.

2.2. Linux参考文档：Git LFS - Installation instructions
Linux Debian：
curl -s http://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash

RPM packages：
curl -s http://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh | sudo bash

2.3. MacOSbrew install git-lfs

2.4. Windows目前lfs已经集成在了Git for Windows 中，直接下载和使用最新版本的Windows Git即可。
3. 让仓库支持Git LFSgit lfs install

如果官方脚本安装，那么不需要执行这条命令。
4. 使用Git LFS1、设置LFS追踪 mp4 类型的文件
git lfs track &quot;*.mp4&quot;

track 命令实际上是修改了仓库中的 .gitattributes 文件。
2、查看LFS追踪的文件类型
git lfs track

3、提交 .gitattributes 文件
git add .gitattributesgit commit -m &quot;Add LFS config&quot;

4、放入一个 mp4 类型的文件到仓库中
5、提交并推送 mp4 类型文件
git add test.mp4git commit -m &quot;Add a bigfile&quot;git push

推送后，查看该mp4文件，会显示 Stored with Git LFS 。
注意：github中fork的仓库是不支持LFS上传大文件的，详情参考：can not upload new objects to public fork。如果想要上传大文件，需要源仓库管理员授权push的权限。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
        <tag>github</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git pre-commit 代码质量检查</title>
    <url>/dev-git-pre-commit/</url>
    <content><![CDATA[1. pre-commit 简介Git 提供了一种称为钩子（hook）的特性，帮助我们在关键时刻自动执行自定义脚本。其中，名为 pre-commit 的钩子会在每次 commit 之前运行，能够作为保障代码质量的工具。


2. 使用 pre-commit在项目的根目录下，找到 .git/hooks 文件夹，这个文件夹中存放的就是各种钩子脚本。
创建一个新文件命名为 pre-commit ，务必不要添加后缀，如 .sh 或 .py。
在 pre-commit 文件中，我们可以添加任何可以执行的脚本。例如，如果我们针对 Python 的 .py 文件进行检查，则我们可能的 pre-commit 内容如下：
#!/usr/bin/env bashFILES_PATTERN=&#x27;\.(py)(\..+)?$&#x27;FORBIDDEN=&#x27;pdb.set_trace()&#x27;git diff --cached --name-only | \    while read FILE; do        if [[ &quot;$&#123;FILE&#125;&quot; =~ $&#123;FILES_PATTERN&#125; ]]; then            if grep --quiet &quot;$&#123;FORBIDDEN&#125;&quot; &quot;$&#123;FILE&#125;&quot;; then                echo &quot;$&#123;FILE&#125; contains $&#123;FORBIDDEN&#125;&quot;                exit 1            fi        fi    done || exit $?

这个检查脚本的意思是，如果我们的 .py 文件中含有 pdb.set_trace()，那么拒绝该次 commit。
设置完后，我们需要使脚本具有执行权限，使用 chmod +x .git/hooks/pre-commit 命令即可。
之后，每一次的 git commit，在提交之前，都会执行我们定义好的 pre-commit 脚本，保证代码符合我们设置的规范，从而提升代码质量。
3. pre-commit 框架当我们有大量项目时，pre-commit 脚本的管理维护就变成了一件痛苦的事情。此时，就需要 pre-commit 框架出马了。pre-commit 框架是一个支持多语言的 pre-commit 脚本的管理器，能够简化我们的 pre-commit 脚本配置。使用 pre-commit 框架时，在 .pre-commit-config.yaml 配置文件指定所需的linter列表（脚本列表），然后 pre-commit 框架会自动下载这些linter并运行。
需要特别说明的是：一些最好的 linter 可能是项目中不使用的语言编写的。例如 scss-lint 是一个用于检查SCSS的 linter，但是它是用Ruby编写的。而我们使用 pre-commit 框架时，完全不用关心 scss-lint 的安装配置，只要在linter列表中指定使用它即可。
参考文档：

pre-commit 框架
github - pre-commit/pre-commit
《Python代码质量检查》

4. 安装 pre-commit 框架pip install pre-commit

5. 使用 pre-commit 框架1、创建 pre-commit 框架配置文件 .pre-commit-config.yaml
default_stages: [ commit ]# Install# 1. pip install pre-commit# 2. pre-commit install(the first time you download the repo, it will be cached for future use)repos:  - repo: https://github.com/pycqa/isort    rev: 5.11.5    hooks:      - id: isort        args: [&#x27;--profile&#x27;, &#x27;black&#x27;]        exclude: &gt;-            (?x)^(            .*__init__\.py$            )  - repo: https://github.com/astral-sh/ruff-pre-commit    # Ruff version.    rev: v0.0.284    hooks:      - id: ruff  - repo: https://github.com/psf/black    rev: 23.3.0    hooks:      - id: black        args: [&#x27;--line-length&#x27;, &#x27;120&#x27;]

2、运行pre-commit框架
pre-commit run --all-files

3、配置commit前自动调用pre-commit框架
cat &lt;&lt;EOF  &gt; .git/hooks/pre-commit#!/bin/bashsource $HOME/.bash_profilepre-commit run --all-filesEOFchmod a+x .git/hooks/pre-commit

4、测试commit
# modify somethinggit add .git commit -m &quot;something&quot;

6. github 配置使用 pre-commit 框架github actions 中，也可以配置使用 pre-commit 框架，用于检查代码规范。
项目根目录中，新建 .github/workflows/pre-commit.yaml，内容为：
name: Pre-commit checkson:  pull_request:    branches:      - &#x27;**&#x27;  push:     branches:       - &#x27;**&#x27;          jobs:  pre-commit-check:    runs-on: ubuntu-latest    steps:    - name: Checkout Source Code      uses: actions/checkout@v2    - name: Setup Python      uses: actions/setup-python@v2      with:        python-version: &#x27;3.9.17&#x27;            - name: Install pre-commit      run: pip install pre-commit          - name: Initialize pre-commit      run: pre-commit install          - name: Run pre-commit hooks      run: pre-commit run --all-files

提交到github，就OK了。
7. 扩展阅读：git hooksGit钩子是一些在Git执行特定操作时触发的脚本，可以用于自定义和自动化工作流程，不同的Git钩子有不同的调用时间。
客户端钩子：

pre-commit：在每次提交之前运行。
pre-push：在git push之前运行。
post-commit：在每次提交之后运行。
post-checkout：在切换分支或检出文件后运行。
post-merge：在合并操作完成后运行。

服务器端钩子：

pre-receive：在远程仓库接收到推送前运行。
update：在远程仓库接收到推送后，对每个要更新的引用（分支或标签）运行一次。
post-receive：在远程仓库接收到推送后，对所有要更新的引用运行一次。

每个项目的.git/hooks的目录中，看到这些钩子的官方示例。示例文件以.sample结尾，去掉.sample后缀可激活该钩子脚本。
参考文档：

githooks - Hooks used by Git
pre-commit
Git项目管理，代码规范pre-commit使用详解
用 pre-commit hook 解决 Python 项目编码规范

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>git</category>
        <category>testing</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>git</tag>
        <tag>好好学Git</tag>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>GitHub Actions配置代码质量检查</title>
    <url>/dev-github-actions-code-quality-check/</url>
    <content><![CDATA[1. 前言Gitlab CI中可以配置代码质量检查，同样的，GitHub Actions中也可以配置代码质量检查。本文中，我们学习在GitHub Actions中配置Python的编码规范检查和运行单元测试。
参考文档：

《GitLab CI配置代码质量检查》
GitHub Actions - 构建和测试 Python
github python coverage badge



2. Python编码规范检查详情参考 《git pre-commit 代码质量检查》。
3. Python单元测试3.1. Python通用单元测试使用多个Python版本运行单元测试。
name: Python application teston:  pull_request:    branches:      - &#x27;**&#x27;  push:     branches:       - &#x27;**&#x27;jobs:  build:    runs-on: ubuntu-latest    strategy:      matrix:        python-version: [&#x27;3.9&#x27;, &#x27;3.10&#x27;, &#x27;3.11&#x27;]    steps:    - uses: actions/checkout@v4    - name: Set up Python $&#123;&#123; matrix.python-version &#125;&#125;      uses: actions/setup-python@v4      with:        python-version: $&#123;&#123; matrix.python-version &#125;&#125;    - name: Install dependencies      run: |        python -m pip install --upgrade pip        pip install -e.    - name: Test with pytest      run: |        pip install pytest        # export OPENAI_API_KEY=&quot;$&#123;&#123; secrets.OPENAI_API_KEY &#125;&#125;&quot; OPENAI_API_MODEL=&quot;gpt-3.5-turbo-1106&quot;        pytest tests/ --doctest-modules --junitxml=junit/test-results-$&#123;&#123; matrix.python-version &#125;&#125;.xml      # continue-on-error: true


3.2. 单测并上传单测结果name: Python application teston:  pull_request:    branches:      - &#x27;**&#x27;  push:     branches:       - &#x27;**&#x27;jobs:  build:    runs-on: ubuntu-latest    strategy:      matrix:        python-version: [&#x27;3.9&#x27;, &#x27;3.10&#x27;, &#x27;3.11&#x27;]    steps:    - uses: actions/checkout@v4    - name: Set up Python $&#123;&#123; matrix.python-version &#125;&#125;      uses: actions/setup-python@v4      with:        python-version: $&#123;&#123; matrix.python-version &#125;&#125;    - name: Install dependencies      run: |        python -m pip install --upgrade pip        pip install -e.    - name: Test with pytest      run: |        pip install pytest        pytest tests/ --doctest-modules --junitxml=junit/test-results-$&#123;&#123; matrix.python-version &#125;&#125;.xml    - name: Upload pytest test results      uses: actions/upload-artifact@v3      with:        name: pytest-results-$&#123;&#123; matrix.python-version &#125;&#125;        path: junit/test-results-$&#123;&#123; matrix.python-version &#125;&#125;.xml        retention-days: 3      # Use always() to always run this step to publish test results when there are test failures      if: $&#123;&#123; always() &#125;&#125;

4. Python单元测试覆盖率4.1. 生成单测覆盖率并展示1、安装单测依赖
pip install pytest pytest-cov coverage-badge

2、生成单测覆盖率结果文件 .coverage 和 html 格式报告
pytest tests/ --doctest-modules --cov=. --cov-report=html

pytest参数说明：

tests/ 指定测试文件的路径
--doctest-modules 运行所有发现的doctest模块
--junitxml=junit/test-results.xml 指定生成junitxml格式的测试报告
--cov=. 生成单测覆盖率结果文件 .coverage 必须的参数，指定要收集哪些代码的覆盖率信息，. 表示收集当前目录下所有代码的覆盖率信息
--cov-report=html 指定生成 html 格式的测试覆盖率报告，存到 htmlcov 目录中，等同于单独运行 coverage html -i
--cov-report=xml 指定生成 xml 格式的测试覆盖率报告，存储为 coverage.xml ，等同于单独运行 coverage xml -i

3、查看测试覆盖率
coverage report -m

4、生成badge svg
coverage-badge -o coverage.svg

5、github README 引用 badge
![Coverage Status](/path/to/coverage.svg)

或者：
[![Coverage Status](/path/to/coverage.svg)](https://your-badge-link)

4.2. 单测覆盖率整合到github actions1、github actions配置
name: Python application teston:  pull_request:    branches:      - &#x27;**&#x27;  push:     branches:       - &#x27;**&#x27;jobs:  build:    runs-on: ubuntu-latest    strategy:      matrix:        python-version: [&#x27;3.9&#x27;, &#x27;3.10&#x27;, &#x27;3.11&#x27;]    steps:    - uses: actions/checkout@v4    - name: Set up Python $&#123;&#123; matrix.python-version &#125;&#125;      uses: actions/setup-python@v4      with:        python-version: $&#123;&#123; matrix.python-version &#125;&#125;    - name: Install dependencies      run: |        python -m pip install --upgrade pip        pip install -e.    - name: Test with pytest      run: |        pip install pytest pytest-cov pytest-html coverage-badge        # export OPENAI_API_KEY=&quot;$&#123;&#123; secrets.OPENAI_API_KEY &#125;&#125;&quot; OPENAI_API_MODEL=&quot;gpt-3.5-turbo-1106&quot;        pytest tests/ --doctest-modules --junitxml=junit/test-results-$&#123;&#123; matrix.python-version &#125;&#125;.xml --cov=. --cov-report=xml:cov.xml --cov-report=html:htmlcov        coverage report -m        coverage-badge -o coverage.svg        # upload coverage.svg to a object storage    - name: Upload pytest test results      uses: actions/upload-artifact@v3      with:        name: pytest-results-$&#123;&#123; matrix.python-version &#125;&#125;        path: |          ./junit/test-results-$&#123;&#123; matrix.python-version &#125;&#125;.xml          ./htmlcov/        retention-days: 3      if: $&#123;&#123; always() &#125;&#125;

2、github README 引用 badge
![Coverage Status](https://object-storage-domain/path/to/coverage.svg)

4.3. 忽略指定目录项目更目录中，创建 coverage 配置文件 .coveragerc 
[run]omit =    */tests/*    */migrations/*exclude_lines =    # 忽略异常抛出代码    raise NotImplementedError    # 忽略抽象基类中的函数定义    @abstractmethod    # 忽略带有pragma: no cover注释的行    pragma: no cover

omit 用于指定不应该包含在覆盖率统计中的文件或目录，exclude_lines 用于指定不应该包含在覆盖率统计中的代码行。
4.4. 使用Codecovjobs:  build:    steps:    - name: Upload coverage to Codecov      uses: codecov/codecov-action@v2      with:        token: $&#123;&#123; secrets.CODECOV_TOKEN &#125;&#125;        file: ./cov.xml      if: $&#123;&#123; always() &#125;&#125;

5. PR单测审批用户提交PR后，想要触发审批，配置是很简单的，例如：
on:  pull_request:    branches:      - &#x27;**&#x27;

但是，如果我们在workflow中用到了secret，那配置就没有这么简单了。因为github限制了pull_request触发的workflow读取secret的权限，所以副本仓库提交到主仓库的pr，读取不到主仓库secret。这是一种对主仓库secret的保护，详情参考文档github actions main repository secret not picked up from pull request build
如果确实需要让pr的workflow读取到secret，该怎么处理？Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests 这篇文章提供了两种方法可供参考。方法一：pr事件触发workflow1，workflow1结束事件触发workflow2，此时workflow2就有权限读取secret了。但是，这种方法中，workflow1和workflow2之间缺少直观的关联，在pr页面和github actions页面上都看不出来联系，不友好。方法二：使用 pull_request_target 代替 pull_request ，此时触发的workflow有权限读取secret。配置简单，页面也能直观看到，但是，这种方法降低了安全性。
方法一和方法二各有优劣，个人更推荐方法二，因为方法二可以配合environment加一道审批，增强安全性，弥补缺陷。此外，使用 pull_request_target 时， actions/checkout@v4 默认切分支是切到 target 分支，想要测试pr分支，需要指定切分支到pr head。详情参考Checkout V4
on:  pull_request_target:jobs:  build:    runs-on: ubuntu-latest    environment: unittest    strategy:      matrix:        python-version: [&#x27;3.9&#x27;]    steps:    - uses: actions/checkout@v4      with:        ref: $&#123;&#123; github.event.pull_request.head.sha &#125;&#125;

pull_request 和 pull_request_target 是有很大区别的，Events that trigger workflows - pull_request_target 文档中说：

This event runs in the context of the base of the pull request, rather than in the context of the merge commit, as the pull_request event does. 

个人理解为：pull_request 的workflow是根据pr合并后的workflow yaml运行，而pull_request_target 的workflow是根据pr合并前的workflow yaml运行的，详情参考文档：What is the difference between pull_request and pull_request_target event in GitHubActions
注意：如果一个pr中修改了 workflow ，那么在github actions页面看到的 workflow yaml 就是修改后的。但是如果是pull_request_target 事件触发的workflow，那么虽然看到了修改后的yaml，实际执行会按照修改前的yaml执行。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
        <category>git</category>
        <category>testing</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>git</tag>
        <tag>github</tag>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git项目瘦身</title>
    <url>/dev-make-git-thinner/</url>
    <content><![CDATA[1. 前言在使用Git进行版本控制时，随着时间的推移，.git目录可能会变得庞大，导致存储空间浪费和性能下降。本文将学习一些Git项目瘦身方法，以便压缩.git目录。


2. git垃圾回收git gc（垃圾回收）是Git提供的一个命令，用于清理和压缩 .git 目录中的文件。它会自动删除不再需要的对象，并合并多个文件以节省空间。
注意：该命令仅适用于本地项目清理，不能推到远程项目，对远程项目并没有影响。
3. 删除大文件如果Git仓库中包含一些大文件，可以使用git filter-repo工具来删除这些文件。
1、安装 git-filter-repo 工具
pip install git-filter-repo

2、生成文件大小报告，并查看
git filter-repo --analyzehead .git/filter-repo/analysis/path-all-sizes.txt

3、删除所有大于10MB的文件
git filter-repo --strip-blobs-bigger-than 10M

注意：执行该命令后，所有commit记录的hash值都会被重写。
4、再次生成文件大小报告，并查看
rm -rf .git/filter-repo/analysisgit filter-repo --analyzehead .git/filter-repo/analysis/path-all-sizes.txt

5、推送到远程
git push -f

4. 压缩历史记录通过重写历史记录，可以减少提交数量，从而减小.git目录的大小。
git rebase -i HEAD~n

git rebase更详细的使用方法，请参考文档 《好好学Git：Git提交后变更》
5. 删除分支定期检查并删除不再需要的分支，可以有效减少.git目录的大小。
# 删除本地分支git branch -d branch_name# 删除远程分支git push origin --delete branch_name

6. 使用 .gitignore确保在项目中使用.gitignore文件，避免将不必要的文件（如临时文件、编译产物等）添加到版本控制中，可以防止.git目录膨胀。
对于已经track的文件，使用 .gitignore 是无法忽略的。解决办法是先删除本地缓存，然后推送到远程仓库。详细操作方法参考文档 《好好学Git：gitignore用法》
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
        <tag>github</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置青龙面板</title>
    <url>/dev-docker-qinglong/</url>
    <content><![CDATA[1. 青龙面板简介
青龙面板是一个支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台

Linux自带的crontab可以配置定时任务，但是并不方便。而使用青龙面板，能够可视化配置定时任务，简单快捷。本文中，我们学习一下青龙面板的安装配置，试一试签到薅羊毛。
参考文档：

《Linux设置定时任务》
github - whyour/qinglong
利用拾光坞开启青龙面板薅羊毛（保姆级教程）



2. 安装青龙版面1、下载青龙面板Docker镜像
docker pull whyour/qinglong:debian

2、启动青龙面板
docker run --name qinglong -d \  -p 5700:5700 \  -v /opt/ql/data:/ql/data \  -e QlBaseUrl=&quot;/&quot; \  -e QlPort=&quot;5700&quot; \  --hostname qinglong \  --restart unless-stopped \  whyour/qinglong:debian

以上命令：

命名容器为qinglong，后台运行
映射宿主机5700端口到容器的5700
挂载宿主机文件 /opt/ql/data 到容器目录 /ql/data
设置环境变量 QlBaseUrl 和 QlPort，青龙面板服务启动时使用
设置容器主机名为 qinglong
设置容器随docker启动自动启动

3、验证安装
docker psdocker logs qinglong

4、登录进容器
docker exec -it qinglong /bin/bash

5、web访问青龙面板浏览器访问 http:&lt;ip&gt;:5700初次访问时，会提示进行初始化配置。
3. 内置命令3.1. task# 依次执行，如果设置了随机延迟，将随机延迟一定秒数task &lt;file_path&gt;                                             # 依次执行，无论是否设置了随机延迟，均立即运行，前台会输出日，同时记录在日志文件中task &lt;file_path&gt; now                                         # 并发执行，无论是否设置了随机延迟，均立即运行，前台不产生日，直接记录在日志文件中，且可指定账号执行task &lt;file_path&gt; conc &lt;env_name&gt; &lt;account_number&gt;(可选的) # 指定账号执行，无论是否设置了随机延迟，均立即运行 task &lt;file_path&gt; desi &lt;env_name&gt; &lt;account_number&gt;      # 设置任务超时时间   task -m &lt;max_time&gt; &lt;file_path&gt;# 使用 -- 分割，-- 后面的参数会传给脚本，下面的例子，脚本就可接收到参数 -u whyour -p passwordtask &lt;file_path&gt; -- -u whyour -p password

3.2. ql# 更新并重启青龙ql update# 运行自定义脚本extra.shql extra# 添加单个脚本文件ql raw &lt;file_url&gt;# 添加单个仓库的指定脚本ql repo &lt;repo_url&gt; &lt;whitelist&gt; &lt;blacklist&gt; &lt;dependence&gt; &lt;branch&gt; &lt;extensions&gt;# 删除旧日志ql rmlog &lt;days&gt;# 启动tg-botql bot# 检测青龙环境并修复ql check# 重置登录错误次数ql resetlet                                                  # 禁用两步登录ql resettfa

参数说明：

file_url: 脚本地址
repo_url: 仓库地址
whitelist: 拉取仓库时的白名单，即就是需要拉取的脚本的路径包含的字符串，多个竖线分割
blacklist: 拉取仓库时的黑名单，即就是需要拉取的脚本的路径不包含的字符串，多个竖线分割
dependence: 拉取仓库需要的依赖文件，会直接从仓库拷贝到scripts下的仓库目录，不受黑名单影响，多个竖线分割
extensions: 拉取仓库的文件后缀，多个竖线分割
branch: 拉取仓库的分支
days: 需要保留的日志的天数
file_path: 任务执行时的文件路径
env_name: 任务执行时需要并发或者指定时的环境变量名称
account_number: 任务执行时指定某个环境变量需要执行的账号序号
max_time: 超时时间，后缀”s”代表秒(默认值), “m”代表分, “h”代表小时, “d”代表天

4. 配置青龙面板4.1. 配置支持更多脚本类型点击左侧配置文件，找到 RepoFileExtensions=&quot;js py&quot; ，修改为 RepoFileExtensions=&quot;js py sh ts&quot;，右上角保存。
点击左侧对比工具，对比修改前后的变化。
4.2. 安装脚本需要的依赖点击左侧依赖管理，创建依赖，名称一栏中输入脚本可能需要的依赖，多个依赖换行输入，点击确定即可下载安装依赖。
4.3. 配置面板通知点击左侧通知设置，选择一个通知方式，根据提示填入必要的信息，保存。建议使用飞书机器人，配置最简单：飞书群里新建一个自定义机器人，把webhook地址的最后一段粘贴到larkKey即可。
4.4. 配置脚本通知青龙面板设置里的那个只是用来面板级别的通知，比如订阅更新。而脚本里的通知，需要在配置文件里去配置的。详情参考文档如何设置任务执行完毕后发送通知
1、微信登录 Server酱，获取一个SendKey，测试通知
2、点击左侧配置文件，找到 PUSH_KEY ，填入上一步获取到的 SendKey，保存
4.5. 配置代理点击左侧环境变量，添加两个关于代理的环境变量。
http_proxy=http://192.168.5.233:20171https_proxy=http://192.168.5.233:20171

之所以要配置代理，是因为很多订阅是从github拉取的，而github从国内访问是需要科学上网的，因此最好配置一下代理。这两个代理变量，平时禁用，拉取订阅时启用。
5. 配置签到任务5.1. 阿里云盘签到（已废弃）参考文档：github - mrabit/aliyundriveDailyCheck
1、获取阿里云盘 refresh_tokenweb登录阿里云盘后，控制台粘贴
copy(JSON.parse(localStorage.token).refresh_token); console.log(JSON.parse(localStorage.token).refresh_token);

2、青龙面板添加环境变量 refreshToken如果有多个账号要签到，那就添加多个refreshToken。
3、安装js依赖
axios

4、添加订阅
ql repo https://github.com/mrabit/aliyundriveDailyCheck.git &quot;autoSignin&quot; &quot;&quot; &quot;qlApi&quot;

添加订阅后，点击运行，即可从github拉取定时任务。拉取成功之后，建议禁用订阅。
5、配置定时任务点击定时任务页面，找到 autoSignin.js 任务，点击运行，测试运行。测试没有问题，编辑定时任务，设置一个合适的运行时间。
6、配置自动更新 refreshToken系统设置，应用设置，创建一个应用 autoSignin（名称随意）。autoSignin 应用的 CLIENT_ID 和 CLIENT_SECRET 配置到环境变量中。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>废弃笔记本电脑复活：第零篇 安装Ubuntu系统</title>
    <url>/dev-laptop-recycling-00-install-ubuntu/</url>
    <content><![CDATA[1. 前言最近买了个85寸电视，就想搞个NAS。看了看价格，绿联NAS DX4600 2000左右，蜗牛星际NAS 1000左右，硬盘还需要另外购买。贫穷，让我想到了废弃已久的华为荣耀笔记本电脑。要不然，用它作为NAS？还可以作为软路由？还可以部署Nginx、Jupyter、ChatGPT代理等常用服务，还可以参考 Awesome-Selfhosted 部署更多有趣的项目，想想还挺不错。搞起来！
首先，给笔记本电脑安装一个Linux系统。个人习惯使用Ubuntu和CentOS，考虑到CentOS已经不再维护，因此选择Ubuntu。


2. 安装Ubuntu系统参考文档：Create a bootable USB stick on macOS
1、服务器系统选择 ubuntu-18.04.6-desktop-amd64.iso之所以选择desktop版，是因为desktop版是图形化界面，初始化配置很方便。后期不想要desktop的话，可以卸载掉。
PS：再选择一次的话，我会选择安装最新版Ubuntu22，因为很多软件只支持新版本的Ubuntu。如果已经安装了Ubuntu18，想要升级Ubuntu22，可以参考文档：《好好学Linux：Ubuntu18 升级到 Ubuntu22》。
2、使用balenaEtcher制作一个U盘启动盘
3、笔记本插入U盘，开机选择从U盘启动
4、根据提示安装Ubuntu系统
系统分区时注意事项：

建议手动进行分区，New Partition Table，如果分错了就Revert重新搞。
32MB给 bios，格式reserved bios boot area
500MB给 efi，格式efi
2GB 给 /boot，格式ext4
其余空间给 /，格式ext4

在现代电脑系统（使用UEFI模式）中，电脑首先引导到EFI分区，EFI分区中的UEFI启动加载器，接着加载/boot分区中的操作系统内核，然后操作系统开始运行。
参考文档：

如何安装 Ubuntu 22.04 LTS 桌面版 (图文教程) 
Ubuntu22.04安装时手动分区
XFS vs. EXT4：哪个更适合你的应用场景？

3. 系列文档
《废弃笔记本电脑复活：第零篇 安装Ubuntu系统》
《废弃笔记本电脑复活：第一篇 Ubuntu系统基础配置》
《废弃笔记本电脑复活：第二篇 安装配置Samba和WebDav》
《废弃笔记本电脑复活：第三篇 利用阿里云盘扩展存储》
《废弃笔记本电脑复活：第四篇 远程控制Ubuntu桌面》

]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>废弃笔记本电脑复活：第一篇 Ubuntu系统基础配置</title>
    <url>/dev-laptop-recycling-01-ubuntu-basic-config/</url>
    <content><![CDATA[1. 前言本篇中，我们对Ubuntu系统进行基础的配置，以便远程使用。


2. 安装SSH Server安装SSH Server，以便远程操作。
apt install y openssh-server

3. 配置固定IP方法一：图形界面配置。
方法二：命令行配置，详情参考《Ubuntu18配置静态IP》。
4. 关闭自动更新systemctl status unattended-upgradessystemctl stop unattended-upgradessystemctl disable unattended-upgrades

5. 配置合上屏幕不休眠1、编辑logind配置
vim /etc/systemd/logind.conf

修改其中的配置为：
HandleLidSwitch=ignore

HandleLidSwitch是指笔记本屏幕合起来，设置为ignore，也就是笔记本屏幕合起来时不进行任何改变。
2、重启 systemd-logind
systemctl restart systemd-logind

6. 断网自动重连1、修改网络管理器配置
vim /etc/NetworkManager/NetworkManager.conf

其中的 ifupdown 部分修改为：
[ifupdown]managed=true

2、重启网络管理器
service network-manager restart

参考文档：解决Ubuntu18.04网络不自动连接问题
]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>废弃笔记本电脑复活：第二篇 安装配置Samba和WebDav</title>
    <url>/dev-laptop-recycling-02-install-samba-and-webdav/</url>
    <content><![CDATA[1. 前言NAS（Network Attached Storage，网络附属存储）本质上就是专用的存储硬件+软件。最常用的NAS软件，是NAS系统。而如果我们只是想让通用设备作为NAS，那么常用软件是Samba和WebDAV。
本篇中，我们学习安装配置Samba和WebDAV，让笔记本设备成为一台NAS。


2. Samba简介SMB（Server Message Block）协议是一种网络通信协议，最初由IBM开发，后来被微软用于Windows操作系统中的文件和打印服务共享。Samba 是一个开源软件，它实现了SMB协议，允许Windows和Unix/Linux系统相互通信，提供了在不同操作系统之间共享文件和打印服务的功能。
3. 安装配置Samba3.1. 安装Samba1、安装Samba
apt install samba

2、查看Samba服务状态
systemctl status nmbd

3.2. 配置Samba1、创建共享目录，并给sambashare用户组设置所有目录的读写权限
mkdir -p /data/samba/videofind /data/samba -type d -exec chgrp sambashare &#123;&#125; \;find /data/samba -type d -exec chmod 770 &#123;&#125; \;

其中，sambashare用户组是在安装Samba过程中自动创建的。
3、创建samba用户并授权
useradd -m -s /usr/sbin/nologin -G sambashare sambasmbpasswd -a samba

Samba权限控制使用Linux用户和组的权限系统，但具有自己的身份验证机制，与标准Linux身份验证分开。
4、配置共享目录和用户编辑samba配置文件
vim /etc/samba/smb.conf

在文件最后添加：
[video]    path = /data/samba/video    browseable = yes    read only = no    force create mode = 0660    force directory mode = 2770    valid users = @sambashare @sadmin#[doc]#    path = /data/samba/doc#    browseable = no#    read only = yes#    force create mode = 0660#    force directory mode = 2770#    valid users = samba @sambashare

配置说明：

[video] 和 [doc] 表示登录时使用的共享名称。
path 表示共享的目录。
browseable 表示可浏览，是否可在共享列表中列出此共享。如果设置为no，其他用户将看不到共享目录。
read only 表示valid users列表中指定的用户是否设置仅允许读，如果值yes，则仅允许读。
force create mode 表示此共享创建文件时设置的文件权限模式。
force directory mode 表示此共享创建目录时设置目录权限的模式。
valid users 表示允许访问共享的用户和组的列表。群组以@符号为前缀。

更多配置选项，请参考文档 smb.conf — The configuration file for the Samba suite
5、重启samba
systemctl restart nmbdsystemctl status nmbd

3.3. 测试使用Samba1、安装 samba client
apt install smbclient

2、创建测试文件
touch test.txt

3、连接samba server
smbclient //192.168.5.233/users -U samba

4、上传下载文件
smb: \&gt; lssmb: \&gt; put test.txt test.txtsmb: \&gt; get test.txt test2.txtsmb: \&gt; exit

下载文件时，还可以使用smbget工具
smbget -U samba smb://192.168.5.233/video/test.txt -o test3.txt

3.4. 配置防火墙ufw allow &#x27;Samba&#x27;

4. WebDAV简介WebDAV（Web-based Distributed Authoring and Versioning）是一种基于HTTP协议的通信协议，用于使用户可以方便地对服务器上的文档进行远程编辑和管理。WebDAV扩展了HTTP/1.1协议，增加了一系列的方法，以支持文件的创建、移动、复制和删除等操作。
使用WebDAV需要两个基本组件：WebDAV客户端和服务器。WebDAV客户端可以是任何支持该协议标准的软件，如一些文件管理器、专门的WebDAV应用程序或甚至是某些操作系统内置的文件浏览功能。服务器端则需要运行支持WebDAV的服务器软件，如Apache的mod_dav模块。
设置WebDAV环境时，通常需要在服务器上进行配置，以确保正确的文件权限、认证和安全设置。出于安全考虑，WebDAV通信应该通过SSL/TLS加密来保护数据传输过程中的安全性（使用HTTPS协议而不是HTTP）。
相较于Samba，WebDAV的权限检查更严格，比如目录中存在权限为问号的文件，那么整个目录都无法访问。
5. 安装配置WebDAV5.1. 安装WebDAV服务端1、安装Apache HTTP服务器
apt updateapt install apache2

2、启用Apache的WebDAV模块
a2enmod dav dav_fs

3、重启Apache
systemctl restart apache2

5.2. 配置WebDAV服务端1、创建WebDAV共享目录，并授权
mkdir -p /data/sambafind /data/samba -type d -exec chmod 777 &#123;&#125; \;
这里复用Samba共享目录，同时修改权限为777，否则 www-data 用户无权读写。
2、配置WebDAV服务
vim /etc/apache2/sites-available/000-default.conf

VirtualHost标签中，添加如下内容：
Alias /webdav /data/samba&lt;Location /webdav&gt;    DAV On    AuthType Basic    AuthName &quot;WebDAV&quot;    AuthUserFile /etc/apache2/webdav.password    Require valid-user&lt;/Location&gt;

3、创建用户名和密码
htpasswd -bc /etc/apache2/webdav.password user1 password1

增加用户名和密码：
htpasswd -b /etc/apache2/webdav.password user2 password2

4、重启Apache
systemctl restart apache2

5.3. 测试使用WebDAV5.3.1. 浏览器访问假设主机IP为 192.168.5.233浏览器访问 http://192.168.5.233/webdav访问时，会提示输入用户名和密码，证明我们的webdav配置正常。
登录后，会提示 Forbidden You don’t have permission to access this resource.这是正常的，因为缺少 index.html 这种文件。
假设有一个文件路径为：/data/samba/test.png那么通过浏览器可以直接访问：http://192.168.5.233/webdav/test.png
5.3.2. cadver客户端访问apt install cadavercadaver http://192.168.5.233/webdav

连接时，会提示输入用户名和密码。
连接成功后，会看到 cadaver 命令行提示符，此时可以使用以下命令：

ls: 列出当前目录中的文件和目录。
cd directory: 更改到名为 directory 的目录。
pwd: 显示当前目录的完整路径。
get filename: 下载文件 filename 到本地当前目录。
put filename: 上传本地文件 filename 到当前的WebDAV目录。
mget *.ext: 下载当前目录中所有扩展名为 .ext 的文件。
mput *.ext: 上传当前本地目录中所有扩展名为 .ext 的文件。
delete filename: 删除文件 filename。
mkcol directory: 创建一个名为 directory 的新目录。
move source destination: 移动或重命名文件 source 为 destination。
copy source destination: 复制文件 source 到 destination。

]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>废弃笔记本电脑复活：第四篇 远程控制Ubuntu桌面</title>
    <url>/dev-laptop-recycling-04-ubuntu-desktop-remote-control/</url>
    <content><![CDATA[1. 前言在笔记本电脑作为Ubuntu服务器使用时，大部分时候是不需要使用桌面（图形界面）的。但是，某些软件是必须要图形界面的，比如自带的浏览器，比如办公软件等等。
我们当然可以打开笔记本，直接使用桌面，但是这样就失去了笔记本电脑作为服务器的初衷。我们想要的是，服务器固定在一个位置，不需要再变动，一切操作都可以远程。


2. 远程控制Ubuntu桌面概述远程控制Ubuntu桌面，常用的方法包括：

使用VNCServer：安装配置VNCServer，通过VNC远程
使用自带的远程控制功能：开启Ubuntu22自带的远程控制，通过RDP或VNC远程
使用专业的远程控制软件：安装使用专业的远程控制软件，比如向日葵和RustDesk

3. 使用VNCServer3.1. 安装配置VNCServer1、安装图形化界面gnome
apt-get update &amp;&amp; apt-get upgrade -yapt-get install -y ubuntu-desktop gnome-panel gnome-settings-daemon metacity nautilus gnome-terminalreboot

2、安装vncserver并运行
apt-get install -y vnc4server # Ubuntu18/Ubuntu20# apt-get install -y tightvncserver # Ubuntu22vncserver # 按照提示配置vnc连接时的密码ps -ef | grep vnc # 看到 Xvnc4 :1 ，说明是 5901 端口

3、修改vncserver配置
cp -p ~/.vnc/xstartup ~/.vnc/xstartup.bak vim ~/.vnc/xstartup

写入内容为：
#!/bin/shexport XKL_XMODMAP_DISABLE=1export XDG_CURRENT_DESKTOP=&quot;GNOME-Flashback:GNOME&quot;export XDG_MENU_PREFIX=&quot;gnome-flashback-&quot;gnome-session --session=gnome-flashback-metacity --disable-acceleration-check &amp;

4、重启vnc会话
vncserver -kill :1 vncserver -geometry 1920x1080 :1

参考文档：

百度智能云 - Ubuntu18.04安装图形化界面并使用VNCViewer连接
阿里云 - 通过VNC搭建Ubuntu 18.04和20.04图形界面
在 Ubuntu 22.04 上安装和配置 VNC 远程桌面

3.2. 使用VNCViewer远程1、本地安装VNCViewer
2、启动VNCViewer，新建连接参数包括：

主机的IP和端口（5901）
VNC连接密码

3.3. 问题：VNC连接灰屏问题描述：在使用VNCViewer连接Ubuntu22中安装的vncserver时，第一次连接正常，第二次连接出现了灰屏。
解决办法：修改 xstartup 配置，然后重启vnc会话
#!/bin/shautocutsel -forkxrdb $HOME/.Xresourcesxsetroot -solid greyexport XKL_XMODMAP_DISABLE=1export XDG_CURRENT_DESKTOP=&quot;GNOME-Flashback:Unity&quot;export XDG_MENU_PREFIX=&quot;gnome-flashback-&quot;unset DBUS_SESSION_BUS_ADDRESSgnome-session --session=gnome-flashback-metacity --disable-acceleration-check --debug &amp;

参考文档：Ubuntu 安装 TightVNCServer 时灰屏
3.4. 问题：弹窗prevents Wi-Fi scans问题描述：在使用VNCViewer连接Ubuntu22中安装的vncserver时，正常连接，但是会不停弹窗提示输入密码。弹窗提示内容为：
System policy prevents Wi-Fi scansAn application is attempting to perform an action that requires privilegesAuthentication is required to perform this action.

输入密码后，还会继续弹窗，无限循环。
解决办法：wifi scan授权。
vim /etc/polkit-1/localauthority/50-local.d/wifi.scan.pkla

写入如下内容：
[Allow Wifi Scan]Identity=unix-user:*Action=org.freedesktop.NetworkManager.wifi.scan;org.freedesktop.NetworkManager.enable-disable-wifi;org.freedesktop.NetworkManager.settings.modify.own;org.freedesktop.NetworkManager.settings.modify.system;org.freedesktop.NetworkManager.network-controlResultAny=yesResultInactive=yesResultActive=yes

参考文档：解决Ubuntu”Authentication required. System policy prevents WiFi scans”问题
4. 使用自带的远程控制功能4.1. 开启Ubuntu22自带的远程控制【安装配置VNC Server】一节中的方法，同样适用Ubuntu22。除此之外，Ubuntu22内置了远程控制功能，可以直接开启RDP和VNC。因此，在使用Ubuntu22时，这种方法更加简单。
图形界面操作：Settings -&gt; Sharing -&gt; Remote Desktop

开启 Remote Desktop：支持Windows的Remote Desktop Connection远程连接
开启 Remote Control：支持使用远程连接软件进行操作控制
开启 Enable Legacy VNC Protocol：支持使用VNC客户端连接和操作，同时建议选择 Require a password
设置 Password：在Authentication部分设置密码，用于VNC连接

这种方法开启的VNC服务，端口默认为 5900 
参考文档：

Ubuntu22.04远程桌面配置（RDP，VNC）

4.2. 问题：VNC连接报错bad rectangle在使用VNCViewer连接Ubuntu22自带的VNC时，可能会报错：
Attempting to reconnect to VNC Server...Protocol error: bad rectangle: 0x0 at 16191,5376 exceeds 1920x1080 at 0,0

解决办法：在VNCViewer中的Properties配置中，把Options中的 Picture quality 从 Automatic 调成 High参考文档：

How do I get headless VNC working?
rfb protocol error bad rectangle 16384 的解决方案

4.3. 问题：Ubuntu22自带的远程控制重启失效问题描述：Ubuntu22自带的远程控制功能，在重启后会失效。具体来说：

原生的登录功能，需要我们的设备连接显示器，并解锁登录才能够使用。
原生的桌面共享，设置的用于连接的用户密码，会随着系统重启而使用随机字符串填充，影响我们登录。

解决办法：使用脚本自动对系统远程控制相关功能进行配置。
wget https://github.com/soulteary/ubuntu-remote-control-helper/raw/main/example/installer.shbash installer.shUBUNTU_REMOTE_USER=ubuntu UBUNTU_REMOTE_PASS=ubuntu123 urch

参考文档：

使用 Urch 让 Ubuntu 原生远程控制功能稳定可靠

5. 使用专业的远程控制软件5.1. 安装配置向日葵配置了VNC，为什么还需要向日葵？答：使用向日葵的好处是不用考虑vnc配置问题，直接连Ubuntu原生桌面。
1、参考向日葵官方文档，安装向日葵安装过程可能报错：
dpkg: dependency problems prevent configuration of sunloginclient: sunloginclient depends on libgconf-2-4; however:  Package libgconf-2-4 is not installed.

解决办法：安装 libgconf-2-4
apt-get install libgconf-2-4apt --fix-broken installapt-get install libgconf-2-4

安装过程可能报错报错缺少 libwebkit2gtk-xxx解决办法：安装 libwebkit2gtk ，修改并重新打包 deb
参考文档：

Linux安装向日葵解决libgconf-2-4依赖问题
Ubuntu-24.10无法安装Sunlogin-15.2的解决方案
ubuntu24.04缺少libwebkit2gtk-4.0-37 依赖
pkgs.org - Search Results for libwebkit2gtk

2、安装 lightdm ，并重启主机
apt install -y lightdmreboot

3、查看向日葵服务状态
systemctl status runsunloginclient

4、运行向日葵图形界面运行（推荐）：图形界面启动向日葵，登录个人账号，并设置开机自动启动。
命令行运行：
sudo /usr/local/sunlogin/bin/sunloginclient

参考文档：

向日葵 - Linux 个人版（命令行版本）使用手册
Ubuntu重启后向日葵远程连接一直卡在连接界面

5.2. 问题：向日葵连接卡住或黑屏问题描述：使用向日葵远程，可能会卡在 正在建立远程连接… ，或者登录后黑屏。
问题原因：向日葵要求显示管理器使用 lightdm，或者 gdm 不开启 wayland。
解决办法：显示管理器切换 lightdm，或者 gdm 关闭 wayland。
切换lightdm：
cat /etc/X11/default-display-managerdpkg-reconfigure lightdm # 选择 lightdmreboot

gdm 关闭 wayland：
cat /etc/X11/default-display-managervim /etc/gdm3/custom.conf # 修改 gdm3 配置：WaylandEnable=falsesystemctl restart gdm3reboot

参考文档：《Ubuntu22安装使用安卓模拟器Waydroid》
5.3. 安装配置RustDesk参考文档：《好好学Docker：自建RustDesk Server》
]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>废弃笔记本电脑复活：第三篇 利用阿里云盘扩展存储</title>
    <url>/dev-laptop-recycling-03-aliyunpan/</url>
    <content><![CDATA[1. 前言笔记本电脑的存储容量是有限的，扩展容量最简单的方法是外接硬盘。此外，我们还可以使用网盘来扩展存储。
本篇中，我们学习使用阿里云盘来扩展笔记本电脑的存储。


2. 本地挂载阿里云盘2.1. 安装Docker参考文档：《Docker入门篇》
2.2. 获取阿里云盘refresh token参考文档dockerhub - messense/aliyundrive-webdav，通过在线工具获取 refresh token。
以前通过浏览器自行获取refresh token的方法已失效，详情参考 Error: Invalid refresh token value found in –refresh-token argument。
2.3. 启动阿里云盘webdav容器1、拉取webdav镜像
docker pull messense/aliyundrive-webdav:2.3.3

2、启动webdav容器（本地文件服务器）
docker run -d --name=aliyundrive-webdav \  --restart=unless-stopped \  -p 8080:8080 \  -e REFRESH_TOKEN=&#x27;your token&#x27; \  -e WEBDAV_AUTH_USER=admin \  -e WEBDAV_AUTH_PASSWORD=admin\  messense/aliyundrive-webdav:2.3.3

3、查看webdav容器状态
docker psdocker logs aliyundrive-webdav

2.4. 挂载阿里云盘1、安装webdav驱动
apt install -y davfs2

2、创建挂载目录
mkdir -p /data/aliyundrive

3、挂载阿里云盘
mount -t davfs http://192.168.5.233:8080/ /data/aliyundrive

4、查看挂载
cd /data/aliyundrivels -l

2.5. 通过samba共享本地目录挂载阿里云盘后，和其他普通目录一样，也可以通过samba共享给其他主机。samba配置如下：
[aliyundrive]    path = /data/aliyundrive    browseable = yes    read only = yes    valid users = @sambashare @sadmin

2.6. 拷贝阿里云盘中的文件到本地如果担心访问阿里云盘时候的网络不稳定，可以把一部分文件下载到本地磁盘。
screen rsync -P -azv &#x27;爱情公寓&#x27; /data/samba/video

下载速度很快，家庭千兆宽带大概平均 10MB/s，峰值 30MB/s。
3. 安装使用阿里云盘客户端2024年，阿里云盘的WebDav成为了一个收费项，并且官方目前还没有推出Linux客户端，所以我们需要找到一个在Linux中使用阿里云盘的替代方案。经过检索，找到了一款开源的阿里云盘命令行客户端tickstep/aliyunpan，本节安装使用它。
3.1. 安装 aliyunpanwget https://github.com/tickstep/aliyunpan/releases/download/v0.3.4/aliyunpan-v0.3.4-linux-amd64.zipunzip aliyunpan-v0.3.4-linux-amd64.zipcp aliyunpan-v0.3.4-linux-amd64/aliyunpan /usr/local/bin

3.2. 使用 aliyunpan1、查看帮助
aliyunpan --help

2、设置配置目录和下载目录
mkdir -p /opt/aliyunpan/&#123;config,downloads&#125;export ALIYUNPAN_CONFIG_DIR=/opt/aliyunpan/configecho &#x27;export ALIYUNPAN_CONFIG_DIR=/opt/aliyunpan/config&#x27; &gt;&gt; ~/.bashrcaliyunpan configaliyunpan config set -savedir=/opt/aliyunpan/downloads # 执行命令后 ALIYUNPAN_CONFIG_DIR 中会生成配置文件 aliyunpan_config.json

默认设置为：

默认配置目录 /usr/local/bin ，也就是 aliyunpan 的父目录
默认下载目录 $HOME/Downloads 

3、登录需要先登录，已经登录过的可以跳过此步。登录是在浏览器上进行，需要进行两次登录。
# aliyunpan提示: 方向键上下可切换历史命令.提示: Ctrl + A / E 跳转命令 首 / 尾.提示: 输入 help 获取帮助.aliyunpan &gt; login请在浏览器打开以下链接进行登录，链接有效时间为5分钟。注意：你需要进行一次授权一次扫码的两次登录。https://openapi.alipan.com/oauth/authorize?client_id=cf9f70e8fc61430f8ec5ab5cadf31375&amp;redirect_uri请在浏览器里面完成扫码登录，然后再按Enter键继续...阿里云盘登录成功:  tickstepaliyunpan:/ tickstep(备份盘)$

4、操作文件（查看，下载，上传，同步）
lsdownload IMG_0106.JPGupload /Users/tickstep/Downloads/apt.zip /tmpsync start -ldir &quot;/tickstep/Documents/设计文档&quot; -pdir &quot;/备份盘/我的文档&quot; -mode &quot;upload&quot; -drive &quot;backup&quot;

注意：因为阿里云盘开始限速了，所以最大下载速度 1.5MBps ，详情参考最近下载速度非常不稳定
5、切换网盘支持切换备份盘、资源库，默认工作在备份盘下。
drive

]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS救援模式使用说明</title>
    <url>/dev-centos-rescue-mode/</url>
    <content><![CDATA[1. 前言当CentOS系统崩溃无法正常启动时，往往需要进入救援模式进行恢复。本文中，我们学习一下CentOS救援模式的使用方法。
参考文档：

Linux紧急救援模式(Centos7) 
centos 8 进入紧急救援模式
Basic System Recovery
centos7 出現welcome to emergency mode的解決方法



2. 进入救援模式2.1. 自带救援模式通过外接设备（KVM/显示器键盘）或者VNC连接到主机。
1、开机启动，在出现内核选项时按下 E 进入编辑
2、添加内核参数centos7找到 linux16 开头的行并在行尾添加内核参数 rd.breakcentos8找到 linux 开头的行并在行尾添加内核参数 rd.break
3、按下 Ctrl+X 进入单用户模式 
4、挂载并更改根目录
mount –o remount,rw /sysroot chroot /sysroot 

2.2. 第三方系统救援模式通过外接设备（KVM/显示器键盘）或者VNC连接到主机。
1、插入包含CentOS系统的光盘或者U盘
2、开机启动，按下F12，选择从光盘或者U盘启动
3、启动后选择 Troubleshooting，Rescue a CentOS system
4、更改根目录
chroot /mnt/sysimage

3. 救援模式常用操作3.1. 更改root密码1、进入自带救援模式
2、操作修改root密码
passwd roottouch /.autorelabel exit reboot

3.2. 修复启动失败1、进入自带救援模式
2、查看系统启动日志，找到具体的启动失败的原因
journalctl -xb

断电后的启动失败，极大概率是因为挂载问题。例如，如果提示时因为 /data 目录的挂载问题，那么就在 /etc/fstab 中注释掉 /data 目录的挂载。然后重启主机即可 reboot
3.3. 修复grub问题原因：rm /boot/grub* -rf问题表现：启动时出现报错 error: file ‘/grub2/i386-pc/normal.mod’ not found.
1、进入第三方系统救援模式
2、修复grub
rm /boot/grub* -rfls /bootmkdir /boot/grub2grub2-mkconfig -o /boot/grub2/grub.cfggrub2-install /dev/sdaexitreboot

3.4. 修复/boot问题原因：rm /boot/* -rf问题表现：启动时出现报错 error: file ‘/grub2/i386-pc/normal.mod’ not found.
1、进入第三方系统救援模式
2、修复/boot
ls /bootmount /dev/sr0 /mnt rpm -ivh /mnt/Packages/kernel-3.10.0-693.e17.x86_64.rpm.rpm --forcels /bootgrub2-install /dev/sdagrub2-mkconfig -o /boot/grub2/grub.cfgexitreboot

4. 救援模式与紧急模式救援模式（Rescue Mode）：也称作单用户模式，主要用于修复系统问题。在这种模式下，系统会尝试挂载所有的文件系统，启动网络服务，让你有机会对系统进行彻底的维护。你可以更改系统文件，修复系统问题（比如重设root密码，修复GRUB启动器问题等）。这是一个维护和修复系统问题的模式。
紧急模式（Emergency Mode）：这是最低级别的启动模式，只会提供最少的系统功能（仅启动根文件系统，这个文件系统默认会以只读方式进行挂载），不会启动网络连接，不会尝试挂载其他文件系统。这种模式主要用于在无法进入 Rescue 模式时进行最低级别的系统问题排查。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Ubuntu救援模式使用说明</title>
    <url>/dev-ubuntu-rescue-mode/</url>
    <content><![CDATA[1. 前言当Ubuntu系统崩溃无法正常启动时，往往需要进入救援模式进行恢复。本文中，我们以Ubuntu22为例，学习一下Ubuntu救援模式的使用方法。
参考文档：

详解在 Ubuntu 中引导到救援模式或紧急模式
如何启动 Ubuntu 22.04 进入救援/紧急模式
ubuntu系统崩溃，甚至Recovery模式和tty都无法进入，这个方法可以救援！
修复ubuntu22.04启动引导



2. 进入救援模式2.1. 自带救援模式通过外接设备（KVM/显示器键盘）或者VNC连接到主机。
1、开机启动，在启动时按下 ESC 键，进入 GRUB 引导页面
2、选择Ubuntu，按下 E 按键启动编辑
3、添加内核参数找到 linux 开头的行，并在行尾添加内核参数 systemd.unit=rescue.target
4、按下 Ctrl+X 或 F10 进入单用户模式 
5、以读写模式挂载根目录
mount -n -o remount,rw /

2.2. 第三方系统救援模式通过外接设备（KVM/显示器键盘）或者VNC连接到主机。
1、插入包含Ubuntu系统的光盘或者U盘
2、开机启动，按下F12，选择从光盘或者U盘启动
3、启动后选择 Try or Install Ubuntu 或者 Ubuntu (safe graphics) ，建议选择后者
4、挂载并更改根目录
mount /dev/sdb1 /mntmount --bind /dev /mnt/devmount --bind /proc /mnt/procmount --bind /sys /mnt/syscp /etc/resolv.conf /mnt/etc chroot /mnt

这里的 /dev/sdb1 是原来的系统盘，按需修改。
3. 救援模式常用操作3.1. 更改root密码1、进入自带救援模式
2、操作修改root密码
passwd rootreboot

3.2. 修复启动失败1、进入自带救援模式
2、查看系统启动日志，找到具体的启动失败的原因
journalctl -xb

断电后的启动失败，极大概率是因为挂载问题。例如，如果提示时因为 /data 目录的挂载问题，那么就在 /etc/fstab 中注释掉 /data 目录的挂载。然后重启主机即可 reboot
3.3. 修复grub1、进入第三方系统救援模式
2、安装 boot-repair
add-apt-repository ppa:yannubuntu/boot-repair apt update apt install boot-repair

3、启动 boot-repair
boot-repair

boot-repair需要图形化界面，纯命令行界面没有尝试过，不确定是否可行。
4. 救援模式与紧急模式救援模式（Rescue Mode）：也称作单用户模式，主要用于修复系统问题。在这种模式下，系统会尝试挂载所有的文件系统，启动网络服务，让你有机会对系统进行彻底的维护。你可以更改系统文件，修复系统问题（比如重设root密码，修复GRUB启动器问题等）。这是一个维护和修复系统问题的模式。
紧急模式（Emergency Mode）：这是最低级别的启动模式，只会提供最少的系统功能（仅启动根文件系统，这个文件系统默认会以只读方式进行挂载），不会启动网络连接，不会尝试挂载其他文件系统。这种模式主要用于在无法进入 Rescue 模式时进行最低级别的系统问题排查。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>ubuntu</tag>
        <tag>问题排查</tag>
      </tags>
  </entry>
  <entry>
    <title>网络排查工具MTR入门篇</title>
    <url>/dev-mtr-start/</url>
    <content><![CDATA[1. MTR简介网络排查工程常常包含了诊断网络中断和稳定性问题。在这些场合下，网络工程师们通常会使用一些有效的网络工具来帮助他们准确地查找问题所在。其中，MTR是一款极其实用的网络诊断工具。
MTR，全名为My Traceroute，是一款网络诊断工具，结合了traceoute和ping两种常见工具的功能。通过源自源主机到目标主机的网络连接的持续跟踪，MTR能提供每个路由节点的响应速度和系统稳定性情况。
参考文档：

关于网络排查工具MTR那些你必须了解的事情
使用 MTR 分析网络延迟及丢包
云服务器网络访问丢包



2. 安装MTRMTR是开源的，我们可以在大多数的Linux发行版中找到它。
在Ubuntu或Debian系统中，我们可以通过下列命令来进行安装：
sudo apt-get install mtr

如果我们正在使用CentOS或者Fedora，可以使用
sudo yum install mtr

3. 运行MTRmtr 192.168.56.101

运行后，MTR将运行统计报告，展示当前主机到目标主机的每一跳联网情况。
运行结果的主要信息如下：

Hostname：到目的服务器要经过的每个主机 IP 或名称。
Nr：经过节点的数量。
Loss%：对应节点的丢包率。
Sent：发送的数据包数量。
Recv：接收到响应的数量。
Best：最短的响应时间。
Avrg：平均响应时间。
Worst：最长的响应时间。
Last：最近一次的响应时间。

在MTR运行时，我们能看到各路由节点的丢包率，如果某一跳的丢包率很高，那么这就可能是网络问题的所在。大多数情况，只需要关注最后一跳（目的地址）是否有丢包即可。
此外，MTR并不仅仅提供实时的网络连接状态，还可以通过生成报告的方式保存网络状态。通过在命令后添加”–report”，MTR将在运行一定时间后生成一个报告。
mtr --report 192.168.56.101







]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS7内核升级</title>
    <url>/dev-centos7-kernal-upgrade/</url>
    <content><![CDATA[1. 前言本文学习并实践CentOS7内核升级方法，备忘。
参考文档：

CentOS-7 升级内核
ELRepo官网



2. 查看系统版本和当前内核版本cat /etc/redhat-releaseuname -r

3. 导入ELRepo项目1、导入public key
yum -y updaterpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org

2、安装ELRepo仓库的yum源
yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm

3、查看可用内核包
yum --disablerepo=&quot;*&quot; --enablerepo=&quot;elrepo-kernel&quot; list available

输出内容示例：
Available Packageskernel-lt.x86_64                       5.4.256-1.el7.elrepo            elrepo-kernelkernel-lt-devel.x86_64                 5.4.256-1.el7.elrepo            elrepo-kernelkernel-lt-doc.noarch                   5.4.256-1.el7.elrepo            elrepo-kernelkernel-lt-headers.x86_64               5.4.256-1.el7.elrepo            elrepo-kernelkernel-lt-tools.x86_64                 5.4.256-1.el7.elrepo            elrepo-kernelkernel-lt-tools-libs.x86_64            5.4.256-1.el7.elrepo            elrepo-kernelkernel-lt-tools-libs-devel.x86_64      5.4.256-1.el7.elrepo            elrepo-kernelkernel-ml.x86_64                       6.5.2-1.el7.elrepo              elrepo-kernelkernel-ml-devel.x86_64                 6.5.2-1.el7.elrepo              elrepo-kernelkernel-ml-doc.noarch                   6.5.2-1.el7.elrepo              elrepo-kernelkernel-ml-headers.x86_64               6.5.2-1.el7.elrepo              elrepo-kernelkernel-ml-tools.x86_64                 6.5.2-1.el7.elrepo              elrepo-kernelkernel-ml-tools-libs.x86_64            6.5.2-1.el7.elrepo              elrepo-kernelkernel-ml-tools-libs-devel.x86_64      6.5.2-1.el7.elrepo              elrepo-kernelperf.x86_64                            5.4.256-1.el7.elrepo            elrepo-kernelpython-perf.x86_64                     5.4.256-1.el7.elrepo            elrepo-kernel

长期维护版本为 kernal-lt ，最新主线稳定版为 kernal-ml ，这里选择 kernal-lt
4. 安装新版内核1、安装新版内核 kernal-lt
yum  --enablerepo=elrepo-kernel  install  -y  kernel-lt

安装成功会输出：
Running transaction  Installing : kernel-lt-5.4.256-1.el7.elrepo.x86_64                                                                                                                          1/1  Verifying  : kernel-lt-5.4.256-1.el7.elrepo.x86_64                                                                                                                          1/1Installed:  kernel-lt.x86_64 0:5.4.256-1.el7.elrepo

2、查看可用内核版本及启动顺序
awk -F\&#x27; &#x27;$1==&quot;menuentry &quot; &#123;print i++ &quot; : &quot; $2&#125;&#x27; /boot/grub2/grub.cfg

输出内容示例：
0 : CentOS Linux 7 Rescue 05110432cb69b45d6c089de56b266fed (5.4.258-1.el7.elrepo.x86_64)1 : CentOS Linux (5.4.256-1.el7.elrepo.x86_64) 7 (Core)2 : CentOS Linux (4.19.188-10.el7.ucloud.x86_64) 7 (Core)3 : CentOS Linux (0-rescue-05110432cb69b45d6c089de56b266fed) 7 (Core)4 : CentOS Linux (0-rescue-8e8ed229018f0a9cfe91d03b023e4f67) 7 (Core)

5. 设置内核启动顺序1、安装内核升级辅助工具 grub2-pc
yum install -y grub2-pc

2、设置默认内核
grub2-set-default 1# orgrub2-set-default &#x27;CentOS Linux (5.4.256-1.el7.elrepo.x86_64) 7 (Core)&#x27;

3、生成 grub 配置文件，重新创建内核配置
grub2-mkconfig -o /boot/grub2/grub.cfg

4、再次查看可用内核版本及启动顺序
awk -F\&#x27; &#x27;$1==&quot;menuentry &quot; &#123;print i++ &quot; : &quot; $2&#125;&#x27; /boot/grub2/grub.cfg
此时，CentOS Linux (5.4.256-1.el7.elrepo.x86_64) 7 (Core)的排序号会变成0。
5、修改grub配置
vim /etc/default/grub

修改 GRUB_DEFAULT 的值为 0 （默认为saved），其他参数不用变。
6. 重启系统1、重启系统
reboot

2、查看当前内核版本
uname -r

以上，内核升级完成。
7. 删除旧内核（可选）1、查看可用内核列表
rpm -qa | grep kernel

输出示例：
kernel-tools-libs-4.19.188-10.el7.ucloud.x86_64kernel-4.19.188-10.el7.ucloud.x86_64kernel-tools-4.19.188-10.el7.ucloud.x86_64kernel-headers-4.19.188-10.el7.ucloud.x86_64kernel-devel-4.19.188-10.el7.ucloud.x86_64kernel-lt-5.4.256-1.el7.elrepo.x86_64kernel-tools-libs-devel-4.19.188-10.el7.ucloud.x86_64

2、删除旧内核
yum remove -y kernel-4.19.188 kernel-devel-4.19.188 kernel-headers-4.19.188

3、删除旧版本工具包
yum remove kernel-tools-libs-devel-4.19.188-10 kernel-tools-4.19.188-10

PS：安装yum-utils 工具，当系统安装的内核大于3个时，会自动删除旧的内核版本
yum install -y  yum-utils

8. 安装新版本工具包（可选）1、安装新版本工具包
yum --disablerepo=\* --enablerepo=elrepo-kernel install -y kernel-lt-tools.x86_64

2、查看已安装内核和工具包
rpm -qa | grep kernel

9. 后记2024年底centos7停止更新了，官方仓库和第三方都清空了kernel安装源，需要手动更新。参考文档：

2025年后手动更新centos 7内核
centos7 kernal下载地址

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo本地搜索加速（七牛版）</title>
    <url>/dev-hexo-local-search-accelerate-qiniu/</url>
    <content><![CDATA[1. 前言《Hexo本地搜索加速》一文中，给 search.xml 文件配置了 jsdelivr CDN。但是，jsdelivr CDN 目前国内已经无法访问了，只能另外寻找一个替代。
可选的替代包括：

statically.io
githack.com

配置方法可以参考hexo-theme-next/commit/2ff6c11，但是，这些替代CDN，国内也无法访问。找来找去，最终锁定了七牛，一直用它作为图床，其实用做 search.xml 文件的存储仓库，也很合适。
本文就来学习一下使用七牛作为 search.xml 存储仓库的方法。


2. 准备工作2.1. 安装配置hexo环境参考文档《Hexo环境搭建2021年8月版》，安装配置hexo环境。
执行 hexo g 命令，在 public 目录中生成要部署的静态文件，其中包含 search.xml 。
2.2. 获取七牛api key访问七牛密钥管理页面，创建一个密钥对（AccessKey/SecretKey）。
2.3. 安装七牛依赖npm install qiniu --save

3. 上传 search.xml参考文档：对象存储 - Node.js SDK
1、实现上传脚本 upload-qiniu.js
const qiniu = require(&#x27;qiniu&#x27;);const fs = require(&#x27;fs&#x27;);// 七牛配置const accessKey = &#x27;您的七牛Access Key&#x27;;const secretKey = &#x27;您的七牛Secret Key&#x27;;const bucketName = &#x27;您的存储空间名称&#x27;;const key = &#x27;doc/search.xml&#x27;; // 如果需要覆盖之前文件/图片，使用相同的keyconst mac = new qiniu.auth.digest.Mac(accessKey, secretKey);const config = new qiniu.conf.Config();config.zone = qiniu.zone.Zone_z0; // 华东地区，根据需要选择合适的地区const formUploader = new qiniu.form_up.FormUploader(config);const putExtra = new qiniu.form_up.PutExtra();// 上传文件const uploadImage = () =&gt; &#123;  // 读取本地文件  const localFile = &#x27;./public/search.xml&#x27;;  const readStream = fs.createReadStream(localFile);  // 生成上传凭证  const options = &#123;    scope: `$&#123;bucketName&#125;:$&#123;key&#125;`,    force: true, // 强制覆盖同名文件  &#125;;  const putPolicy = new qiniu.rs.PutPolicy(options);  const uploadToken = putPolicy.uploadToken(mac);  // 开始上传  formUploader.putStream(uploadToken, key, readStream, putExtra, (respErr, respBody, respInfo) =&gt; &#123;    if (respErr) &#123;      throw respErr;    &#125;    if (respInfo.statusCode === 200) &#123;      console.log(&#x27;上传成功&#x27;, respBody);    &#125; else &#123;      console.log(&#x27;上传失败&#x27;, respInfo.statusCode, respBody);    &#125;  &#125;);&#125;;uploadImage();

2、测试上传脚本
node upload-qiniu.js

上传成功后出现提示：
上传成功 &#123; hash: &#x27;FkYxkyrsSytBGvgYgEvPs28lVI40&#x27;, key: &#x27;doc/search.xml&#x27; &#125;

4. 主题中配置使用七牛CDNNext主题中，配置使用七牛CDN中的 search.xml。具体配置方法参考hexo-theme-next/commit/2ff6c11
5. 整合到自动部署参考《Hexo配置GitHub Actions自动构建发布》，配置自动部署。具体配置方法参考hexo-deploy/commit/0e3503d。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker和Makefile结合简化构建部署流程</title>
    <url>/dev-docker-makefile/</url>
    <content><![CDATA[1. 前言在现代软件开发中，Docker 已经成为一项极为重要的技术，而 Makefile 则是构建和自动化流程的有力工具。将 Docker 和 Makefile 结合起来，可以显著简化应用程序的构建、测试和部署流程，提高开发效率和可维护性。


2. Docker简介Docker 是一种容器化技术，允许开发者将应用程序及其依赖项打包成一个独立的容器。这个容器包含了运行应用所需的所有组件，包括操作系统、库、环境变量等。这使得应用在不同的环境中能够一致地运行，避免了“在我的机器上能运行”的问题。
Docker 的主要概念包括镜像（Image）和容器（Container）：
镜像：一个镜像是一个只读的模板，包含了运行应用所需的文件系统、代码和配置。镜像是构建容器的基础。容器：容器是基于镜像创建的一个运行实例，它隔离了应用及其依赖项，使得应用可以在一个隔离的环境中运行。
3. Makefile简介Makefile 是一个文本文件，其中定义了一系列规则，用于自动化构建过程。通过 Makefile，开发者可以定义源代码的依赖关系，以及如何编译、链接和构建最终的应用程序。
Makefile 的基本结构如下：
target: prerequisites    recipe


目标（Target）：表示要生成的文件或执行的操作。
依赖项（Prerequisites）：指定目标所依赖的文件或其他目标。
命令（Recipe）：在目标和依赖项之间，定义了如何生成目标的操作。

以下是一个简单的示例 Makefile，用于编译一个包含两个源文件的 C 程序：
CC = gccCFLAGS = -Wallall: myprogrammyprogram: main.o utils.o    $(CC) $(CFLAGS) -o myprogram main.o utils.omain.o: main.c utils.h    $(CC) $(CFLAGS) -c main.cutils.o: utils.c utils.h    $(CC) $(CFLAGS) -c utils.cclean:    rm -f *.o myprogram

在这个示例中，有三个目标：

all：默认目标，运行 myprogram 目标，生成最终的可执行文件。
myprogram：生成可执行文件 myprogram，依赖于 main.o 和 utils.o。
clean：删除生成的目标文件和可执行文件。

通过在命令行中运行 make 命令，Make 工具会查找当前目录下的 Makefile 文件，并根据定义的规则执行构建操作。例如，运行 make 将会编译源文件并生成可执行文件。
.PHONY 是一个特殊的目标，用于声明一组伪目标（Phony Targets）。伪目标是那些并不代表真实文件的目标，而是代表一些操作或动作。使用 .PHONY 可以告诉 Make 工具，这些目标不应该被当作文件名进行比较，而是始终会执行对应的命令，无论文件是否已经存在。
带有 .PHONY 的示例 Makefile：
.PHONY: clean build runclean:    rm -rf *.obuild: clean    gcc -o myprogram main.crun: build    ./myprogram

在这个示例中，.PHONY 声明了三个伪目标：clean, build, 和 run。当你运行 make clean，make build，或 make run 时，Make 工具将会执行与这些目标关联的命令，而不会考虑文件名的存在与否。
Makefile 可以更加复杂，支持条件语句、循环、变量等功能，使其能够适应各种项目的构建需求。使用 Makefile 可以提高开发效率，确保代码在不同环境下能够正确地构建。
4. Docker和Makefile结合示例参考项目：github - voidking/python-wechaty-template
项目中Makefile内容为：
P=$(shell pwd).PHONY: buildbuild:        docker build -t py-wechaty-template-bot:latest ..PHONY: dockerrundockerrun:        docker stop bot &amp;&amp; docker rm bot        docker run -it -d -v $(P):/bot --name bot -p 8004:8004 py-wechaty-template-bot:latest.PHONY: botbot:        make build        make dockerrun.PHONY: installinstall:        pip install -r requirements.txt.PHONY: runrun:        python bot.py

项目中Dockerfile内容为：
FROM python:3.9WORKDIR /botCOPY requirements.txt requirements.txtCOPY Makefile MakefileRUN make installCOPY . .CMD [ &quot;make&quot;, &quot;run&quot;]

宿主机上执行 make bot ，调用流程如下：
1、从Makefile中找到目标bot对应的命令，执行
make buildmake dockerrun

2、从Makefile中找到build和dockerrun对应的命令，执行
docker build -t py-wechaty-template-bot:latest .docker stop bot &amp;&amp; docker rm botdocker run -it -d -v $(P):/bot --name bot -p 8004:8004 py-wechaty-template-bot:latest

3、执行docker build时，当前目录中的Makefile和其他文件一起被拷贝到了容器中
4、执行docker build中的make install时，从Makefile中找到install对应的命令，容器中执行
pip install -r requirements.txt

5、执行docker build结束时，指定镜像的启动命令为make run，从Makefile中找到run对应的命令为python bot.py
6、执行docker run时，因为没有指定启动命令，所以执行默认启动命令
python bot.py

5. 优势和总结将 Docker 和 Makefile 结合，为开发者带来了许多优势：

自动化流程：通过 Makefile，可以定义一系列自动化的构建、测试和部署流程，减少手动操作的需要。
一致性：Docker 容器保证了应用在不同环境中的一致运行，而 Makefile 确保了构建过程的一致性。
可维护性：将构建和部署步骤定义在 Makefile 中，使得团队成员可以轻松了解和执行这些操作。

综上所述，结合 Docker 和 Makefile 可以使得容器化应用的构建和部署变得更加高效、可靠，并且易于维护。这种结合为现代软件开发提供了一种强大的工具，能够帮助开发者更好地管理复杂的构建流程。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Markdown使用Mermaid绘图入门篇</title>
    <url>/dev-markdown-mermaid-start/</url>
    <content><![CDATA[1. 前言Mermaid是一种用于描述图表的文本语言，它支持多种图表类型，如流程图、序列图、甘特图等。通过简单的文本描述，就可以生成复杂的图表，而不需要手动绘制。
Mermaid可以与Markdown结合使用，方便快捷地在文档中插入图表。本文将介绍如何使用Mermaid语言在Markdown文档中绘制各种类型的图表。
参考文档：

github - mermaid-js/mermaid
Mermaid官方文档
使用 markdown 画流程图、时序图
Markdown流程图语法示例



2. 安装与配置Mermaid通常不需要额外的安装，因为它是在浏览器中渲染的。想要在Markdown中使用Mermaid语言，需要Markdown编辑器支持Mermaid语法，并启用了Mermaid渲染插件。
3. 在Markdown中插入Mermaid图表在Markdown文档中插入Mermaid图表非常简单。只需使用三个反引号（```）将Mermaid代码块括起来，并在反引号后指定Mermaid语言。

4. Hexo Next主题启用Mermaid参考文档：

Next - Mermaid
Hexo NexT 主题渲染 Mermaid 绘图

1、安装 hexo-filter-mermaid-diagrams 插件
npm install hexo-filter-mermaid-diagrams --save

2、Next主题启用Mermaid
# Mermaid tagmermaid:  enable: true  # Available themes: default | dark | forest | neutral  theme: default

具体修改参考next主题修改commit/0790595d。
5. 绘制图表5.1. 绘制流程图流程图是描述流程、决策和步骤的图表。以下是一个简单的流程图示例：
graph LR    A[开始] --&gt; B[中间步骤]    B --&gt; C[结束]

上述代码使用Mermaid语法绘制了一个由三个节点组成的流程图。
graph LR
    A[开始] --> B[中间步骤]
    B --> C[结束]


5.2. 绘制序列图序列图用于展示多个参与者之间的交互过程。以下是一个简单的序列图示例：
sequenceDiagram    participant Alice    participant Bob    Alice -&gt;&gt; Bob: 你好，Bob！    Bob --&gt;&gt; Alice: 你好，Alice！

上述代码使用Mermaid语法绘制了Alice和Bob之间的简单交互过程。
sequenceDiagram
    participant Alice
    participant Bob
    Alice ->> Bob: 你好，Bob！
    Bob -->> Alice: 你好，Alice！

5.3. 绘制甘特图甘特图用于展示任务的时间轴和进度。以下是一个简单的甘特图示例：
gantt    title 项目计划    dateFormat  YYYY-MM-DD    section 项目A    任务1      :a1, 2023-08-01, 7d    任务2      :after a1, 3d    section 项目B    任务3      :2023-08-10, 5d    任务4      :2023-08-15, 4d

上述代码使用Mermaid语法绘制了一个包含两个项目的甘特图，每个项目有多个任务。
gantt
    title 项目计划
    dateFormat  YYYY-MM-DD
    section 项目A
    任务1      :a1, 2023-08-01, 7d
    任务2      :after a1, 3d
    section 项目B
    任务3      :2023-08-10, 5d
    任务4      :2023-08-15, 4d

5.4. 高级用法与样式Mermaid支持许多高级用法和样式设置，例如节点样式、连接线样式、图表方向等。更多信息请参考Mermaid官方文档。
6. 小结通过Mermaid语言，我们可以在Markdown文档中轻松绘制流程图、序列图、甘特图等各种类型的图表。只需按照本教程的指导，使用Mermaid语法编写图表代码，并确保我们的Markdown编辑器支持Mermaid渲染，即可轻松创建复杂的图表内容。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>markdown</tag>
        <tag>好好学Hexo</tag>
        <tag>mermaid</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python调用OpenAI API</title>
    <url>/dev-python-openai-api/</url>
    <content><![CDATA[1. 前言本文学习使用Python调用OpenAI API的方法，基于openai python库 openai==0.27.8注意：openai python库版本更新很快，本文并不适用更高版本openai python库。
参考文档：

OpenAI - Introduction
OpenAI - Python library
github - openai/openai-python
《使用Docker安装配置OPANAI API代理》



2. 安装OpenAI客户端pip install openai==0.27.8

3. 命令行访问OpenAI3.1. 命令行参数usage: openai [-h] [-V] [-v] [-b API_BASE] [-k API_KEY] [-p PROXY [PROXY ...]] [-o ORGANIZATION] &#123;api,tools,wandb&#125; ...positional arguments:  &#123;api,tools,wandb&#125;    api                 Direct API calls    tools               Client side tools for convenience    wandb               Logging with Weights &amp; Biasesoptional arguments:  -h, --help            show this help message and exit  -V, --version         show program&#x27;s version number and exit  -v, --verbose         Set verbosity.  -b API_BASE, --api-base API_BASE                        What API base url to use.  -k API_KEY, --api-key API_KEY                        What API key to use.  -p PROXY [PROXY ...], --proxy PROXY [PROXY ...]                        What proxy to use.  -o ORGANIZATION, --organization ORGANIZATION                        Which organization to run as (will use your default organization if not specified)

3.2. Hello worldexport OPENAI_API_KEY=&quot;sk-xxx&quot;openai api chat_completions.create -m gpt-3.5-turbo -g user &quot;Hello world&quot;


3.3. 测试OPENAI API是否畅通# 测试官方OPENAI APIopenai api chat_completions.create -m gpt-3.5-turbo -g user &quot;Hello world&quot;# 测试OPENAI API代理openai -b &quot;https://api.openai-forward.com/v1&quot; api chat_completions.create -m gpt-3.5-turbo -g user &quot;Hello world&quot;

3.4. 可能的问题3.4.1. 配置KEY问题执行openai命令前，需要先配置KEY，否则会报错：
Error: No API key provided. You can set your API key in code using &#x27;openai.api_key = &lt;API-KEY&gt;&#x27;, or you can set the environment variable OPENAI_API_KEY=&lt;API-KEY&gt;). If your API key is stored in a file, you can point the openai module at it with &#x27;openai.api_key_path = &lt;PATH&gt;&#x27;. You can generate API keys in the OpenAI web interface. See https://platform.openai.com/account/api-keys for details.

或者，执行openai命令时，使用参数-k配置KEY。
3.4.2. 网络问题执行可能会卡住一段时间后报错：
[2023-08-05 08:55:26,852] Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by &#x27;NewConnectionError(&#x27;&lt;urllib3.connection.HTTPSConnection object at 0x7f94afde5d30&gt;: Failed to establish a new connection: [Errno 110] Connection timed out&#x27;)&#x27;: /v1/chat/completions

也可能会直接报错：
Error: Error communicating with OpenAI: (&#x27;Connection aborted.&#x27;, ConnectionResetError(54, &#x27;Connection reset by peer&#x27;))

解决办法一：配置全局代理（推荐）
export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890openai api chat_completions.create -m gpt-3.5-turbo -g user &quot;Hello world&quot;

解决方法二：使用OPENAI API代理
openai -b &quot;https://api.openai-forward.com/v1&quot; api chat_completions.create -m gpt-3.5-turbo -g user &quot;Hello world&quot;

解决办法三：执行命令时指定代理（验证时没有生效）
openai -p http://127.0.0.1:7890 -k sk-xxx api chat_completions.create -m gpt-3.5-turbo -g user &quot;Hello world&quot;

使用-p参数时，必须配合使用-k参数，否则报错：
usage: openai [-h] [-V] [-v] [-b API_BASE] [-k API_KEY] [-p PROXY [PROXY ...]] [-o ORGANIZATION] &#123;api,tools,wandb&#125; ...openai: error: argument &#123;api,tools,wandb&#125;: invalid choice: &#x27;gpt-3.5-turbo&#x27; (choose from &#x27;api&#x27;, &#x27;tools&#x27;, &#x27;wandb&#x27;)

4. Python访问OpenAI4.1. Hello world1、新建文件 hello.py
import osimport openai# openai.api_base = os.getenv(&quot;OPENAI_API_BASE&quot;)# openai.api_key = os.getenv(&quot;OPENAI_API_KEY&quot;)openai.api_base = &#x27;https://api.openai.com/v1&#x27;openai.api_key = &#x27;sk-xxx&#x27;chat_completion = openai.ChatCompletion.create(model=&quot;gpt-3.5-turbo&quot;, messages=[&#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello world&quot;&#125;])print(chat_completion)

2、执行 hello.py
python hello.py

返回值json结构：
&#123;  &quot;id&quot;: &quot;chatcmpl-7ksejsnNZEmq7Q0YPGQ1iRa1x5LHf&quot;,  &quot;object&quot;: &quot;chat.completion&quot;,  &quot;created&quot;: 1691407541,  &quot;model&quot;: &quot;gpt-3.5-turbo-0613&quot;,  &quot;choices&quot;: [    &#123;      &quot;index&quot;: 0,      &quot;message&quot;: &#123;        &quot;role&quot;: &quot;assistant&quot;,        &quot;content&quot;: &quot;Hello! How can I assist you today?&quot;      &#125;,      &quot;finish_reason&quot;: &quot;stop&quot;    &#125;  ],  &quot;usage&quot;: &#123;    &quot;prompt_tokens&quot;: 9,    &quot;completion_tokens&quot;: 9,    &quot;total_tokens&quot;: 18  &#125;&#125;

4.2. 可能的问题4.2.1. 文件名问题测试文件，不要命名为 openai.py ，否则会报错：
AttributeError: partially initialized module &#x27;openai&#x27; has no attribute &#x27;ChatCompletion&#x27; (most likely due to a circular import)




]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>llm</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>Wechaty入门篇</title>
    <url>/dev-wechaty-start/</url>
    <content><![CDATA[1. Wechaty简介
Wechaty 是一个开源聊天机器人框架SDK，具有高度封装、高可用的特性，支持NodeJs, Python, Go 和Java 等多语言版本。在过去的4年中，服务了数万名开发者，收获了 Github 的 1w+ Star。同时配置了完整的 DevOps 体系并持续按照 Apache 的方式管理技术社区。

参考文档：

github - wechaty/getting-started
github - Wechaty/python-wechaty
github - wechaty/python-wechaty-template
github - wechaty/python-wechaty-getting-started
github - wechaty/docker-getting-started
Everything about Wechaty
python-wechaty首页
使用Padlocal协议
github - wechaty/puppet-padlocal
使用Paimon协议



2. Wechaty核心概念2.1. 协议运行微信机器人有多种协议可以使用，有以下几种不同的协议方式：

Padlocal协议(推荐使用)
Paimon协议(最简单)
免费Web协议(目前已不可用)
puppet-xp协议（仅windows下可用）

每个协议都有不用的特点，如果不知道选什么，那么建议使用Padlocal协议。
2.2. TokenToken 是 Wechaty 开放源代码项目中，所设计和支持的一种认证技术，可以使用第三方的 Wechaty Puppet Service 服务。Wechaty 社区中，提供 Puppet Service 的第三方，称之为 Wechaty Puppet Service Provider 。句子互动公司是 Wechaty PadPlus, WxWork, Donut 等 Puppet Service 的 Provider，提供对应 Puppet Service 的 Token ，实现开发者可以使用对应的协议服务。
在设置 Token 后，每个 Token 将可以使得一个微信号以 iPad 或 Mac 登录的方式进行登录，避免了既有网页端登录受限的问题。
每个 Token 可以使得一个微信号登录，如果希望多个号同时登录，需要获取多个 Token 。 但 Token 不与微信号绑定，即可以多个号不同时段轮流使用一个 Token 进行登录。
2.3. PuppetWechaty 中的术语 Puppet 是用于实现协议插件的抽象类。插件是帮助控制 微信/Whatspp/TikTok 的组件，这就是我们称之为Puttet（傀儡）的原因。
3. Padlocal Getting Started参考文档：

github - voidking/python-wechaty-template
使用Padlocal协议
Python Wechaty如何使用PadLocal Puppet Service
Wechaty Gateway 使用教程

3.1. 申请Token申请 Padlocal Token 链接：Padlocal免费试用Token可以使用7天，续费200元/月。建议成为Wechaty开发者，可以免费续期Token，参考文档How to be a Wechaty Contributor。
3.2. 启动Gateway1、启动Gateway Docker
git clone https://github.com/voidking/python-wechaty-template.gitcd python-wechaty-template/./start_gateway_docker.sh puppet_padlocal_xxx

其中 puppet_padlocal_xxx 要替换成自己的Token。

最后一行的链接，是一个微信登录的二维码，python-wechaty连接gateway之后能够自动获取到这个链接并展示，以便我们登录。当然，直接在浏览器中也能访问这个二维码，微信扫码可以登录，不过没什么用。
Gateway启动时，会生成一个WECHATY_TOKEN，Gateway使用；同时会填入到 .env 文件中，给机器人使用。运行自己的机器人代码时，也要用到这个 WECHATY_TOKEN。
2、验证Gateway是否启动成功
curl https://api.chatie.io/v0/hosties/$WECHATY_TOKEN


3.3. 运行机器人make bot

报错：
Traceback (most recent call last):  File &quot;/usr/local/lib/python3.9/site-packages/wechaty/wechaty.py&quot;, line 445, in start    await self.puppet.start()  File &quot;/usr/local/lib/python3.9/site-packages/wechaty_puppet_service/puppet.py&quot;, line 911, in start    self._init_puppet()  File &quot;/usr/local/lib/python3.9/site-packages/wechaty_puppet_service/puppet.py&quot;, line 894, in _init_puppet    raise WechatyPuppetConfigurationError(wechaty_puppet.exceptions.WechatyPuppetConfigurationError: WechatyPuppetConfigurationError(&quot;can&#x27;t not ping endpoint: 42.194.xxx.xxx:8080&quot;, None, None)

这是因为，机器人连接Gateway Docker时，使用了外网IP，而外网IP的8080端口并未开放。有两个解决办法：（1）开放外网IP的8080端口；（2）配置使用 WECHATY_PUPPET_SERVICE_ENDPOINT 变量。
这里选择第二种方法：
docker run -it --name bot -d \    -v $(pwd):/bot \    -p 8004:8004 \    -e WECHATY_PUPPET_SERVICE_ENDPOINT=&quot;192.168.56.101:8080&quot; \    py-wechaty-template-bot:latestdocker logs -f bot
这里有个错误：Wechaty - ERROR - internal error &lt;&gt;参考Wechaty - ERROR - internal error and failed to scan，忽略即可。
扫描登录，第一次登录时，会询问你登录的是 mac、windows还是ipad，这里要选择ipad。
登录成功后，半分钟左右，会自动退出，提示为了你的账号安全，请重新登录。
然后，再扫描登录，登录成功后还是会退出，后来再扫描二维码就没有反应了。。。
Padlocal Get Started，以失败告终。
4. Paimon Get StartedPaimon相对于Padlocal，缺点很多：发送/接收图片、文件较慢；不支持接收/转发动图；不支持接收/发送语音消息；不支持转发音频/视频等。不过，Paimon最大的优点就是简单，这就够了，关键是能用。
参考文档：

github - wechaty/python-wechaty-getting-started
使用Paimon协议

4.1. 申请Token申请Token链接：Puppet Service: Paimon免费试用Token可以使用7天，续费200元/月。建议成为Wechaty开发者，可以免费续期Token，参考文档How to be a Wechaty Contributor。
4.2. 运行机器人1、安装依赖
pip install wechaty -i https://pypi.tuna.tsinghua.edu.cn/simple

2、编写脚本 test.py
import asyncio, osfrom typing import List, Optional, Unionfrom wechaty_puppet import FileBox  # type: ignorefrom wechaty import Wechaty, Contactfrom wechaty.user import Message, Roomclass MyBot(Wechaty):    async def on_message(self, msg: Message):        &quot;&quot;&quot;        listen for message event        &quot;&quot;&quot;        from_contact: Optional[Contact] = msg.talker()        text = msg.text()        room: Optional[Room] = msg.room()        if text == &#x27;ding&#x27;:            conversation: Union[                Room, Contact] = from_contact if room is None else room            await conversation.ready()            await conversation.say(&#x27;dong&#x27;)            # file_box = FileBox.from_url(            #     &#x27;https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/&#x27;            #     &#x27;u=1116676390,2305043183&amp;fm=26&amp;gp=0.jpg&#x27;,            #     name=&#x27;ding-dong.jpg&#x27;)            file_box = FileBox.from_url(                &#x27;https://cdn.voidking.com/img/about/about.png?imageView2/0/w/1000&#x27;,                name=&#x27;ding-dong.jpg&#x27;)            await conversation.say(file_box)os.environ[&#x27;TOKEN&#x27;] = &quot;puppet_paimon_1fe5f846-3cfb-401d-b20c-XXXXX&quot;asyncio.run(MyBot().start())

3、启动脚本
python test.py
使用微信号B扫码登录微信。
4、测试 ding dong使用微信号A给微信号B发送一个ding，可以收到自动回复的dong，但是并没有接收到图片消息。代码出现了报错：
经查，是因为教程中给的图片地址过期了，换成可用的地址后，可以正常接收到图片消息了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>docker</tag>
        <tag>微信</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置OPANAI API代理</title>
    <url>/dev-docker-openai-api-proxy/</url>
    <content><![CDATA[1. 前言OPENAI API代理，顾名思义，是一种用于代理OPENAI API的专用代理。目前OPENAI API代理中，做的比较好的是 openai-forward。本文中，我们基于Docker安装配置一下openai-forward。
参考文档：

github - KenyonY/openai-forward
《Python调用OpenAI API》



2. 使用openai-forward代理KenyonY/openai-forward项目作者提供了三个公开的代理服务。

原始 OpenAI 服务地址1：https://api.openai-forward.com
原始 OpenAI 服务地址2：https://render.openai-forward.com
开启缓存的服务地址（用户请求结果将被保存一段时间）：https://smart.openai-forward.com

命令行使用OPENAI API代理的方法：
openai -b &quot;https://api.openai-forward.com/v1&quot; api chat_completions.create -m gpt-3.5-turbo -g user &quot;Hello world&quot;

Python使用OPEANAI API代理的方法：
import osimport openaiopenai.api_base = &#x27;https://api.openai-forward.com/v1&#x27;openai.api_key = &#x27;sk-xxx&#x27;chat_completion = openai.ChatCompletion.create(model=&quot;gpt-3.5-turbo&quot;, messages=[&#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello world&quot;&#125;])print(chat_completion)

3. 自建openai-forward代理3.1. 机器准备如果打算自建 openai-forward 服务，那么首先要准备一台海外的可以访问 OPENAI API 的主机。
主机测试方法：
curl https://api.openai.com/v1

正常响应：
&#123;  &quot;error&quot;: &#123;    &quot;type&quot;: &quot;invalid_request_error&quot;,    &quot;code&quot;: &quot;unknown_url&quot;,    &quot;message&quot;: &quot;Unknown request URL: GET /v1. Please check the URL for typos, or see the docs at https://platform.openai.com/docs/api-reference/.&quot;,    &quot;param&quot;: null  &#125;&#125;

如果有正常响应，那么说明这台主机是满足需求的。
3.2. 安装配置openai-forward1、下载openai-forward镜像
docker pull beidongjiedeguang/openai-forward:latest

2、启动openai-forward容器
docker run --name openai-forward -d \    --restart=always \    -p 8000:8000 \    beidongjiedeguang/openai-forward:latest 

3、检查容器状态
docker psdocker logs openai-forward

4. 配置Nginxhttp配置：
http &#123;    map $http_upgrade $connection_upgrade &#123;      default upgrade;      &#x27;&#x27;      close;    &#125;&#125;

server配置：
server &#123;    listen 80;    server_name openai-forward.voidking.com;    rewrite ^(.*) https://openai-forward.voidking.com$1 permanent;&#125;server &#123;    listen 443 ssl;    server_name openai-forward.voidking.com;    access_log off;    error_log off;    ssl_certificate /etc/nginx/ssl/openai-forward.voidking.com/openai-forward.voidking.com_bundle.crt;    ssl_certificate_key /etc/nginx/ssl/openai-forward.voidking.com/openai-forward.voidking.com.key;    location / &#123;        proxy_pass http://127.0.0.1:8000;        proxy_redirect off;        proxy_set_header Host $host;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection $connection_upgrade;        proxy_max_temp_file_size 0;        client_max_body_size 10m;        client_body_buffer_size 128k;        proxy_connect_timeout 90;        proxy_send_timeout 90;        proxy_read_timeout 90;        proxy_temp_file_write_size 64k;        proxy_buffering off;    &#125;&#125;



]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>nginx</tag>
        <tag>chatgpt</tag>
        <tag>aigc</tag>
        <tag>llm</tag>
      </tags>
  </entry>
  <entry>
    <title>MetaGPT快速体验</title>
    <url>/dev-metagpt-quickstart/</url>
    <content><![CDATA[1. 前言MetaGPT是一个多智能体编程框架。我们提出需求，MetaGPT能够通过模拟出公司里的各种角色，把产品做出来。
例如，我们提出需求“写一个命令行贪吃蛇游戏”。那么当MetaGPT程序运行完成之后，会生成设计文档、图片、代码文件等交付给我们。然后，运行MetaGPT生成的代码，就可以愉快地玩游戏了！
github仓库：geekan/MetaGPT
而很多同学想要体验MetaGPT的同学，都卡在了第一步：安装配置基础环境。本文的目标就是为了弱化环境限制，让大家快速体验到MetaGPT的魅力。在不依赖任何本地环境的基础上（只要你有一个浏览器），让MetaGPT开始工作，整个操作大概需要5到10分钟。


2. Step 1：准备云端环境不依赖本地环境，那么我们需要一个云端环境。这里我们选择使用Docker Playground，因为这个环境具备Docker，同时可以访问 https://api.openai.com/v1，关键还免费。
具体操作：

访问Docker Playground，使用docker hub账号登录。没有账号的话先注册一个，也是免费的。


注意：因为资源有限，所以有时候可能登录失败，报错：We are really sorry but we are out of capacity and cannot create your session at the moment. Please try again later.
如果出现上面的提示，可以等待一会儿后再次登录，也可以改用Kubernetes Playground。当然，如果你有更合适的云端环境，也是可以的。

登录到Docker Playground后，点击 ADD NEW INSTANCE，启动一个虚拟机。

3. Step 2：准备MetaGPT配置参考文档 MetaGPT README 的 Installation by Docker 部分，执行操作：
1、 下载镜像
docker pull metagpt/metagpt:v0.3.1

2、 准备挂载目录和配置文件
mkdir -p /opt/metagpt/&#123;config,workspace&#125;docker run --rm metagpt/metagpt:v0.3.1 cat /app/metagpt/config/config.yaml &gt; /opt/metagpt/config/key.yamlvim /opt/metagpt/config/key.yaml # Change the config


编辑 key.yaml 时，如果使用的是OPENAI的API，那么只需要修改 OPENAI_API_MODEL 和 OPENAI_API_KEY，这两个值需要对应，gpt-4要对应 gpt-4 的KEY，gpt-3.5-turbo要对应 gpt-3.5 的KEY。
4. Step 3：让MetaGPT工作1、 运行一个MetaGPT容器
docker run --name metagpt -d \    --privileged \    -v /opt/metagpt/config/key.yaml:/app/metagpt/config/key.yaml \    -v /opt/metagpt/workspace:/app/metagpt/workspace \    metagpt/metagpt:v0.3.1

2、 进入容器内部执行命令
docker exec -it metagpt /bin/bash$ python startup.py &quot;write a cli snake game&quot;


容器内生成的文件会存放在目录 /app/metagpt/workspace ，对应宿主机目录 /opt/metagpt/workspace 。
5. Step 4：下载项目1、 打包workspace
# 宿主机中执行cd /opt/metagpttar -czvf workspace.tgz workspace

2、 启动web服务以便下载项目
python -m http.server 9999

3、 Docker Playground开放端口点击页面上的 OPEN PORT，输入 9999，点击确定，即可打开Web页面。

4、 下载项目在Web页面上，点击 workspace.tgz ，即可下载MetaGTP生成的项目。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>docker</tag>
        <tag>chatgpt</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S Dashboard</title>
    <url>/dev-k8s-dashboard/</url>
    <content><![CDATA[1. K8S Dashboard简介K8S Dashboard 是基于网页的 Kubernetes 用户界面。使用 Dashboard 可以将容器应用部署到 Kubernetes 集群中，也可以对容器应用排错，还能管理集群资源。 可以使用 Dashboard 获取运行在集群中的应用的概览信息，也可以创建或者修改 Kubernetes 资源 （如 Deployment，Job，DaemonSet 等等）。例如，可以对 Deployment 实现弹性伸缩、发起滚动升级、重启 Pod 或者使用向导创建新的应用。
Dashboard 同时展示了 Kubernetes 集群中的资源状态信息和所有报错信息。
参考文档：

部署和访问 Kubernetes 仪表板（Dashboard）
安装 Kubernetes Dashboard



2. 安装K8S Dashboard1、执行安装
wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yamlkubectl apply -f recommended.yaml

2、查看安装
kubectl get all -n kubernetes-dashboard

3、开放外部访问方法一：修改servcie类型为NodePort方法二：添加ingress方法三：代理K8S API kubectl proxy --port=6880方法四：代理服务端口 kubectl port-forward service/kubernetes-dashboard --address 0.0.0.0 10443:443
4、访问Dashboard浏览器访问Dashboard，提示输入Token或者选择Kubeconfig文件进行登录。（Token的获取方法下一节有说明）登录后，就可以看到K8S中的资源对象，并且对它们进行操作。
3. 获取Token1、创建sa
kubectl create sa admin-user -n kubernetes-dashboard

2、添加sa到clusterrolebinding
kubectl edit clusterrolebinding cluster-admin
添加：
subjects:- kind: ServiceAccount  name: admin-user  namespace: kubernetes-dashboard

3、获取token
kubectl create token admin-user -n kubernetes-dashboard --duration=8760h

更多关于sa的知识，参考文档《K8S中创建用户账户和服务账户》
4. 开启统计参考文档《K8S问题记录》的【kubectl top命令执行报错】部分。
5. 扩展5.1. GrafanaGrafana是一个开源的可视化和监控平台，支持与Kubernetes集成。
安装使用方法参考文档：《使用Docker安装配置Grafana》
5.2. KubeViewKubeView是一个基于Web的Kubernetes资源可视化工具，可以以图形方式显示Kubernetes集群中的资源关系和拓扑。
KubeView功能较单一，只能显示资源拓扑。
参考文档：

benc-uk/kubeview
Kubernetes 监控：Kubeview

【安装KubeView】
1、 下载helm chart
git clone https://github.com/benc-uk/kubeviewcd kubeview/charts/

2、修改配置
vim kubeview/values.yaml

如下修改：

image.repository: ghcr.dockerproxy.com/benc-uk/kubeview
loadBalancer.enabled: false

3、安装kubeview
helm install kubeview kubeview -n kubeview --create-namespacekubectl get all -n kubeview

4、暴露服务
kubectl patch service/kubeview -n kubeview -p &#x27;&#123;&quot;spec&quot;: &#123;&quot;type&quot;: &quot;NodePort&quot;&#125;&#125;&#x27;

5.3. Weave ScopeWeave Scope是一个开源工具，提供了对Kubernetes集群的实时可视化和监控功能。
Weave Scope除了可以查看同层资源的关系和拓扑，还可以查看资源使用情况。
注意：Weave Scope已经停止更新。
参考文档：

weaveworks/scope
Weave Cloud: End of Service Announcement
Installing Weave Scope

【安装Weave Scope】
1、安装Weave Scope
wget https://github.com/weaveworks/scope/releases/download/v1.13.2/k8s-scope.yamlkubectl apply -f k8s-scope.yamlkubectl get all -n weave

2、暴露服务
kubectl patch service/weave-scope-app -n weave -p &#x27;&#123;&quot;spec&quot;: &#123;&quot;type&quot;: &quot;NodePort&quot;&#125;&#125;&#x27;


]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>监控</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作 kube-prometheus-stack 集群镜像</title>
    <url>/dev-sealos-kube-prometheus-stack/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个kube-prometheus-stack集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署kube-prometheus-stack。
参考文档：

构建一个 Ingress 集群镜像
《K8S中安装配置Prometheus官方套装》



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作kube-prometheus-stack集群镜像1、添加prometheus-community chart repo
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update

2、下载chart包
mkdir prometheus &amp;&amp; cd prometheushelm search repo prometheus-community/kube-prometheus-stack -lhelm pull prometheus-community/kube-prometheus-stack --version 45.25.0

这里下载 chart45.25.0 版本，对应 app v0.63.0 版本。下载的文件名为：kube-prometheus-stack-45.25.0.tgz
3、修改chart
tar -xzvf kube-prometheus-stack-45.25.0.tgzvim kube-prometheus-stack/values.yamltar -czvf kube-prometheus-stack-45.25.0-custom.tgz kube-prometheus-stack
修改chart的values.yaml，给一些默认值，然后重新打包chart，后续Dockerfile中的CMD中可以少填一些参数。
具体修改的内容，参考文档《K8S中安装配置Prometheus官方套装》
3、添加镜像列表
helm template prometheus kube-prometheus-stack | grep &#x27;image:&#x27;mkdir -p images/shim/vim images/shim/images-list.txt

内容为：
quay.mirrors.ustc.edu.cn/prometheus/node-exporter:v1.5.0quay.mirrors.ustc.edu.cn/kiwigrid/k8s-sidecar:1.22.0docker.io/grafana/grafana:9.5.1docker.io/dyrnq/kube-state-metrics:v2.8.2quay.mirrors.ustc.edu.cn/prometheus-operator/prometheus-operator:v0.63.0quay.mirrors.ustc.edu.cn/prometheus/alertmanager:v0.25.0quay.mirrors.ustc.edu.cn/prometheus/prometheus:v2.42.0docker.io/bats/bats:v1.4.1docker.io/dyrnq/kube-webhook-certgen:v20221220-controller-v1.5.1-58-g787ea74b6

4、编写Dockerfile
FROM scratchCOPY ../prometheus .CMD [&quot;kubectl apply -f kube-prometheus-stack/crds/ --server-side \    &amp;&amp; helm install prometheus kube-prometheus-stack-45.25.0-custom.tgz \    -n prometheus --create-namespace&quot;]

5、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/kube-prometheus-stack:v0.63.0 .sealos inspect docker.io/voidking/kube-prometheus-stack:v0.63.0

4. 使用kube-prometheus-stack集群镜像4.1. 自定义安装kube-prometheus-stacksealos run docker.io/voidking/kube-prometheus-stack:v0.63.0 --cmd=&quot;kubectl apply -f kube-prometheus-stack/crds/ --server-side \    &amp;&amp; helm install prometheus kube-prometheus-stack-45.25.0-custom.tgz \    -n prometheus --create-namespace \    --set grafana.defaultDashboardsTimezone=Asia/Shanghai \    --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \    &amp;&amp; kubectl expose deployment prometheus-grafana \    --port=80 --target-port=3000 --type=NodePort --name grafana-external -n prometheus&quot;    kubectl get all -n prometheus

4.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/kube-prometheus-stack:v0.63.0sealos tag docker.io/voidking/kube-prometheus-stack:v0.63.0 hub.sealos.cn/voidking/kube-prometheus-stack:v0.63.0sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/kube-prometheus-stack:v0.63.0

上传到私有镜像仓库
sealos tag docker.io/voidking/kube-prometheus-stack:v0.63.0 harbor.voidking.com/sealos/kube-prometheus-stack:v0.63.0sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/kube-prometheus-stack:v0.63.0




]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作PrometheusAlert集群镜像</title>
    <url>/dev-sealos-prometheusalert/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个PrometheusAlert集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署单节点的PrometheusAlert。
参考文档：

构建一个 Ingress 集群镜像
《PrometheusAlert全家桶》



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作PrometheusAlert集群镜像1、下载PrometheusAlert源码
mkdir prometheusalert &amp;&amp; cd prometheusalertgit clone https://github.com/feiyu563/PrometheusAlert.git pa

项目文件很大，为了使用chart要下载整个项目，不友好。
2、修改chart
mv pa/example/helm/prometheusalert .vim prometheusalert/values.yamlvim prometheusalert/config/app.confcat prometheus/Chart.yaml # 查看chart版本和app版本，分别是1.0.0和1.2.0tar -czvf prometheusalert-1.0.0-custom.tgz prometheusalertrm pa -rf

修改chart的values.yaml 和 app.conf ，给一些默认值，然后重新打包chart，后续Dockerfile中的CMD中可以少填一些参数。
如下修改：

service.type 改为 NodePort
ingress.enabled 改为 false
open-feishu 改为 1

3、添加镜像列表
helm template prometheusalert prometheusalert-1.0.0-custom.tgz | grep imagemkdir -p images/shim/vim images/shim/images-list.txt

内容为：
feiyu563/prometheus-alert:v4.8

4、编写Dockerfile
FROM scratchCOPY ../prometheusalert .CMD [&quot;helm install prometheusalert prometheusalert-1.0.0-custom.tgz \     -n prometheusalert --create-namespace&quot;]

5、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/prometheusalert:v1.2.0 .sealos inspect docker.io/voidking/prometheusalert:v1.2.0

4. 使用PrometheusAlert集群镜像4.1. 自定义安装PrometheusAlertsealos run docker.io/voidking/prometheusalert:v1.2.0 --cmd=&quot;helm install prometheusalert prometheusalert-1.0.0-custom.tgz \    -n prometheusalert --create-namespace&quot;kubectl get all -n prometheusalert

4.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/prometheusalert:v1.2.0sealos tag docker.io/voidking/prometheusalert:v1.2.0 hub.sealos.cn/voidking/prometheusalert:v1.2.0sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/prometheusalert:v1.2.0

上传到私有镜像仓库
sealos tag docker.io/voidking/prometheusalert:v1.2.0 harbor.voidking.com/sealos/prometheusalert:v1.2.0sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/prometheusalert:v1.2.0


]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作imagepullsecret-patcher集群镜像</title>
    <url>/dev-sealos-imagepullsecret-patcher/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个Milvus集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署imagepullsecret-patcher。
参考文档：

构建一个 Ingress 集群镜像
《K8S配置使用imagePullSecrets》
imagepullsecret-patcher
imagepullsecret-patcher deploy-example



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作imagepullsecret-patcher集群镜像1、准备helm chart
mkdir imagepullsecret-patcher &amp;&amp; cd imagepullsecret-patchergit clone https://github.com/voidking/hexo-storage.gitcp -r hexo-storage/make-helm-chart/imagepullsecret-patcher .tar -xzvf imagepullsecret-patcher-0.1.0.tgz imagepullsecret-patcherrm hexo-storage -rfrf imagepullsecret-patcher -rf 

【TODO：共享Chart仓库】
资源清单内容参考文档：make-helm-chart/v2/imagepullsecret-patcher
2、添加镜像列表
cat manifests/* | grep imagemkdir -p images/shim/vim images/shim/images-list.txt

images-list.txt 内容为：
quay.io/titansoft/imagepullsecret-patcher:v0.14

3、编写Dockerfile
FROM scratchCOPY ../imagepullsecret-patcher .CMD [&quot;helm install imagepullsecret-patcher imagepullsecret-patcher-0.1.0.tgz \    -n imagepullsecret-patcher --create-namespace&quot;]

默认安装到 imagepullsecret-patcher 命名空间。
4、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/imagepullsecret-patcher:v0.14 .sealos inspect docker.io/voidking/imagepullsecret-patcher:v0.14

4. 使用imagepullsecret-patcher集群镜像4.1. 自定义安装sealos run docker.io/voidking/imagepullsecret-patcher:v0.14 --cmd=&quot;helm install imagepullsecret-patcher imagepullsecret-patcher-0.1.0.tgz \    -n imagepullsecret-patcher --create-namespace \    --set imageCredentials.registry=harbor.voidking.com \    --set imageCredentials.username=haojin \    --set imageCredentials.password=haojin123&quot;kubectl get all -n imagepullsecret-patcher

4.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/imagepullsecret-patcher:v0.14sealos tag docker.io/voidking/imagepullsecret-patcher:v0.14 hub.sealos.cn/voidking/imagepullsecret-patcher:v0.14sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/imagepullsecret-patcher:v0.14

上传到私有镜像仓库
sealos tag docker.io/voidking/imagepullsecret-patcher:v0.14 harbor.voidking.com/sealos/imagepullsecret-patcher:v0.14sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/cert-manager:v0.14

4.3. 修改harbor账密imagepullsecret-patcher helm安装包有些问题，源secret和目标secret名称都是harbor-secret，需要修一下。想要修改harbor账密时，需要先把服务停掉，再修改harbor-secret，否则会被服务自动改回。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作KubeSphere集群镜像</title>
    <url>/dev-sealos-kubesphere/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个Kubesphere集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署KubeSphere。
参考文档：

构建一个 Ingress 集群镜像
《K8S中安装配置KubeSphere》
KubeSphere - 安装准备工作
KubeSphere - 安装概述
KubeSphere - 离线安装



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作KubeSphere集群镜像1、准备资源清单
mkdir kubesphere &amp;&amp; cd kubespheremkdir manifestswget -O manifests/01-kubesphere-installer.yaml https://github.com/kubesphere/ks-installer/releases/download/v3.3.2/kubesphere-installer.yaml wget -O manifests/02-cluster-configuration.yaml https://github.com/kubesphere/ks-installer/releases/download/v3.3.2/cluster-configuration.yaml

2、修改 manifests/02-cluster-configuration.yamlks-apiserver和ks-controller-manager的资源调整示例如下：
spec:  common:    core:      console:        enableMultiLogin: true        port: 30880      apiserver:        resources:          requests:            cpu: 100m            memory: 400Mi          limits:            cpu: &quot;2&quot;            memory: 8Gi      controllerManager:        resources:          requests:            cpu: 100m            memory: 400Mi          limits:            cpu: &quot;2&quot;            memory: 8Gi    minio:      volumeSize: 200Gi # Minio PVC size.

如果不想安装Prometheus，那么需要修改monitoring使用外部Prometheus（可以配置个假的地址）。
spec:  common:    monitoring:      type: external      endpoint: http://prometheus-operated.prometheus.svc:9090
参考文档：安装时如何不安装Prometheus
3、添加镜像列表
cat manifests/* | grep imagemkdir -p images/shim/curl -L -O https://github.com/kubesphere/ks-installer/releases/download/v3.3.2/images-list.txtvim images-list.txtmv images-list.txt images/shim/

编辑images-list.txt时，因为我们只想要一个kubesphere集群用来管理k8s资源，所以只保留kubesphere-images和kubesphere-monitoring-images。
##kubesphere-imageskubesphere/ks-installer:v3.3.2kubesphere/ks-apiserver:v3.3.2kubesphere/ks-console:v3.3.2kubesphere/ks-controller-manager:v3.3.2#kubesphere/kubectl:v1.20.0kubesphere/kubectl:v1.22.0kubesphere/kubefed:v0.8.1kubesphere/tower:v0.2.0minio/minio:RELEASE.2019-08-07T01-59-21Zminio/mc:RELEASE.2019-08-07T23-14-43Zcsiplugin/snapshot-controller:v4.0.0kubesphere/nginx-ingress-controller:v1.1.0mirrorgooglecontainers/defaultbackend-amd64:1.4kubesphere/metrics-server:v0.4.2redis:5.0.14-alpinehaproxy:2.0.25-alpinealpine:3.14osixia/openldap:1.3.0kubesphere/netshoot:v1.0##kubesphere-monitoring-imagesjimmidyson/configmap-reload:v0.5.0prom/prometheus:v2.34.0kubesphere/prometheus-config-reloader:v0.55.1kubesphere/prometheus-operator:v0.55.1kubesphere/kube-rbac-proxy:v0.11.0kubesphere/kube-state-metrics:v2.5.0prom/node-exporter:v1.3.1prom/alertmanager:v0.23.0thanosio/thanos:v0.25.2grafana/grafana:8.3.3kubesphere/kube-rbac-proxy:v0.8.0kubesphere/notification-manager-operator:v1.4.0kubesphere/notification-manager:v1.4.0kubesphere/notification-tenant-sidecar:v3.2.0

4、编写Dockerfile
FROM scratchCOPY ../kubesphere .CMD [&quot;kubectl apply -f manifests/01-kubesphere-installer.yaml \  &amp;&amp; kubectl apply -f manifests/02-cluster-configuration.yaml&quot;]

5、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/kubesphere:v3.3.2 .sealos inspect docker.io/voidking/kubesphere:v3.3.2

4. 使用KubeSphere集群镜像4.1. 自定义安装KubeSpheresealos run docker.io/voidking/kubesphere:v3.3.2 --cmd=&quot;kubectl apply -f manifests/01-kubesphere-installer.yaml \  &amp;&amp; kubectl apply -f manifests/02-cluster-configuration.yaml&quot;kubectl get all -n kubesphere-system

默认帐户 admin ，默认密码 P@88w0rd
4.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/kubesphere:v3.3.2sealos tag docker.io/voidking/kubesphere:v3.3.2 hub.sealos.cn/voidking/kubesphere:v3.3.2sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/kubesphere:v3.3.2

上传到私有镜像仓库
sealos tag docker.io/voidking/kubesphere:v3.3.2 harbor.voidking.com/sealos/kubesphere:v3.3.2sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/kubesphere:v3.3.2




]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>ipvs</tag>
        <tag>sealos</tag>
        <tag>kubesphere</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作Milvus集群镜像</title>
    <url>/dev-sealos-milvus/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个Milvus集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署Milvus2.2.0。
参考文档：

构建一个 Ingress 集群镜像
《K8S中安装配置Milvus》
Install Milvus Cluster with Milvus Operator
Install Milvus Offline with Helm Charts



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作cert-manager集群镜像1、准备资源清单
mkdir cert-manager &amp;&amp; cd cert-managermkdir manifestswget -O manifests/cert-manager.yaml https://github.com/jetstack/cert-manager/releases/download/v1.5.3/cert-manager.yaml 

2、添加镜像列表
cat manifests/* | grep imagemkdir -p images/shim/vim images/shim/cert-manager-images.txt

cert-manager-images.txt 内容为：
quay.io/jetstack/cert-manager-cainjector:v1.5.3quay.io/jetstack/cert-manager-controller:v1.5.3quay.io/jetstack/cert-manager-webhook:v1.5.3

3、编写Dockerfile
FROM scratchCOPY ../cert-manager .CMD [&quot;kubectl apply -f manifests&quot;]

默认安装到 cert-manager 命名空间。
4、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/cert-manager:v1.5.3 .sealos inspect docker.io/voidking/cert-manager:v1.5.3

4. 使用cert-manager集群镜像4.1. 自定义安装安装sealos run docker.io/voidking/cert-manager:v1.5.3 --cmd=&quot;kubectl apply -f manifests&quot;kubectl get all -n cert-manager

4.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/cert-manager:v1.5.3sealos tag docker.io/voidking/cert-manager:v1.5.3 hub.sealos.cn/voidking/cert-manager:v1.5.3sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/cert-manager:v1.5.3

上传到私有镜像仓库
sealos tag docker.io/voidking/cert-manager:v1.5.3 harbor.voidking.com/sealos/cert-manager:v1.5.3sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/cert-manager:v1.5.3

5. 制作Milvus集群镜像1、下载chart
mkdir milvus &amp;&amp; cd milvushelm repo add milvus https://milvus-io.github.io/milvus-helm/helm search repo milvus/milvus -lhelm fetch milvus/milvus --version 3.3.6

这里下载chart3.3.6，对应milvus版本2.2.0。下载的文件为：milvus-3.3.6.tgz
2、修改chart
tar -xzvf milvus-3.3.6.tgzvim milvus/values.yamltar -czvf milvus-3.3.6-custom.tgz milvusrm milvus -rf

values.yaml 如下修改：

cluster.enabled 建议修改为false，为 true 时非常消耗资源
service.type 改为 NodePort
x.resources 保持默认，按需修改 
x.persistence.storageClass 保持默认，使用默认storageclass
x.persistence.size 保持默认，按需修改
etcd.replicaCount 改为 1，节省资源
minio.mode 改为 standalone，节省资源
pulsar.enabled 保持 true，pulsar是一个分布式发布订阅消息系统，必须安装
metrics.enabled 保持 true，对外暴露监控数据
metrics.serviceMonitor.enabled 改为 true，使prometheus可以获取到监控数据

milvus开放两个端口，19530是客户端连接端口，9091是http api端口。
注意：milvus不用配置用户名和密码。
3、获取镜像列表
helm template milvus-test milvus-3.3.6-custom.tgz &gt; milvus_manifest.yamlpython get_image.py --manifest milvus_manifest.yamlrm milvus_manifest.yaml get_image.py

get_image.py内容为：
import argparseimport gzipimport osimport yamlfrom nested_lookup import nested_lookupif __name__ == &quot;__main__&quot;:    parser = argparse.ArgumentParser(        description=&quot;Save Docker images&quot;)    parser.add_argument(&quot;--manifest&quot;,                        required=True,                        help=&quot;Path to the manifest yaml&quot;)    parser.add_argument(&quot;--save_path&quot;,                        type=str,                        default=&#x27;images&#x27;,                        help=&#x27;Directory to save images to&#x27;)    arguments = parser.parse_args()    with open(arguments.manifest, &#x27;r&#x27;) as file:        template = file.read()    images=[]    parts = template.split(&#x27;---&#x27;)    for p in parts:        y = yaml.safe_load(p)        matches = nested_lookup(&quot;image&quot;, y)        if (len(matches)):                images += matches    for image_name in set(images):        print(image_name)

get_image.py 改写自脚本 save_image.py。
4、添加镜像列表
mkdir -p images/shim/vim images/shim/milvus-images.txt

milvus-images.txt 内容为：
milvusdb/milvus:v2.2.0docker.io/milvusdb/etcd:3.5.0-r7minio/minio:RELEASE.2022-03-17T06-34-49Zmilvusdb/milvus-config-tool:v0.1.0apachepulsar/pulsar:2.8.2

5、编写Dockerfile
FROM scratchCOPY ../milvus .CMD [&quot;helm install milvus milvus-3.3.6-custom.tgz \  -n milvus --create-namespace&quot;]

6、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/milvus:v2.2.0 .sealos inspect docker.io/voidking/milvus:v2.2.0

6. 使用Milvus集群镜像6.1. 自定义安装Milvussealos run docker.io/voidking/milvus:v2.2.0 --cmd=&quot;helm install milvus milvus-3.3.6-custom.tgz \  -n milvus --create-namespace \  --set service.type=NodePort \  --set metrics.serviceMonitor.enabled=true&quot;kubectl get all -n milvus

6.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/milvus:v2.2.0sealos tag docker.io/voidking/milvus:v2.2.0 hub.sealos.cn/voidking/milvus:v2.2.0sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/milvus:v2.2.0

上传到私有镜像仓库
sealos tag docker.io/voidking/milvus:v2.2.0 harbor.voidking.com/sealos/milvus:v2.2.0sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/milvus:v2.2.0




]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>database</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
        <tag>milvus</tag>
        <tag>cert-manager</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作MySQL集群镜像</title>
    <url>/dev-sealos-mysql/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个MySQL集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署单节点的MySQL 8.0.29。
参考文档：

构建一个 Ingress 集群镜像
《K8S中安装配置MySQL》



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作MySQL集群镜像1、添加chart仓库
helm repo add bitnami https://charts.bitnami.com/bitnami

2、下载chart
mkdir mysql &amp;&amp; cd mysqlhelm search repo bitnami/mysql -lhelm fetch bitnami/mysql --version 9.2.1

这里下载 chart9.2.1 版本，对应 mysql8.0.29 版本。下载的文件名为：mysql-9.2.1.tgz
3、修改chart
tar -xzvf mysql-9.2.1.tgzvim mysql/values.yamltar -czvf mysql-9.2.1-custom.tgz mysqlrm mysql -rf
修改chart的values.yaml，给一些默认值，然后重新打包chart，后续Dockerfile中的CMD中可以少填一些参数。
如下修改：

global.storageClass 不变，使用默认的storageclass即可
architecture 修改为 standalone
auth.rootPassword 修改为 mysql123
primary.persistence.size 修改为期望的存储大小 500Gi
primary.service.type 修改为 NodePort

3、添加镜像列表
mkdir -p images/shim/vim images/shim/mysql-images.txt

内容为：
docker.io/bitnami/mysql:8.0.29-debian-11-r9

4、编写Dockerfile
FROM scratchCOPY ../mysql .CMD [&quot;helm install mysql mysql-9.2.1-custom.tgz \     -n mysql --create-namespace&quot;]

5、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/mysql:v8.0.29 .sealos inspect docker.io/voidking/mysql:v8.0.29

4. 使用mysql集群镜像4.1. 安装mysql运行mysql应用，安装mysql
sealos run docker.io/voidking/mysql:v8.0.29

报错：
Error: INSTALLATION FAILED: failed to download &quot;nfs-subdir-external-provisioner-4.0.18-custom.tgz&quot;Error: exit status 1

这是个新的坑：mysql镜像run，居然使用了nfs-provisioner:v4.0.0这个镜像的CMD。待办：提个issue。
nfs-provisioner:v4.0.0镜像的制作方法，参考文档《sealos制作nfs-provisioner集群镜像》
4.2. 自定义安装mysqlsealos run docker.io/voidking/mysql:v8.0.29 --cmd=&quot;helm install mysql mysql-9.2.1-custom.tgz \     --namespace mysql --create-namespace \     --set auth.rootPassword=mysql123&quot;kubectl get all -n mysql

4.3. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/mysql:v8.0.29sealos tag docker.io/voidking/mysql:v8.0.29 hub.sealos.cn/voidking/mysql:v8.0.29sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/mysql:v8.0.29

上传到私有镜像仓库
sealos tag docker.io/voidking/mysql:v8.0.29 harbor.voidking.com/sealos/mysql:v8.0.29sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/mysql:v8.0.29

4.4. 卸载mysqlhelm list -Ahelm uninstall mysql -n mysqlsealos pssealos rm default-xxxcd /var/lib/sealos/data/default/applications &amp;&amp; rm deafult-xxx -rf

4.5. 删除历史镜像sealos images | grep &#x27;&lt;none&gt;&#x27; | awk &#x27;&#123;print $3&#125;&#x27; | xargs sealos rmi# sealos rmi docker.io/voidking/mysql:v8.0.29




]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>database</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>mysql</tag>
        <tag>问题排查</tag>
        <tag>bitnami</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作MinIO集群镜像</title>
    <url>/dev-sealos-minio/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个MinIO集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署MinIO 2023.3.24。
参考文档：

构建一个 Ingress 集群镜像
《K8S中安装配置MinIO》
MinIO官网
MinIO中文网
artifacthub - helm/bitnami/minio
What is MinIO and How to Configure It in Kubernetes
MinIO Download Page
MinIO中国加速镜像



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作MinIO集群镜像1、添加chart仓库
helm repo add bitnami https://charts.bitnami.com/bitnamihelm repo update

2、下载chart
mkdir minio &amp;&amp; cd miniohelm search repo bitnami/minio -lhelm fetch bitnami/minio --version 12.2.5

这里下载 chart12.2.5 版本，对应 minio2023.3.24 版本（minio以日期作为版本号，也是很有意思）。下载的文件名为：minio-12.2.5.tgz
3、修改chart
tar -xzvf minio-12.2.5.tgzvim minio/values.yamltar -czvf minio-12.2.5-custom.tgz miniorm minio -rf
修改chart的values.yaml，给一些默认值，然后重新打包chart，后续Dockerfile中的CMD中可以少填一些参数。
如下修改：

global.storageClass 不变，使用默认的storageclass
mode 建议保持standalone，单机部署；可选改为 distributed，分布式部署
statefulset.replicaCount 分布式部署时，修改为 4（分布式部署要求&gt;=4）
auth.rootUser 不变，默认 admin
auth.rootPassword 修改minio密码 minio123
resources.limits.cpu 修改为 1 
resources.limits.memory 修改为 2Gi
tls.enabled 不变，默认 false
service.type 修改为NodePort
persistence.enabled 不变，默认 true
persistence.storageClass 不变，使用默认的storageclass
persistence.size 修改为 500Gi
metrics.serviceMonitor.enabled 修改为 true
metrics.serviceMonitor.namespace 修改为 Prometheus 所在的namespace

3、添加镜像列表
mkdir -p images/shim/vim images/shim/minio-images.txt

内容为：
docker.io/bitnami/minio:2023.3.24-debian-11-r6

4、编写Dockerfile
FROM scratchCOPY ../minio .CMD [&quot;helm install minio minio-12.2.5-custom.tgz \    -n minio --create-namespace&quot;]

5、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/minio:2023.3.24 .sealos inspect docker.io/voidking/minio:2023.3.24

4. 使用MinIO集群镜像4.1. 自定义安装MinIOsealos run docker.io/voidking/minio:2023.3.24 --cmd=&quot;helm install minio minio-12.2.5-custom.tgz \    -n minio --create-namespace \    --set auth.rootUser=admin \    --set auth.rootPassword=minio123 \    --set metrics.serviceMonitor.enabled=true \    --set metrics.serviceMonitor.namespace=prometheus&quot;    kubectl get all -n minio

使用9001端口对应的NodePort，可以通过浏览器外部访问minio控制台。
4.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/minio:2023.3.24sealos tag docker.io/voidking/minio:2023.3.24 hub.sealos.cn/voidking/minio:2023.3.24sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/minio:2023.3.24

上传到私有镜像仓库
sealos tag docker.io/voidking/minio:2023.3.24 harbor.voidking.com/sealos/minio:2023.3.24sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/minio:2023.3.24




]]></content>
      <categories>
        <category>engineering</category>
        <category>storage</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>存储</tag>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>bitnami</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
        <tag>minio</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作RabbitMQ集群镜像</title>
    <url>/dev-sealos-rabbitmq/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个MinIO集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署RabbitMQ 3.11.5。
参考文档：

构建一个 Ingress 集群镜像
《K8S中安装配置RabbitMQ》



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作RabbitMQ集群镜像1、添加chart仓库
helm repo add bitnami https://charts.bitnami.com/bitnamihelm repo update

2、下载chart
mkdir rabbitmq &amp;&amp; cd rabbitmqhelm search repo bitnami/rabbitmq -lhelm fetch bitnami/rabbitmq --version 11.3.0
这里下载 chart 11.3.0 版本，对应 rabbitmq 3.11.5 版本。下载的文件名为：rabbitmq-11.3.0.tgz
3、修改chart
tar -xzvf rabbitmq-11.3.0.tgzvim rabbitmq/values.yamltar -czvf rabbitmq-11.3.0-custom.tgz rabbitmqrm rabbitmq -rf
修改chart的values.yaml，给一些默认值，然后重新打包chart，后续Dockerfile中的CMD中可以少填一些参数。
如下修改：

global.storageClass 不变，使用默认的storageclass
auth.username 不变，默认为user
auth.password 修改为 rabbitmq123
persistence.size 改为 200Gi
service.type 改为 NodePort

3、添加镜像列表
mkdir -p images/shim/vim images/shim/rabbitmq-images.txt

内容为：
docker.io/bitnami/rabbitmq:3.11.5-debian-11-r2

4、编写Dockerfile
FROM scratchCOPY ../rabbitmq .CMD [&quot;helm install rabbitmq rabbitmq-11.3.0-custom.tgz \    -n rabbitmq --create-namespace&quot;]

5、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/rabbitmq:v3.11.5 .sealos inspect docker.io/voidking/rabbitmq:v3.11.5

4. 使用RabbitMQ集群镜像4.1. 自定义安装RabbitMQsealos run docker.io/voidking/rabbitmq:v3.11.5 --cmd=&quot;helm install rabbitmq rabbitmq-11.3.0-custom.tgz \    -n rabbitmq --create-namespace \    --set auth.username=user \    --set auth.password=rabbitmq123 \    --set service.type=NodePort&quot;    kubectl get all -n rabbitmq

4.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/rabbitmq:v3.11.5sealos tag docker.io/voidking/rabbitmq:v3.11.5 hub.sealos.cn/voidking/rabbitmq:v3.11.5sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/rabbitmq:v3.11.5

上传到私有镜像仓库
sealos tag docker.io/voidking/rabbitmq:v3.11.5 harbor.voidking.com/sealos/rabbitmq:v3.11.5sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/rabbitmq:v3.11.5




]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>bitnami</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作Redis集群镜像</title>
    <url>/dev-sealos-redis/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个Redis集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署单节点Redis7.0.7。
参考文档：

构建一个 Ingress 集群镜像
《K8S中安装配置Redis》



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作Redis集群镜像1、添加chart仓库
helm repo add bitnami https://charts.bitnami.com/bitnami

2、下载chart
mkdir redis &amp;&amp; cd redishelm search repo bitnami/redis -lhelm fetch bitnami/redis --version 17.4.1

这里下载 chart17.4.1 版本，对应 redis7.0.7 版本。下载的文件名为：redis-17.4.1.tgz
3、修改chart
tar -xzvf redis-17.4.1.tgzvim redis/values.yamltar -czvf redis-17.4.1-custom.tgz redisrm redis -rf
修改chart的values.yaml，给一些默认值，然后重新打包chart，后续Dockerfile中的CMD中可以少填一些参数。
如下修改：

global.storageClass 不变，使用默认的storageclass即可
global.redis.password 修改redis密码 redis123
auth.password 修改redis密码 redis123
architecture 单机模式修改为standalone
master.persistence.size 修改为期望的存储大小 200Gi
master.service.type 修改为NodePort
master.resources.requests 按需修改，推荐1C4Gi
master.resources.limits 按需修改，推荐2C8Gi
commonConfiguration 按需修改，对应 redis.conf 配置文件内容

3、添加镜像列表
mkdir -p images/shim/vim images/shim/redis-images.txt

内容为：
docker.io/bitnami/redis:7.0.7-debian-11-r7

4、编写Dockerfile
FROM scratchCOPY ../redis .CMD [&quot;helm install redis redis-17.4.1-custom.tgz \    -n redis --create-namespace&quot;]

5、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/redis:v7.0.7 .sealos inspect docker.io/voidking/redis:v7.0.7

4. 使用redis集群镜像4.1. 自定义安装redissealos run docker.io/voidking/redis:v7.0.7 --cmd=&quot;helm install redis redis-17.4.1-custom.tgz \    -n redis --create-namespace \    --set global.redis.password=redis123 \    --set auth.password=redis123&quot;    kubectl get all -n redis

4.2. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/redis:v7.0.7sealos tag docker.io/voidking/redis:v7.0.7 hub.sealos.cn/voidking/redis:v7.0.7sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/redis:v7.0.7

上传到私有镜像仓库
sealos tag docker.io/voidking/redis:v7.0.7 harbor.voidking.com/sealos/redis:v7.0.7sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/redis:v7.0.7

4.3. 卸载redishelm list -Ahelm uninstall redis -n redissealos pssealos rm default-xxxcd /var/lib/sealos/data/default/applications &amp;&amp; rm deafult-xxx -rf

4.4. 删除历史镜像sealos images | grep &#x27;&lt;none&gt;&#x27; | awk &#x27;&#123;print $3&#125;&#x27; | xargs sealos rmi# sealos rmi docker.io/voidking/redis:v7.0.7




]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>database</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>问题排查</tag>
        <tag>bitnami</tag>
        <tag>sealos</tag>
        <tag>redis</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>Helm官方文档摘录</title>
    <url>/dev-helm-doc-extract/</url>
    <content><![CDATA[1. 前言Helm官方文档写的很棒，本文会摘录Helm官方文档中的一些基础知识和最佳实践，备忘。更细致全面的内容，请移步阅读Helm官方文档。
参考文档：

《Helm入门篇》
Helm - Getting Started



2. Helm三大概念Chart 代表着 Helm 包。它包含在 Kubernetes 集群内部运行应用程序，工具或服务所需的所有资源定义。你可以把它看作是 Homebrew formula，Apt dpkg，或 Yum RPM 在Kubernetes 中的等价物。
Repository（仓库） 是用来存放和共享 charts 的地方。它就像 Perl 的 CPAN 档案库网络 或是 Fedora 的 软件包仓库，只不过它是供 Kubernetes 包所使用的。
Release 是运行在 Kubernetes 集群中的 chart 的实例。一个 chart 通常可以在同一个集群中安装多次。每一次安装都会创建一个新的 release。以 MySQL chart为例，如果你想在你的集群中运行两个数据库，你可以安装该chart两次。每一个数据库都会拥有它自己的 release 和 release name。
在了解了上述这些概念以后，我们就可以这样来解释 Helm：Helm 安装 charts 到 Kubernetes 集群中，每次安装都会创建一个新的 release。你可以在 Helm 的 chart repositories 中寻找新的 chart。
3. Helm安装资源的顺序Helm按照以下顺序安装资源：

Namespace
NetworkPolicy
ResourceQuota
LimitRange
PodSecurityPolicy
PodDisruptionBudget
ServiceAccount
Secret
SecretList
ConfigMap
StorageClass
PersistentVolume
PersistentVolumeClaim
CustomResourceDefinition
ClusterRole
ClusterRoleList
ClusterRoleBinding
ClusterRoleBindingList
Role
RoleList
RoleBinding
RoleBindingList
Service
DaemonSet
Pod
ReplicationController
ReplicaSet
Deployment
HorizontalPodAutoscaler
StatefulSet
Job
CronJob
Ingress
APIService

4. Chart模板指南内容太多，建议直接阅读Helm官方文档 - Chart模板指南，了解基础概念和语法。
本节只简单说明几个概念和语法。
4.1. 模板templates/目录中的.yaml文件和.tpl文件，就是模板。当Helm评估chart时，会通过模板渲染引擎将所有文件发送到templates/目录中。 然后收集模板的结果并发送给Kubernetes。
values.yaml 文件也导入到了模板。这个文件包含了chart的 默认值 。这些值会在用户执行helm install 或 helm upgrade时被覆盖。
Chart.yaml 文件包含了该chart的描述。你可以从模板中访问它。charts/目录 可以 包含其他的chart(称之为 子chart)。
模板中横线的作用：

&#123;&#123;-`：表示在模板渲染时要去掉前面的空白字符和换行符。
- `-&#125;&#125;：表示在模板渲染时要去掉后面的空白字符和换行符。

4.2. 变量和作用域Helm模板中，变量是对另一个对象的命名引用。
变量的作用域一般不是全局的，而是其声明所在的块。所谓块，是指if/else、with、range、define、template、block定义的范围。
.代表对当前作用域的引用，.Values表示在当前作用域查找Values对象。每个模板中，默认可以访问到的内置对象有Release、Values、Chart、Files、Capabilities和Template。如果定义了块，那么在块中不可以通过.Values找到Values对象。
$代表对根作用域的引用，$.Values表示在根作用域查找Values对象。如果定义了块，那么在块中可以通过$.Values找到Values对象。
变量定义格式示例：&#123;&#123;- $name := .Release.Name -&#125;&#125;在模板顶层定义变量，变量的作用域会是整个模板；在块中定义变量，变量的作用域只在块内。
4.3. 变量类型Helm 模板语言是用强类型Go编程语言实现的。 因此，模板中的变量是 有类型的。大多数情况下，变量将作为以下类型之一显示：

string: 文本字符串
bool: true 或 false
int: 整型值（包含8位，16位，32位，和64有符号和无符号整数）
float64: 64位浮点数(也有8位，16位，32位类型)
字节切片([]byte)，一般用于保存（可能的）二进制数据
struct: 有属性和方法的对象
上述某种类型的切片(索引列表)
字符串键map (map[string]interface&#123;&#125;) 值是上述某种类型

Go里面有很多其他类型，有时你需要在模板里转换。调试对象类型最简便的方式是在模板中传递给printf “%t”，这样会打印类型。 也可以使用 typeOf 和 kindOf 函数。
4.4. 命名模板命名模板（也被称作一个 部分 或一个 子模板）名称是全局的。一个常见的命名惯例是用chart名称作为模板前缀。使用特定chart名称作为前缀可以避免可能因为 两个不同chart使用了相同名称的模板而引起的冲突。
用define和template声明和使用模板。按照惯例，define声明的命名模板一般放在_helpers.tpl文件中。因为是模板名称是全局的，因此可以被.yaml文件引用。
template渲染时，一般传入.作为命名模板可以访问的范围。include是template的替代，只是为了更好地处理YAML文档的输出格式，方便缩进。
4.5. NOTES.txt文件在helm install 或 helm upgrade命令的最后，Helm会打印出对用户有用的信息。 使用模板可以高度自定义这部分信息。要在chart添加安装说明，只需创建 templates/NOTES.txt 文件即可。该文件是纯文本，但会像模板一样处理， 所有正常的模板函数和对象都是可用的。
4.6. .helmignore 文件.helmignore 文件用来指定你不想包含在你的helm chart中的文件。如果该文件存在，helm package 命令会在打包应用时忽略所有在 .helmignore 文件中匹配的文件。
一些值得注意的和.gitignore不同之处：

不支持**语法。
globbing库是Go的 ‘filepath.Match’，不是fnmatch(3)
末尾空格总会被忽略(不支持转义序列)
不支持!作为特殊的引导序列
默认不会排除自身，需要显式添加 .helmignore

4.7. YAML技术参考文档：《YAML语言》
5. Chart开发技巧5.1. 模板方法Helm使用Go模板，同时增加了两个特殊模板方法：include和required。
include方法允许你引入另一个模板，并将结果传递给其他模板方法。比如，这个模板片段包含了一个叫mytpl的模板，然后将其转成小写，并使用双引号括起来。
value: &#123;&#123; include &quot;mytpl&quot; . | lower | quote &#125;&#125;

required方法可以让你声明模板渲染所需的特定值。如果这个值是空的，模板渲染会出错并打印用户提交的错误信息。下面这个required方法的例子声明了一个.Values.who需要的条目，并且当这个条目不存在时会打印错误信息：
value: &#123;&#123; required &quot;A valid .Values.who entry required!&quot; .Values.who &#125;&#125;

tpl方法允许开发者在模板中使用字符串作为模板。将模板字符串作为值传给chart或渲染额外的配置文件时会很有用。语法： &#123;&#123; tpl TEMPLATE_STRING VALUES &#125;&#125;
5.2. 使用Partials和模板引用有时你想在chart中创建可以重复利用的部分，不管是块还是局部模板。通常将这些文件保存在自己的文件中会更干净。
在 templates/ 目录中，任何以下划线(_)开始的文件不希望输出到Kubernetes清单文件中。因此按照惯例，辅助模板和局部模板会被放在_helpers.tpl文件中。
5.3. 创建镜像拉取密钥1、定义 values.yaml
imageCredentials:  registry: quay.io  username: someone  password: sillyness  email: someone@host.com

2、定义辅助模板（写在 _helpers.tpl 文件里）
&#123;&#123;- define &quot;imagePullSecret&quot; &#125;&#125;&#123;&#123;- with .Values.imageCredentials &#125;&#125;&#123;&#123;- printf &quot;&#123;\&quot;auths\&quot;:&#123;\&quot;%s\&quot;:&#123;\&quot;username\&quot;:\&quot;%s\&quot;,\&quot;password\&quot;:\&quot;%s\&quot;,\&quot;email\&quot;:\&quot;%s\&quot;,\&quot;auth\&quot;:\&quot;%s\&quot;&#125;&#125;&#125;&quot; .registry .username .password .email (printf &quot;%s:%s&quot; .username .password | b64enc) | b64enc &#125;&#125;&#123;&#123;- end &#125;&#125;&#123;&#123;- end &#125;&#125;

3、模板中使用辅助模板
apiVersion: v1kind: Secretmetadata:  name: myregistrykeytype: kubernetes.io/dockerconfigjsondata:  .dockerconfigjson: &#123;&#123; template &quot;imagePullSecret&quot; . &#125;&#125;

5.4. YAML是JSON的超集根据YAML规范，YAML是JSON的超集。这意味着任意的合法JSON结构在YAML中应该是合法的。
这有个优势：有时候模板开发者会发现使用类JSON语法更容易表达数据结构而不是处理YAML的空白敏感度。
作为最佳实践，模板应遵循类YAML语法 除非 JSON语法大大降低了格式问题的风险。
5.5. 构建复杂Chart在CNCF的 Artifact Hub 中的很多chart是创建更先进应用的“组成部分”。但是chart可能被用于创建大规模应用实例。 在这种场景中，一个总的chart会有很多子chart，每一个是整体功能的一部分。
当前从离散组件组成一个复杂应用的最佳实践是创建一个顶层总体chart构建全局配置，然后使用charts子目录嵌入每个组件。
6. 最佳实践6.1. 一般惯例6.1.1. chart名称chart名称必须是小写字母和数字。单词之间可以使用横杠分隔(-)。
6.1.2. 版本号Helm尽可能使用 SemVer 2来表示版本号。（注意Docker镜像的tag不一定遵循SemVer， 因此被认为是一个不幸的例外规则。）
当SemVer版本存储在Kubernetes标签中时，我们通常把+字符改成_，因为标签不允许使用+作为值进行签名。
6.1.3. 格式化YAMLYAML 文件应该按照 双空格 缩进(绝不要使用tab键)。
6.1.4. Helm 和 Chart的用法以下是几个 Helm 和 helm 的惯用方法。

Helm 是指整个项目
helm 是指客户端命令
chart 不是专有名词，不需要首字母大写
Chart.yaml 需要首字母大写，因为文件名大小写敏感

若有疑问，使用 Helm (‘H’大写)。
6.2. Values6.2.1. 命名规范变量名称以小写字母开头，单词按驼峰区分
6.2.2. 扁平或嵌套的ValueYAML是一种灵活格式，值可以嵌套得很深，也可以是扁平的。大多数场景中，扁平的优于嵌套的。因为对模板开发者和用户来说更加简单。
6.2.3. 类型清楚YAML的类型强制规则有时候是很反常的。比如，foo: false 和 foo: “false” 是不一样的。大整型数如：foo: 12345678 有时会被转换成科学计数法。
避免类型强制规则错误最简单的方式是字符串明确定义，其他都是不明确的。或者，简单来讲， 给所有字符串打引号。
通常，为了避免整数转换问题，将整型存储为字符串更好，并用 &#123;&#123; int $value &#125;&#125; 在模板中将字符串转回整型。
在大多数场景中，显式的类型标记更好，所以 foo: !!string 1234 会将1234作为字符串对待。 但是，YAML解析器会消耗标记，因此类型数据在一次解析后会丢失。
6.2.4. 考虑用户使用value有三种value来源:

chart的values.yaml文件
由helm install -f提供的values文件
在执行helm install 时传递给–set 或 –set-string 参数的values

当设计values的结构时，记得你的chart用户可能会通过-f 参数或–set选项覆盖他们。由于–set在表现上更有限，编写你values.yaml文件的第一指导原则是确保它容易被–set覆盖。因此使用map构建values文件更好。
6.2.5. 给values.yaml写文档values.yaml中每个定义的属性都应该文档化。文档字符串应该以它要描述的属性开头，并至少给出一句描述。
6.3. 模板6.3.1. templates 结构templates/目录结构应该如下：

如果生成YAML输出，模板文件应该有扩展名.yaml。 扩展名是.tpl可用于生成非格式化内容的模板文件。
模板文件名称应该使用横杠符号(my-example-configmap.yaml)，不用驼峰记法。
每个资源的定义应该在它自己的模板文件中。
模板文件的名称应该反映名称中的资源类型。比如：foo-pod.yaml， bar-svc.yaml

6.3.2. 定义模板的名称定义的模板(在&#123;&#123; define &#125;&#125;命令中定义的模板)是可全局访问的。这就意味着chart和所有的子chart都可以访问用&#123;&#123; define &#125;&#125;创建的所有模板。
因此， 所有定义的模板名称应该被命名空间化。
正确的：
&#123;&#123;- define &quot;nginx.fullname&quot; &#125;&#125;&#123;&#123;/* ... */&#125;&#125;&#123;&#123; end -&#125;&#125;

不正确的：
&#123;&#123;- define &quot;fullname&quot; -&#125;&#125;&#123;&#123;/* ... */&#125;&#125;&#123;&#123; end -&#125;&#125;

强烈建议通过helm create命令创建新chart，因为模板名称是根据此最佳实践自动定义的。
6.3.3. 格式化模板模板应该使用两个 空格 缩进（永远不要用tab）。模板命令的大括号前后应该使用空格。正确的：
&#123;&#123; .foo &#125;&#125;&#123;&#123; print &quot;foo&quot; &#125;&#125;&#123;&#123;- print &quot;bar&quot; -&#125;&#125;

不正确的：
&#123;&#123;.foo&#125;&#125;&#123;&#123;print &quot;foo&quot;&#125;&#125;&#123;&#123;-print &quot;bar&quot;-&#125;&#125;

6.3.4. 生成模板中的空格最好在生成的模板中将空格量保持在最小值。尤其是大量的空行不应该相邻出现。但偶尔有空行（尤其在逻辑块之间）是没问题的。
6.3.5. 注释YAML和Helm模板都有注释标记符。YAML注释：
# This is a commenttype: sprocket

模板注释：
&#123;&#123;- /*This is a comment.*/&#125;&#125;type: frobnitz

6.3.6. 在模板和模板输出中使用JSONYAML是JSON的超集。在某些情况下，使用JSON语法比其他YAML表示更具可读性。
比如，这个YAML更接近表示列表的普通YAML方法：
arguments:  - &quot;--dirname&quot;  - &quot;/foo&quot;

但是折叠成JSON列表样式时会更易阅读:
arguments: [&quot;--dirname&quot;, &quot;/foo&quot;]

使用JSON可以很好地提高易读性。然而，JSON语法不应用于表示更复杂的结构。
6.4. 标签和注释建议使用 helm.sh/chart: NAME-VERSION 作为标签，以便操作员可以找到特定chart的所有实例。
如果元数据项不是用于查询，就应该设置为注释。
标准标签参考文档：标签和注释
6.5. Pod和Pod模板Pod指的是Pod，Pod模板指的是Deployment、ReplicationController、ReplicaSet、DaemonSet、StatefulSet等。
容器镜像应该使用固定的tag或镜像SHA。不应该使用latest, head, canary等标签或其他被设计为“浮动的”标签。
所有的Pod模板部分应该指定一个selector。比如：
selector:  matchLabels:      app.kubernetes.io/name: MyNametemplate:  metadata:    labels:      app.kubernetes.io/name: MyName

6.6. 基于角色的访问控制RBAC 资源有：

ServiceAccount (namespaced)
Role (namespaced)
ClusterRole
RoleBinding (namespaced)
ClusterRoleBinding

RBAC和服务账户配置应该使用独立的key。它们是独立的内容。在YAML中将这两个概念分开可以消除歧义使其更加清晰。
rbac:  # Specifies whether RBAC resources should be created  create: trueserviceAccount:  # Specifies whether a ServiceAccount should be created  create: true  # The name of the ServiceAccount to use.  # If not set and create is true, a name is generated using the fullname template  name:

rbac.create 应该是布尔值，用于控制RBAC资源是否被创建。默认是 true。用户想自己管理RBAC访问控制时可以设置为false (示例如下)。
serviceAccount.name 要设置为由chart创建的访问控制资源的ServiceAccount的名称。 如果serviceAccount.create是true，则使用该名称的ServiceAccount会被创建。如果没有设置名称， 则会使用fullname模板生成一个名称。如果serviceAccount.create是false，则不会被创建，但仍然会与相同的资源关联， 以便后续手动创建的引用它的RBAC资源可以正常工作。如果serviceAccount.create是false且没有指定名称， 会使用默认的ServiceAccount。
&#123;&#123;/*Create the name of the service account to use*/&#125;&#125;&#123;&#123;- define &quot;mychart.serviceAccountName&quot; -&#125;&#125;&#123;&#123;- if .Values.serviceAccount.create -&#125;&#125;    &#123;&#123; default (include &quot;mychart.fullname&quot; .) .Values.serviceAccount.name &#125;&#125;&#123;&#123;- else -&#125;&#125;    &#123;&#123; default &quot;default&quot; .Values.serviceAccount.name &#125;&#125;&#123;&#123;- end -&#125;&#125;&#123;&#123;- end -&#125;&#125;

7. 调试7.1. 语法检查helm lint

7.2. 测试本地渲染helm template --debug

7.3. 测试服务器渲染helm install --dry-run --debug

7.4. 忽略检查测试服务器渲染helm install --dry-run --disable-openapi-validation

7.5. 查看安装在服务上的模板helm get manifest




]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos制作nfs-provisioner集群镜像</title>
    <url>/dev-sealos-nfs-provisioner/</url>
    <content><![CDATA[1. 前言本文中，我们会基于sealos制作一个nfs-provisioner集群镜像。使用这个集群镜像，我们能够在sealos拉起的K8S集群中，一键部署nfs-provisioner，让K8S集群准备好storageclass，具有动态提供PV的能力。
参考文档：

构建一个 Ingress 集群镜像
《K8S中安装配置StorageClass》



2. 前置条件安装sealos4.2.0，拉起K8S 1.24.8集群（同时安装helm），具体方法参考《sealos入门篇》。
3. 制作nfs-provisioner集群镜像1、添加chart仓库
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/

2、下载chart
mkdir nfs-provisioner &amp;&amp; cd nfs-provisionerhelm search repo nfs-subdir-external-provisioner/nfs-subdir-external-provisioner -lhelm fetch nfs-subdir-external-provisioner/nfs-subdir-external-provisioner --version 4.0.18
这里下载 chart4.0.18 版本，对应 nfs-provisoner4.0.2 版本。下载的文件名为：nfs-subdir-external-provisioner-4.0.18.tgz
3、修改chart
tar -xzvf nfs-subdir-external-provisioner-4.0.18.tgzvim nfs-subdir-external-provisioner/values.yamltar -czvf nfs-subdir-external-provisioner-4.0.18-custom.tgz nfs-subdir-external-provisionerrm nfs-subdir-external-provisioner -rf

修改chart的values.yaml，给一些默认值，然后重新打包chart，后续Dockerfile中的CMD中可以少填一些参数。
如下修改：

image.repository 修改为 registry.cn-beijing.aliyuncs.com/mydlq/nfs-subdir-external-provisioner
image.tag 修改为 v4.0.0

4、添加镜像列表
mkdir -p images/shim/vim images/shim/nfs-provisioner-images.txt

内容为：
registry.cn-beijing.aliyuncs.com/mydlq/nfs-subdir-external-provisioner:v4.0.0

5、编写Dockerfile
FROM scratchCOPY ../nfs-provisioner .CMD [&quot;helm install nfs-provisioner nfs-subdir-external-provisioner-4.0.18-custom.tgz \     --namespace nfs-provisioner --create-namespace \     --set nfs.server=192.168.56.101 \     --set nfs.path=/data/nfs&quot;]

6、构建集群镜像
sealos build -f Dockerfile -t docker.io/voidking/nfs-provisioner:v4.0.0 .sealos inspect docker.io/voidking/nfs-provisioner:v4.0.0

4. 使用nfs-provisioner集群镜像4.1. 安装nfs-provisioner运行nfs-provisioner应用，安装nfs-provisioner
sealos run docker.io/voidking/nfs-provisioner:v4.0.0

报错：bash: [helm: 未找到命令排查解决过程参见【未找到helm命令问题排查】一节。
4.2. 自定义安装nfs-provisionersealos run docker.io/voidking/nfs-provisioner:v4.0.0 --cmd=&quot;helm install nfs-provisioner nfs-subdir-external-provisioner-4.0.18-custom.tgz \     --namespace nfs-provisioner --create-namespace \     --set nfs.server=192.168.56.102 \     --set nfs.path=/data/nfs \     --set storageClass.name=nfs-client \     --set storageClass.defaultClass=true&quot;kubectl get all -n nfs-provisioner

4.3. 上传集群镜像上传到公共镜像仓库
sealos push docker.io/voidking/nfs-provisioner:v4.0.0sealos tag docker.io/voidking/nfs-provisioner:v4.0.0 hub.sealos.cn/voidking/nfs-provisioner:v4.0.0sealos login -k config.yml hub.sealos.cnsealos push hub.sealos.cn/voidking/nfs-provisioner:v4.0.0

上传到私有镜像仓库
sealos tag docker.io/voidking/nfs-provisioner:v4.0.0 harbor.voidking.com/sealos/nfs-provisioner:v4.0.0sealos login harbor.voidking.comsealos push harbor.voidking.com/sealos/nfs-provisioner:v4.0.0

4.4. 卸载nfs-provisionerhelm list -Ahelm uninstall nfs-privisioner -n nfs-privisionersealos pssealos rm default-xxxcd /var/lib/sealos/data/default/applications &amp;&amp; rm deafult-xxx -rf

4.5. 删除历史镜像sealos images | grep &#x27;&lt;none&gt;&#x27; | awk &#x27;&#123;print $3&#125;&#x27; | xargs sealos rmi# sealos rmi docker.io/voidking/nfs-provisioner:v4.0.0

5. 未找到helm命令问题排查5.1. 问题描述运行nfs-provisioner应用，安装nfs-provisioner
sealos run docker.io/voidking/nfs-provisioner:v4.0.0

报错：bash: [helm: 未找到命令
5.2. helm放入PATHFROM scratchCOPY ../nfs-provisioner .COPY ../nfs-provisioner/helm /usr/local/bin/CMD [&quot;helm install nfs-privisioner nfs-subdir-external-provisioner-4.0.18-custom.tgz \     --namespace nfs-provisioner --create-namespace \     --set nfs.server=192.168.56.101 \     --set nfs.path=/data/nfs&quot;]
在Dockerfile中把helm拷贝到 /bin/、/usr/bin/、/usr/local/bin/ ，报错依旧。
5.3. 工作路径执行helmFROM scratchCOPY ../nfs-provisioner .CMD [&quot;./helm install nfs-privisioner nfs-subdir-external-provisioner-4.0.18-custom.tgz \     --namespace nfs-provisioner --create-namespace \     --set nfs.server=192.168.56.101 \     --set nfs.path=/data/nfs&quot;]
在Dockerfile中把helm拷贝到 workdir，报错 bash: [./helm: 没有那个文件或目录。
5.4. 提前准备helm在 sealos 中，宿主机/var/lib/sealos/data/default/rootfs 是应用的 root 文件系统的位置。直接把helm放到宿主机/var/lib/sealos/data/default/rootfs目录中，报错依旧。
5.5. 存在缓存？在 sealos 中，/var/lib/sealos/data/default/applications 是应用的目录。sealos ps查看应用的id，然后在应用目录找到对应的目录，查看workdir中的文件。发现多次构建然后run，对应的workdir内容并不会变化，这里有点坑了。
临时解决办法：删除container，删除workdir。
sealos pssealos rm deafult-xxxcd /var/lib/sealos/data/default/applications &amp;&amp; rm deafult-xxx -rf

5.6. 命令未改变？想要查看下执行命令的路径，于是在Dockerfile中把命令修改为pwd，构建，run。这时神奇的问题出现了，依然提示bash: [helm: 未找到命令，但是我们的镜像里明明已经没有了helm这条命。这说明，构建或者run的机制有问题。
那就自己指定命令：
sealos run docker.io/voidking/nfs-provisioner:v4.0.0 --cmd=[&quot;pwd&quot;]
报错：bash: [pwd]: 未找到命令。
不使用括号，直接传入命令：
sealos run docker.io/voidking/nfs-provisioner:v4.0.0 --cmd=&quot;pwd&quot;

正常运行了，拿到path为/var/lib/sealos/data/default/applications/default-3slb7ema/workdir
5.7. 重新安装K8S修改Dockerfile中的CMD，重新打包镜像，新的CMD命令并不会生效。虽然可以在sealos run时直接传入命令，但是这样就没有办法测试镜像的默认启动命令了。
没有找到解决办法，想要复现bash: [helm: 未找到命令这个问题，然后提个issue。
于是重新安装K8S，然后重新安装nfs-provisioner。安装nfs-provisioner时，使用【制作nfs-provisioner集群镜像】一节中最初的Dockerfile，这次居然没有报错！安装nfs-provisioner成功！说好的缺少helm呢？
发现问题失败，于是删除/usr/bin/helm和/usr/local/bin/helm，再次重新安装K8S，再次安装nfs-provisioner。然后，再次安装nfs-provisioner成功！
神奇了，还没办法复现了。重新找了一台虚拟机，安装K8S，安装nfs-provisioner，顺利安装成功！行吧，打扰了。。。
5.8. 小结bash: [helm: 未找到命令 问题，可能是虚拟机环境引起的。因为在全新的环境中，可以找到helm命令。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>问题排查</tag>
        <tag>ipvs</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>搭建Spark on Yarn集群</title>
    <url>/dev-spark-on-yarn/</url>
    <content><![CDATA[1. 前言计划在三台Linux主机中搭建Spark on Yarn集群，Spark版本3.2.3，Hadoop版本3.2.3。
主机配置为4C8G，操作系统为CentOS7，hosts配置为：
192.168.56.101 spark-master192.168.56.102 spark-slave1192.168.56.103 spark-slave2
选择101作为master节点，另外两个作为worker节点。
参考文档：

《Linux中安装配置Hadoop》
《Linux中搭建Spark集群》
Hadoop 3.2.2 安装与使用文档超详细图文步骤
spark3.2+hadoop3.3.2三节点分布式部署初体验
搭建Spark on Yarn集群
spark3.3.0安装&amp;部署过程



2. 安装Java参考文档《全平台安装JDK》
3. 安装Hadoop3.1. 下载Hadoop安装包wget https://dlcdn.apache.org/hadoop/common/hadoop-3.2.3/hadoop-3.2.3.tar.gz --no-check-certificatemkdir -p /usr/local/hadoop/tar -xzvf hadoop-3.2.3.tar.gz -C /usr/local/hadoop/

3.2. 修改Hadoop配置1、修改hadoop-env.sh
cd /usr/local/hadoop/hadoop-3.2.3vim etc/hadoop/hadoop-env.sh
修改JAVA_HOME为绝对路径。
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161

2、验证环境
./bin/hadoop version

3.3. 单机测试运行mkdir ./inputcp ./etc/hadoop/*.xml ./input./bin/hadoop jar ./share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar grep ./input ./output &#x27;dfs[a-z.]+&#x27;cat ./output/*

看到结果1  dfsadmin表明运行成功。
3.4. master节点配置HDFS1、修改etc/hadoop/core-site.xml
&lt;configuration&gt;    &lt;!-- 指定hdfs中nomenode的地址 --&gt;    &lt;property&gt;        &lt;name&gt;fs.defaultFS&lt;/name&gt;        &lt;value&gt;hdfs://spark-master:9000&lt;/value&gt;    &lt;/property&gt;    &lt;!-- 指定hadoop运行时产生文件的存储目录 --&gt;    &lt;property&gt;        &lt;name&gt;hadoop.tmp.dir&lt;/name&gt;        &lt;value&gt;file:/usr/local/hadoop/hadoop-3.2.3/data/tmp&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

配置参考文档：hadoop3.2.3 core-default.xml
2、修改etc/hadoop/hdfs-site.xml
&lt;configuration&gt;    &lt;!-- 设置dfs副本数 --&gt;    &lt;property&gt;        &lt;name&gt;dfs.replication&lt;/name&gt;        &lt;value&gt;1&lt;/value&gt;    &lt;/property&gt;    &lt;!-- hdfs的web管理页面的端口 --&gt;    &lt;property&gt;        &lt;name&gt;dfs.http.address&lt;/name&gt;        &lt;value&gt;0.0.0.0:50070&lt;/value&gt;    &lt;/property&gt;    &lt;!-- 设置secondnamenode的端口 --&gt;    &lt;property&gt;        &lt;name&gt;dfs.namenode.secondary.http-address&lt;/name&gt;        &lt;value&gt;spark-master:9001&lt;/value&gt;    &lt;/property&gt;    &lt;!-- namenode目录 --&gt;    &lt;property&gt;        &lt;name&gt;dfs.namenode.name.dir&lt;/name&gt;        &lt;value&gt;file:/usr/local/hadoop/hadoop-3.2.3/data/dfs/name&lt;/value&gt;    &lt;/property&gt;    &lt;!-- datanode目录 --&gt;    &lt;property&gt;        &lt;name&gt;dfs.datanode.data.dir&lt;/name&gt;        &lt;value&gt;file:/usr/local/hadoop/hadoop-3.2.3/data/dfs/data&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

配置参考文档：hadoop3.2.3 hdfs-default.xml
3、修改etc/hadoop/workers
spark-masterspark-slave1spark-slave2

3.5. master节点配置配置Yarn1、编辑 etc/hadoop/mapred-site.xml
&lt;configuration&gt;    &lt;!-- 指定mr运行在yarn上 --&gt;    &lt;property&gt;      &lt;name&gt;mapreduce.framework.name&lt;/name&gt;      &lt;value&gt;yarn&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

2、编辑 etc/hadoop/yarn-site.xml
&lt;configuration&gt;    &lt;!-- reducer 获取数据的方式 --&gt;    &lt;property&gt;        &lt;name&gt;yarn.nodemanager.aux-services&lt;/name&gt;        &lt;value&gt;mapreduce_shuffle&lt;/value&gt;    &lt;/property&gt;    &lt;!-- 指定 YARN 的 ResourceManager 的地址 --&gt;    &lt;property&gt;        &lt;name&gt;yarn.resourcemanager.hostname&lt;/name&gt;        &lt;value&gt;spark-master&lt;/value&gt;    &lt;/property&gt;     &lt;!-- 该节点上YARN可使用的物理内存总量，默认是 8192（MB）--&gt;    &lt;!-- 注意，如果你的节点内存资源不够8GB，则需要调减小这个值 --&gt;    &lt;property&gt;        &lt;name&gt;yarn.nodemanager.resource.memory-mb&lt;/name&gt;        &lt;value&gt;4096&lt;/value&gt;    &lt;/property&gt;     &lt;!-- 单个任务可申请最少内存，默认 1024 MB --&gt;    &lt;property&gt;        &lt;name&gt;yarn.scheduler.minimum-allocation-mb&lt;/name&gt;        &lt;value&gt;1024&lt;/value&gt;    &lt;/property&gt;      &lt;!-- 单个任务可申请最大内存，默认 8192 MB --&gt;    &lt;property&gt;        &lt;name&gt;yarn.scheduler.maximum-allocation-mb&lt;/name&gt;        &lt;value&gt;4096&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

3.6. 配置环境变量vim /etc/profile
添加：
export HADOOP_HOME=/usr/local/hadoop/hadoop-3.2.3export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH

使生效：
source /etc/profile

3.7. 拷贝配置到slave节点rsync -P -avz /usr/local/hadoop 192.168.56.102:/usr/local/rsync -P -avz /usr/local/hadoop 192.168.56.103:/usr/local/rsync -P -avz /etc/profile 192.168.56.102:/etc/profilersync -P -avz /etc/profile 192.168.56.103:/etc/profile

3.8. master节点启动HDFS1、格式化namenode
./bin/hdfs namenode -format

执行完成，没有报错，当前目录中出现了tmp目录，表明格式化成功。slave节点不会出现tmp目录。
2、添加HDFS用户环境变量/etc/profile中添加
export HDFS_DATANODE_USER=rootexport HDFS_DATANODE_SECURE_USER=hdfsexport HDFS_NAMENODE_USER=rootexport HDFS_SECONDARYNAMENODE_USER=root

或者sbin/start-dfs.sh 和 sbin/stop-dfs.sh，文件顶部添加
HDFS_DATANODE_USER=rootHDFS_DATANODE_SECURE_USER=hdfsHDFS_NAMENODE_USER=rootHDFS_SECONDARYNAMENODE_USER=root

否则执行可能报错：Starting namenodes on [spark-master]ERROR: Attempting to operate on hdfs namenode as rootERROR: but there is no HDFS_NAMENODE_USER defined. Aborting operation.Starting datanodesERROR: Attempting to operate on hdfs datanode as rootERROR: but there is no HDFS_DATANODE_USER defined. Aborting operation.Starting secondary namenodes [spark-master]ERROR: Attempting to operate on hdfs secondarynamenode as rootERROR: but there is no HDFS_SECONDARYNAMENODE_USER defined. Aborting operation.
3、启动hdfs服务
./sbin/start-dfs.sh
这条命令会在master和slave节点同时启动hdfs，会提示输入密码，最好提前配置好免密登录。
4、查看hdfs进程
jps -l

master节点可以看到：

org.apache.hadoop.hdfs.server.namenode.NameNode
org.apache.hadoop.hdfs.server.datanode.DataNode
org.apache.hadoop.hdfs.server.namenode.SecondaryNameNode

slave节点可以看到：

org.apache.hadoop.hdfs.server.datanode.DataNode

5、浏览器访问浏览器访问 http://192.168.56.101:50070可以看到 NameNode 和 Datanode 的信息。
3.9. master节点启动Yarn1、修改 sbin/start-yarn.sh 和 sbin/stop-yarn.sh，文件顶部添加
YARN_RESOURCEMANAGER_USER=rootHADOOP_SECURE_DN_USER=yarnYARN_NODEMANAGER_USER=root

2、启动yarn
./sbin/start-yarn.sh

3、查看进程
jps -l

master可以看到：

org.apache.hadoop.yarn.server.nodemanager.NodeManager
org.apache.hadoop.yarn.server.resourcemanager.ResourceManager

slave节点可以看到：

org.apache.hadoop.yarn.server.nodemanager.NodeManager

4、浏览器访问浏览器访问 http://192.168.56.101:8088可以看到 Yarn ResourceManager的信息。
4. 安装Spark4.1. master节点配置1、下载spark并解压
wget https://archive.apache.org/dist/spark/spark-3.2.3/spark-3.2.3-bin-hadoop3.2-scala2.13.tgzmkdir -p /usr/local/sparktar -xzvf spark-3.2.3-bin-hadoop3.2-scala2.13.tgz -C /usr/local/spark

更多版本的spark，可以在Spark release archives页面找到。
2、创建配置文件
cd /usr/local/spark/spark-3.2.3-bin-hadoop3.2-scala2.13/confcp workers.template workerscp spark-defaults.conf.template spark-defaults.confcp spark-env.sh.template spark-env.sh

3、修改配置（1）workers中删除localhost，添加
spark-masterspark-slave1spark-slave2

（2）spark-defaults.conf暂时不变
（3）spark-env.sh中添加
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161export SPARK_MASTER_HOST=spark-masterexport SPARK_MASTER_PORT=7077export SPARK_HOME=/usr/local/spark/spark-3.2.3-bin-hadoop3.2-scala2.13export HADOOP_HOME=/usr/local/hadoop/hadoop-3.2.3export HADOOP_CONF_DIR=/usr/local/hadoop/hadoop-3.2.3/etc/hadoop/export YARN_CONF_DIR=/usr/local/hadoop/hadoop-3.2.3/etc/hadoop/

4、/etc/profile中添加环境变量
export SPARK_HOME=/usr/local/spark/spark-3.2.3-bin-hadoop3.2-scala2.13export PATH=$SPARK_HOME/bin:$PATH

5、使配置生效
source /etc/profile

4.2. master配置同步到workerrsync -P -avz /usr/local/spark 192.168.56.102:/usr/local/rsync -P -avz /usr/local/spark 192.168.56.103:/usr/local/rsync -P -avz /etc/profile 192.168.56.102:/etc/profilersync -P -avz /etc/profile 192.168.56.103:/etc/profile

4.3. 运行Spark1、启动spark（在master节点上执行）
cd /usr/local/spark/spark-3.2.3-bin-hadoop3.2-scala2.13/sbin./start-all.sh

根据提示，依次输入两台worker节点的密码。（这里最好配置上免密登录）这样，三个节点上的spark就都可以启动起来。
2、验证安装
jps -l

master节点看到Master和Worker进程，worker节点看到Worker进程。
3、浏览器访问浏览器访问 http://192.168.56.101:8080可以看到spark master信息。浏览器访问 http://192.168.56.102:8081可以看到spark slave节点信息。
5. 测试使用Spark on Yarn1、spark-submit提交任务
./bin/spark-submit \  --class org.apache.spark.examples.SparkPi \  --master yarn \  ./examples/jars/spark-examples_2.13-3.2.3.jar 1000

3、浏览器查看进度浏览器访问 http://192.168.50.105:8088在Yarn ResourceManager页面，可以查看到任务详情。
6. vcore数量不对问题6.1. 问题描述理论上：vcores使用数 = executor-cores * num-executors + 1但是实际提交任务后，配置的executor-cores并没有起作用。
例如：
./bin/spark-submit --class org.apache.spark.examples.SparkPi \--master yarn \--deploy-mode cluster \--driver-memory 2g \--executor-memory 1g \--num-executors 4 \--executor-cores 2 \./examples/jars/spark-examples_2.13-3.2.3.jar 10

上面的提交，理论上应该使用2 * 4 + 1 = 9核，实际上从Yarn ResourceManager页面查看Allocated CPU VCores，只分配了5核。也就是说executor-cores没有生效，使用了默认值1。
6.2. 解决办法1、编辑 capacity-scheduler.xml 
cd /usr/local/hadoop/hadoop-3.2.3vim etc/hadoop/capacity-scheduler.xml
如下修改：
&lt;property&gt;  &lt;name&gt;yarn.scheduler.capacity.resource-calculator&lt;/name&gt;  &lt;!-- &lt;value&gt;org.apache.hadoop.yarn.util.resource.DefaultResourceCalculator&lt;/value&gt; --&gt;  &lt;value&gt;org.apache.hadoop.yarn.util.resource.DominantResourceCalculator&lt;/value&gt;  &lt;description&gt;    The ResourceCalculator implementation to be used to compare    Resources in the scheduler.    The default i.e. DefaultResourceCalculator only uses Memory while    DominantResourceCalculator uses dominant-resource to compare    multi-dimensional resources such as Memory, CPU etc.  &lt;/description&gt;&lt;/property&gt;

2、同步到slave节点
rsync -P -avz /usr/local/hadoop/hadoop-3.2.3/etc/hadoop/ 192.168.56.102:/usr/local/hadoop/hadoop-3.2.3/etc/hadooprsync -P -avz /usr/local/hadoop/hadoop-3.2.3/etc/hadoop/ 192.168.56.103:/usr/local/hadoop/hadoop-3.2.3/etc/hadoop

3、重启yarn
./sbin/stop-yarn.sh./sbin/start-yarn.sh

参考文档：spark on yarn提交后vcore数不对
]]></content>
      <categories>
        <category>engineering</category>
        <category>troubleshooting</category>
        <category>java</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>问题排查</tag>
        <tag>java</tag>
        <tag>spark</tag>
        <tag>hadoop</tag>
      </tags>
  </entry>
  <entry>
    <title>安卓阅读神器Legado</title>
    <url>/dev-legado/</url>
    <content><![CDATA[1. Legado简介
Legado is a free and open source novel reader for Android.

Legado是一个免费开源的安卓小说阅读器，APP名称为”阅读”。
参考文档：

gedoor/legado
Legado - 使用说明
Legado使用小技巧



2. 安装Legado访问legado/releases页面，下载apk安装包，例如legado_app_3.23.040420.apk，然后点击安装即可。
3. 使用Legado3.1. 导入书源方法一：1、浏览器访问 http://dy.miaogongzi.cc2、网页里点击喵公子订阅源，直接跳转到app打开，完成。如果第一个订阅源打不开，可以导入第二个备用订阅源。
方法二：1、查找下载订阅源例如，科技卫星地图公众号，回复SY406，获取订阅源并下载2、阅读APP中导入订阅源我的，书源管理，本地导入。注意：不要直接从订阅菜单中进入书源管理，这个入口导入书源会失败，可能是bug。
参考文档：

简单教程，简单使用
导入书源

3.2. 配置语音朗读离线朗读配置方法：下载安装离线TTS，阅读APP中选择使用该TTS。
离线TTS推荐：

Speech Services by Google（Play商店可以下载安装）
讯飞语记（郭嘉语音最佳）

注意：这些离线TTS一定要配置应用启动管理，手动管理，允许后台活动，允许关联启动
在线朗读配置方法：配置在线TTS参数，阅读APP中选择使用该TTS。
参考文档：

本地TTS推荐
自定义在线朗读教程
阅读内置的百度TTS问题

]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
      </tags>
  </entry>
  <entry>
    <title>部署ChatGPT镜像站</title>
    <url>/dev-chatgpt-mirror-site/</url>
    <content><![CDATA[1. ChatGPT镜像站简介使用ChatGPT是有一定门槛的：科学上网，并且是特定地区科学上网。近期ChatGPT开始封号，如果科学上网工具的出口IP频繁变化，那么ChatGPT账号可能被封。
而搭建使用ChatGPT镜像站，可以降低国内使用ChatGPT的门槛，并且避免账号被封。
参考文档：

教程：部署个人专属的 ChatGPT 镜像教程



2. 整体部署思路1、注册ChatGPT账号2、获取OpenAI API Key3、准备国外主机（或者代理）4、准备Docker环境5、部署ChatGPT镜像站6、配置域名
3. 注册ChatGPT账号参考文档：

国内开通Chat GPT Plus保姆级教程【典藏】
ChatGPT学习手册
推荐一个方便好用的 ChatGPT 客户端

4. 获取OpenAI API Key访问OpenAI - API keys，Create new secret key
5. 准备国外主机（或者代理）选择一个提供国外主机的云厂商，购买一台国外主机（推荐美国），并且主机的IP国内可以访问，安装Linux系统。
微软作为openai的大股东，选择微软azure的云主机，可能更不容易被封号。azure注册需要visa或者master信用卡。注册后azure提供$200的免费试用额度，期限一个月，升级到即用即付订阅，可以延长免费期限到一年。
相关文档：

Azure官网
Azure 中的虚拟机
定价计算器
虚拟机选择器

如果没有国外主机的话，那么可以在国内主机上配置使用代理。

方案一：使用openai专用代理，例如beidongjiedeguang/openai-forward
方案二：使用可以访问openai的通用代理，配置方法参考《Linux配置网络代理》

6. 准备Docker环境安装Docker，参考文档：《Docker入门篇》
7. 部署ChatGPT镜像站7.1. ChuanhuChatGPT简介ChatGPT镜像站源码很多，本文中选择Star数很高的ChuanhuChatGPT。
ChuanhuChatGPT优点：

流式传输 / 无限对话 / 保存对话 / 预设Prompt集 / 联网搜索 / 根据文件回答
渲染LaTeX / 渲染表格 / 代码高亮 / 自动亮暗色切换 / 自适应界面 / “小而美”的体验
自定义api-Host / 多参数可调 / 多API Key均衡负载 / 多用户显示 / 适配GPT-4 / 支持本地部署LLM

参考文档：

GaiZhenbiao/ChuanhuChatGPT
ChuanhuChatGPT - 使用教程
API host选项

7.2. 部署ChuanhuChatGPT这里我们使用Docker方式部署。
1、拉取镜像
docker pull tuchuanhuhuhu/chuanhuchatgpt:20240121

更多版本，请访问：tuchuanhuhuhu/chuanhuchatgpt版本对应配置，请访问：GaiZhenbiao/ChuanhuChatGPT
2、准备配置文件
mkdir -p /opt/chuanhuchatgpt/docker run --rm \    tuchuanhuhuhu/chuanhuchatgpt:20240121 \    cat /app/config_example.json &gt; /opt/chuanhuchatgpt/config.jsonvim /opt/chuanhuchatgpt/config.json

如下修改：

openai_api_key：改为自己的key
openai_api_base：改为openai专用代理地址，例如 https://api.openai-forward.com
users：改为想要配置的用户名和密码，例如 [[&quot;myname&quot;,&quot;mypassword&quot;]]

更多配置参考文档：配置 config.json
3、启动ChuanhuChatGPT
docker run --name chuanhuchatgpt -d \    -v /opt/chuanhuchatgpt/history:/app/history \    -v /opt/chuanhuchatgpt/config.json:/app/config.json \    -p 7860:7860 \    tuchuanhuhuhu/chuanhuchatgpt:20240121

4、查看服务状态
docker logs chuanhuchatgptdocker update --restart=always chuanhuchatgpt

5、访问服务浏览器访问 http://&lt;主机IP&gt;:7860
8. 配置域名注意：配置域名时，需要配置支持websocket，否则会出现 Connection errored out 
参考文档：

《Nginx入门篇》
ChuanhuChatGPT - 使用教程
设置登录账号密码后Connection errored out
连接错误Connection errored out

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>chatgpt</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中Pod抓包</title>
    <url>/dev-k8s-pod-packet-capture/</url>
    <content><![CDATA[1. Pod抓包概述排查网络问题的最好方法就是抓包分析，在 Kubernetes 中对 Pod 进行抓包的常见方法包括：

在 Pod 中安装 tcpdump 工具，直接对流量进行抓包，捕获网络流量并输出到文件或终端上。这种方法的优点是灵活性高，可以根据实际情况进行定制和调试，但需要在 Pod 中安装额外的工具，可能会增加 Pod 的大小和复杂度。
使用 kubectl port-forward 命令将 Pod 的网络流量转发到本地，然后使用本地的 tcpdump 工具对流量进行抓包。这种方法的优点是简单易用，不需要在 Pod 中安装额外的工具，但需要手动进行转发和抓包，可能不太方便。
使用 Kubernetes 中的网络插件提供的抓包功能，例如 Cilium、Weave Net、Calico 等。这些网络插件一般都提供了抓包工具或者抓包接口，可以直接对 Pod 的网络流量进行抓包，而无需在 Pod 中安装额外的工具或者进行手动转发。
登录 Pod 所在节点，进入容器 netns，然后使用节点上 tcpdump 工具进行抓包。

以上方法都比较繁琐，令人惊喜的是出现了 ksniff，可以让我们在 Kubernetes 更简便地抓包。本文中，我们会学习使用ksniff来进行Pod抓包。
参考文档：

Wireshark入门篇
eldadru/ksniff
kubernetes 实用技巧: 使用 ksniff 抓包
如何在 Kubernetes Pod 内进行网络抓包
Kubernetes 网络异常分类及排错指南



2. ksniff抓包原理2.1. 普通模式ksniff 上传 tcpdump 二进制文件到目标 Pod 的一个容器里，然后执行二进制文件来实现抓包。
2.2. 特权模式ksniff 启动另外一个pod，和目标pod共享network namespace，抓取目标pod中的网络包。
3. 安装wiresharkwireshark安装方法参考文档《Wireshark入门篇》
4. 安装ksniff1、安装krewkrew安装方法参考文档《kubectl插件管理器krew》
2、安装ksniff
kubectl krew install sniffkubectl sniff --help

5. 使用ksniff5.1. 创建测试容器kubectl run test --image=busybox --command sleep 7200

5.2. 实时抓包kubectl sniff test -n default
执行该命令后，会自动弹出本地安装的 wireshark 并实时捕获。如果报错Error: signal: abort trap，可以尝试再次执行该命令。
5.3. 执行一些网络命令kubectl exec -it test -- /bin/shnslookup www.baidu.com
pod中执行命令后，会在wireshark中看到捕获的网络包。
5.4. 抓包保存成文件有时我们可能无法直接在本地执行 kubectl，这时可以先抓包保存成文件，然后拷贝到本地使用wireshark进行分析。
kubectl sniff test -n default -o test.pcap

5.5. 特权模式kubectl sniff test -n default -p

5.6. 查看明文如果数据包内容很多都是明文 (比如 HTTP)，只希望大概看下明文内容，可以指定 -o - 将抓包内容直接打印到标准输出 (stdout)
kubectl sniff test -n default -o -

5.7. 抓包时过滤指定 tcpdump 过滤条件，屏蔽掉不需要的数据，避免数据量过大。
kubectl sniff test -n default -f &quot;port 80&quot;

6. 抓包报错 139 问题6.1. 问题描述抓包时，报错：Error: executing sniffer failed, exit code: ‘139’
6.2. 解决办法参考文档：kubectl sniff command returning 139
解决办法：在容器中手动安装合适版本的tcpdump。
例如，对于基于Debian的容器：
kubectl exec -i -t your-pod -- bash -c &quot;apt update &amp;&amp; apt install tcpdump -y &amp;&amp; \    rm /tmp/static-tcpdump &amp;&amp; \    ln /bin/tcpdump /tmp/static-tcpdump&quot;


]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>k8s</tag>
        <tag>chatgpt</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
        <tag>wireshark</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：GitLab Pipeline统计</title>
    <url>/dev-gitlab-pipeline-statistics/</url>
    <content><![CDATA[1. 需求描述计划对CICD进行优化，主要包括降低pipeline的失败率和降低构建发布时间。
最开始，要对pipeline进行统计，搞清楚当前的情况，做到心里有“数”，制定合理的目标。优化一段时间后，需要再用相同的统计方法对pipeline进行统计，看看优化的效果。
好了，问题来了：怎样统计最近两个月的gitlab pipeline？具体需求包括： 

总的pipeline次数
成功的pipeline次数
失败的pipeline次数
取消的pipeline次数
跳过的pipeline次数
耗时超过5分钟的pipeline次数
耗时超过10分钟的pipeline次数
耗时超过半小时的pipeline次数
耗时超过1小时的pipeline次数
pipeline的失败率

使用GitLab的Analytics可以看到部分我们想要的数据，比如某个月的pipeline总次数，失败的总次数等。但是并不能根据我们的需要选择时间段进行统计，也不能统计pipeline的持续时间，因此最好的方法是通过GitLab API进行统计。
参考文档：Pipelines API


2. 需求处理以下内容主要来自chatgpt，稍作修改。
2.1. 获取API Token在 GitLab 中获取 API 访问 token 的步骤如下：

登录到 GitLab 网站。

点击右上角头像，选择 “Settings”。

在左侧菜单栏中，选择 “Access Tokens”。

输入一个描述信息，并选择需要授予 API 访问权限的范围，然后点击 “Create Personal Access Token”。

复制生成的访问 token。


请注意，访问 token 是敏感信息，请妥善保管，不要泄露给他人。此外，为了保证安全性，建议定期更换访问 token。
2.2. 代码完整代码请访问：gitlab-pipeline-statistics/statistics.py
以上代码适用于GitLab 14.0.5 ，其他版本不一定适用。
注意：成功次数+失败次数+取消次数+跳过次数 != 总次数因为除了这四种常见状态，还有七种状态，具体状态可以参考Pipelines API。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>chatgpt</tag>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo配置GitHub Actions自动构建发布</title>
    <url>/dev-hexo-github-actions/</url>
    <content><![CDATA[1. Travis CI必须付费了2023年2月25日，使用travis ci部署hexo项目，发现并没有触发任务。
登录travis ci，发现提示：

Builds have been temporarily disabled for public repositories due to a negative credit balance. Please go to the Plan page to replenish your credit balance or alter your Consume paid credits for OSS setting.

在travis ci plan页面关闭了Consume paid credits for OSS，但是依然提示：

Builds have been temporarily disabled for private and public repositories due to a negative credit balance. Please go to the Plan page to replenish your credit balance.

点击Change plan，发现已经没有免费的plan了，而最便宜的plan，也要$69/monthly，太贵了，放弃。不过可以理解，毕竟跑CI是需要服务器的，服务器是需要花钱的。
那就换成GitHub Actions吧，有付费版，也有免费版。


2. Quickstart for GitHub Actions参考文档：Quickstart for GitHub Actions
1、 创建新的分支（因为原分支是适用于travis ci的）
git checkout -b github-actiongit push origin HEAD:github-action

2、 创建 github-actions-demo.yml 文件voidking/hexo-deploy项目中执行
mkdir -p .github/workflowsvim .github/workflows/github-actions-demo.yml

github-actions-demo.yml 文件内容如下：
name: GitHub Actions Demorun-name: $&#123;&#123; github.actor &#125;&#125; is testing out GitHub Actions 🚀on: [push]jobs:  Explore-GitHub-Actions:    runs-on: ubuntu-latest    steps:      - run: echo &quot;🎉 The job was automatically triggered by a $&#123;&#123; github.event_name &#125;&#125; event.&quot;      - run: echo &quot;🐧 This job is now running on a $&#123;&#123; runner.os &#125;&#125; server hosted by GitHub!&quot;      - run: echo &quot;🔎 The name of your branch is $&#123;&#123; github.ref &#125;&#125; and your repository is $&#123;&#123; github.repository &#125;&#125;.&quot;      - name: Check out repository code        uses: actions/checkout@v3      - run: echo &quot;💡 The $&#123;&#123; github.repository &#125;&#125; repository has been cloned to the runner.&quot;      - run: echo &quot;🖥️ The workflow is now ready to test your code on the runner.&quot;      - name: List files in the repository        run: |          ls $&#123;&#123; github.workspace &#125;&#125;      - run: echo &quot;🍏 This job&#x27;s status is $&#123;&#123; job.status &#125;&#125;.&quot;

3、 上传代码
git add .git commit -m &quot;github action test&quot;git push

报错：refusing to allow a Personal Access Token to create or update workflow .github/workflows/github-actions-demo.yml without workflow scope
这是因为我们的Access Token需要具有create or update workflow的权限，因此需要重新创建一个Access Token。具体操作方法：访问Personal access tokens (classic)页面，Generate new token，生成token时一定要勾选repo和workflow。
然后，使用新的token再次push即可。修改token的方法参考文档《Git实用命令》。
4、查看Actions访问voidking/hexo-deploy Actions页面，即可看到CI workflows（相当于gitlab中的pipelines）。
3. Hexo配置GitHub Actions参考文档：

GitHub Actions
Workflow syntax for GitHub Actions
Hexo Action
利用 Github Actions 自动部署 Hexo 博客
GitHub Actions 教程：定时发送天气邮件

3.1. CICD思路
拉取负责部署的hexo-deploy repo，里面是hexo的配置文件（关于站点配置和构建配置等，详情参考《Hexo配置Travis CI自动构建发布》）
拉取hexo theme repo，里面是站点主题
拉取hexo-backup repo，里面是markdown文档
hexo-backup中的文档放到hexo可以构建（编译）的位置
安装nodejs
安装hexo、gulp等依赖
执行构建（编译）
上传编译后的html等静态文件到两个pages repoa. github pages是国外流量的源站b. aliyun server是国内流量的源站c. aliyun server从gitee pages拉取最新版本d. gitee pages本身不对外提供服务，只是作为一个git仓库
aliyun server从gitee pages拉取最新版本

3.2. 准备github-actions.ymlgithub-actions.yml内容如下：
name: Hexo CICDrun-name: $&#123;&#123; github.actor &#125;&#125; build and deploy hexo!on:  push:    branches:      - master      - github-actionsenv:  GIT_USER: voidking  HEXO_BACKUP_REPO: voidking/hexo-backup  HEXO_BACKUP_REPO_BRANCH: master  HEXO_THEME_REPO: voidking/hexo-theme-next  HEXO_THEME_REPO_BRANCH: root  GITHUB_PAGES_REPO: github.com/voidking/voidking.github.io.git  GITEE_PAGES_REPO: gitee.com/voidking/voidking.git  GITHUB_PAGES_URL: &quot;https://$&#123;GIT_USER&#125;:$&#123;&#123; secrets.GH_TOKEN &#125;&#125;@$&#123;GITHUB_PAGES_REPO&#125;&quot;  GITEE_PAGES_URL: &quot;https://$&#123;GIT_USER&#125;:$&#123;&#123; secrets.GITEE_TOKEN &#125;&#125;@$&#123;GITEE_PAGES_REPO&#125;&quot;  ALI_IP: 8.136.13.58  ALI_USER: voidking  jobs:  build:    name: Build on node $&#123;&#123; matrix.node_version &#125;&#125; and $&#123;&#123; matrix.os &#125;&#125;    runs-on: ubuntu-latest    strategy:      matrix:        os: [ubuntu-latest]        node_version: [12.22.5]    steps:    - name: Checkout repo      uses: actions/checkout@v2    - name: Checkout theme repo      uses: actions/checkout@v2      with:        repository: $&#123;&#123; env.HEXO_THEME_REPO &#125;&#125;        ref: $&#123;&#123; env.HEXO_THEME_REPO_BRANCH &#125;&#125;        path: themes/next    - name: Checkout hexo-backup repo      uses: actions/checkout@v2      with:        repository: $&#123;&#123; env.HEXO_BACKUP_REPO &#125;&#125;        ref: $&#123;&#123; env.HEXO_BACKUP_REPO_BRANCH &#125;&#125;        path: hexo-backup        token: $&#123;&#123; secrets.GH_TOKEN &#125;&#125;    - name: Move markdown articles to current directory      run: mv hexo-backup/source . &amp;&amp; rm -rf source/private    - name: Install nodejs $&#123;&#123; matrix.node_version &#125;&#125;      uses: actions/setup-node@v3      with:        node-version: $&#123;&#123; matrix.node_version &#125;&#125;    - name: Install nodejs dependencies      run: npm install    - name: Build nodejs project      run: npm run build    - name: Push to voidking.github.io and gitee      run: |        git config --global user.name &quot;voidking&quot;        git config --global user.email &quot;voidking@qq.com&quot;        git clone https://$&#123;&#123; secrets.GH_TOKEN &#125;&#125;@$&#123;&#123; env.GITHUB_PAGES_REPO &#125;&#125; voidking        cd voidking        rm -rfv `ls -a | grep -vw &#x27;\.&#x27; | grep -vw &#x27;\.git&#x27; | xargs`        ls -al        # unalias cp         cp -rf ../public/. .        cp ../source/.travis.yml .        git add . &amp;&amp; git commit -m &quot;GitHub Actions Auto Builder&quot;        git push --force --quiet $&#123;&#123; env.GITHUB_PAGES_URL &#125;&#125; master:master        git push --force --quiet $&#123;&#123; env.GITEE_PAGES_URL &#125;&#125; master:master    deploy:    name: Deploy to Aliyun Server    needs: build    runs-on: ubuntu-latest    steps:    - name: Configure id_rsa      run: |        mkdir -p ~/.ssh/        echo $&#123;&#123; secrets.ID_RSA &#125;&#125; | base64 -d &gt; ~/.ssh/id_rsa        chmod 600 ~/.ssh/id_rsa    - name: Execute ssh command      run: |        ssh -o StrictHostKeyChecking=no \        -o PubkeyAcceptedKeyTypes=ssh-rsa \        $&#123;&#123; env.ALI_USER &#125;&#125;@$&#123;&#123; env.ALI_IP &#125;&#125; \        &quot;cd /opt/nginx/work/voidking/ &amp;&amp; git pull --force --quiet $&#123;&#123; env.GITEE_PAGES_URL &#125;&#125; master:master&quot;

说明：

github action中定义变量有三种方式：vars、secrets和env。
vars/secrets在workflow触发前定义，env在workflow触发后定义。
$&#123;&#123;&#125;&#125;双花括号表示上下文引用，详情参考Context availability。
$&#123;&#125;单花括号表示shell方式使用中env变量。
使用env中的变量有两种方式：上下文引用和shell方式使用。

3.3. 配置加密变量上面的workflow配置中，用到了一些敏感变量，GH_TOKEN、GITEE_TOKEN和ID_RSA，这些变量需要加密。
配置加密变量的方法：

repository级别变量：Project -&gt; Settings -&gt; Actions secrets and variables -&gt; New repository secret
environments级别变量：Project -&gt; Settings -&gt; Actions secrets and variables -&gt; Manage enviroments -&gt; New enviroment -&gt; Environment secrets -&gt; Add secret

其中ID_RSA的获取方法为：
ssh-keygenALI_IP=&quot;8.136.13.58&quot;ssh-copy-id -i ~/.ssh/id_rsa.pub voidking@$&#123;ALI_IP&#125;cat .ssh/id_rsa | base64 | tr -d &#x27;\n&#x27;

3.4. CICD测试git add .git commit -m &quot;github action test&quot;git push

访问voidking/hexo-deploy Actions页面，即可看到最新的workflow。最开始难免出错，根据报错进行调整即可。
最终，实现了和travis ci上一样的功能，nice。
4. 踩坑记录4.1. env不能用在job:if当使用job:if时，如下定义：
env:  BUILD: &quot;false&quot;jobs:  build:    name: Build    if: $&#123;&#123; env.BUILD == &quot;true&quot; &#125;&#125;    runs-on: ubuntu-latest

报错： Unrecognized named-value: ‘env’. Located at position 1 within expression: env.BUILD == “true” .github/workflows/github-actions.yml
这是个github action的坑，参考文档：

Workflow level env does not work properly in all fields.
workflow level env. is unrecognised on job level’s ‘if’-expression when calling reusable workflow

4.2. workflow_dispatch无效当使用job:if时，如下定义：
on:  workflow_dispatch:    inputs:      build:        description: &quot;build project&quot;        required: true        default: true        type: booleanjobs:  build:    name: Build    if: $&#123;&#123; inputs.build &#125;&#125;    runs-on: ubuntu-latest

无法触发build job。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>troubleshooting</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>问题排查</tag>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
        <tag>travis-ci</tag>
        <tag>github-actions</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Ubuntu18配置静态IP</title>
    <url>/dev-ubuntu18-static-ip/</url>
    <content><![CDATA[1. 前言《好好学Linux：Linux系统配置静态IP》一文中，描述了 CentOS7 和 Ubuntu16.04 系统中配置静态IP的方法。但是，Ubuntu18之后的版本，网络配置方法发生了较大的变更，本文学习一下新的配置方法。


2. 单网卡静态IP配置方法1、编辑网卡配置文件
vim /etc/netplan/01-network-manager-all.yaml

如下修改：
# Let NetworkManager manage all devices on this systemnetwork:  version: 2  renderer: NetworkManager  ethernets:    eth0:      dhcp4: false                          # 禁止动态IP      addresses: [192.168.56.111/24]       # IP地址和掩码      gateway4: 192.168.56.1              # 网关      nameservers:        addresses: [180.76.76.76,114.114.114.114]   #DNS服务器

其中 dhcp4 参数的值可以改为 no ，和 false 等价。 
2、使配置生效
netplan apply

3、验证配置
ip addr show dev eth0

3. 多网卡静态IP配置方法3.1. 配置方法一：单文件配置编辑网卡配置文件
vim /etc/netplan/01-network-manager-all.yaml

如下修改：
# Let NetworkManager manage all devices on this systemnetwork:  version: 2  renderer: NetworkManager  ethernets:    eth0:      dhcp4: false                          # 禁止动态IP      addresses: [192.168.56.111/24]       # IP地址和掩码      gateway4: 192.168.56.1              # 网关      nameservers:        addresses: [180.76.76.76,114.114.114.114]   #DNS服务器    eth1:      dhcp4: false      addresses: [172.16.0.111/24]

3.2. 配置方法二：多文件配置拷贝网卡配置文件
cp /etc/netplan/01-network-manager-all.yaml /etc/netplan/02-network-manager-all.yamlvim /etc/netplan/02-network-manager-all.yaml

如下修改：
# Let NetworkManager manage all devices on this systemnetwork:  version: 2  renderer: NetworkManager  ethernets:    eth1:      dhcp4: false      addresses: [172.16.0.111/24]

4. 临时配置固定IP给 eth0 配置两个固定IP：
ip addr add 192.168.1.100/24 dev eth0ip addr add 192.168.1.101/24 dev eth0

5. 配置默认网关配置文件中，可以通过 gateway4 指定网关。
如果多个网卡有不同的网关，该怎样指定默认网关？答：在 netplan 配置文件中，我们可以针对不同的网卡配置不同的网关。但是，请不要这么做，因为一个系统只能有一个默认网关，否则会导致混乱出现网络问题。如果有多个网卡，每个网卡的网关不同，那么请选择出想要配置为默认网关的网卡，给该网卡配置上网关，其他的网卡不要配置网关。
6. 扩展：禁用和启用指定的网卡如果一台主机上有多个网卡，想要禁用和启用其中一张或多张网卡，可以使用下面的方法。
6.1. 临时禁用和启用ifconfig eth1 down # 禁用ifconfig eth1 up # 启用

6.2. 永久禁用和启用6.2.1. 基于netplan（推荐）永久禁用方法：1、修改netplay配置
vim /etc/netplan/01-network-manager-all.yaml

添加：
network:  version: 2  renderer: NetworkManager  ethernets:    eth1:      dhcp4: false      optional: true

2、应用netplan配置
netplan apply

3、重启网络服务
systemctl restart systemd-networkd

永久启用方法：修改netplay配置（删除其中的禁用配置），应用netplan配置，重启网络服务。
6.2.2. 基于interfaces使用 /etc/network/interfaces（如果我们的系统使用此旧方法配置网络），这种方法在新版的Ubuntu中并不常见，因为Netplan取代了/etc/network/interfaces文件，但如果我们的系统依然使用它，可以使用下面的方法。
永久禁用方法：1、修改interfaces配置
/etc/network/interfaces

将指定的网卡设置为不自动启动
auto eth0iface eth0 inet manual
2、重启网络服务
systemctl restart networking

永久启用方法：修改interfaces配置（删除其中的禁用配置），重启网络服务。
6.2.3. 基于initramfs永久禁用方法：1、配置 blacklist.conf 
vim /etc/modprobe.d/blacklist.conf 

在文件最后添加：
blacklist eth1

2、更新initramfs
update-initramfs -u
该命令用于更新 Linux 系统的初始化 RAM 磁盘文件系统（initramfs）。在系统引导过程中，initramfs 用于加载必要的驱动程序并初始化硬件，以便在挂载根文件系统之前完成这些工作。该命令生成一个新的 initramfs映像文件并更新符号链接到最新的映像文件。在更新内核或任何与内核相关的配置文件更改之后，通常会运行此命令，以确保系统可以正确引导。
3、重启主机进行验证（可选）
reboot

永久启用方法：blacklist.conf 删除禁用的配置，更新initramfs。
7. 扩展：Netplan 和 NetworkManager的关系在 Ubuntu 和一些其他基于 Debian 的 Linux 发行版中，Netplan 和 NetworkManager 有着紧密的合作关系。
Netplan 是一个配置网络的工具，它使用简单的 YAML 文件来描述网络接口的配置。Netplan 是 Ubuntu 17.10 以后的版本中引入的默认网络配置工具，用于统一和简化网络配置过程。Netplan 本身并不直接管理网络连接；它是一个抽象层，将配置转换成底层网络管理服务可以理解的格式。
Netplan 支持将配置转换给以下两种网络后端服务之一：

systemd-networkd: 主要用于服务器和无界面系统，是一个系统服务，用于配置网络接口。
NetworkManager: 适用于桌面和服务器环境，特别是需要频繁连接和断开网络的场景。NetworkManager 提供了丰富的命令行界面 (CLI) 和图形用户界面 (GUI)，便于用户进行网络配置。

这些后端是 Netplan 生成的配置的 “消费者”，负责实际的网络接口管理工作。
NetworkManager 是一个动态网络控制和配置系统，可以响应网络环境的变化并进行相应的调整。NetworkManager 旨在简化和自动化网络配置和连接管理，尤其是在 WLAN（无线局域网）和移动宽带连接方面。
它们的关系在使用上很简单：
1、系统管理员或用户编写一个 YAML 格式的 Netplan 配置文件，定义了网络接口的期望配置。2、运行 netplan apply 命令时，Netplan 读取 YAML 文件，然后根据当前系统所使用的网络后端（systemd-networkd 或 NetworkManager），生成相应的配置。

如果是 NetworkManager，Netplan 将生成 NetworkManager 可以理解的配置，并将这些配置置于 /etc/NetworkManager/system-connections/目录下。
如果是 systemd-networkd，Netplan 将生成适当的单位文件放在 /etc/systemd/network/ 目录下。3、NetworkManager 或 systemd-networkd 接管，应用这些配置，并管理网络连接。

由此，Netplan 作为一个中间层，可以无缝地与 NetworkManager（或 systemd-networkd）配合工作，让用户能够通过统一的接口来处理网络配置，而无需直接与底层网络管理工具交互。这降低了配置的复杂性，特别是在有多种网络接口和连接类型需要管理的环境中。
8. 设置静态IP无效问题8.1. 问题描述/etc/netplan/01-network-manager-all.yaml 配置为：
network:  version: 2  renderer: NetworkManager  ethernets:    ens5f0:      dhcp4: false      addresses: [172.22.3.211/24]      gateway4: 172.22.3.1    ens5f2:      dhcp4: false      optional: true
上面的配置中给ens5f0网卡配置了一个静态IP 172.22.3.211 ，但是实际上该网卡获取到的IP为 172.22.3.56
8.2. 问题排查解决1、重新执行 netplan apply，无效。2、重新启动NetworkManager，systemctl restart NetworkManager，无效。3、重启主机，无效。4、查看查看网络接口状态和配置信息 nmcli，发现了异常：
ens5f0：已连接 到 配置 1        &quot;Intel I350&quot;        ethernet (igb), B4:96:91:12:9E:26, 硬件, mtu 1500        ip4 默认        inet4 172.22.3.56/24        route4 0.0.0.0/0        route4 172.22.3.0/24        inet6 fe80::a0e0:cc7b:xxx:xxx/64        route6 fe80::/64ens5f2：已连接 到 netplan-ens5f2        &quot;Intel I350&quot;        ethernet (igb), B4:96:91:12:9E:28, 硬件, mtu 1500        inet4 169.254.136.104/16        inet4 169.254.249.45/16        route4 224.0.0.0/4        route4 169.254.0.0/16        route4 169.254.0.0/16        inet6 fe80::b696:91ff:xxx:xxx/64        route6 fe80::/64

ens5f0 正常应该连接到 netplan-ens5f0，但是上面显示连接到配置1，这明显是有问题的。
5、检查网络配置方式在 Linux 系统中，网络配置可能通过多种不同的方式进行管理：

命令行工具：如 nmcli（NetworkManager 的命令行界面）和 netplan。
配置文件：如 /etc/network/interfaces（传统 Debian 配置），/etc/netplan/*.yaml 文件（Ubuntu 的 netplan 工具），或者 /etc/NetworkManager/system-connections/ 目录下的文件（由 NetworkManager 管理）。
图形用户界面：如 GNOME 的网络设置或其他桌面环境提供的网络管理器GUI。

在检查 /etc/NetworkManager/system-connections/ 目录下的文件时，找到了配置1。由此证明是NetworkManager和netplan协调工作时出现了问题。
# cat /etc/NetworkManager/system-connections/&#x27;配置 1.nmconnection&#x27;[connection]id=配置 1uuid=657c4d21-ebc8-48a9-9ccf-41a234060f68type=ethernetpermissions=timestamp=1691305627[ethernet]mac-address-blacklist=[ipv4]dns-search=method=auto[ipv6]addr-gen-mode=stable-privacydns-search=method=auto[proxy]

解决办法：删除配置1，重新生成NetworkManager配置。
mv /etc/NetworkManager/system-connections/&#x27;配置 1.nmconnection&#x27; ~netplan apply

执行上面的命令之后，IP成功设置为 172.22.3.211 ，但是 /etc/NetworkManager/system-connections/ 目录中并没有生成新的配置，且不管它。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>ubuntu</tag>
        <tag>libvirt</tag>
        <tag>virsh</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker安装配置DolphinScheduler</title>
    <url>/dev-docker-dolphinscheduler/</url>
    <content><![CDATA[1. DolphinScheduler简介
Apache DolphinScheduler provides a distributed and easy to expand visual workflow task scheduling open-source platform. It is suitable for enterprise-level scenarios. It provides a solution to visualize operation tasks, workflows, and the entire data processing procedures.

参考文档：

About DolphinScheduler



2. 安装Docker参考文档《Docker入门篇》
3. 安装DS参考文档：DolphinScheduler 3.1.2 - Docker Quick Start
mkdir -p /data/dscd /data/dswget https://dlcdn.apache.org/dolphinscheduler/3.1.2/apache-dolphinscheduler-3.1.2-src.tar.gztar -xzvf apache-dolphinscheduler-3.1.2-src.tar.gzcp -r apache-dolphinscheduler-3.1.2-src/deploy/docker .cd dockerdocker-compose --profile schema up -ddocker-compose --profile all up -ddocker-compose --profile all ps

暂时不要安装3.1.3，web ui会出现白屏。
4. 访问DShttp://192.168.56.101:12345/dolphinscheduler/ui用户名：admin密码：dolphinscheduler123
5. 使用DS参考文档DolphinScheduler 3.1.2 - 快速上手
6. 添加MySQL数据源参考文档：数据源配置
6.1. 添加jar驱动添加MySQL数据源之前，需要先配置好MySQL驱动，否则会报错：Faild to load driver class.com.class.cj.jdbc.Driver in either of HikariConfig class loader or Thread context classloader
1、下载mysql-connector-java jar包
mkdir -p /data/ds/libscd /data/ds/libswget https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.16/mysql-connector-java-8.0.16.jar

2、修改docker-compose.yml配置，添加jar包挂载
volumes:- /data/ds/libs/mysql-connector-java-8.0.16.jar:/opt/dolphinscheduler/libs/mysql-connector-java-8.0.16.jar
api-server、alert-server、master-server、worker-server四个容器都需要挂载。
3、重建DS
docker-compose --profile all downdocker-compose --profile schema downdocker-compose --profile schema up -d#如果报错Error response from daemon: network xxx not found，那就改成下面的命令：#docker-compose --profile schema up -d --force-recreatedocker-compose --profile all up -ddocker-compose --profile all ps

重建后，之前的数据还是存在的，因为我们没有删除volume数据。
6.2. 添加MySQL数据源http://192.168.56.101:12345/dolphinscheduler/ui/datasource
页面操作，添加MySQL数据源即可。
7. 添加Spark数据源7.1. 启动Spark thriftserver启动Spark thriftserver
cd /usr/local/spark/spark-3.3.1-bin-hadoop3-scala2.13/sbin/start-thriftserver.sh --helpsbin/start-thriftserver.sh 

7.2. 添加Spark数据源页面操作，添加Spark数据源。用户名可以随便填（建议填入spark），密码为空；数据库名填入default。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
        <category>bigdata</category>
        <category>spark</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>问题排查</tag>
        <tag>大数据</tag>
        <tag>docker-compose</tag>
        <tag>dolphinscheduler</tag>
        <tag>spark</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos Hub私有容器镜像仓库</title>
    <url>/dev-sealos-hub/</url>
    <content><![CDATA[1. sealos hub简介在使用sealos安装k8s时，会自动安装一个docker registry，它就是sealos hub，用于存储容器镜像。sealos hub在集群中的主机名和端口为sealos.hub:5000，默认用户名admin，密码passw0rd。
参考文档：

labring/sealos
Sealos - 集群生命周期管理
Sealos 命令说明



2. sealos hub部署方式sealos hub会部署到宿主机上，我们可以通过systemctl对它进行管理。
查看registry状态：
systemctl status registry

配置文件路径：/etc/registry/registry_config.yml密码路径：/etc/registry/registry_htpasswd
3. sealos hub原理3.1. 存储容器镜像sealos build 的过程中，如何知道软件中有哪些容器镜像？因为我们需要把这些镜像缓存下来，不管是扫描用户的 yaml 文件还是用 helm template 之后扫描都是不完美的。首先不能确定用户的编排方式是什么，其次有些软件不把镜像地址写在编排文件中，而是通过自己的程序去拉起，无法保证 build 成功运行就一定没问题。
对于这个问题，sealos的处理方法比较原始，需要用户自己找到需要的容器镜像列表，写入到 images/shim/images-list.txt 文件中。
sealos build 打包集群镜像完成后，容器镜像就存储到了集群镜像中，在使用 sealos run 安装应用时，容器镜像就会被加载到本地存储 sealos hub仓库中。
参考文档：

揭秘sealer背后实现整个集群一键交付的奥秘 | 龙蜥技术
Sealos Run 的工作原理
《sealos制作MySQL集群镜像》
《sealos制作 kube-prometheus-stack 集群镜像》

3.2. 下载容器镜像容器镜像被打包到了集群镜像里，sealos run时容器镜像被存储到了私有仓库sealos hub中。此时，编排文件中写的容器镜像地址 和 私有仓库容器镜像地址 是不一致的，怎么保证用户从私有仓库中下载容器镜像？特别是怎么保证用户 alwayPull 的时候还是能够在私有仓库中下载到镜像？
对于这个问题，有两种解决方案：第一种是 hack docker，让docker具备只要私有镜像仓库中有就直接从私有镜像中拉取，没有才去公网拉取镜像的能力。第二种方案是无侵入 docker 的 proxy，把 docker 请求全部打给代理，让代理去决定如果私有仓库有就从私有仓库拉取。同时增强了 registry 的能力让 registry 可以 cache 多个远程仓库的能力。
sealos采用的是第二种方案：docker 的 proxy，image-cri-shim。

image-cri-shim 是一个基于 CRI (Container Runtime Interface) 和 kubelet 的 gRPC (Google Remote Procedure Call) shim。CRI 是 Kubernetes 中用于与容器运行时进行交互的接口，而 kubelet 是负责维护容器运行状态和节点级别的资源管理的 Kubernetes 组件。
image-cri-shim 的主要功能是自动识别镜像名称，让用户在使用 Kubernetes 部署容器时无需手动指定镜像名称。这样可以降低用户的操作难度，提高部署容器的便利性。
在实际使用中，image-cri-shim 可以作为一个中间件，接收来自 kubelet 的请求，然后将请求转发给容器运行时。通过自动识别镜像名称，image-cri-shim 可以简化容器镜像的部署流程，减轻用户的操作负担。
/usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --container-runtime=remote --container-runtime-endpoint=/var/run/cri-dockerd.sock --container-runtime=remote --pod-infra-container-image=sealos.hub:5000/pause:3.2 --runtime-request-timeout=15m --container-runtime-endpoint=unix:///var/run/cri-dockerd.sock --image-service-endpoint=unix:///var/run/image-cri-shim.sock

上面的kubelet启动命令中，通过指定 --image-service-endpoint=unix:///var/run/image-cri-shim.sock ，实现了 image-cri-shim 中间件的配置，很巧妙。
image-cri-shim 示例配置：
shim: /var/run/image-cri-shim.sockcri: /run/containerd/containerd.sockaddress: http://sealos.hub:5000force: truedebug: truetimeout: 15mauth: admin:passw0rdregistries:- address: http://172.18.1.38:5000  auth: admin:passw0rd

配置说明：

shim: 指定 image-cri-shim 的 UNIX 套接字文件路径。这个路径用于与 kubelet 之间的通信。
cri: 指定容器运行时（如 containerd）的 UNIX 套接字文件路径。image-cri-shim 会使用这个路径与容器运行时进行通信。
address: 定义镜像仓库的地址。在本例中，镜像仓库地址为 http://sealos.hub:5000。
force: 设置为 true 时，image-cri-shim 会在强制启动shim,无需等待cri启动后启动。
debug: 设置为 true 时，启用调试模式，输出更多的日志信息。
timeout: 定义镜像操作的超时时间。在本例中，超时时间为 15 分钟（15m）。
auth: 定义用于访问镜像仓库的身份验证凭据。在本例中，用户名为 admin，密码为 passw0rd。

此外，配置文件还包含了一个 registries 列表，用于定义其他镜像仓库及其身份验证凭据，以便 sealos hub 从其他镜像仓库拉取镜像。在这个例子中，只有一个其他仓库：

address: 该仓库的地址为 http://172.18.1.38:5000。
auth: 用于访问该仓库的身份验证凭据。在本例中，用户名为 admin，密码为 passw0rd。 这个配置文件为 image-cri-shim 提供了所需的信息，以便正确地与 kubelet 和容器运行时（如 containerd）进行通信，以及访问和管理镜像仓库。

image-cri-shim 通常作为一个系统服务运行。
systemctl start image-cri-shimsystemctl stop image-cri-shimsystemctl restart image-cri-shimsystemctl status image-cri-shim

参考文档：

揭秘sealer背后实现整个集群一键交付的奥秘 | 龙蜥技术
image-cri-shim 使用指南
github - labring/image-cri-shim
如何使用 sealos作为私有容器仓库

4. sealos hub弱密码问题4.1. 问题描述sealos hub的默认admin密码为passw0rd，是一个弱密码，可能会被漏洞扫描工具扫描出来，因此最好改成强密码。
4.2. 解决办法（错误）修改 /etc/registry/registry_htpasswd 密码本文件中的admin用户的密码。
htpasswd -Bb /etc/registry/registry_htpasswd admin xxxsystemctl restart registry

修改密码后，每个主机都使用新密码重新登录一下registry。
docker logout sealos.hub:5000docker login sealos.hub:5000

PS：htpasswd的安装使用方法，参考文档《shell命令之htpasswd》
4.3. 新的问题使用 htpasswd 修改了 /etc/registry/registry_htpasswd 中的密码，docker login也可以使用新密码登录了，但是查看registry日志，很多请求还是使用旧密码，而且主机重启后很多原本正常running的pod无法拉取镜像了。看来我们上面修改sealos hub密码的方法并不正确。
4.4. 解决办法（正确）sealos群里咨询大佬们，获悉可以使用最新版本的sealos来修改registry密码。参考文档：

Sealos：sealos registry passwd 命令详解与使用指南
refactor(main): replace registry password config generator

wget https://github.com/labring/sealos/releases/download/v4.2.3/sealos_4.2.3_linux_amd64.tar.gztar -xzvf sealos_4.2.3_linux_amd64.tar.gz sealos./sealos registry passwd

此外，修改完registry密码后，还需要修改 ~/.sealos/default/Clusterfile 中的registry密码配置，以便在新增节点时使用新密码。
status:  mounts:  - env:      registryDomain: sealos.hub      registryPassword: new_password

上面的配置，在实测时并没有生效，不知道应该修改哪里的配置，留个坑。新增节点时，最终采用的方案是先改回原密码passw0rd，新增节点后密码再改回。
对于4.2.0之前的版本，如果想要手动修改密码的话，所有机器的 registry.yml 还有 image-shim 以及 containerd 的配置应该都需要改，比较麻烦。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>sealos</tag>
        <tag>htpasswd</tag>
        <tag>好好学Sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos Cloud</title>
    <url>/dev-sealos-cloud/</url>
    <content><![CDATA[1. Sealos Cloud简介《sealos入门篇》一文中我们提到：sealos最初的目标是把kubernetes安装做进一条命令，而如今，sealos 是以 kubernetes 为内核的云操作系统发行版。
而 Sealos Cloud ，是环界云运行和维护的 sealos 集群，用来提供对外的公有云服务。 用户可以直接使用 Sealos Cloud ，用户也可以在自己的私有化环境中运行 sealos，即可拥有与 Sealos Cloud 完全一样的能力。
Sealos Cloud 是一款 ALL IN 云原生的公有云服务，提供我们在公有云常见的服务能力如 AWS 的数据库服务，函数计算服务，对象存储服务等。 与 AWS 这些公有云最大的不同点是技术架构完全采用云原生（狭义上的云原生，围绕 kubernetes 的技术生态所构建）架构实现。
参考文档：

labring/sealos
Sealos Cloud 介绍



2. 注册Sealos Cloud访问sealos cloud，注册一个账号。
3. 使用Sealos Cloud就像使用本地操作系统一样，通过浏览器使用sealos cloud系统即可。当然，使用起来感觉没有本地流畅，这是正常的，毕竟要通过网络访问。等到5G更加普及，sealos cloud肯定更好用。
4. 使用sealos registry4.1. sealos registry作用sealos registry的域名为 hub.sealos.cn，使用它可以作为docker hub的替代品，能够加快上传下载速度。
4.2. 创建组织1、登录sealos cloud
2、打开Terminal应用
3、准备组织定义文件 organization.yaml
apiVersion: imagehub.sealos.io/v1kind: Organizationmetadata:  name: voidkingspec:  name: voidking # same as metadata.name  creator: ns-arr7bbkd # find at sealos cloud page  manager: [ ns-arr7bbkd ] # you can add other user as manager

4、创建组织
kubectl apply -f organization.yaml

4.3. 获取config.yaml1、登录sealos cloud
2、点击头像
3、获取config.yaml点击头像下方的kubeconfig右侧加号，下载kubeconfig.yaml，重命名为config.yaml
4.4. 上传镜像sealos login -k config.yml hub.sealos.cnsealos tag your-image-id hub.sealos.cn/your-organization-name/your-image-name:tagsealos push hub.sealos.cn/your-organization-name/your-image-name:tag

注意：无论config.yml是否正确，sealos login都会提示success。因此，只有sealos push成功才能证明config.yaml配置正确。
4.5. 查看镜像访问sealos registry，查看所有镜像，包括我们刚刚上传的镜像。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>ipvs</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
        <tag>sealer</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Sealos：Sealos入门篇</title>
    <url>/dev-sealos-start/</url>
    <content><![CDATA[1. sealos简介2018年8月，方海涛大佬提交了sealos的一个commit，最初的目标是把kubernetes安装做进一条命令。如今，sealos 是以 kubernetes 为内核的云操作系统发行版。单机操作系统如同 linux 发行版本可以在上面安装和使用各种单机应用，如 PPT，Word，Excel 等。 云操作系统只需要把这些单机应用替换成各种云应用，如数据库，对象存储，消息队列等，就很容易理解了，这些应用都是分布式高可用的。 sealos 就是能支撑运行各种分布式应用的云操作系统。有了 sealos 就拥有了一朵云。
参考文档：

labring/sealos
Sealos - 集群生命周期管理
Sealos 命令说明
kubernetes 高可用部署工具：sealos
使用Sealos一键安装K8S
使用 sealos 一键开启 Kubernetes 应用的可观测性
sealos 作者创业心路历程
速读开源项目 Sealos 的源码



2. sealos原理简介2.1. 高可用通过内核 ipvs 对 apiserver 进行负载均衡，并且带 apiserver 健康检测。
2.2. 定制kubeadm首先是由于 kubeadm 把证书过期时间写死了，所以需要定制把它改成 99 年，虽然大部分人可以自己去签个新证书，但是我们还是不想再依赖个别的工具，就直接改源码了。
其次就是做本地负载时修改 kubeadm 代码是最方便的，因为在 join 时我们需要做两个事，第一是 join 之前先创建好 ipvs 规则，第二是创建 static pod。如果这块不去定制 kubeadm 就把报静态 pod 目录已存在的错误，忽略这个错误很不优雅。 而且 kubeadm 中已经提供了一些很好用的 sdk 供我们去实现这个功能。
且这样做之后最核心的功能都集成到 kubeadm 中了，sealos 就单单变成分发和执行上层命令的轻量级工具了，增加节点时我们也就可以直接用 kubeadm 了。
2.3. k8s安装流程1、通过 sftp或者 wget 把离线安装包拷贝到目标机器上（masters 和 nodes）。2、在 master0 上执行 kubeadm init。3、在其它 master 上执行 kubeadm join 并设置控制面，这个过程会在其它 master 上起动 etcd 并与 master0 的 etcd 组成集群，并启动控制平面的组件（apiserver、controller 等）。4、join node 节点，会在 node 上配置 ipvs 规则，配置 /etc/hosts 等。
3. 安装使用sealos3.1. 安装sealoswget https://github.com/labring/sealos/releases/download/v4.2.3/sealos_4.2.3_linux_amd64.tar.gztar -zxvf sealos_4.2.3_linux_amd64.tar.gz sealos &amp;&amp;  chmod +x sealos &amp;&amp; mv sealos /usr/bin 

3.2. 安装k8s集群参考文档：Run: Execute Cluster Images
1、安装k8s集群
mkdir -p /data/k8s/dockersealos run labring/kubernetes-docker:v1.24.8 labring/helm:v3.8.2 labring/calico:v3.24.1 \    --env criData=/data/k8s/docker \    --masters 192.168.56.100 \    --nodes 192.168.56.101 -p voidking

注意：（1）不要安装labring/kubernetes:v1.24.8，这种版本默认使用的CRI是containerd，而不是docker。否则，安装软件时pod调度完成后可能会报错：Error: failed to create containerd container: error unpacking image: failed to extract layer sha256:xxx: failed to get reader from content store: content digest sha256:xxx: not found
（2）calico和kubernetes版本要对应，否则calico可能无法正常启动。对应关系参考文档Kubernetes requirements和Calico3.23 - Kubernetes requirements等。
更多k8s版本，请访问hub.sealos.cn获取。
2、查看安装
docker pskubectl get nodeskubectl get all -n kube-systemkubectl get all -n calico-system

安装速度太快了，真的是一键安装！！！不用做各种初始化，这一点吊打kubeadm（sealos底层也是使用的kubeadm）。另外，sealos安装的k8s集群，证书是100年，不用考虑证书续期问题，也是省了很多事情。
3、去掉master节点污点（可选）
kubectl taint nodes 192.168.56.100 node-role.kubernetes.io/master:NoSchedule-kubectl taint nodes 192.168.56.100 node-role.kubernetes.io/control-plane:NoSchedule-

3.3. 安装失败处理办法如果安装失败，再次安装时可能会报错：
error Applied to cluster error: failed to init runtime, cluster image kubernetes version cannot be empty

这时需要先重置一下sealos，然后重新安装。
sealos resetrm /root/.sealos -rf

3.4. 添加节点sealos add --nodes 192.168.56.200sealos add --masters 192.168.56.101 --nodes 192.168.56.200sealos add --masters 192.168.56.101-192.168.56.102 --nodes 192.168.56.200-192.168.56.203

3.5. 删除节点sealos delete --nodes 192.168.56.202,192.168.56.203 sealos delete --masters 192.168.56.101,192.168.56.102

3.6. 自定义安装k8s集群参考文档：

Sealos - 自定义配置安装
kubeadm config

1、生成集群配置
mkdir -p /data/k8s/dockersealos gen labring/kubernetes-docker:v1.24.8 labring/helm:v3.8.2 labring/calico:v3.24.1 \    --env criData=/data/k8s/docker \    --masters 192.168.56.100 \    --nodes 192.168.56.101 -p voidking &gt; Clusterfile

2、修改POD和SVC的CIDR搜索kubeadm.k8s.io，Networking部分修改为：
---apiVersion: kubeadm.k8s.io/v1beta3kind: ClusterConfigurationNetworking:  PodSubnet: 172.16.0.0/16  ServiceSubnet: 172.17.0.0/16

3、安装k8s集群
sealos apply -f Clusterfile

集群运行成功后会把 Clusterfile 保存到 .sealos/default/Clusterfile 文件中，可以修改其中字段来重新 apply 对集群进行变更。
4. sealos问题记录4.1. docker版本coredns启动失败系统环境：sealos4.1.3，系统CentOS7，内核Linux 3.10.0-957.el7.x86_64
labring/kubernetes:v1.22.3版本，可以正常安装运行。
labring/kubernetes-docker:v1.22.3版本，coredns pod无法启动，一直处于Blocked状态。切换ubuntu18系统，也存在同样的问题。
Status:         PendingReason:         NoNewPrivsMessage:        Cannot enforce NoNewPrivs: illegal version string &quot;v1alpha2&quot;Containers:  coredns:    Container ID:    Image:         k8s.gcr.io/coredns:1.7.0    Image ID:    Ports:         53/UDP, 53/TCP, 9153/TCP    Host Ports:    0/UDP, 0/TCP, 0/TCP    Args:      -conf      /etc/coredns/Corefile    State:          Waiting      Reason:       Blocked    Ready:          False

no_new_privs相关概念参考文档Linux Capabilities 入门教程：基础实战篇
经过多次尝试，发现labring/kubernetes-docker:v1.24.8版本可以安装成功，再低的版本都会有这个问题。
请教方海涛大佬得知，sealos4.1.4 之后修复了这个问题，实际验证确实可以。
一个验证可行的K8S和Calico匹配版本为：labring/kubernetes-docker:v1.20.14 和 labring/calico:v3.22.1
4.2. 一键安装软件参考文档sealos - Kubernetes 生命周期管理
一键安装软件：
sealos run labring/minio-operator:v4.4.16 labring/ingress-nginx:4.1.0 \    labring/mysql-operator:8.0.23-14.1 labring/redis-operator:3.1.4

安装时卡住了，报错：Error: INSTALLATION FAILED: failed pre-install: timed out waiting for the condition2023-01-06T16:22:50 error Applied to cluster error: exit status 1
再次安装，没有报错，但是软件并没有安装成功。
kubectl describe  pod/ingress-nginx-admission-create-xsnmg -n ingress-nginx
Pod处于Blocked状态。这个问题，和上面的coredns问题本质是相同的问题，只不过表现形式不同。
4.3. 自定义软件配置？一键安装软件很方便，但是，怎样对这些软件进行自定义配置？答：逐个软件安装，使用--cmd自定义安装命令和配置。
4.4. 卸载软件？怎样卸载软件？答：sealos不考虑软件卸载，如果有需要卸载，直接执行helm uninstall即可。
4.5. 怎样指定Calico使用的网卡？在使用Calico作为Kubernetes集群的CNI（容器网络接口）时，节点上可能具有多个网络接口和IP地址。Calico需要为每个节点选择一个IP地址以便于在集群内的节点之间建立隧道（如使用IPIP或VXLAN隧道技术）以及通信。如果没有做任何配置，Calico将自动选择节点的默认路由出口的IP地址。这是最常用的网口IP地址。但是，这可能会导致calico建立tun时使用的是不同的网段，最终导致节点间通信产生问题。
因此，为保证稳定性，最好是能够指定Calico使用的网卡。在使用sealos已经安装好的K8s集群中，Calico本质上是通过helm的方式安装的，chart路径为：/var/lib/sealos/data/default/rootfs/charts/calico指定Calico使用的网卡，方法为修改Calico配置，然后更新Calico：1、修改Calico配置
cd /var/lib/sealos/data/default/rootfs/charts/calicovim values.yaml

修改 installation.calicoNetwork 部分的配置：
installation:  enabled: true  kubernetesProvider: &quot;&quot;  calicoNetwork:    nodeAddressAutodetectionV4:      #interface: eth.*|en.*      interface: ens5f2|ens9f2

2、更新Calico
helm upgrade calico . -n tigera-operator

3、确认效果
route -n
检查 tunl0 的网关IP是否符合预期。
5. sealos和sealer是什么关系？参考文档：《sealer入门篇》
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>ipvs</tag>
        <tag>sealos</tag>
        <tag>好好学Sealos</tag>
        <tag>sealer</tag>
      </tags>
  </entry>
  <entry>
    <title>shell命令之htpasswd</title>
    <url>/dev-shell-htpasswd/</url>
    <content><![CDATA[1. htpasswd简介htpasswd命令是Apache的Web服务器内置工具，用于创建和更新储存用户名、域和用户基本认证的密码文件。
参考文档：htpasswd命令


2. htpasswd语法说明语法格式：
htpasswd [参数] [用户和密码]

常用参数：

-c  创建一个加密文件
-n  不更新加密文件，只将加密后的用户名密码显示在屏幕上
-m  默认采用MD5算法对密码进行加密
-d  采用CRYPT算法对密码进行加密
-p  不对密码进行进行加密，即明文密码
-s  采用SHA算法对密码进行加密
-B  强制对密码进行bcrypt加密（非常安全）
-b  在命令行中一并输入用户名和密码而不是根据提示输入密码
-D  删除指定的用户

3. 安装htpasswd# centosyum install httpd-tools # ubuntuapt install apache2-utils

4. htpasswd常用命令4.1. 生成用户密码对使用 md5 算法生成 .passwd 密码文件，同时添加一对用户和密码
htpasswd -bc .passwd voidking 123456

在上面生成的密码文件中，再添加一对用户和密码
htpasswd -b .passwd voidking2 123456

使用 bcrypt 算法生成 .passwd 密码文件，同时添加一对用户和密码
htpasswd -B -bc .passwd voidking 123456

4.2. 删除用户密码对删除用户名和密码
htpasswd -D .passwd voidking2

4.3. 修改密码htpasswd -b .passwd voidking 654321

5. salt 说明多次执行 htpasswd 命令，输入相同的用户名和密码，生成的加密后的密码是不同的。
这是因为htpasswd和许多密码哈希函数在工作时都会使用一项称为 salt 的技术。一个 salt 是一个随机生成的字符串，在哈希过程中和密码组合在一起，然后一起被哈希。结果，即使密码是相同的，但是由于 salt 是随机的，所以最终生成的哈希值会不同。
这个 salt 值通常会被嵌入到生成的哈希值中，这样在进行密码验证时，可以使用同样的 salt 来进行哈希操作，然后比较结果。因此，即使每次执行htpasswd生成的哈希值看起来不同（因为 salt 不同），但是所有这些哈希值实际上还是可以正确地验证同一个密码。
使用 salt 可以提高密码的安全性，使得它更难以被破解，因为每次哈希的结果都不同，这使得对哈希值的彩虹表攻击变得无效。
salt 值不仅嵌入到生成的哈希值中，而且按照特定的格式进行了组织。因此，当我们有一个哈希值（包括 salt 值）和一个要验证的原始密码时，我们可以从哈希值中提取 salt ，然后将其用于哈希原始密码，产生一个新的哈希值，然后和原哈希进行比较。
不同的哈希函数可能会有不同的格式来存储 salt 和哈希值，但是基本都是将 salt 值和哈希值组合在一起，用特殊的字符进行分隔。
例如，bcrypt使用的哈希格式看起来大致上如下：
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
其中，$2a$表示这是一个bcrypt哈希，10$表示使用的成本参数，N9qo8uLOickgx2ZMRZoMye是 salt ，而 IjZAgcfl7p92ldGxad68LJZdL17lhWy 是实际的哈希值。
当验证密码时，我们可以从哈希字符串中取出 salt 值和哈希值，再使用同样的哈希函数和相同的 salt 来哈希要验证的密码，然后将结果与原哈希值进行比较。如果二者匹配，那么密码就是有效的。
6. python实现htpasswd在不方便安装 htpasswd 的环境中，如果有 python 环境，那么可以基于python实现和 htpasswd 相同的功能。
6.1. md5算法import getpassfrom passlib.apache import HtpasswdFile# 输入用户名username = input(&quot;Enter username: &quot;)# 输入密码password = getpass.getpass(&quot;Enter password: &quot;)# 新建一个htpasswd文件ht = HtpasswdFile(&quot;.passwd&quot;, new=True)# 将用户名和相应的哈希密码添加到文件中ht.set_password(username, password)# 保存文件ht.save()

6.2. bcrypt 算法import getpassimport bcryptfrom passlib.apache import HtpasswdFile# 输入用户名username = input(&quot;Enter username: &quot;)# 输入密码password = getpass.getpass(&quot;Enter password: &quot;)# 用bcrypt加密密码hashed_password = bcrypt.hashpw(password.encode(&quot;utf-8&quot;), bcrypt.gensalt())# 将hashed password转化为htpasswd能接受的格式hashed_password_htpasswd = hashed_password.decode().replace(&quot;$2b$&quot;, &quot;$2y$&quot;)# 新建一个htpasswd文件ht = HtpasswdFile(&quot;.passwd&quot;, new=True)# 将用户名和相应的hashed密码添加到文件中ht.set_password(username, hashed_password)# 保存文件ht.save()


]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>shell</tag>
        <tag>加密</tag>
        <tag>htpasswd</tag>
      </tags>
  </entry>
  <entry>
    <title>阿里云ACK使用GPU问题</title>
    <url>/dev-ack-gpu-problem/</url>
    <content><![CDATA[1. 阿里云托管版ACK限制问题1.1. 问题描述想要使用腾讯云GPUManager调度ACK集群中的GPU资源，但是安装过程需要修改kube-scheduler，该怎么处理？
1.2. 问题解答阿里云托管Pro版ACK，因为无法对kube-scheduler配置进行修改，所以无法安装腾讯云的GPUManager，只能安装阿里云的GPUShare。想要完全自主控制K8S集群，最好还是购买专有版ACK。


2. 阿里云ACK GPU节点notready问题2.1. 问题描述阿里云GPU机器，安装nvidia-docker后，测试跑GPU容器正常。但是，机器加入到阿里云托管Pro版ACK，安装了ack-ai-installer后，发现GPU节点处于notready状态，偶尔变成ready状态，也会很快再次变成notready。
2.2. 解决方案修改/etc/docker/daemon.json，去掉&quot;default-runtime&quot;: &quot;nvidia&quot;后，GPU节点变成ready状态。
3. 阿里云ACK GPU调度失败问题3.1. 问题描述解决完阿里云ACK GPU节点notready问题后，开始尝试调度GPU。GPU调度时会报错Insufficient aliyun.com/gpu-mem，而且使用kubectl inspect cgpu也找不到GPU资源。查看kube-system中的gpushare-device-plugin，都没有 running，应该就是这里的问题，设备插件没有正常运行。gpushare-device-plugin报错：init nvml failed,reason: could not load NVML library
尝试在GPU节点运行GPU容器：
docker run --rm --gpus all nvidia/cuda:11.6.2-base-ubuntu20.04 nvidia-smi
启动卡死，一直没有反应，而且系统重出现大量的/usr/bin/nvidia-container-toolkit prestart进程。
kill掉docker run，重启docker后，/usr/bin/nvidia-container-toolkit prestart进程消失。
多次尝试运行GPU容器时，极少情况下会出现报错：starting container process caused: exec: “nvidia-smi”: executable file not found in $PATH: unknown.
3.2. 报错排查kubelet、docker日志中缺少有用的报错信息。
重启kubelet、docker、containerd，无效。
降级docker版本到19.03.5，无效。
重新安装nvidia-docker2和nvidia-container-runtime，无效。
修改/etc/docker/daemon.json，加回&quot;default-runtime&quot;: &quot;nvidia&quot;，无效。而且修改后启动普通容器也会卡住，看起来大概率是runtime的问题。
最终找阿里云售后帮助解决，给出的结论是：我们手动安装的nvidia-docker，和ACK自动安装的nvidia-container-runtime冲突了，需要重新添加节点，添加时选择自动添加节点，节点的系统会被重装。
参考文档：

《K8S节点异常问题排查》
《Docker使用GPU》
《K8S使用GPU》
“exec: &quot;nvidia-smi&quot;: executable file not found in $PATH”.
WARNING: The NVIDIA Driver was not detected. GPU functionality will not be available

4. 阿里云ACK调度GPU参考文档
AliyunContainerService/gpushare-scheduler-extender
ACK调度GPU使用的节点标签说明

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>gpu</tag>
        <tag>ack</tag>
        <tag>问题排查</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker问题记录</title>
    <url>/dev-docker-problem/</url>
    <content><![CDATA[1. 前言本文记录使用Docker过程中遇到的问题和解决办法。


2. docker启动失败2.1. 问题描述docker启动卡住，查看日志
systemctl status docker -ljournalctl -ru docker
报错：
Error (Unable to complete atomic operation, key modified) deleting object [endpoint 622bf1a499580702606742e5f5554ac99e7c0d61abcd5d9063881fc2da33d16f afdce62ce70de2cbe5a971b05521280940947e4968c163e48c3e5252919a4fae], retrying....

2.2. 解决办法ps -ef | grep dockerkill -9 xxxsystemctl stop containerdsystemctl start docker

3. OCI runtime create failed3.1. 问题描述原本可以正常启动nvidia runtime的容器，使用相同的启动命令，突然有一天开始报错：
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: Running hook #1:: error running hook: exit status 1, stdout: , stderr: nvidia-container-cli: initialization error: driver error: failed to process request: unknown.docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: Running hook #1:: error running hook: exit status 1, stdout: , stderr: Auto-detected mode as &#x27;legacy&#x27;nvidia-container-cli: initialization error: load library failed: /usr/bin/../lib64/libm.so.6: symbol __strtof128_nan, version glibc_private not defined in file libc.so.6 with link time reference: unknown.

3.2. 解决办法尝试了重启docker、重启主机、重装nvidia-container-tookit、重装nvidia驱动，全部无效。
最终解决办法：检查libm.so.6的软链指向，修改指向到另外的libm。
cd /usr/bin/../lib64/ln -snf libm-2.17.so  libm.so.6

4. error pulling image configuration4.1. 问题描述docker pull ubuntu 报错：
Using default tag: latestlatest: Pulling from library/ubuntu6b851dcae6ca: Pulling fs layererror pulling image configuration: Get https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/99/99284ca6cea039c7784d1414608c6e846dd56830c2a13e1341be681c3ffcc8ac/data?verify=1687183284-D5%2FDxEvl7V%2BLtRcgWmwP0MDFiTo%3D: dial tcp 104.18.125.25:443: i/o timeout

4.2. 问题分析production.cloudflare.docker.com 的IP是会变化的，当前的这个 104.18.121.25 刚好被墙了，无法访问。查看是否被墙的办法：访问ITDOG，输入 production.cloudflare.docker.com 这个网址。
4.3. 解决办法一配置使用镜像站，从镜像站拉取镜像，避开 production.cloudflare.docker.com 这个域名。
1、编辑docker配置文件 
vim /etc/docker/daemon.json

添加 registry-mirrors 配置，配置方法参考《Docker镜像站的配置和使用》
2、重启docker
systemctl restart dockerdocker info

4.4. 解决办法二解决办法一，有时也不可行，因为有可能全国都被墙了，镜像站也无法从源站拉取我们需要的镜像。
此时只能使用终极解决办法：使用梯子。方法一：机器上直接配置使用梯子，配置方法参考《Linux配置网络代理》。方法二：搭建一个docker本地镜像站，配置使用梯子；其他机器配置使用这个docker镜像站。配置方法参考《Docker镜像站的配置和使用》。
5. failed to compute cache key5.1. 问题描述已知Dockerfile内容为：
FROM alpine:3.7.3WORKDIR /home/workCOPY Dockerfile .

Dockerfile所在目录，执行docker build报错：
failed to compute cache key: failed to calculate checksum of ref moby::heujqm1lca2el12jsbquzoarg: &quot;/Dockerfile&quot;: not found

5.2. 解决办法检查 .dockerignore 文件，大概率是因为该文件中忽略了 Dockerfile 。
6. Docker打满CPU内存问题6.1. 问题描述系统环境：

主机系统CentOS7.9，kernal 4.19.188-10.el7.ucloud.x86_64
K8S集群版本1.20.14，calico版本3.22.1，集群是使用sealos4.2.3安装的，详情参考《sealos入门篇》
Docker版本19.03.15 

当一个节点上的pod数量超过一定数量，或者pod比较繁忙时，CPU和内存就会被Docker进程进行打满。

pod的limits都是进行过限制的，总和远小于宿主机CPU资源总量
通过容器监控看，pod总的CPU资源用量远小于100%，大约只有20%到30%
通过主机监控看，busy system（cpu处于核心态的占比）约90%，busy user（cpu处理用户态的占比）约6%
通过主机监控看，从busy system占比90%开始，内存使用率逐步增加，经过一个小时左右内存使用率增长到100%

此时查看docker日志，发现大量报错：
Handler for GET /v1.40/containers/xxx/json returned error: write unix /var/run/docker.sock-&gt;@: write: broken pipe

6.2. 解决办法没有检索到解决办法。尝试升级Docker版本到 24.0.6 ，无效。
经过和正常集群的对比，发现有问题主机的内核版本为 4.19.188-10.el7.ucloud.x86_64 ，没问题主机的内核版本为 5.4.148-1.el7.elrepo.x86_64 。中间差了一个大版本，而且有问题主机的内核版本还是ucloud定制版。因此，尝试升级内核，升级方法参考《CentOS7内核升级》。升级后，问题消失了。。。
7. Docker启动容器很慢问题7.1. 问题描述docker run --name test -d busybox tail -f /dev/null

Docker启动任何容器都很慢，需要十几秒甚至几分钟才能启动成功。
7.2. 解决办法查看一下主机磁盘使用情况，可能是IO使用率高。找到占用大量IO的进程，判断是否是正常占用，非正常占用的话就干掉它。
8. docker run的内容重定向到文件8.1. 问题描述使用docker批量运行一些任务时，需要多个容器同时运行，同时容器跑完即删，例如：
for i in `seq 0 3`;doscreen -dmS part$&#123;i&#125; docker run --rm \-e PART=$&#123;i&#125; \-v /data/test:/data/test \test-image-name:latest \/bin/bash -c &quot;xxx&quot;done
想要查看某个容器的输出时，使用命令 screen -ls + screen -r yyy.partN 实现。但是，这样并不方便查看输出，出错时容器直接就没了，不方便追查问题。因此，我们需要把docker run的内容重定向到文件中，该怎样修改命令？
8.2. 解决办法单独执行 docker run --rm 时，怎样重定向内容到文件？答：重定向标准输出和错误输出到文件即可。方法一：直接重定向，这种方法拿到的数据的最全的。
docker run --rm nginx &gt; output.log 2&gt;&amp;1

方法二：使用tee重定向，但是拿到的是缺失的数据，这是因为管道符只能重定向标准输出。
docker run --rm nginx | tee output.log # 缺失nginx日志输出docker run --rm nginx 2&gt;&amp;1 | tee output.log # 缺失ctrl+c后的数据

screen -dm + docker run --rm 时，怎样重定向内容到文件？答：首先，上面的两种办法，经测试都是不可行的，需要考虑screen的输出机制。执行 screen -dm 时，screen创建一个新会话并立即后台运行（Detached）。当screen后台运行时，它关闭了标准输入、输出和错误流。
方法一：打开screen的标准输出，输出文件名不可控，默认为 screenlog.0
screen -L -dmS nginx docker run --rm nginx

方法二：在screen session中使用tee
screen -dm bash -c &quot;docker run --rm nginx 2&gt;&amp;1 | tee output.log&quot;

方法三：改用nohup（推荐）
nohup docker run --rm nginx &gt; output.log 2&gt;&amp;1 &amp;

综上，脚本修改为：
for i in `seq 0 3`;donohup docker run --rm --name part$&#123;i&#125; \-e PART=$&#123;i&#125; \-v /data/test:/data/test \test-image-name:latest \/bin/bash -c &quot;xxx&quot; &gt; part$&#123;i&#125;.log 2&gt;&amp;1 &amp;done

9. /run 目录下没有空间9.1. 问题描述docker run启动一个容器时，报错：
docker: Error response from daemon: failed to create task for container: failed to start shim: write /run/containerd/io.containerd.runtime.v2.task/moby/781958621c259371c2fec92ea1ab86f0937fa480bcf4aefc15d30ce3c5da90eb/config.json: no space left on device: unknown.

9.2. 问题排查1、查看磁盘使用情况
df -h

发现 /run 目录已经打满
Filesystem      Size  Used Avail Use% Mounted on/dev/root        29G   16G   14G  53% /devtmpfs        3.9G     0  3.9G   0% /devtmpfs           3.9G     0  3.9G   0% /dev/shmtmpfs           794M  794M     0 100% /run...

2、查看 /run 目录下是哪些文件占用了磁盘
du -h --max-depth=1 /run

9.3. 解决办法一找到占用磁盘空间的文件，并清理
9.4. 解决办法二扩容tmpfs 
mount -o remount,size=4G /run

扩容tmpfs，并且系统重启后自动生效。编辑 /etc/fstab ，添加配置：
tmpfs /run tmpfs defaults,size=4G 0 0

注意：

tmpfs 的大小是动态的，实际占用的内存会根据存储的数据量而变化。
tmpfs 数据在系统重启后会丢失，因此不适合存储持久数据。
确保在修改大小时考虑到系统的可用内存，以避免影响系统性能。

10. 重启系统后无法启动Docker容器10.1. 问题描述20.04.6 重启系统后，发现 Docker 容器没有正常启动。启动Docker服务，发现虽然Docker服务启动成功，但是日志中出现了很多报错：
Error response from daemon: AppArmor enabled on system but the docker-default profile could not be loaded: strconv.Atoi: parsing &quot;file&quot;: invalid syntax

通过 docker start xxx 命令手动启动某个容器，同样会出现上面的错误。
10.2. 问题排查解决AppArmor（Application Armor）是Linux上的一个安全模块，用于限制进程的权限和行为。它通过定义访问控制规则来保护系统中的应用程序免受潜在的安全威胁。
1、尝试重新安装AppArmor
apt remove --assume-yes --purge apparmorrm -rf /etc/apparmor*apt install apparmorsystemctl restart apparmorsystemctl restart docker

问题依然存在。
2、卸载AppArmor
apt remove --assume-yes --purge apparmorsystemctl restart docker

卸载AppArmor后，容器就可以正常启动了。
参考文档：

docker: Error response from daemon: AppArmor enabled on system but the docker-default profile could not be loaded
tankala/appArmorDockerProfileCorrpution.md
docker运行报错docker: Error response from daemon: AppArmor enabled on system but the docker-default prof

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>gpu</tag>
        <tag>问题排查</tag>
        <tag>cloudflare</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：GitLab CI配置代码质量检查</title>
    <url>/dev-gitlab-ci-code-quality-check/</url>
    <content><![CDATA[1. 怎样保证代码质量？保证代码质量有三种常用的方法：Code Review、编码规范和单元测试。
Code Review需要别人帮助Review，现在可以找ChatGPT帮助Review。而对于编码规范检查和运行单元测试，是可以通过CI流水线自动完成的。本文中，我们学习在GitLab CI流水线中配置Python的编码规范检查和运行单元测试。


2. 编码规范检查参考文档：

Code Quality
gitlab-org/ci-cd/codequality

GitLab官方给的文档，使用codeclimate进行编码规范检查。针对python，我们不使用codeclimate，而是使用更加常用的pylint或者flake8。
示例：
stages:  - code_checkcode_check:  stage: code_check  image: python:3.7.10-slim-buster  before_script:   - pip install pylint -i https://pypi.tuna.tsinghua.edu.cn/simple  script:    - pylint **/*.py

编码规范检查，如果检查项比较多，建议使用 pre-commit 框架，详情参考 《git pre-commit 代码质量检查》。
3. 单元测试示例：
stages:  - unittestunittest:  stage: unittest  image: python:3.7.10-slim-buster  before_script:     - pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple  script:    - pytest --junitxml=report.xml tests/  artifacts:    reports:      junit: report.xml

下载report.xml的方法：进入CI/CD Pipelines页面，找到Job对应的Pipeline，最右边三个点，点击下载artifacts。
参考文档：

《Python单元测试》
《GitLab CI/CD入门篇》

4. 单元测试覆盖率示例：
stages:  - unittestunittest:  stage: unittest  image: python:3.7.10-slim-buster  before_script:     - pip install pytest pytest-cov -i https://pypi.tuna.tsinghua.edu.cn/simple  script:    - pytest --cov-report xml --cov=. tests/    - coverage report --precision=2  coverage: &#x27;/\d+\.\d+%$/&#x27;  artifacts:    reports:      cobertura: coverage.xml

其中coverage report输出内容结构为：
Name    Stmts    Miss   Cover-----------------------------...main.py   36      36       0%-----------------------------TOTAL   3130    3127    0.10%

正则匹配coverage，最终输出到UI的是最后一个匹配到的值0.10。
下载coverage.xml的方法：进入CI/CD Pipelines页面，找到Job对应的Pipeline，最右边三个点，点击下载artifacts。
查看单测覆盖率的方法：进入CI/CD Pipelines页面，进入Job对应的Pipeline详情，点击Jobs，可以看到Coverage。
查看单测覆盖率的方法2：Job详情页，直接查看coverage report命令的输出内容。
单元测试和单元测试覆盖率，可以合并为一个stage：
stages:  - unittestunittest:  stage: unittest  image: python:3.7.10-slim-buster  before_script:     - pip install pytest pytest-cov -i https://pypi.tuna.tsinghua.edu.cn/simple  script:    #- export PYTHONPATH=&quot;$CI_PROJECT_DIR&quot;    - pytest --junitxml=report.xml --cov-report xml --cov=. tests/    - coverage report --precision=2  coverage: &#x27;/\d+\.\d+%$/&#x27;  artifacts:    reports:      junit: report.xml      cobertura: coverage.xml

参考文档：

Test coverage visualization
《GitLab CI/CD入门篇》

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
        <category>git</category>
        <category>testing</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>git</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：GitLab CI问题记录</title>
    <url>/dev-gitlab-ci-problems/</url>
    <content><![CDATA[1. Job is stuckShell Runner跑CI任务，报错：
Job is stuck. Check runners. allowed to fail

解决办法：经查是因为gitlab-runner版本比较高（15.5.0），新版本的runner，要求.gitlab-ci.yml必须要配置tags，指定runner。


2. setting GIT_CLONE_PATH is not allowedShell Runner跑CI任务，报错：
ERROR: Job failed: setting GIT_CLONE_PATH is not allowed, enable `custom_build_dir` feature

解决办法：启用custom_build_dir
vim /etc/gitlab-runner/config.toml

添加custom_build_dir配置
[[runners]]  [runners.custom_build_dir]    enabled = true

重启gitlab-runner
gitlab-runner restart

3. fatal: git fetch-packShell Runner跑CI任务，报错：
fatal: git fetch-pack: expected shallow listfatal: The remote end hung up unexpectedly

解决办法：git版本问题，升级git
rpm -ivh http://opensource.wandisco.com/centos/7/git/x86_64/wandisco-git-release-7-1.noarch.rpmyum install -y git 

4. 不能锁定配置文件Shell Runner跑CI任务，报错：error: 不能锁定配置文件 /home/gitlab-runner/builds/builds/xxx/main.tmp/git-template/config: 没有那个文件或目录
解决办法：没有检索到找到解决办法，尝试降级到14.5.0，问题解决。降级方法参考【Linux环境安装runner】一节。
5. No such file or directoryShell Runner跑CI任务，报错：
Skipping Git checkoutSkipping Git submodules setup...$ pip3 install -r requirements.txtERROR: Could not open requirements file: [Errno 2] No such file or directory: &#x27;requirements.txt&#x27;

解决办法：.gitlab-ci.yml文件中添加GIT_CHECKOUT变量
variables:  GIT_CHECKOUT: &quot;true&quot;

6. 下载镜像报错Docker Machine Runner跑CI任务，一直正常。Docker Machine Runner没有任何变动，突然有一天，执行docker build的时候，下载基础镜像报错：ERROR: failed to do request: Head https://192.168.56.101:5000/v2/library/python/manifests/3.8: http: server gave HTTP response to HTTPS client
重启gitlab-runner问题依旧，重启机器问题依旧。
解决办法：runner配置里，指定docker image的版本为docker:19.03，而不要使用docker，否则一直会拉取最新版的docker，而最新版的docker不支持发送http请求镜像仓库。
7. 指定Python版本Shell Runner跑CI任务，想要指定Python版本。
解决办法一：使用指定版本Python的绝对路径
/data/miniconda3/envs/my_env/bin/python /path/to/test.py

解决办法二：gitlab-runner用户初始化脚本中，修改PATH环境变量
vim /home/gitlab-runner/.bashrcvim /home/gitlab-runner/.profile

添加内容：
export PATH=/data/miniconda3/envs/my_env/bin:$PATH



]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>troubleshooting</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>问题排查</tag>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：GitLab Runner入门篇</title>
    <url>/dev-gitlab-runner-start/</url>
    <content><![CDATA[1. GitLab Runner简介GitLab Runner 是一个开源软件，它与 GitLab CI/CD 配合，在管道流中运行作业。GitLab Runner 是用Go编写，几乎可以直接运行在任何操作系统（个别系统需要自行编译）。GitLab Runner 也可以在 Docker 容器内运行或部署到 Kubernetes 集群中。
GitLab Runner能不能替换成其他的Runner？可以，但是没有必要，毕竟Gitlab Runner是开源的，有什么个性化需求就自己改一改。而且，也没有找到合适的替代产品。
参考文档：

GitLab Runner 
GitLab CI/CD中文文档
GitLab CI/CD文档
GitLab Runner
Executors
The Shell executor
《GitLab CI/CD入门篇》



2. GitLab、Runner和Executor关于GitLab、Runner和Executor的关系，有一个比喻很恰当：GitLab是老板，会去查看需求单（.gitlab-ci.yml），建立一张又一张有先后顺序的工单（CI Pipeline）。GitLab Runner 是执行 CI Job 的工人，定期去询问老板（GitLab）现在有分配给自己的工作（CI Job）吗？如果拿到工作，就开始执行，并在执行过程中将进度即时写在工单上。Executor 是工人（Runner）执行 CI Job 的工作环境，例如一个 CI Job 是打印出”hello”，那么Runner可以在本地Shell环境中执行，可以在虚拟机环境中执行，还可以在Docker环境中执行；而如果一个CI Job需要在基础镜像上进行构建，那么就需要Docker环境或者K8S环境了。
Runner通常在安装Runner的同一台机器上处理作业。但是，我们也可以让Runner在容器、k8s 集群、或者云上的自动缩放实例中处理作业。
安装Runner后，想要使用它，需要在GitLab平台进行注册，注册做的工作就是建立Gitlab与Runner之间的通信。注册完成后，Runner就可以运行来自 GitLab 的 CI/CD 作业了。
当我们注册一个Runner时，必须选择一个Executor。Executor决定了每个作业运行的环境。
Gitlab Runner的安装环境包括Linux、MacOS、Windows、Docker和Kubernetes。而不同安装环境的Runner，支持不同的Executors。根据Executor的不同，同一个Runner程序，可以注册为不同Executor类型的Runner，例如SSH、Shell、Parallels、VirtualBox、Docker、Docker Machine (auto-scaling)、Kubernetes、Custom。
例如：

如果我们希望 CI/CD 作业运行 PowerShell 命令，那么可以在 Windows 服务器上安装 GitLab Runner，然后注册一个使用 Shell Executor的Runner。
如果我们希望 CI/CD 作业在自定义 Docker 容器中运行命令，那么可以在 Linux 服务器上安装 GitLab Runner 并注册一个使用 Docker Executor 的 Runner。
我们还可以在虚拟机上安装 GitLab Runner，并让它使用另一个虚拟机作为Executor。

最常用的Executor：Shell、Docker、Docker Machine、Kubernetes。
参考文档：

GitLab Runner 
Executors
GitLab CI 之 Runner 的 Executor 該如何選擇？

3. Executor简介3.1. ShellShell 是最简单的配置执行器。构建所需的所有依赖项都需要手动安装在安装 GitLab Runner 的同一台机器上。
3.2. Docker executor一个很好的选择是使用 Docker，因为它允许一个干净的构建环境，并且具有简单的依赖项管理（构建项目的所有依赖项都可以放在 Docker 映像中）。 Docker 执行器允许您轻松创建具有依赖服务的构建环境，例如 MySQL。
当我们在 Docker 容器中安装 GitLab Runner 并选择 Docker executor来运行作业时，它有时被称为 Docker-in-Docker （dind）配置。
注意：有些Docker容器中，默认的shell并不是bash，shell选择逻辑如下
if [ -x /usr/local/bin/bash ]; then    exec /usr/local/bin/bash $@elif [ -x /usr/bin/bash ]; then    exec /usr/bin/bash $@elif [ -x /bin/bash ]; then    exec /bin/bash $@elif [ -x /usr/local/bin/sh ]; then    exec /usr/local/bin/sh $@elif [ -x /usr/bin/sh ]; then    exec /usr/bin/sh $@elif [ -x /bin/sh ]; then    exec /bin/sh $@else    echo shell not found    exit 1fi

参考文档：

How To Run Docker in Docker
How To Run Docker in Docker Container
Docker-in-Docker in Gitlab Runners
shell in docker executor isn’t bash

3.3. Docker Machine executorDocker Machine 是 Docker 执行器的特殊版本，支持自动缩放。它像普通的 Docker 执行器一样工作，但使用 Docker Machine 按需创建的构建主机。
Docker官方已经废弃了Docker Machine，GitLab Runner使用的是自己维护的Docker Machine。
Docker Machine executor原理：

安装Runner并配置gitlab-runner用户具有启动虚拟机的权限
Runner调用驱动启动Docker Machine VM，VM系统为Boot2Docker
当VM中开始跑CI Job时，会启动一个Docker容器作为执行容器，在容器中跑CI Job
CI Job中可以包含docker build等命令，此时会共享调用VM的Docker，是Docker in Docker模式的一种

参考文档：

Install and register GitLab Runner for autoscaling with Docker Machine
Docker Machine Executor autoscale configuration

3.4. Kubernetes executorKubernetes 执行器允许您使用现有的 Kubernetes 集群进行构建。执行器将调用 Kubernetes 集群 API 并为每个 GitLab CI 作业创建一个新的 Pod（带有构建容器和服务容器）。
Kubernetes executor也是支持 Docker-in-Docker 的。
参考文档：

The Kubernetes executor for GitLab Runner
The Kubernetes executor for GitLab Runner - Using Docker in your builds
Cannot access docker daemon in gitlab runner using Kubernetes executor

3.5. SSH executorSSH 执行器是为了完整性而添加的，但它是所有执行器中受支持最少的。它使 GitLab Runner 连接到外部服务器并在那里运行构建。我们有一些来自使用此执行器的组织的成功案例，但通常我们建议使用其他类型之一。
3.6. Virtual Machine executor这种类型的执行器允许您使用已创建的虚拟机，该虚拟机被克隆并用于运行您的构建。我们提供两个完整的系统虚拟化选项：VirtualBox 和 Parallels。如果您想在不同的操作系统上运行构建，它们可能会很有用，因为它允许在 Windows、Linux、macOS 或 FreeBSD 上创建虚拟机，然后 GitLab Runner 连接到虚拟机并在其上运行构建。它的使用也有助于降低基础设施成本。
3.7. Custom executor自定义执行器允许您指定自己的执行环境。当 GitLab Runner 不提供执行器（例如，LXC 容器）时，您可以向 GitLab Runner 提供自己的可执行文件，以配置和清理您想要使用的任何环境。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：GitLab Runner安装实践</title>
    <url>/dev-gitlab-runner-install/</url>
    <content><![CDATA[1. GitLab Runner版本说明出于兼容性原因，GitLab Runner major.minor 版本应与 GitLab major.minor 版本保持同步。较旧的runner可能仍然可以使用较新的 GitLab 版本，反之亦然。但是，如果存在版本差异，功能可能无法使用或无法正常工作。minor版本更新时，会保障向后兼容性。但是，有时 GitLab 的minor版本更新会引入新功能，这些新功能需要 GitLab Runner 在同一minor版本上。
需要特别注意的是：GitLab Runner 15.0 对注册 API 请求格式进行了更改。它阻止 GitLab Runner 与低于 14.8 的 GitLab 版本通信。我们必须使用适合 GitLab 版本的 Runner 版本，或升级 GitLab 应用程序。
更多内容参考文档GitLab Runner


2. 查看安装教程2.1. 指定项目的Runner打开gitlab项目 -&gt; Settings -&gt; CI/CD -&gt; Runners -&gt; Expand -&gt; Show Runner installation instructions
页面的 registration token，用于注册指定项目（当前项目）的runner。
2.2. 共享的Runner查看Runner：https://gitlab.voidking.com/admin/runners  
打开gitlab runner管理页面 -&gt; Show Runner installation instructions
页面的 registration token，用于注册共享runner。
3. TOML语法gitlab-runner配置文件为config.toml，使用TOML语法。TOML的目标是成为一种易于阅读的最小配置文件格式。TOML被设计为明确地映射到哈希表。TOML应该很容易解析成各种语言的数据结构。
参考文档：

Configuring GitLab Runner
TOML
TOML 1.0格式语法
Convert TOML To JSON

3.1. 注释#，井号后面表示注释
3.2. 键值对键名为字符串，值可以为字符串、整数、浮点数、布尔值、日期、时刻、数组、行内表等。键名在等号的左边，值在等号的右边。
例如：
name = &quot;voidking&quot;

3.3. 表表的表示方法：方括号+表名，例如[table]。表是键值对的集合，类似于json中的对象（花括号中内容）。在它下方，直至下一个表头或文件结束，都是这个表的键值对。顶层表，又被称为根表，于文档开始处开始并在第一个表头（或文件结束处）前结束。
表名的规则和键名的规则相同。
表的层级结构以点.分隔，分隔的每个部分都是一个表名。定义一个多层级表时，如果最后一个表名前的表没有被创建，那么会被自动创建。
例如：
[table]# 定义一个名为 table 的表key1 = &quot;some string&quot;key2 = 123fruit.apple.color = &quot;红色&quot;# 定义一个名为 fruit 的表# 定义一个名为 fruit.apple 的表fruit.apple.taste.sweet = true# 定义一个名为 fruit.apple.taste 的表# fruit 和 fruit.apple 已经创建过了

3.4. 表数组表数组表示方法：双层方括号+表名，例如[[fruits]]。表数组是表的数组，类似于json中的对象数组。
表数组的第一例定义了这个数组及其首个表元素，而后续的每个表数组在该数组中创建并定义一个新的表元素。
[[fruits]]name = &quot;苹果&quot;[fruits.physical]  # 子表color = &quot;红色&quot;shape = &quot;圆形&quot;[[fruits.varieties]]  # 嵌套表数组name = &quot;蛇果&quot;[[fruits.varieties]]name = &quot;澳洲青苹&quot;[[fruits]]name = &quot;香蕉&quot;[[fruits.varieties]]name = &quot;车前草&quot;

对应的json格式为：
&#123;  &quot;fruits&quot;: [    &#123;      &quot;name&quot;: &quot;苹果&quot;,      &quot;physical&quot;: &#123;        &quot;color&quot;: &quot;红色&quot;,        &quot;shape&quot;: &quot;圆形&quot;      &#125;,      &quot;varieties&quot;: [        &#123; &quot;name&quot;: &quot;蛇果&quot; &#125;,        &#123; &quot;name&quot;: &quot;澳洲青苹&quot; &#125;      ]    &#125;,    &#123;      &quot;name&quot;: &quot;香蕉&quot;,      &quot;varieties&quot;: [        &#123; &quot;name&quot;: &quot;车前草&quot; &#125;      ]    &#125;  ]&#125;

3.5. 缩进TOML中的缩进没有意义，只是为了方便读者理解层级结构。
4. Linux环境安装Runner官方文档：Install GitLab Runner注意：gitlab runner安装包下载地址变化比较频繁，如果下载地址失效，请参考官方文档获取最新的下载地址。
1、安装runner
# Download the binary for your systemsudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64# Download specific version binary# sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/v14.5.0/binaries/gitlab-runner-linux-amd64# Give it permissions to executesudo chmod +x /usr/local/bin/gitlab-runner# Create a GitLab CI usersudo useradd --comment &#x27;GitLab Runner&#x27; --create-home gitlab-runner --shell /bin/bash# Install and run as servicesudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runnersudo gitlab-runner startsudo gitlab-runner -v

PS：其他版本的runner源码和二进制文件，我们还可以在gitlab-runner Releases页面找到。
对于centos系统，可以直接使用rpm包安装。
yum install -y https://gitlab-runner.downloads.s3.amazonaws.com/latest/rpm/gitlab-runner_amd64.rpm 

2、添加到gitlab-runner到docker用户组（可选）如果gitlab-runner执行的ci脚本需要运行docker，那么需要将gitlab-runner到docker用户组
sudo usermod -a -G docker gitlab-runner# if no docker groupsudo usermod -a -G root gitlab-runnersudo -u gitlab-runner -H docker info

3、gitlab-runner添加sudo权限（可选）
vim /etc/sudoers

添加配置：
gitlab-runner ALL=(ALL) NOPASSWD: ALL

5. 注册Shell类型RunnerShell类型的Executor，在Runner程序所在主机上运行CI任务。
前置条件：安装好了Runner。
sudo gitlab-runner register -hsudo gitlab-runner register --url https://gitlab.voidking.com/ --registration-token $REGISTRATION_TOKEN
REGISTRATION_TOKEN可以在gitlab页面获取到，详情参考上文【查看安装教程】一节。根据提示，填写url、token、description、tags（多个tag以英文逗号分隔）、executor类型等信息，这里executor类型填写shell。
6. 注册Docker类型RunnerDocker类型的Executor，在Runner程序所在主机上的Docker容器中运行CI任务。
前置条件：安装好了Runner，而且Runner所在主机已经安装配置好了Docker。
sudo gitlab-runner register --url https://gitlab.voidking.com/ --registration-token $REGISTRATION_TOKEN

根据提示，填写url、token、description、tags、executor类型等信息，这里executor类型填写docker，最后填写一个默认的镜像。
如果一个Shell类型的Runner，想要改成Docker类型的Runner，那么需要取消注册后重新注册。
sudo gitlab-runner stopsudo gitlab-runner listsudo gitlab-runner unregister --name &quot;your-runner-name&quot;sudo gitlab-runner register --url https://gitlab.voidking.com/ --registration-token $REGISTRATION_TOKEN

7. 注册Docker Machine类型Runner参考文档：

Install and register GitLab Runner for autoscaling with Docker Machine
Docker Machine Executor autoscale configuration
Docker Machine
docker-machine的安装与使用

7.1. 安装虚拟机驱动可选驱动：

virtualbox（推荐），参考文档Linux下使用VirtualBox
qemu，参考文档machine-drivers/docker-machine-driver-qemu
kvm，参考文档dhiltgen/docker-machine-kvm

1、安装virtualbox驱动
yum install https://download.virtualbox.org/virtualbox/6.1.26/VirtualBox-6.1-6.1.26_145957_el7-1.x86_64.rpm

2、安装编译环境
yum install -y kernel-devel kernel-devel install gcc make perl kernel-headers

3、激活virtualbox内核支持
/sbin/vboxconfig

7.2. 安装Docker Machine1、下载docker-machine二进制文件
wget https://gitlab.com/gitlab-org/ci-cd/docker-machine/-/releases/v0.16.2-gitlab.15/downloads/docker-machine-Linux-x86_64mv docker-machine-Linux-x86_64 /usr/sbin/docker-machinechmod +x /usr/sbin/docker-machine

更多版本可以访问docker-machine/releases页面查找。
2、测试启动虚拟机
docker-machine create -d virtualbox test

7.3. 注册Runner前置条件：安装好了Runner。
sudo gitlab-runner register --url https://gitlab.voidking.com/ --registration-token $REGISTRATION_TOKEN

根据提示，填写url、token、description、tags、executor类型等信息，这里executor类型填写docker+machine，最后填写一个默认的镜像。
7.4. 配置镜像加速（可选）参考文档《Docker镜像站的配置和使用》，搭建本地镜像站。
7.5. 配置缓存1、启动minio容器
docker run -d --name minio \--restart always \-p 9000:9000 \-p 9001:9001  \-v /data/minio/.minio:/data/.minio \-v /data/minio/export:/export \-e &quot;MINIO_ROOT_USER=root&quot; \-e &quot;MINIO_ROOT_PASSWORD=xxxxxx&quot; \minio/minio:latest server /export --console-address &quot;:9001&quot;

2、页面访问http://192.168.56.101:9001/ 
7.6. 配置Runner1、修改 /etc/gitlab-runner/config.toml
concurrent = 3check_interval = 0[session_server]  session_timeout = 1800[[runners]]  limit = 3  name = &quot;dockermachine&quot;  url = &quot;https://gitlab.voidking.com&quot;  token = &quot;xxx&quot;  executor = &quot;docker+machine&quot;  [runners.custom_build_dir]    enabled = true  [runners.cache]    Type = &quot;s3&quot;    Path = &quot;runner&quot;    Shared = true    [runners.cache.s3]      ServerAddress = &quot;192.168.56.101:9000&quot;      AccessKey = &quot;root&quot;      SecretKey = &quot;xxx&quot;      BucketName = &quot;runner&quot;      Insecure = true    [runners.cache.gcs]    [runners.cache.azure]  [runners.docker]    tls_verify = false    image = &quot;docker:19.03&quot;    privileged = true    disable_entrypoint_overwrite = false    oom_kill_disable = false    disable_cache = false    volumes = [&quot;/certs/client&quot;,&quot;/cache&quot;, &quot;/var/run/docker.sock:/var/run/docker.sock&quot;]    pull_policy = [&quot;if-not-present&quot;]    shm_size = 0  [runners.machine]    IdleCount = 5    MaxGrowthRate = 1    IdleTime = 1800    MachineDriver = &quot;virtualbox&quot;    MachineName = &quot;auto-scale-%s&quot;    MachineOptions = [      &quot;engine-registry-mirror=http://192.168.56.101:5000&quot;,      &quot;engine-registry-mirror=http://192.168.56.101:5001&quot;,      &quot;virtualbox-memory=4048&quot;,      &quot;virtualbox-disk-size=204800&quot;,      &quot;virtualbox-cpu-count=2&quot;    ]

注意：runners.docker.image要指定版本，否则每次构建都会拉取最新版的docker，而最新版本的docker不支持http请求镜像仓库。更多docker版本可以访问dockerhub - docker获取。
其中MachineOptions中给定的参数，是docker-machine create命令接收的参数。
2、重新启动runner
gitlab-runner restart

7.7. 管理Docker Machinedocker-machine lsdocker-machine ssh xxx

8. K8S环境安装Runner本节中，我们在K8S中通过helm安装Runner。
参考文档：

GitLab Runner Helm Chart
GitLab Runner Helm Chart - Running Docker-in-Docker containers with GitLab Runner
GitLab CI CD | Install and Configure GitLab Runner on Kubernetes with Helm
Runner构建优化
在 Kubernetes 上安装 Gitlab CI Runner

8.1. 准备存储通过helm安装runner，values当前还不支持配置storageclass。因此，需要先自行准备好runner的存储。主要参考文档《K8S中安装配置StorageClass》
准备pvc定义 runner-pvc.yaml
apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: gitlab-runner-cachespec:  storageClassName: nfs-storage  accessModes:    - ReadWriteMany  resources:    requests:      storage: 50Gi

8.2. 准备Runner配置1、添加gitlab repo
helm repo add gitlab https://charts.gitlab.iohelm repo update

2、查看可用的runner版本
helm search repo -l gitlab/gitlab-runner

CHART VERSION有对应的APP VERSION版本，选择需要的APP VERSION版本。这里选择14.0.0版本，对应CHART VERSION为0.30.0
3、下载chart
helm fetch gitlab/gitlab-runner --version 0.30.0tar -xzvf gitlab-runner-0.30.0.tgz

4、values.yaml修改配置

image：指定版本gitlab/gitlab-runner:alpine-v14.0.1，更多镜像版本可以访问docker hub查找
gitlabUrl：改成我们自己的的gitlab地址
runnerRegistrationToken：参考【查看安装教程】一节获取
resources：修改申请和限制的资源
rbac.create：改成true，创建sa用于创建runner
name：改成runner想要在gitlab中显示的名称
tags：改成runner想要注册的tags，多个tag以英文逗号分隔
replicas：改成runner期望的副本数，这些runner副本使用同一个name。
privileged：改成true，启用docker in docker
runners.config：支持自定义构建目录，支持docker in docker

runners.config内容：
[runners.custom_build_dir]  enabled = true[[runners.kubernetes.volumes.host_path]]  name = &quot;docker&quot;  mount_path = &quot;/var/run/docker.sock&quot;  read_only = true  host_path = &quot;/var/run/docker.sock&quot;

我们想要对构建缓存使用持久化存储，因此需要添加
## configure build cachecibuild:  cache:    pvcName: gitlab-runner-cache    mountPath: /home/gitlab-runner/ci-build-cache

同时，需要修改gitlab-runner/templates/configmap.yamldata.entrypoint部分添加runner配置，自动挂载存储
# add build cache and cat &lt;&lt;EOF &gt;&gt;/home/gitlab-runner/.gitlab-runner/config.toml  [[runners.kubernetes.volumes.pvc]]    name = &quot;&#123;&#123;.Values.cibuild.cache.pvcName&#125;&#125;&quot;    mount_path = &quot;&#123;&#123;.Values.cibuild.cache.mountPath&#125;&#125;&quot;EOF# Start the runnerexec /entrypoint run --user=gitlab-runner \  --working-directory=/home/gitlab-runner

8.3. 安装Runner1、安装runner
kubectl create ns gitlab-14-0kubectl apply -f runner-pvc.yaml -n gitlab-14-0helm install --namespace gitlab-14-0 gitlab-runner ./gitlab-runner

2、查看安装
kubectl get all -n gitlab-14-0kubectl get pvc -n gitlab-14-0kubectl logs pod/gitlab-runner-gitlab-runner-849988b584-kllqv -n gitlab-14-0

3、查看runner注册结果https://gitlab.voidking.com/admin/runners
找到新的runner，点击右侧的Edit按钮，可以做进一步的配置。
9. 自定义builds路径参考文档Optimize GitLab for large repositories
1、准备builds目录
mkdir /buildsmkdir /cachechown gitlab-runner:gitlab-runner -R /buildschown gitlab-runner:gitlab-runner -R /cache

2、修改runner配置
vim /etc/gitlab-runner/config.toml

添加配置
[[runners]]  builds_dir = &quot;/builds&quot;  cache_dir = &quot;/cache&quot;  [runners.custom_build_dir]    enabled = true

3、重启runner
gitlab-runner restart

10. 取消注册Runner# 查看tokengitlab-runner list# token还可以在/etc/gitlab-runner/config.toml中查看# 取消注册runnergitlab-runner unregister -t $token -u https://gitlab.voidking.com/



]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
        <tag>virtualbox</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：GitLab CI报错ERROR: Job failed (system failure)</title>
    <url>/dev-gitlab-ci-job-faild-system-failure/</url>
    <content><![CDATA[1. 问题描述GitLab CI任务，Runner使用的是docker machine executor类型的执行器，执行失败报错：
Running on runner-h6ezaymy-project-1037-concurrent-0 via runner-h6ezaymy-auto-scale-1668060271-ce458595......WARNING: Failed to pull image with policy &quot;if-not-present&quot;: error during connect: Post https://192.168.99.251:2376/v1.25/images/create?fromImage=registry.gitlab.com%2Fgitlab-org%2Fgitlab-runner%2Fgitlab-runner-helper&amp;tag=x86_64-58ba2b95: dial tcp 192.168.99.251:2376: connect: no route to host (manager.go:205:3s)ERROR: Job failed (system failure): error during connect: Post https://192.168.99.251:2376/v1.25/containers/47272fbe49e4be0f85724ad99f0657b72f568810ce0f4914c57e7fcf114764e2/wait: dial tcp 192.168.99.251:2376: connect: no route to host

重试，问题依旧。


2. 排查思路问题原因猜测：

偶发问题？确认是否能稳定复现
CICD配置问题？确认CICD配置，检查用法是否正确
runner问题？重启runner，再次尝试复现

排查确认：

偶发问题，重试失败，但是夜间可以成功执行
CICD配置正常，报错与配置无关，而且夜间可以成功执行
runner问题无法排除，需要进一步排查

3. Runner问题排查3.1. 网络问题？登录到runner所在宿主机，进入到runner
docker-machine ssh runner-h6ezaymy-auto-scale-1668060271-ce458595
提示vm不存在。
docker-machine ls
查看当前的vm，负责运行任务的vm确实不存在了，看来不是网络问题。怀疑是vm出问题被干掉了，然后重新创建了一个vm。那么vm为什么会被干掉？看看日志吧。
3.2. 查看系统日志journalctl -xe# orcat /var/log/messages | grep -i kill -A3 -B3

确实找到了程序被干掉的证据，刚好和gitlabci任务失败的时间吻合。
11月 10 17:13:42 runner kernel: Out of memory: Kill process 12293 (MainHGCMthread) score 215 or sacrifice child11月 10 17:13:42 runner kernel: Killed process 12294 (EMT-0), UID 0, total-vm:4345420kB, anon-rss:45340kB, file-rss:2622104kB, shmem-rss:4kB

那么这个vm为什么被干掉了？oom？
3.3. CI任务占用内存太高？检查ci任务的代码，并没有发现导致超高内存占用的逻辑。
3.4. Linux OOM机制回想起Linux OOM的逻辑，并不是一个程序占用内存高就把它干掉，而是整个系统的内存高，才会选出一个打分最高的程序干掉。
这就合理了，下午的时候runner所在宿主机上跑满了任务，很有可能出现内存紧张的情况。而这个被干掉的runner当时被打分最高，所以被干掉了。
Linux OOM机制参考：

Linux OOM机制分析
如何理解和配置 Linux 下的 OOM Killer？

4. 解决办法方法一：增加资源升级宿主机内存，或者添加runner宿主机
方法二：错峰执行ci任务比如这个失败的任务，还是夜间执行吧
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>troubleshooting</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>问题排查</tag>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
      </tags>
  </entry>
  <entry>
    <title>ArgoCD报错Unable to load data问题</title>
    <url>/dev-argocd-unable-to-load-data/</url>
    <content><![CDATA[1. 问题描述断电后，k8s集群重新拉起。argocd无法同步gitlab中的数据，报错：
Unable to load data: Failed to fetch default: git fetch origin --tags --force failed exit status 128: fatal: unable to access ‘https://gitlab.voidking.com/devops/argocd.git/&#39;: server certificate verification failed. CAfile: none CRLfile: none


2. 查日志2.1. 查看argocd repo-server日志repo-server负责从gitlab同步数据，查看一下它的日志。
kubectl logs --tail=100 argo-cd-argocd-repo-server-547b6cf9f9-dff7d -n argocd

内容为：
time=&quot;2022-11-03T06:04:33Z&quot; level=error msg=&quot;finished unary call with code Unknown&quot; error=&quot;Get \&quot;https://gitlab.voidking.com/devops/argocd.git/info/refs?service=git-upload-pack\&quot;: x509: certificate is not valid for any names, but wanted to match gitlab.voidking.com&quot; grpc.code=Unknown grpc.method=GenerateManifest grpc.request.deadline=&quot;2022-11-03T06:05:33Z&quot; grpc.service=repository.RepoServerService grpc.start_time=&quot;2022-11-03T06:04:33Z&quot; grpc.time_ms=38.558 span.kind=server system=grpc

2.2. 查看argocd其他组件日志挨个查看argocd其他组件日志，其中dex-server看着有些问题
kubectl logs --tail=100 argo-cd-argocd-dex-server-7cc5cc5455-c7q29  -n argocd

内容为：
time=&quot;2022-10-31T03:24:16Z&quot; level=info msg=&quot;keys expired, rotating&quot;time=&quot;2022-10-31T03:24:16Z&quot; level=info msg=&quot;keys rotated, next rotation: 2022-10-31 09:24:16.595711381 +0000 UTC&quot;

3. 证书问题？怀疑是证书到期导致的，打算对证书进行更新，参考文档TLS configuration
但是，怎么确认证书已经到期了呢？具体怎么操作更新证书？没有找到方法，先看看还有没有其他可能。
4. 重启一下试试？k get pod argo-cd-argocd-repo-server-547b6cf9f9-dff7d -n argocd -oyaml | k replace --force -f -

重建pod后，问题解决了。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>argocd</tag>
        <tag>cicd</tag>
        <tag>devops</tag>
      </tags>
  </entry>
  <entry>
    <title>KubeSphere版本升级</title>
    <url>/dev-kubesphere-upgrade/</url>
    <content><![CDATA[1. 需求描述当前KubeSphere版本v3.2.1，但是因为权限管理不好用（不能针对不同集群单独授权），因此计划升级到v3.3.1。KubeSphere v3.3.0之后支持为每个集群单独设置集群成员和集群角色，提供了更细粒度的权限管控机制，进一步完善了 KubeSphere 的多租户系统。
参考文档：

KubeSphere 3.3.0 全新升级，来了！
Release Notes - 3.3.1 版本说明
使用 ks-installer 升级
使用 ks-installer 离线升级



2. 注意事项
您应该先在测试环境中实施升级模拟。在测试环境中成功升级并且所有应用程序都正常运行之后，再在生产环境中升级您的集群。
在升级过程中，应用程序可能会短暂中断（尤其是单副本容器组），请安排合理的升级时间。
建议在生产环境中升级之前备份 etcd 和有状态应用程序。您可以使用 Velero 来备份和迁移 Kubernetes 资源以及持久化存储卷。

参考文档：

升级 - 概述
Velero
详解kubernetes备份恢复利器 Velero

3. 思路1、备份2、kubesphere执行升级
4. 备份4.1. etcd备份etcd，参考 《K8S集群中etcd备份和恢复》
4.2. 有状态应用程序未完待续
5. 执行升级1、下载yaml文件&amp;提交到k8s集群
wget https://github.com/kubesphere/ks-installer/releases/download/v3.3.1/kubesphere-installer.yamlkubectl apply -f kubesphere-installer.yaml  --force

2、观察升级进展
kubectl get all -n kubesphere-systemkubectl get pods -n kubesphere-systemkubectl describe job.batch/ks-upgrade -n kubesphere-systemkubectl logs -f ks-installer-895b8994d-5qrjs -n kubesphere-system

3、验证升级后的版本
kubectl get pod -n kubesphere-system -oyaml | grep image:

浏览器访问ks，右上角点击用户名，关于，就可以看到当前ks版本。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>kubesphere</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker容器网络限速</title>
    <url>/dev-docker-container-network-speed-limit/</url>
    <content><![CDATA[1. 前言为避免Docker容器打满带宽，有时需要对容器进行限速。目前比较好的方案是使用 tc-docker ，本文中我们就来学习一下。
参考文档：

《虚拟机在线迁移过程中的故障注入》
Linux服务器限制网络带宽流量速率
github - CodyGuo/tc-docker
docker限制容器下载速度



2. 启动限速容器docker run -d \    --name tc-docker \    --network host \    --privileged \    --restart always \    -v /var/run/docker.sock:/var/run/docker.sock \    -v /var/run/docker/netns:/var/run/docker/netns:shared \    codyguo/tc-docker:latest

3. 对容器限速容器限速示例：启动 gcloud cli 容器，启动时添加限速
docker run --name gcloud-cli -d \  --label &quot;org.label-schema.tc.enabled=1&quot; \  --label &quot;org.label-schema.tc.rate=10mbps&quot; \  --label &quot;org.label-schema.tc.ceil=50mbps&quot; \  -v /data/gcloud:/data/gcloud \  gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \  tail -f /dev/null

这个配置中，容器被限速为10mbps。

org.label-schema.tc.enabled：当设置为1时，启用限速
org.label-schema.tc.rate：容器的带宽或速率限制，限制此类及其所有子类的最大速率。
org.label-schema.tc.ceil：容器的带宽或最大速度限制，如果类的父类有空余带宽，子类可以发送的最大速率。

相关文档：《谷歌云 gcloud CLI 使用说明》
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：kubectl 插件管理器 krew 安装使用说明</title>
    <url>/dev-kubectl-krew/</url>
    <content><![CDATA[1. krew简介
Krew itself is a kubectl plugin that is installed and updated via Krew (yes, Krew self-hosts).

Krew 本身是一个 kubectl 的插件，同时也是一个 kubectl 的插件包管理器。Krew 帮助我们发现插件，在我们的机器上安装和管理它们。它类似于 apt、dnf 或 brew 等工具。当前，Krew 上提供了 200 多个 kubectl 插件。
参考文档：

krew - Installing
Krew



2. 安装配置krew2.1. 安装krew2.1.1. MacOS/Linux中通用安装方法(  set -x; cd &quot;$(mktemp -d)&quot; &amp;&amp;  OS=&quot;$(uname | tr &#x27;[:upper:]&#x27; &#x27;[:lower:]&#x27;)&quot; &amp;&amp;  ARCH=&quot;$(uname -m | sed -e &#x27;s/x86_64/amd64/&#x27; -e &#x27;s/\(arm\)\(64\)\?.*/\1\2/&#x27; -e &#x27;s/aarch64$/arm64/&#x27;)&quot; &amp;&amp;  KREW=&quot;krew-$&#123;OS&#125;_$&#123;ARCH&#125;&quot; &amp;&amp;  curl -fsSLO &quot;https://github.com/kubernetes-sigs/krew/releases/latest/download/$&#123;KREW&#125;.tar.gz&quot; &amp;&amp;  tar zxvf &quot;$&#123;KREW&#125;.tar.gz&quot; &amp;&amp;  ./&quot;$&#123;KREW&#125;&quot; install krew)

2.1.2. MacOS中安装方法uname | tr &#x27;[:upper:]&#x27; &#x27;[:lower:]&#x27;wget https://github.com/kubernetes-sigs/krew/releases/download/v0.4.3/krew.yamlwget https://github.com/kubernetes-sigs/krew/releases/download/v0.4.3/krew-darwin_amd64.tar.gztar -xzvf krew-darwin_amd64.tar.gz./krew-darwin_amd64 install --manifest=krew.yaml --archive=krew-darwin_amd64.tar.gz

2.1.3. Linux中安装方法uname | tr &#x27;[:upper:]&#x27; &#x27;[:lower:]&#x27;wget https://github.com/kubernetes-sigs/krew/releases/download/v0.4.3/krew.yamlwget https://github.com/kubernetes-sigs/krew/releases/download/v0.4.3/krew-linux_amd64.tar.gztar -xzvf krew-linux_amd64.tar.gz./krew-linux_amd64 install --manifest=krew.yaml --archive=krew-linux_amd64.tar.gz

2.2. 添加krew到环境变量1、~/.bashrc或者~/.bash_profile中添加PATH
export PATH=&quot;$&#123;PATH&#125;:$&#123;HOME&#125;/.krew/bin&quot;

2、使环境变量生效
source ~/.bashrc

2.3. 验证安装kubectl krew -h

3. 使用krew3.1. 更新本地插件索引kubectl krew update

3.2. 查找插件查找特定插件，比如 neat
kubectl krew search neat

查看所有可获得的插件
kubectl krew search

3.3. 安装插件以安装neat为例
kubectl krew install neatkubectl neat -h

3.4. 查看已安装的插件kubectl brew list

4. 后记除了使用krew安装kubectl插件之外，还有另外一个更加直接的办法安装kubectl插件：下载插件的二进制文件，和kubectl文件放到一起。
以安装neat为例：1、下载并解压neat插件
wget https://github.com/itaysk/kubectl-neat/releases/download/v2.0.3/kubectl-neat_linux_amd64.tar.gztar -xzvf kubectl-neat_linux_amd64.tar.gz

2、和kubectl放到同一个目录
which kubectl # 看到kubectl路径为 /usr/bin/kubectlmv kubectl-neat /usr/bin

3、使用neat插件
kubectl neat -hkubectl get pod test -oyaml | kubectl neat




]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
        <tag>krew</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S配置使用imagePullSecrets</title>
    <url>/dev-k8s-imagepullsecrets/</url>
    <content><![CDATA[1. 前言《Harbor入门篇》一文中，我们已经安装配置好了Harbor。本文中，我们来学习一下怎样在K8S中配置使用imagePullSecrets，从Harbor或者其他私有镜像仓库拉取镜像。
参考文档：

从私有仓库拉取镜像



2. 创建imagePullSecrets创建一个docker-registry类型的secret，名字为harbor-secret
kubectl create secret docker-registry harbor-secret \  --docker-server=harbor.voidking.com \  --docker-username=admin \  --docker-password=Harbor12345

3. 使用imagePullSecretsapiVersion: v1kind: Podmetadata:  name: testpodspec:  containers:  - name: busybox    image: harbor.voidking.com/voidking/busybox:1.31    command:     - sleep    - &quot;3600&quot;  imagePullSecrets:  - name: harbor-secret

4. 给pod添加默认imagePullSecrets上面的配置，已经可以正常从harbor镜像仓库拉取镜像了。但是，每个pod都需要指定一下imagePullSecrets，也是比较麻烦。这里我们可以在命名空间默认sa中添加imagePullSecrets，这样我们就不用在pod中指定imagePullSecrets了，创建pod时会自动注入。
kubectl patch serviceaccount default \  -p &quot;&#123;\&quot;imagePullSecrets\&quot;: [&#123;\&quot;name\&quot;: \&quot;docker-secret\&quot;&#125;]&#125;&quot; \  -n &lt;your-namespace&gt;

5. 全局配置imagePullSecrets如果新增了namespace，那么这个namespace就需要单独添加一次imagePullSecrets，而且这个namespace的sa也需要添加imagePullSecrets。
这里可以使用imagepullsecret-patcher来简化我们的工作，参考文档：

How to use imagePullSecrets cluster-wide??
Kubernetes cluster-wide access to private container registry with imagepullsecret-patcher
imagepullsecret-patcher
imagepullsecret-patcher deploy-example

1、获取 dockerconfigjson
# kubectl get secret harbor-secret -oyamlkubectl create secret docker-registry harbor-secret \  --docker-server=harbor.voidking.com \  --docker-username=admin \  --docker-password=Harbor12345 \  --dry-run=client -oyaml

获取到 dockerconfigjson ，填入到下一步的 deployment.yaml 文件中。
2、准备资源清单

namespace.yaml，定义namespace
rbac.yaml，定义sa权限
deployment，定义imagepullsecret-patcher服务

namespace.yaml 内容如下：
apiVersion: v1kind: Namespacemetadata:  name: imagepullsecret-patcher

rbac.yaml 内容如下：
apiVersion: v1kind: ServiceAccountmetadata:  name: imagepullsecret-patcher  namespace: imagepullsecret-patcher---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:  labels:    k8s-app: imagepullsecret-patcher  name: imagepullsecret-patcherrules:- apiGroups:  - &quot;&quot;  resources:  - secrets  - serviceaccounts  verbs:  - list  - patch  - create  - get  - delete- apiGroups:  - &quot;&quot;  resources:  - namespaces  verbs:  - list  - get---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata:  name: imagepullsecret-patcherroleRef:  apiGroup: rbac.authorization.k8s.io  kind: ClusterRole  name: imagepullsecret-patchersubjects:  - kind: ServiceAccount    name: imagepullsecret-patcher    namespace: imagepullsecret-patcher

deployment.yaml 内容如下：
apiVersion: v1kind: Secrettype: kubernetes.io/dockerconfigjsonmetadata:  name: image-pull-secret-src  namespace: imagepullsecret-patcherdata:  .dockerconfigjson: eyJhdXRocyI6eyJoYXJib3Iudm9pZGtpbmcuY29tIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IkhhcmJvcjEyMzQ1IiwiYXV0aCI6IllXUnRhVzQ2U0dGeVltOXlNVEl6TkRVPSJ9fX0=---apiVersion: apps/v1kind: Deploymentmetadata:  name: imagepullsecret-patcher  namespace: imagepullsecret-patcher  labels:    name: imagepullsecret-patcherspec:  replicas: 1  selector:    matchLabels:      name: imagepullsecret-patcher  template:    metadata:      labels:        name: imagepullsecret-patcher    spec:      automountServiceAccountToken: true      serviceAccountName: imagepullsecret-patcher      containers:        - name: imagepullsecret-patcher          image: &quot;quay.io/titansoft/imagepullsecret-patcher:v0.14&quot;          env:            - name: CONFIG_FORCE              value: &quot;true&quot;            - name: CONFIG_DEBUG              value: &quot;false&quot;            - name: CONFIG_ALLSERVICEACCOUNT              value: &quot;true&quot;            - name: CONFIG_DOCKERCONFIGJSONPATH              value: &quot;/app/secrets/.dockerconfigjson&quot;            - name: CONFIG_SECRETNAME              value: &quot;harbor-secret&quot;          volumeMounts:            - name: src-dockerconfigjson              mountPath: &quot;/app/secrets&quot;              readOnly: true          resources:            requests:              cpu: 0.1              memory: 15Mi            limits:              cpu: 0.2              memory: 30Mi      volumes:        - name: src-dockerconfigjson          secret:             secretName: image-pull-secret-src

其中dockerconfigjson改成上一步中获取到的dockerconfigjson，CONFIG_SECRETNAME变量的value改成期望的secret名称。
3、安装imagepullsecret-patcher
kubectl apply -f namespace.yamlkubectl apply -f rbac.yamlkubectl apply -f deployment.yaml

4、查看安装
kubectl get all -n imagepullsecret-patcherkubectl get sa -n imagepullsecret-patcherkubectl get sa default -n imagepullsecret-patcher -oyaml
可以发现，harbor-secret 已经注入到了sa中。
6. 修改imagePullSecrets有时候，我们需要修改imagePullSecrets，比如修改用户名和密码。这时可以直接修改 image-pull-secret-src 这个secret，修改完成后，imagepullsecret-patcher会自动完成所有namespace下的 harbor-secret 的修改。
kubectl edit secret image-pull-secret-src -n imagepullsecret-patcher

7. 删除imagePullSecrets有时候，我们需要替换imagePullSecrets，比如imagePullSecrets名称发生了变更。这时就需要删除原本的imagePullSecrets。
单个sa删除imagePullSecrets方法：
INDEX=$(kubectl get sa default -n imagepullsecret-patcher -o json | jq &#x27;.imagePullSecrets | map(.name == &quot;harbor-secret&quot;) | index(true)&#x27;)kubectl patch sa default --type=json -p=&quot;[&#123;&#x27;op&#x27;: &#x27;remove&#x27;, &#x27;path&#x27;: &#x27;/imagePullSecrets/$INDEX&#x27;&#125;]&quot; -n imagepullsecret-patcher

批量sa删除imagePullSecrets方法：
#!/bin/bashkubectl get ns | awk &#x27;&#123;print $1&#125;&#x27; | grep -v &quot;NAME&quot; &gt; namespace.txtfor namespace in `cat namespace.txt`;do  INDEX=$(kubectl get sa default -n $namespace -o json | jq &#x27;.imagePullSecrets | map(.name == &quot;harbor-secret&quot;) | index(true)&#x27;)  kubectl patch sa default --type=json -p=&quot;[&#123;&#x27;op&#x27;: &#x27;remove&#x27;, &#x27;path&#x27;: &#x27;/imagePullSecrets/$INDEX&#x27;&#125;]&quot; -n $namespacedone

8. 添加多个imagePullSecrets问：如果有多个镜像仓库需要配置imagePullSecrets，该怎么处理？答：可以把多个imagePullSecrets写入到同一个dockerconfigjson。
8.1. dockerconfigjson解析dockerconfigjson是一个base64加密的字符串，执行
echo &quot;eyJhdXRocyI6eyJoYXJib3Iudm9pZGtpbmcuY29tIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IkhhcmJvcjEyMzQ1IiwiYXV0aCI6IllXUnRhVzQ2U0dGeVltOXlNVEl6TkRVPSJ9fX0=&quot; | base64 -d
结果为：
&#123;&quot;auths&quot;:&#123;&quot;harbor.voidking.com&quot;:&#123;&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;Harbor12345&quot;,&quot;auth&quot;:&quot;YWRtaW46SGFyYm9yMTIzNDU=&quot;&#125;&#125;&#125;

实际上，它的结构和docker login后产生的$HOME/.docker/config.json完全一致。
其中，auth的value也是base64加密的字符串，执行
echo &quot;YWRtaW46SGFyYm9yMTIzNDU=&quot; | base64 -d
结果为：
admin:Harbor12345

8.2. dockerconfigjson拼接知道了dockerconfigjson的结构，那么我们可以知道，如果有多个imagePullSecrets，我们可以自己拼接一个dockerconfigjson明文字符串，然后进行base64加密。
dockerconfigjson1 内容为：
&#123;&quot;auths&quot;:&#123;&quot;harbor.voidking.com&quot;:&#123;&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;Harbor12345&quot;,&quot;auth&quot;:&quot;YWRtaW46SGFyYm9yMTIzNDU=&quot;&#125;&#125;&#125;

dockerconfigjson2 内容为：
&#123;  &quot;auths&quot;: &#123;    &quot;harbor1.voidking.com&quot;: &#123;      &quot;username&quot;: &quot;haojin&quot;,      &quot;password&quot;: &quot;haojin123&quot;,      &quot;auth&quot;: &quot;aGFvamluOmhhb2ppbjEyMwo=&quot;    &#125;  &#125;&#125;

那么 dockerconfigjson1 和 dockerconfigjson2 可以拼接成一个明文dockerconfigjson：
jq -n &#x27;reduce inputs as $i (&#123;&#125;; . * $i)&#x27; dockerconfigjson1 dockerconfigjson2 | jq -c

然后通过base64加密，就是我们需要的加密后的dockerconfigjson。
jq -n &#x27;reduce inputs as $i (&#123;&#125;; . * $i)&#x27; dockerconfigjson1 dockerconfigjson2 | jq -c | base64 | tr -d &#x27;\n&#x27;

参考文档：

Multiple deployments for multiple secrets / private registries?
《jq命令的安装使用》

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>shell</tag>
        <tag>好好学K8S</tag>
        <tag>harbor</tag>
      </tags>
  </entry>
  <entry>
    <title>KubeSphere登录后报错Session Timeout</title>
    <url>/dev-kubesphere-session-timeout/</url>
    <content><![CDATA[1. 问题描述大部分用户能够正常登录和使用ks，但是个别用户（voidking01）登录ks后报错。用户登录ks，正常；登录后查看工作台，正常；查看host集群和只包含host集群的企业空间，正常。
但是，选择一个非host集群或者选择一个包含非host集群的企业空间后，页面就会弹出错误提示：Session timeout or this account is logged in elsewhere, please login again
然后转到登录页面，再次登录，继续弹出上面的错误提示。循环往复。

2. 权限问题？更改用户权限为admin，问题依旧。
重新给用户授权，问题依旧。
3. ks版本问题？ks版本从3.2.1升级到了3.3.1，问题依旧。
4. kube-events组件问题？参考文档登录成功后，总是提示会话超时或此账户在其他登录地方登录，请重新登录
经检查，并没有kube-events失败日志，不是它的问题。
5. ks配置问题？参考文档：

Session Timeout
Set Up External Authentication

阅读ks文档，怀疑是ks配置不对引起的。那就按照官方文档重新配置一下试试。
5.1. host集群1、编辑ks-installer配置文件
kubectl -n kubesphere-system edit cc ks-installer

2、修改authentication部分配置
spec:  authentication:    jwtSecret: &#x27;&#x27;    authenticateRateLimiterMaxTries: 10    authenticateRateLimiterDuration: 10m0s    loginHistoryRetentionPeriod: 168h    maximumClockSkew: 10s    multipleLogin: true    oauthOptions:      accessTokenMaxAge: 1h      accessTokenInactivityTimeout: 30m      identityProviders:      - name: LDAP        type: LDAPIdentityProvider        mappingMethod: auto        provider:          host: 192.168.0.2:389          managerDN: uid=root,cn=users,dc=nas          managerPassword: ********          userSearchBase: cn=users,dc=nas          loginAttribute: uid          mailAttribute: mail

5.2. member集群member集群只要配置jwtSecret即可，和host集群保持一致。详情参考Direct Connection
修改完成，ks pod重建后，问题依旧。
6. 删除用户重建？删除用户，用户重新登录，重新授权，问题依旧。
7. 时钟问题？参考Session Timeout文档，还有一种可能是节点时钟偏差。

The node clock skew affects time-sensitive operations such as validating the expiration time of a user token. You can configure the server time synchronization with an NTP server. MaximumClockSkew can also be set, which defaults to 10 seconds.

分别登录host集群和member集群的apiserver pod，执行date命令查看时间。发现不同集群的时区配置不同，啊哈，大概率是这个问题了！！！
sudo cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

统一所有集群节点时区为Asia/Shanghai，然后重建ks pod。问题依旧。
把maximumClockSkew也调大一些，默认10s，调整到60s试试。问题依旧。
8. 查看日志参考文档恢复主集群对成员集群的访问权限，查看member集群的api-server日志。
kubectl -n kubesphere-system logs ks-apiserver-7c9c9456bd-qv6bs

出现报错信息：
W1104 11:13:28.432457       1 clusterroles.go:117] invalid aggregation role found: cluster-admin, role-template-manage-configmapsW1104 11:13:28.432464       1 clusterroles.go:117] invalid aggregation role found: cluster-admin, role-template-view-secretsW1104 11:13:28.432469       1 clusterroles.go:117] invalid aggregation role found: cluster-admin, role-template-manage-secretsW1104 11:13:28.432475       1 clusterroles.go:117] invalid aggregation role found: cluster-admin, role-template-view-service-accountsW1104 11:13:28.432484       1 clusterroles.go:117] invalid aggregation role found: cluster-admin, role-template-manage-service-accountsE1104 11:13:51.879574       1 upgradeaware.go:401] Error proxying data from backend to client: readfrom tcp 10.244.161.166:9090-&gt;192.168.50.74:34540: write tcp 10.244.161.166:9090-&gt;192.168.50.74:34540: write: connection reset by peer

顺着这个日志，在kubesphere社区找到了一个相同的问题kubesphere使用子账户…会跳转到登录界面

看起来是多集群同步出问题了，看下 host 集群 kube-federation-system 这个 namespace 下的 pod 是否都正常

根据论坛大佬的提示，检查kube-federation-system 这个 namespace 下的pod信息。
kubectl get pod -n kube-federation-systemkubectl logs --tail=100 kubefed-admission-webhook-657959d4d6-2sx5f -n kube-federation-systemkubectl logs --tail=100 kubefed-controller-manager-54fbd87f7f-6t7jq -n kube-federation-systemkubectl logs --tail=100 kubefed-controller-manager-54fbd87f7f-fh9pk -n kube-federation-system

看着也没有什么明显的报错信息。
根据论坛的提示，有两个解决办法：一个是替换kubeconfig，一个是升级kubefed controller。

Update the version of kubefed controller 
fix: controller-manager panic when kubeconfig set filed insecure-skip-tls-verify

这里我们选择升级kubefed controller。
9. 升级kubefed controller1、查看当前kubefed controller版本
kubectl get deployment.apps/kubefed-controller-manager -n kube-federation-system -oyaml| grep image
查看到当前版本v0.8.1
2、查看kubefed controller最新版本访问dockerhub - kubesphere/kubefed找到当前最新版本，也是v0.8.1
莫非不是版本问题？再对比本地镜像和线上镜像的DIGEST，发现它们是不同的。看来是修复bug后，使用了原来的镜像tag。
docker inspect kubesphere/kubefed:v0.8.1 | grep -i id

3、缩容kubefed controller
kubectl scale --replicas=0 deployment.apps/kubefed-controller-manager -n kube-federation-system

4、删除宿主机本地镜像&amp;拉取最新镜像
docker rmi kubesphere/kubefed:v0.8.1 docker pull kubesphere/kubefed:v0.8.1

5、重新拉起kubefed controller
kubectl scale --replicas=1 deployment.apps/kubefed-controller-manager -n kube-federation-system

问题依旧。。。
10. 废弃集群的问题？再次查看kubefed-controller-manager的日志，感觉像是废弃集群引起的问题。
E1104 08:10:15.107249       1 controller.go:512] failed to delete FederatedGlobalRoleBinding &quot;voidking01-platform-regular&quot;: the following clusters were not ready: vk-dev, edge-clusterI1104 08:10:15.702028       1 controller.go:471] Ensuring deletion of FederatedGlobalRoleBinding &quot;voidking01-platform-admin&quot;I1104 08:10:15.702064       1 controller.go:500] Deserializing delete options of FederatedGlobalRoleBinding &quot;voidking01-platform-admin&quot;I1104 08:10:15.702072       1 controller.go:508] Deleting resources managed by FederatedGlobalRoleBinding &quot;voidking01-platform-admin&quot; from member clusters.E1104 08:10:15.702117       1 controller.go:512] failed to delete FederatedGlobalRoleBinding &quot;voidking01-platform-admin&quot;: the following clusters were not ready: vk-dev, edge-cluster

删除废弃的集群后（未就绪的集群），问题解决。
]]></content>
      <categories>
        <category>engineering</category>
        <category>storage</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>存储</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>kubesphere</tag>
        <tag>storageclass</tag>
      </tags>
  </entry>
  <entry>
    <title>KubeSphere中的多租户管理</title>
    <url>/dev-multi-tenancy-in-kubesphere/</url>
    <content><![CDATA[1. Kubernetes 多租户面临的挑战使用 Kubernetes 的过程中，企业和个人用户在资源共享和安全性方面均面临着诸多挑战。首当其冲的就是企业环境中多租户形态该如何定义，租户的安全边界该如何划分。
多租户是一种常见的软件架构，简单概括就是在多用户环境下实现资源共享，并保证各用户间数据的隔离性。
多租户要解决两大问题：逻辑层面的资源隔离；物理资源的隔离。
逻辑层面的资源隔离主要包括 API 的访问控制，针对用户的权限控制。Kubernetes 中的 RBAC 和命名空间 (namespace) 提供了基本的逻辑隔离能力，但在大部分企业环境中并不适用。企业中的租户往往需要跨多个命名空间甚至是多个集群进行资源管理。除此之外，针对用户的行为审计、租户隔离的日志、事件查询也是不可或缺的能力。
物理资源的隔离主要包括节点、网络的隔离，当然也包括容器运行时安全。您可以通过 NetworkPolicy 对网络进行划分，通过 PodSecurityPolicy 限制容器的行为，Kata Containers 也提供了更安全的容器运行时。
为了解决上述问题，KubeSphere 提供了基于 Kubernetes 的多租户管理方案。
参考文档：

KubeSphere 中的多租户
添加现有 Kubernetes 命名空间至 KubeSphere 企业空间



2. KubeSphere 中的多租户简介在 KubeSphere 中企业空间是最小的租户单元，企业空间提供了跨集群、跨项目（即 Kubernetes 中的命名空间）共享资源的能力。企业空间中的成员可以在授权集群中创建项目，并通过邀请授权的方式参与项目协同。
用户是 KubeSphere 的帐户实例，可以被设置为平台层面的管理员参与集群的管理，也可以被添加到企业空间中参与项目协同。
多级的权限控制和资源配额限制是 KubeSphere 中资源隔离的基础，奠定了多租户最基本的形态。
3. KubeSphere 中的逻辑隔离与 Kubernetes 相同，KubeSphere 通过 RBAC 对用户的权限加以控制，实现逻辑层面的资源隔离。
KubeSphere 中的权限控制分为平台、企业空间、项目三个层级，通过角色来控制用户在不同层级的资源访问权限。

平台角色：主要控制用户对平台资源的访问权限，如集群的管理、企业空间的管理、平台用户的管理等。
企业空间角色：主要控制企业空间成员在企业空间下的资源访问权限，如企业空间下项目、DevOps 项目的管理等。
项目角色：主要控制项目下资源的访问权限，如工作负载的管理、流水线的管理等。

4. KubeSphere 中的物理隔离
KubeSphere 中可以针对企业空间和项目设置网络隔离策略
KubeSphere 提供了针对用户的操作审计
KubeSphere 完整的认证鉴权链路如下图所示，可以通过 OPA 拓展 Kubernetes 的 RBAC 规则。


5. KubeSphere 多租户配置实践5.1. 明确概念
集群：K8S集群。
企业空间：业务管理基本单元。
公司部门：公司内部的部门和KubeSphere部门没有必然联系。
KubeSphere部门：企业空间中权限划分的单位，每个企业空间包含多个KubeSphere部门，每个KubeSphere部门包含多个用户，方便批量授权。
KubeSphere用户：每个公司成员，对应一个KubeSphere用户，一个KubeSphere用户可以属于多个KubeSphere部门。
项目：项目对应k8s中的namespace，一个项目可以同时分配给不同的KubeSphere部门。

集群和企业空间，是多对多的关系。一个企业空间可以包含多个集群，一个集群可以被多个企业空间包含。
KubeSphere中每个项目只能添加到一个企业空间，因此企业空间按照业务来划分，作为业务管理的基本单位。
企业空间作为操作kubesphere的唯一入口，在企业空间授权一个项目的管理权限后，如果用户通过集群管理找到一个项目，是没有权限的。
每个企业空间，建议设置1-3个管理员。管理员负责本企业空间的具体权限管理，例如添加管理员、新建部门、给新成员授权等
KubeSphere部门划分的目的，是为了项目的权限管理，因此需要先对集群和项目权限进行规划。通过KubeSphere部门，授权可以精确到每个项目和每个人。
5.2. 多租户管理操作流程1、创建企业空间，并关联集群
2、添加现有项目到企业空间
3、创建部门，针对部门精细化授权
4、添加用户到部门
5、入口都使用企业空间，不要使用集群管理
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>kubesphere</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中安装配置KubeSphere</title>
    <url>/dev-k8s-kubesphere/</url>
    <content><![CDATA[1. KubeSphere简介
KubeSphere 愿景是打造一个以 Kubernetes 为内核的云原生分布式操作系统，它的架构可以非常方便地使第三方应用与云原生生态组件进行即插即用（plug-and-play）的集成，支持云原生应用在多云与多集群的统一分发和运维管理。

简单来说，KubeSphere（下文简称ks）就是一个容器管理平台，可以图形化管理多个K8S集群。
相关链接：

KubeSphere官网
KubeSphere学习视频
KubeSphere概述



2. KubeSphere核心功能KubeSphere 为用户提供构建企业级 Kubernetes 环境所需的多项功能，例如多云与多集群管理、Kubernetes 资源管理、DevOps、应用生命周期管理、微服务治理（服务网格）、日志查询与收集、服务与网络、多租户管理、监控告警、事件与审计查询、存储管理、访问权限控制、GPU 支持、网络策略、镜像仓库管理以及安全管理等。
3. KubeSphere核心组件KubeSphere核心组件包括：

ks-apiserver：是整个集群管理的 API 接口和集群内部各个模块之间通信的枢纽，负责集群安全控制。
ks-console：提供 KubeSphere 的控制台服务，用户可以通过控制台进行各种操作和管理。
ks-controller-manager：实现业务逻辑，例如创建企业空间时为其创建对应的权限，或创建服务策略时生成对应的 Istio 配置等。
kubesphere-devops-system：KubeSphere DevOps 系统。
kubesphere-monitoring-system：KubeSphere 监控系统。
kubesphere-logging-system：KubeSphere 日志系统。
kubesphere-alerting-system：KubeSphere 告警和通知。
istio-system：KubeSphere 服务网格。

4. 安装准备参考在 Kubernetes 上安装 KubeSphere - 准备工作，确认环境满足ks安装需求。
4.1. K8S版本需求计划安装kubesphere-v3.3.0版本，需要确认 Kubernetes 版本必须为：v1.19.x，v1.20.x，v1.21.x，v1.22.x 或 v1.23.x（实验性支持）。
4.2. 资源需求CPU &gt; 1 核；内存 &gt; 2 G。CPU 必须为 x86_64，暂时不支持 Arm 架构的 CPU。
4.3. StorageClass需求Kubernetes 集群已配置默认 StorageClass（请使用 kubectl get sc 进行确认）。这一条是最重要的，因为k8s集群默认并不会配置storageclass，需要我们自己单独配置。storage安装配置方法参考《K8S中安装配置StorageClass》
5. 安装KubeSphere参考在 Kubernetes 上安装 KubeSphere - 概述，安装kubesphere-v3.3.0版本
1、下载manifests
wget https://github.com/kubesphere/ks-installer/releases/download/v3.3.0/kubesphere-installer.yamlwget https://github.com/kubesphere/ks-installer/releases/download/v3.3.0/cluster-configuration.yaml

2、修改集群配置 cluster-configuration.yamlks-apiserver和ks-controller-manager的资源调整示例如下：
spec:  common:    core:      console:        enableMultiLogin: true        port: 30880      apiserver:        resources:          requests:            cpu: 100m            memory: 400Mi          limits:            cpu: &quot;2&quot;            memory: 8Gi      controllerManager:        resources:          requests:            cpu: 100m            memory: 400Mi          limits:            cpu: &quot;2&quot;            memory: 8Gi    minio:      volumeSize: 200Gi # Minio PVC size.

因为ks-apiserver和ks-controller-manager容易OOM，所以内存上限设置高一些。
kubesphere的一些功能（数据备份、日志存储、应用数据存储）需要依赖minio，因此把minio的存储调大一些。
opensearch一般用不到，opensearch.enabled 改成false。
3、提交资源清单到k8s集群
kubectl apply -f kubesphere-installer.yamlkubectl apply -f cluster-configuration.yaml

4、查看安装过程
k describe pod/ks-installer-xxx-xxx -n kubesphere-systemkubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l &#x27;app in (ks-install, ks-installer)&#x27; -o jsonpath=&#x27;&#123;.items[0].metadata.name&#125;&#x27;) -f

5、查看ks状态
kubectl get all -n kubesphere-systemkubectl get svc/ks-console -n kubesphere-system

6. StorageClass问题排查ks的redis没有ready，一直处于pending状态，查看详情
kubectl describe pod/redis-d744b7468-45sh4 -n kubesphere-system
报错：0/x nodes are available: x pod has unbound immediate PersistentVolumeClaims.
查看nfs-client-provisioner日志
kubectl logs --tail=100 nfs-client-provisioner-7975f9b954-7fw5l
报错：provision “kubesphere-system/redis-pvc” class “nfs-storage”: unexpected error getting claim reference: selfLink was empty, can’t make reference
这是因为，1.20.x之后的k8s版本，selflink已经弃用了。而nfs-client-provisioner的实现基于selflink，因此报错。
解决办法：apiserver启动时添加参数--feature-gates=RemoveSelfLink=false1、编辑kube-apiserver.yaml
sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
如下修改：
spec:  containers:  - command:    - kube-apiserver    - --...    - --feature-gates=RemoveSelfLink=false

2、重建apiserver pod因为apiserver是static pod，所以在修改完kube-apiserver.yaml后会自动重建。
参考文档：

unexpected error getting claim reference: selfLink was empty, can’t make reference
kubernetes1.20版本 nfs-provisioner报错问题:”selfLink was empty”

7. 验证KubeSphereKS默认对外开放 30880 端口，通过 NodePort (IP:30880) 使用默认帐户和密码 (admin/P@88w0rd) 访问 Web 控制台。http://192.168.56.101:30880/login第一次登录时，会提示修改密码。
8. KubeSphere常用操作8.1. 修改KubeSphere配置如果ks已经安装完成，想要修改ks集群配置的话，可以通过修改 kubesphere-config 和 ks-installer 配置来实现。
kubectl get cm kubesphere-config -n kubesphere-system -oyamlkubectl edit cm kubesphere-config -n kubesphere-system # 例如修改使用集群外部的Prometheuskubectl get clusterconfiguration ks-installer -n kubesphere-system -oyamlkubectl edit clusterconfiguration ks-installer -n kubesphere-system # 例如修改Pod资源
修改ks-installer配置后，相关pod会自动重启（请耐心等待，大概5分钟内会自动重启）。
参考文档：

KubeSphere - LDAP身份提供者
集成您自己的 Prometheus

8.2. 禁用审计日志KubeSphere 审计日志系统默认只接收来自 KubeSphere 的审计日志，同时也可以接收来自 Kubernetes 的审计日志。
用户可以使用以下命令在命名空间 kubesphere-system 中修改 kubesphere-config ConfigMap 中 auditing.enable 的值，停止接收来自 KubeSphere 的审计日志：
kubectl edit cm -n kubesphere-system kubesphere-config

将 auditing.enabled 的值修改为 false，停止接收来自 KubeSphere 的审计日志。
spec:  auditing:    enabled: false

参考文档：接收和自定义审计日志
PS：官方文档给的参数是 auditing.enabled，但是实际在配置文件中看到的是 auditing.enable，建议以配置文件为准。
8.3. 禁用各种组件修改 kubesphere配置：
kubectl edit cm -n kubesphere-system kubesphere-config

禁用 logging、events、alerting、notification
spec:  logging:    enabled: false  events:    enabled: false  alerting:    enabled: false  notification:    enabled: false

参考文档：启用可插拔组件 - 概述
8.4. 修改使用外部Prometheuskubectl edit cm -n kubesphere-system kubesphere-config

monitoring 部分修改为：
monitoring:  endpoint: http://prometheus-operated.kubesphere-monitoring-system.svc:9090

参考文档：集成您自己的 Prometheus
8.5. 安装GrafanaKubeSphere 3.3.0 版本的Grafana需要自行安装。
1、添加helm chart仓库
helm repo add bitnami https://charts.bitnami.com/bitnami

2、获取redis的配置这里选择chart版本9.0.7，app版本10.0.3
helm search repo bitnami/grafana -lhelm pull bitnami/grafana --version 9.0.7tar -xzvf grafana-9.0.7.tgzvim grafana/values.yaml

3、values.yaml 修改配置

global.storageClass 修改为期望的storageclass
admin.user 改为期望的用户名
admin.password 改为期望的密码
service.type 改为NodePort

4、安装grafana
helm install grafana grafana \    -n grafana --create-namespace

5、查看安装
kubectl get all -n grafana

8.6. 重置KubeSphere密码如果忘记了ks的密码，可以通过kubectl命令进行重置。参考文档Reset the Account Password
kubectl patch users &lt;USERNAME&gt; -p &#x27;&#123;&quot;spec&quot;:&#123;&quot;password&quot;:&quot;&lt;YOURPASSWORD&gt;&quot;&#125;&#125;&#x27; --type=&#x27;merge&#x27; &amp;&amp; kubectl annotate users &lt;USERNAME&gt; iam.kubesphere.io/password-encrypted-

8.7. 添加项目到企业空间Kubernetes命名空间就是KubeSphere项目，这些项目可以在 平台管理-&gt;集群管理-&gt;具体集群-&gt;项目 中查看到。
添加现有KubeSphere项目到企业空间：1、以管理员身份登录KubeSphere控制台，转到集群管理页面。点击项目，可以查看在当前集群中运行的所有项目。2、通过 kubectl 创建的命名空间不属于任何企业空间。请点击右侧的三个点，选择分配企业空间。3、在弹出的对话框中，为该项目选择一个企业空间和项目管理员，然后点击确定。4、转到企业空间，可以在项目页面看到该项目已显示。
参考文档：

添加现有 Kubernetes 命名空间至 KubeSphere 企业空间

8.8. 批量添加项目到企业空间kubectl get ns |grep voidking | awk &#x27;&#123;print $1&#125;&#x27; | xargs kubectl patch ns -p &#x27;metadata: &#123;labels: &#123;kubesphere.io/workspace: &quot;voidking&quot;&#125;, annotations: &#123;kubesphere.io/creator: &quot;haojin&quot;&#125;&#125;&#x27;

8.9. 修改KS组件亲和性安装KS时，是不支持配置KS组件的亲和性的。如果自己通过脚本批量修改，会被ks-intaller自动改回，并且不同资源修改的方法是不同的。
下面是一个可行的办法：
1、关闭 ks-intaller
kubectl scale --replicas=0 deployment ks-installer -n kubesphere-system

2、准备亲和性配置 data.yaml
spec:  template:    spec:      nodeSelector:        nodetype: infra      tolerations:        - key: &quot;nodetype&quot;          operator: &quot;Equal&quot;          value: &quot;infra&quot;          effect: &quot;NoSchedule&quot;

3、deployment批量添加节点亲和性
kubectl get deployment -n kubesphere-system -o=jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#x27; | xargs kubectl patch deployment --type=merge --patch-file data.yaml -n kubesphere-systemkubectl get deployment -n kubesphere-monitoring-system -o=jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#x27; | xargs kubectl patch deployment --type=merge --patch-file data.yaml -n kubesphere-monitoring-system


4、准备亲和性配置 data2.yaml
spec:  nodeSelector:    nodetype: infra  tolerations:    - key: &quot;nodetype&quot;      operator: &quot;Equal&quot;      value: &quot;infra&quot;      effect: &quot;NoSchedule&quot;

4、自定义资源添加节点亲和性
kubectl get prometheus k8s -n kubesphere-monitoring-systemkubectl patch prometheus k8s --type=merge --patch-file=data2.yaml -n kubesphere-monitoring-systemkubectl get alertmanager main -n kubesphere-monitoring-systemkubectl patch alertmanager main --type=merge --patch-file=data2.yaml -n kubesphere-monitoring-systemkubectl get notificationmanager -n kubesphere-monitoring-systemkubectl patch notificationmanager/notification-manager --type=merge --patch-file=data2.yaml -n kubesphere-monitoring-system

参考文档：

《kubectl命令——资源对象增删查改篇》
如何配置 cluster-configuration.yaml 将kubesphere安装到指定节点上？

9. KubeSphere问题记录9.1. Could not connect to the container问题描述：配置好nginx，通过域名访问ks正常，但是在ks页面上访问容器终端时报错：
Could not connect to the container. Do you have sufficient privileges?

解决办法：因为页面访问终端需要websocket支持，所以nginx配置中需要添加websocket支持，配置方法参考《Nginx入门篇》。
9.2. Pod详情页没有监控数据问题描述：安装了Prometheus套装后，Pod详情页看不到监控数据。访问获取监控数据的API，看到报错：
error: &quot;execution: found duplicate series for the match group &#123;namespace=&quot;xxx&quot;, pod=&quot;xxx&quot;&#125; on the right hand-side of the operation: [&#123;__name__=&quot;kube_pod_owner&quot;, container=&quot;kube-rbac-proxy-main&quot;, instance=&quot;10.244.92.200:8443&quot;, job=&quot;kube-state-metrics&quot;, namespace=&quot;xxx&quot;, owner_is_controller=&quot;true&quot;, owner_kind=&quot;ReplicaSet&quot;, owner_name=&quot;xxx&quot;, pod=&quot;xxx&quot;, uid=&quot;xxx&quot;&#125;, &#123;__name__=&quot;kube_pod_owner&quot;, container=&quot;kube-rbac-proxy-main&quot;, instance=&quot;10.244.218.131:8443&quot;, job=&quot;kube-state-metrics&quot;, namespace=&quot;act&quot;, owner_is_controller=&quot;true&quot;, owner_kind=&quot;ReplicaSet&quot;, owner_name=&quot;xxx&quot;, pod=&quot;xxx&quot;&#125;];many-to-many matching not allowed: matching labels must be unique on one side&quot;

原因分析：ks自带的Prometheus，和Prometheus套装，两个Prometheus的serviceMonitor存在重合，导致获取了两遍数据。解决办法：删除两个Prometheus的其中一份serviceMonitor。
kubectl get servicemonitor -n kubesphere-monitoring-system -oyaml &gt; servicemonitor.yamlkubectl get servicemonitor -n kubesphere-monitoring-system | awk &#x27;&#123;print $1&#125;&#x27; | grep -v &quot;NAME&quot; | xargs kubectl delete servicemonitor -n kubesphere-monitoring-system

参考文档：pod监控都没有数据展示
10. KubeSphere多集群参考文档：

多集群管理
多集群管理 - 直接连接
Kubernetes 多集群在开源项目 KubeSphere 的应用

10.1. 主集群配置1、设置为主集群
kubectl edit cc ks-installer -n kubesphere-system

修改配置：
multicluster:  clusterRole: host

2、获取jwtSecret
kubectl -n kubesphere-system get cm kubesphere-config -o yaml | grep -v &quot;apiVersion&quot; | grep jwtSecret

10.2. 成员集群配置1、设置为成员集群
kubectl edit cc ks-installer -n kubesphere-system

修改配置：
authentication:  jwtSecret: gfIwilcc0WjNGKJ5DLeksf2JKfcLgTZUmulticluster:  clusterRole: member


其中，authentication.jwtSecret 修改和主集群保持一致；multicluster.clusterRole 修改为member。
10.3. 导入成员集群1、以 admin 身份登录 KubeSphere 控制台，转到集群管理页面点击添加集群。
2、在导入集群页面，输入要导入的集群的基本信息。您也可以点击右上角的编辑模式以 YAML 格式查看并编辑基本信息。编辑完成后，点击下一步。
3、在连接方式，选择直接连接 Kubernetes 集群，复制 kubeconfig 内容并粘贴至文本框。您也可以点击右上角的编辑模式以 YAML 格式编辑的 kubeconfig。请确保 server 地址为该 member 集群可访问的 kube-apiserver 地址（主集群的任何节点都能访问）。
4、点击创建，然后等待集群初始化完成。
]]></content>
      <categories>
        <category>engineering</category>
        <category>storage</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>存储</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>好好学K8S</tag>
        <tag>bitnami</tag>
        <tag>kubesphere</tag>
        <tag>storageclass</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S集群中变更数据存储路径</title>
    <url>/dev-change-data-dir-in-k8s/</url>
    <content><![CDATA[1. 需求描述docker的默认工作目录是/var/lib/docker，会存放镜像文件、容器日志和写入到容器临时目录的文件等，默认挂载在系统盘。
kubelet的默认工作目录是/var/lib/kubelet，会存放volume文件（包括emptyDir volume)、plugin文件等，也是默认挂载在系统盘。
使用kubeadm安装的etcd，默认数据目录是/var/lib/etcd，也是默认挂载在系统盘。
而系统盘一般都不会太大，因此最好把docker工作目录、kubelet工作目录和etcd数据目录更改到数据盘。


2. 操作思路节点分为两类，master节点（多个节点）和worker节点（多个节点）。
单个master节点操作流程：1、master-x禁止调度、驱逐pod2、master-x操作修改docker工作目录、kubelet工作目录3、master-x开放调度4、master-x操作修改etcd数据目录
单个worker节点操作流程：1、worker-x禁止调度、驱逐pod2、worker-x操作修改docker工作目录、kubelet工作目录3、worker-x开放调度
master节点挨个操作，worker节点挨个操作或者分批操作。
3. master节点操作以master-0为例，方便进行描述。
3.1. 禁止调度&amp;驱逐podkubectl drain master-0 --ignore-daemonsets

驱逐master-0节点上的pod，附带效果禁止调度。
3.2. 修改docker工作目录详情参考 《Docker修改工作目录》
3.3. 修改kubelet工作目录详情参考 《kubelet修改工作目录》
3.4. 开放调度kubectl uncordon master-0

3.5. 单个节点修改etcd数据目录详情参考 《etcd修改数据目录》
4. worker节点操作以worker-0为例，方便进行描述。
4.1. 禁止调度&amp;驱逐podkubectl drain worker-0 --ignore-daemonsets
驱逐worker-0节点上的pod，附带效果禁止调度。
4.2. 修改docker工作目录详情参考 《Docker修改工作目录》
4.3. 修改kubelet工作目录详情参考 《kubelet修改工作目录》
4.4. 开放调度kubectl uncordon worker-0







]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：kubeadm重新添加master节点</title>
    <url>/dev-kubeadm-rejoin-master-node/</url>
    <content><![CDATA[1. 需求描述一些情况下，我们需要先把某个master节点从k8s集群中摘除掉，进行某些操作之后再加入到集群。或者，我们需要先把某个master节点从k8s集群中摘除掉，然后把这个节点加入到另外一个k8s集群。
本文中，我们就来研究一下从k8s集群中摘除一个master节点再重新加入集群的方法。（worker节点太简单，不再单独讨论）


2. 思路1、禁止调度&amp;驱逐pod2、k8s集群层面删除节点3、kubeadm reset还原配置4、kubeadm join加入集群
3. 具体操作以master-0为例，方便进行描述。
3.1. 禁止调度&amp;驱逐podkubectl drain master-0 --ignore-daemonsets

驱逐master-0节点上的pod，附带效果禁止调度。
3.2. 删除节点kubectl delete nodes master-0

把master-0节点从集群中删除。
3.3. 还原kubeadm的更改master-0节点上执行：
sudo kubeadm reset
可以还原通过 kubeadm init 或者 kubeadm join 操作对master-0主机所作的更改。
执行后，master-0基本还原到了执行kubeadm init之前的状态。kubelet数据被清除了，etcd数据也被清除了。
如果是外部的etcd，那么kubeadm reset不会删除 etcd 中的数据，可以使用etcd客户端清理
etcdctl del &quot;&quot; --prefix

详情参考kubeadm reset
3.4. 移除节点etcd member虽然通过kubeadm reset清理了etcd的数据，但是，etcd集群里依然有这个节点的信息。因此，需要移除master-0节点的etcd member，否则再次加入k8s集群时时会卡在 [check-etcd] 这个步骤。
1、查看etcd版本，安装对应版本的etcdctl
sudo cat /etc/kubernetes/manifests/etcd.yaml | grep imagewget https://github.com/etcd-io/etcd/releases/download/v3.5.0/etcd-v3.5.0-linux-amd64.tar.gztar -xzvf etcd-v3.5.0-linux-amd64.tar.gzsudo cp etcd-v3.5.0-linux-amd64/etcdctl /usr/local/binsudo chmod a+x /usr/local/bin/etcdctlsudo ln -s /usr/local/bin/etcdctl /usr/bin/

2、查看etcd节点状态
sudo etcdctl  --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/peer.crt --key=/etc/kubernetes/pki/etcd/peer.key member listsudo etcdctl  --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/peer.crt --key=/etc/kubernetes/pki/etcd/peer.key --endpoints=&quot;https://192.168.56.101:2379,https://192.168.56.102:2379,https://192.168.56.103:2379&quot; endpoint health

3、删除master-0节点
sudo etcdctl  --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/peer.crt --key=/etc/kubernetes/pki/etcd/peer.key member remove xxxxx

3.5. 加入k8s集群1、使用另外的master节点（例如master-1）生成新的join命令和certificates
sudo kubeadm token listsudo kubeadm token create --print-join-commandsudo kubeadm init phase upload-certs --upload-certs

2、master-0加入k8s集群
sudo kubeadm join 192.168.56.200:9443 --token xxxxxx.xxxxxx \    --discovery-token-ca-cert-hash sha256:xxxxx \    --control-plane --certificate-key xxxxx

3、查看集群状态
kubectl get nodeskubectl get pods --all-namespaces





]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>kubeadm</tag>
      </tags>
  </entry>
  <entry>
    <title>etcd修改数据目录</title>
    <url>/dev-etcd-data-dir/</url>
    <content><![CDATA[1. 需求描述使用kubeadm安装的etcd，默认数据目录是/var/lib/etcd，默认挂载在系统盘。
而系统盘一般都不会太大，因此最好把etcd的数据目录更改到数据盘。
本文中，我们会把etcd的数据目录从/var/lib/etcd改到/data/etcd，其中/data目录挂载了数据盘。


2. 思路想到两个方法：

修改配置法：拷贝数据到新目录，修改工作目录配置到新目录。
软链法：拷贝数据到新目录，使用新目录的软链替换原来的工作目录。不推荐，不知道有没有什么隐藏坑。

etcd是集群，为了保证etcd服务可用，需要挨个节点停服操作，完成一个节点再进行下一个。
3. 修改配置法1、停止etcd
sudo docker ps | grep etcdsudo mv /etc/kubernetes/manifests/etcd.yaml ~/sudo docker ps | grep etcdsudo etcdctl  --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/peer.crt --key=/etc/kubernetes/pki/etcd/peer.key --endpoints=&quot;https://192.168.56.101:2379,https://192.168.56.102:2379,https://192.168.56.103:2379&quot; endpoint health

2、修改etcd数据目录编辑~/etcd.yaml，etcd-data的path从/var/lib/etcd改为/data/etcd
spec:  volumes:  - hostPath:      path: /etc/kubernetes/pki/etcd      type: DirectoryOrCreate    name: etcd-certs  - hostPath:      path: /data/etcd      type: DirectoryOrCreate    name: etcd-data

3、拷贝数据
sudo mkdir -p /data/etcdsudo cp -rf /var/lib/etcd/. /data/etcdsudo mv /var/lib/etcd /var/lib/etcd.old

4、启动etcd&amp;查看etcd状态
sudo mv ~/etcd.yaml /etc/kubernetes/manifests/sudo docker ps | grep etcdsudo etcdctl  --cacert=/etc/kubernetes/pki/etcd/ca.crt \    --cert=/etc/kubernetes/pki/etcd/peer.crt --key=/etc/kubernetes/pki/etcd/peer.key \    --endpoints=&quot;https://192.168.56.101:2379,https://192.168.56.102:2379,https://192.168.56.103:2379&quot; \    endpoint health

4. 软链法可以参考 《Docker修改工作目录》
5. kubeadm指定etcd数据目录5.1. 修改etcd数据目录使用kubeadm安装的etcd，etcd的数据目录是在kubeadm.conf文件中指定的。
根据kubeadm.conf中的配置，新加入的master节点上会生成/etc/kubernetes/manifests/etcd.yaml文件，然后etcd就会作为static pod运行。
对于已经安装好的k8s集群，可以通过kubectl修改集群中kubeadm的配置
kubectl edit cm kubeadm-config -n kube-system 

其中，etcd数据目录的修改方法为：etcd.local.dataDir 变量修改为 /data/etcd
修改之后，并不会马上生效，有新的master节点加入，才会使用这份新的etcd数据目录配置。
5.2. 初始化前指定etcd数据目录在执行kubeadm init之前，在kubeadm.conf文件中指定数据目录。
etcd:  local:    dataDir: /data/etcd





]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>cloudnative</tag>
        <tag>etcd</tag>
      </tags>
  </entry>
  <entry>
    <title>kubelet修改工作目录</title>
    <url>/dev-kubelet-root-dir/</url>
    <content><![CDATA[1. 需求描述kubelet的默认工作目录（存储目录）是/var/lib/kubelet，会存放volume文件（包括emptyDir volume)、plugin文件等，默认挂载在系统盘。
而系统盘一般都不会太大，因此最好把kubelet工作目录更改到数据盘。
本文中，我们会把docker的工作目录从/var/lib/kubelet改到/data/kubelet，其中/data目录挂载了数据盘。


2. 思路想到两个方法：

修改配置法：拷贝数据到新目录，修改工作目录配置到新目录。
软链法：拷贝数据到新目录，使用新目录的软链替换原来的工作目录。不推荐，不知道有没有什么隐藏坑。

修改kubelet配置之前，为了保证不影响节点上的服务，最好先对节点操作禁止调度和驱逐。
3. 修改配置法1、停止kubelet
systemctl stop kubelet

2、拷贝kubelet工作目录数据文件到新路径
mkdir -p /data/kubeletcp -rf /var/lib/kubelet/pods /data/kubelet/cp -rf /var/lib/kubelet/pod-resources /data/kubelet/mv /var/lib/kubelet/pods&#123;,.old&#125;mv /var/lib/kubelet/pod-resources&#123;,.old&#125;

注意，以下文件和目录一定要保留在老路径，不要移动和删除

/var/lib/kubelet/config.yaml
/var/lib/kubelet/kubeadm-flags.env
/var/lib/kubelet/pki
/var/lib/kubelet/device-plugins

3、添加或修改 /etc/sysconfig/kubelet 配置文件，添加root-dir参数
KUBELET_EXTRA_ARGS=&quot;--root-dir=/data/kubelet&quot;

PS：如果是ubuntu系统，则要修改 /etc/default/kubelet 配置文件
4、重启kubelet
systemctl daemon-reload &amp;&amp; systemctl restart kubeletsystemctl status kubelet

5、确认工作目录
ps -aux | grep kubelet | grep root-dir

PS：如果kubelet启动失败，可以通过查看kubelet详细日志进行排查。
journalctl -xu kubelet -r

6、清理旧工作目录（可选）
rm /var/lib/kubelet/pods.old -rfrm /var/lib/kubelet/pod-resources.old -rf

4. 软链法1、停止kubelet
systemctl stop kubelet

2、拷贝kubelet数据文件到新路径
mkdir -p /data/kubeletcp -rf /var/lib/kubelet/. /data/kubeletmv /var/lib/kubelet /var/lib/kubelet.old

3、创建软链
ln -s /data/kubelet /var/lib/kubelet

4、启动kubelet
systemctl daemon-reload &amp;&amp; systemctl restart kubeletsystemctl status kubelet

5、确认工作目录
ps -aux | grep kubelet | grep root-dir

6、清理旧工作目录（可选）
rm /var/lib/kubelet.old -rf

5. kubeadm指定kubelet工作目录与其临渴掘井，不如未雨绸缪。能否在使用kubeadm部署k8s集群的时候，直接指定好kubelet的工作目录？必须是可以的。
在执行kubeadm init之前，修改kubeadm.conf文件，添加kubeletExtraArgs字段。
apiVersion: kubeadm.k8s.io/v1beta3kind: InitConfigurationnodeRegistration:    kubeletExtraArgs:        root-dir: &quot;/data/kubelet&quot;

参考文档：

kubeadm Configuration - NodeRegistrationOptions

6. kubeadm部署kubelet原理本节属于扩展阅读。在修改kubelet工作目录时，走了很多弯路。其实如果熟悉kubeadm部署kubelet的原理，会发现修改kubelet工作目录是很简单的。好好读文档！好好读文档！好好读文档！
参考文档：

使用 kubeadm 配置集群中的每个 kubelet
kubelet源码分析——kubelet简介与启动

6.1. kubeadm init 时的工作流程当调用 kubeadm init 时，kubelet 的配置会被写入磁盘 /var/lib/kubelet/config.yaml， 并上传到集群 kube-system 命名空间的 kubelet-config ConfigMap。 kubelet 配置信息也被写入 /etc/kubernetes/kubelet.conf，其中包含集群内所有 kubelet 的基线配置。 此配置文件指向允许 kubelet 与 API 服务器通信的客户端证书。 这解决了将集群级配置传播到每个 kubelet 的需求。
针对为特定实例提供配置细节， kubeadm 的解决方法是将环境文件写入 /var/lib/kubelet/kubeadm-flags.env，其中包含了一个标志列表KUBELET_KUBEADM_ARGS
将这两个文件编组到磁盘后，如果使用 systemd，则 kubeadm 尝试运行以下两个命令：
systemctl daemon-reload &amp;&amp; systemctl restart kubelet

6.2. kubeadm join 时的工作流程当运行 kubeadm join 时，kubeadm 使用 Bootstrap Token 证书执行 TLS 引导，该引导会获取一份证书， 该证书需要下载 kubelet-config ConfigMap 并把它写入 /var/lib/kubelet/config.yaml 中。 动态环境文件的生成方式恰好与 kubeadm init 完全相同。
接下来，kubeadm 运行以下两个命令将新配置加载到 kubelet 中：
systemctl daemon-reload &amp;&amp; systemctl restart kubelet

在 kubelet 加载新配置后，kubeadm 将写入 /etc/kubernetes/bootstrap-kubelet.conf KubeConfig 文件中， 该文件包含 CA 证书和引导程序令牌。 kubelet 使用这些证书执行 TLS 引导程序并获取唯一的凭据，该凭据被存储在 /etc/kubernetes/kubelet.conf 中。
当 /etc/kubernetes/kubelet.conf 文件被写入后，kubelet 就完成了 TLS 引导过程。 Kubeadm 在完成 TLS 引导过程后将删除 /etc/kubernetes/bootstrap-kubelet.conf 文件。
6.3. kubelet 的 systemd drop-in 文件通过 kubeadm DEB 包 或者 RPM 包 安装的配置文件被写入 /etc/systemd/system/kubelet.service.d/10-kubeadm.conf 并由 systemd 使用。它对原来的 RPM 版本 kubelet.service 或者 DEB 版本 kubelet.service 做了增强。
10-kubeadm.conf 示例内容如下：
[Service]Environment=&quot;KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf&quot;Environment=&quot;KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml&quot;# 这是 &quot;kubeadm init&quot; 和 &quot;kubeadm join&quot; 运行时生成的文件，# 动态地填充 KUBELET_KUBEADM_ARGS 变量EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env# 这是一个文件，用户在不得已下可以将其用作替代 kubelet args。# 用户最好使用 .NodeRegistration.KubeletExtraArgs 对象在配置文件中替代。# KUBELET_EXTRA_ARGS 应该从此文件中获取。EnvironmentFile=-/etc/default/kubeletExecStart=ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS

参数说明：

用于 TLS 引导程序的 KubeConfig 文件为 /etc/kubernetes/bootstrap-kubelet.conf， 但仅当 /etc/kubernetes/kubelet.conf 不存在时才能使用。
具有唯一 kubelet 标识的 KubeConfig 文件为 /etc/kubernetes/kubelet.conf。
包含 kubelet 的组件配置的文件为 /var/lib/kubelet/config.yaml。
KUBELET_KUBEADM_ARGS 来源于 /var/lib/kubelet/kubeadm-flags.env ，
KUBELET_EXTRA_ARGS 来源于 /etc/default/kubelet（对于 DEB），或者 /etc/sysconfig/kubelet（对于 RPM）。KUBELET_EXTRA_ARGS 在标志链中排在最后，并且在设置冲突时具有最高优先级。

6.4. 指定kubelet工作目录通过上面的原理我们了解到，对于kubelet，可以在KUBELET_KUBEADM_ARGS或者KUBELET_EXTRA_ARGS标志列表中添加root-dir标志，指定kubelet的工作目录。
方法一：/var/lib/kubelet/kubeadm-flags.env 中添加标志
KUBELET_KUBEADM_ARGS=&quot;--root-dir=/data/kubelet&quot;

方法二：/etc/sysconfig/kubelet 中添加标志
KUBELET_EXTRA_ARGS=&quot;--root-dir=/data/kubelet&quot;

添加标志后，重启kubelet即可。
也可以在部署k8s集群时一步到位指定kubelet工作目录，具体操作方法参考【kubeadm指定kubelet工作目录】一节。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>cloudnative</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker修改工作目录</title>
    <url>/dev-docker-data-root-dir/</url>
    <content><![CDATA[1. 需求描述docker的默认工作目录（存储目录）是/var/lib/docker，会存放镜像文件、容器日志和写入到容器临时目录的文件等，默认挂载在系统盘。
而系统盘一般都不会太大，因此最好把docker工作目录更改到数据盘。
本文中，我们会把docker的工作目录从/var/lib/docker改到/data/docker，其中/data目录挂载了数据盘。


2. 思路想到两个方法：

修改配置法：拷贝数据到新目录，修改工作目录配置到新目录。
软链法：拷贝数据到新目录，使用新目录的软链替换原来的工作目录。不推荐，不知道有没有什么隐藏坑。

3. 修改配置法参考文档How to change docker root data directory
0、关闭kubelet（可选操作，只针对k8s节点）
systemctl stop kubelet

1、关闭docker
systemctl stop docker
Warning: Stopping docker.service, but it can still be activated by: docker.socket该警告意味着：如果你试图连接到docker socket，而docker服务没有运行，系统将自动启动docker。这是因为除了docker.service单元文件，还有一个docker.socket单元文件，用于套接字激活。
这里可以使用另外一个命令关闭docker，禁止套接字激活。
systemctl stop docker.socket

2、更改docker数据存储路径
vim /etc/docker/daemon.json

添加：
&#123;    &quot;data-root&quot;: &quot;/data/docker&quot;&#125;

3、拷贝docker数据文件到新路径
mkdir -p /data/dockercp -rf /var/lib/docker/. /data/dockermv /var/lib/docker /var/lib/docker.old

4、重启docker&amp;确认工作目录
systemctl start dockerdocker info | grep Dir

5、重启kubelet（可选操作，只针对k8s节点）
systemctl start kubelet

4. 软链法0、关闭kubelet（可选操作，只针对k8s节点）
systemctl stop kubelet

1、关闭docker
systemctl stop dockersystemctl stop docker.socket

2、拷贝docker数据文件到新路径
mkdir -p /data/dockercp -rf /var/lib/docker/. /data/dockermv /var/lib/docker /var/lib/docker.old

3、创建软链
ln -s /data/docker /var/lib/docker

4、重启docker&amp;确认工作目录
systemctl start dockerdocker info | grep Dir

5、重启kubelet（可选操作，只针对k8s节点）
systemctl start kubelet




]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>cloudnative</tag>
      </tags>
  </entry>
  <entry>
    <title>kubeadm意外配置代理引发的问题</title>
    <url>/dev-kubeadm-proxy-problem/</url>
    <content><![CDATA[1. 问题描述《ansible+kubeadm部署K8S高可用集群》一文中，我们安装完成了K8S高可用集群。表面看一切正常，跑测试pod也没有问题。
但是在《K8S中安装Milvus》时，就出现了问题。
helm install milvus-operator \  -n milvus-operator --create-namespace \  milvus-operator-0.6.5.tgzkubectl get -n milvus-operator deploy/milvus-operatorkubectl -n milvus-operator logs job/milvus-operator-checkerkubectl describe pod/milvus-operator-5dbf664f8b-24hc9 -n milvus-operator

报错：
Warning  FailedMount  82s (x13 over 11m)   kubelet            MountVolume.SetUp failed for volume &quot;cert&quot; : secret &quot;milvus-operator-webhook-cert&quot; not foundWarning  FailedMount  27s (x5 over 9m36s)  kubelet            Unable to attach or mount volumes: unmounted volumes=[cert], unattached volumes=[cert kube-api-access-xnkbb]: timed out waiting for the condition

从报错看，像是cert-manager的问题。


2. cert-manager问题排查参考Verifying the Installation，验证cert-manager-webhook是否正常
kubectl get pods --namespace cert-managercat &lt;&lt;EOF &gt; test-resources.yamlapiVersion: v1kind: Namespacemetadata:  name: cert-manager-test---apiVersion: cert-manager.io/v1kind: Issuermetadata:  name: test-selfsigned  namespace: cert-manager-testspec:  selfSigned: &#123;&#125;---apiVersion: cert-manager.io/v1kind: Certificatemetadata:  name: selfsigned-cert  namespace: cert-manager-testspec:  dnsNames:    - example.com  secretName: selfsigned-cert-tls  issuerRef:    name: test-selfsignedEOFkubectl apply -f test-resources.yamlkubectl describe certificate -n cert-manager-testkubectl delete -f test-resources.yaml

报错：
Error from server (InternalError): error when creating &quot;test-resources.yaml&quot;: Internal error occurred: failed calling webhook &quot;webhook.cert-manager.io&quot;: failed to call webhook: Post &quot;https://cert-manager-webhook.cert-manager.svc:443/mutate?timeout=10s&quot;: proxyconnect tcp: read tcp 192.168.56.102:33020-&gt;192.168.56.1:7890: read: connection reset by peerError from server (InternalError): error when creating &quot;test-resources.yaml&quot;: Internal error occurred: failed calling webhook &quot;webhook.cert-manager.io&quot;: failed to call webhook: Post &quot;https://cert-manager-webhook.cert-manager.svc:443/mutate?timeout=10s&quot;: proxyconnect tcp: read tcp 192.168.56.102:33022-&gt;192.168.56.1:7890: read: connection reset by peer

看到这个报错，我就恍然大明白：肯定是因为代理引起的问题！！！
《ansible+kubeadm部署K8S高可用集群》一文中，配置了上网代理和docker代理：
export http_proxy=http://192.168.56.1:7890export https_proxy=http://192.168.56.1:7890export no_proxy=127.0.0.1,localhost,192.168.56.0/24,10.96.0.0/12,10.244.0.0/16,172.31.0.0/16

3. 取消代理既然是代理引起的问题，那就把它们都取消试试。
1、取消上网代理，问题依旧2、取消docker代理，问题依旧
啊嘞，什么鬼？代理都取消了，问题依然没有解决，还有哪里有配置代理吗？
4. kube-proxy考虑到K8S集群中service，主要是通过kube-proxy组件实现的。查看kube-proxy配置，发现环境变量中确实有代理配置，怀疑是在kubeadm安装集群时从master宿主机继承的。
kubectl get daemonset.apps/kube-proxy -n kube-system -oyaml

spec:  containers:  - command:    - /usr/local/bin/kube-proxy    - --config=/var/lib/kube-proxy/config.conf    - --hostname-override=$(NODE_NAME)    env:    - name: NODE_NAME      valueFrom:        fieldRef:          apiVersion: v1          fieldPath: spec.nodeName    - name: http_proxy      value: http://192.168.56.1:7890    - name: https_proxy      value: http://192.168.56.1:7890    - name: no_proxy      value: 127.0.0.1,localhost,192.168.56.0/24,10.96.0.0/12,10.244.0.0/16,172.31.0.0/16

kubectl edit daemonset.apps/kube-proxy -n kube-system
去掉kube-proxy中关于代理配置的环境变量，问题依然没有解决。。。
5. kubeadm proxykubeadm安装集群时，除了会把代理配置的环境变量放到kube-proxy，会不会也把代理配置的环境变量放到其他组件？经查，还真有！！！除了kube-proxy，还有kube-apiserver、kube-controller-manager、kube-scheduler
取消代理配置的办法：/etc/kubernetes/manifests/目录中，分别编辑这些组件的yaml文件，删掉代理配置的环境变量。
问题终于解决，milvus顺利安装成功，感动哭了。。。
参考文档：

kubeadm init should provide a HTTP_PROXY configure

6. no_proxy疑问虽然代理问题解决了，但是还有一个疑问：我们明明给192.168.56.0/24配置了no_proxy，为啥还是走的代理？这是因为，docker并不能处理CIDR，k8s核心组件也不能处理CIDR，详情参考《Linux配置网络代理》
7. 卸载集群其实，还有一个更简单更暴力的解决办法：卸载整个K8S集群，然后重装。重装前记得把环境变量中的网络代理取消掉。
ansible all -i hosts -m command -a &quot;sudo kubeadm reset -f&quot;ansible all -i hosts -m command -a &quot;sudo rm -rf /var/lib/cni/&quot;ansible all -i hosts -m command -a &quot;sudo rm -rf /etc/cni/&quot;ansible all -i hosts -m command -a &quot;sudo ifconfig cni0 down&quot;ansible all -i hosts -m command -a &quot;sudo ip link delete cni0&quot;

8. 代理引发的其他问题8.1. KubeSphere无法登录登录时报错：Internal error occurred: failed calling webhook “users.iam.kubesphere.io”: Post “https://ks-controller-manager.kubesphere-system.svc:443/validate-email-iam-kubesphsere-io-v1alpha2?timeout=30s&quot;: proxyconnect tcp: read tcp 192.168.56.102:48188-&gt;192.168.56.1:7890: read: connection reset by peer
同样可以通过本文中的方法进行解决。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>问题排查</tag>
      </tags>
  </entry>
  <entry>
    <title>ansible+kubeadm部署K8S高可用集群</title>
    <url>/dev-ansible-kubeadm/</url>
    <content><![CDATA[1. 需求描述当前有十台主机，主机IP为192.168.56.101-110，用户为voidking，具有sudo权限。现在想要用这十台主机搭建一个K8S集群，版本1.22.15，其中3台作为master，7台作为worker。
参考文档：

利用 kubeadm 创建高可用集群
High Availability Considerations
k8s高可用部署：keepalived + haproxy
使用 Keepalived 和 HAproxy 创建高可用 Kubernetes 集群
利用 kubeadm 创建 kubernetes 的高可用集群

2. 安装思路最简单的方法，我们可以使用kubeadm逐个主机进行安装配置，参考《kubeadm安装部署K8S集群——CentOS篇》。
但是挨个安装效率太低了，所以我们希望能够批量安装，使用 ansible+kubeadm 。有一个kubeadm-ansible项目，可以使用ansible+kubeadm实现批量安装，但是已经废弃了（两年没有更新，issue也没人处理）。因此，需要自行实现ansible+kubeadm的批量安装方式。
此外，因为要3台master配置高可用，所以还要安装keepalived+haproxy。
整体操作流程：

管理节点安装配置ansible
所有节点安装docker
所有节点允许iptables检查桥接流量
所有节点安装kubeadm+kubelet+kubectl
master节点安装keepalived+haproxy
初始化master节点（创建集群）
worker节点加入到集群中
测试使用

为方便描述，下文中使用主机IP的最后一段作为该主机的简称，例如：192.168.56.101简称为101。


3. 安装配置ansible参考《Ansible入门篇》，选择一个节点作为管理节点（这里选择101节点），安装ansible，配置管理节点免密登录其他节点。
1、管理节点，工作目录为 /home/voidking
2、/home/voidking/hosts文件内容为
[common]192.168.56.101192.168.56.102192.168.56.103192.168.56.104192.168.56.105192.168.56.106192.168.56.107192.168.56.108192.168.56.109192.168.56.110[common:vars][master]192.168.56.101192.168.56.102192.168.56.103[master:vars][worker]192.168.56.104192.168.56.105192.168.56.106192.168.56.107192.168.56.108192.168.56.109192.168.56.110[worker:vars]

3、权限测试(1) 准备测试文件
touch test.txt

(2) 编写剧本 copy.yaml
---- hosts: common  remote_user: voidking  become_method: sudo  tasks:  - name: &quot;cp test&quot;    copy:      src: /home/voidking/test.txt      dest: /tmp/

(3) 使用sudo拷贝配置到客户机
ansible-playbook -i hosts copy.yaml
如果能正常拷贝，说明sudo是允许执行/bin/sh命令的，没问题。
如果不能正常拷贝，提示：对不起，用户 voidking 无权以 root 的身份在 xxx 上执行 /bin/sh说明sudo权限被限制了，不能sudo执行/bin/sh命令，那么只能换一种方式进行拷贝：添加客户机对管理机的免密访问，然后从客户机发起scp拷贝命令。
方法一：把管理机的id_rsa拷贝到所有客户机，这样其实所有机器都是管理机了，不够优雅方法二：把所有客户机的id_rsa.pub添加到管理机的authrized_keys中，本文选择这种方法
ansible all -i hosts -m command -a &quot;ssh-keygen -t rsa -q -P &#x27;&#x27; -f ~/.ssh/id_rsa&quot;ansible all -i hosts -m command -a &quot;cat ~/.ssh/id_rsa.pub&quot; | grep -v &quot;SUCCESS&quot; | tee -a ~/.ssh/authorized_keysansible all -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/test.txt /tmp/&quot;

下文以sudo权限受限为前提进行部署，如果可以使用ansible copy的话会更加简单。
4. 安装docker1、安装docker
ansible all -i hosts -m command -a &quot;sudo yum install -y yum-utils&quot;ansible all -i hosts -m command -a &quot;sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo&quot;ansible all -i hosts -m command -a &quot;sudo yum makecache fast&quot;ansible all -i hosts -m command -a &quot;sudo yum install docker-ce -y&quot;ansible all -i hosts -m command -a &quot;sudo systemctl start docker&quot;ansible all -i hosts -m command -a &quot;sudo systemctl enable docker&quot;

2、安装docker-compose
curl -L &quot;https://github.com/docker/compose/releases/download/v2.11.1/docker-compose-$(uname -s)-$(uname -m)&quot; -o docker-composechmod a+x docker-composeansible all -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/docker-compose ~/&quot;ansible all -i hosts -m command -a &quot;sudo cp ~/docker-compose /usr/local/bin/&quot;ansible all -i hosts -m command -a &quot;sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose&quot;

5. 配置代理（可选）参考文档《Linux配置网络代理》
这里一定要注意：如果配置了网络代理，那么在执行kubeadm init的时候，环境变量中的网络代理会自动配置到组件中，引发一些奇奇怪怪的问题。
5.1. 配置上网代理1、.bashrc中添加代理配置
export http_proxy=http://192.168.56.1:7890export https_proxy=http://192.168.56.1:7890export no_proxy=127.0.0.1,localhost,192.168.56.0/24,10.96.0.0/12,10.244.0.0/16,172.31.0.0/16

2、拷贝到所有节点
ansible all -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/.bashrc ~/.bashrc&quot;

5.2. 配置docker代理1、创建配置文件
mkdir -p ~/etc/systemd/system/docker.service.dvim ~/etc/systemd/system/docker.service.d/http-proxy.conf

内容为：
[Service]Environment=&quot;HTTP_PROXY=http://192.168.56.1:7890&quot;Environment=&quot;HTTPS_PROXY=http://192.168.56.1:7890&quot;Environment=&quot;NO_PROXY=127.0.0.1,localhost,192.168.56.0/24,10.96.0.0/12,10.244.0.0/16,172.31.0.0/16&quot;

2、拷贝到所有节点
ansible all -i hosts -m command -a &quot;mkdir -p ~/etc/systemd/system/docker.service.d&quot;ansible all -i hosts -m command -a &quot;sudo mkdir -p /etc/systemd/system/docker.service.d&quot;ansible all -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/etc/systemd/system/docker.service.d/http-proxy.conf ~/etc/systemd/system/docker.service.d/&quot;ansible all -i hosts -m command -a &quot;sudo cp ~/etc/systemd/system/docker.service.d/http-proxy.conf /etc/systemd/system/docker.service.d/http-proxy.conf&quot;

3、重启docker
ansible all -i hosts -m command -a &quot;sudo systemctl daemon-reload&quot;ansible all -i hosts -m command -a &quot;sudo systemctl restart docker&quot;sudo docker info | grep Proxy

6. 允许iptables检查桥接流量1、 准备配置文件
mkdir -p etc/modules-load.d/mkdir -p etc/sysctl.d/cat &lt;&lt;EOF | tee etc/modules-load.d/k8s.confbr_netfilterEOFcat &lt;&lt;EOF | tee etc/sysctl.d/k8s.confnet.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1EOF

2、 拷贝配置到所有节点
ansible all -i hosts -m command -a &quot;mkdir -p ~/etc/modules-load.d/&quot;ansible all -i hosts -m command -a &quot;mkdir -p ~/etc/sysctl.d/&quot;ansible all -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/etc/modules-load.d/k8s.conf ~/etc/modules-load.d/&quot;ansible all -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/etc/sysctl.d/k8s.conf ~/etc/sysctl.d/&quot;ansible all -i hosts -m command -a &quot;sudo cp ~/etc/modules-load.d/k8s.conf /etc/modules-load.d/&quot;ansible all -i hosts -m command -a &quot;sudo cp ~/etc/sysctl.d/k8s.conf /etc/sysctl.d/&quot;


7. 安装kubeadm+kubelet+kubectl7.1. 准备kubernetes.repo配置1、当前目录下，创建kubernetes.repo文件
mkdir -p etc/yum.repos.d/cat &lt;&lt;EOF | tee etc/yum.repos.d/kubernetes.repo[kubernetes]name=Kubernetesbaseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-$basearchenabled=1gpgcheck=1repo_gpgcheck=0gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpgexclude=kubelet kubeadm kubectlEOF

如果不能连通google，kubernetes.repo中的baseurl就替换成aliyun
cat &lt;&lt;EOF | tee etc/yum.repos.d/kubernetes.repo[kubernetes]name=Kubernetesbaseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64enabled=1gpgcheck=0EOF

2、拷贝配置到所有节点
ansible all -i hosts -m command -a &quot;mkdir -p ~/etc/yum.repos.d/&quot;ansible all -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/etc/yum.repos.d/kubernetes.repo ~/etc/yum.repos.d/&quot;ansible all -i hosts -m command -a &quot;sudo cp ~/etc/yum.repos.d/kubernetes.repo /etc/yum.repos.d/&quot;

7.2. SELinux设置为permissive模式ansible all -i hosts -m command -a &quot;sudo setenforce 0&quot;ansible all -i hosts -m command -a &quot;sudo sed -i &#x27;s/^SELINUX=enforcing$/SELINUX=permissive/&#x27; /etc/selinux/config&quot;

7.3. 安装kubeadm+kubelet+kubectlansible all -i hosts -m command -a &quot;sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes&quot;ansible all -i hosts -m command -a &quot;sudo systemctl enable --now kubelet&quot;

如果想要安装指定版本的k8s，比如1.22.15，那么需要安装指定版本的kubeadm、kubelet和kubectl。
ansible all -i hosts -m command -a &quot;sudo yum remove -y kubelet kubeadm kubectl&quot;sudo yum list kubeadm kubelet kubectl --showduplicatesansible all -i hosts -m command -a &quot;sudo yum install -y kubeadm-1.22.15-0 kubelet-1.22.15-0 kubectl-1.22.15-0 --disableexcludes=kubernetes&quot;

8. 关闭swapansible all -i hosts -m command -a &quot;sudo swapoff -a&quot;ansible all -i hosts -m command -a &quot;sudo sed -i &#x27;/ swap / s/^/#/&#x27; /etc/fstab&quot;

9. 配置cgroup驱动1、创建docker配置文件
mkdir -p etc/docker/cat &lt;&lt;EOF | tee etc/docker/daemon.json&#123;    &quot;exec-opts&quot;: [        &quot;native.cgroupdriver=systemd&quot;    ]&#125;EOF

2、拷贝配置文件到所有节点
ansible all -i hosts -m command -a &quot;mkdir -p etc/docker/&quot;ansible all -i hosts -m command -a &quot;sudo mkdir -p /etc/docker/&quot;ansible all -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/etc/docker/daemon.json ~/etc/docker/&quot;ansible all -i hosts -m command -a &quot;sudo cp -p ~/etc/docker/daemon.json /etc/docker&quot;

3、重启docker
ansible all -i hosts -m command -a &quot;sudo systemctl daemon-reload&quot;ansible all -i hosts -m command -a &quot;sudo systemctl restart docker&quot;

10. 安装keepalived+haproxykeepalived负责高可用，haproxy负责负载均衡。详情参考《基于 keeplived+lvs 实现nginx的高可用和负载均衡》
只使用keepalived行不行？行，当一个master节点坏掉，可以自动切换到另外一个好的master节点，实现高可用。但是，会有两个master节点一直处于闲置状态。只使用haproxy行不行？行，让一个master节点作为入口，流量可以负载均衡到3个master。但是，当入口master节点坏掉，集群也就坏掉了，不能避免单点问题。综上，我们需要让keepalived和haproxy配合，才能实现负载均衡+高可用。keepalived提供一个VIP，后端对应master三个节点，某个时间提供服务的只有一个节点，如果提供服务的节点坏掉了，那么会自动切换到另外的节点。流量打到提供服务的master节点上的haproxy服务，haproxy把流量分发到三个master节点上的apiserver。
另外，keepalived和haproxy不一定要部署到master节点，如果有单独的机器来部署也是完全可以的。
安装keepalived+haproxy
ansible master -i hosts -m command -a &quot;sudo yum install keepalived haproxy psmisc -y&quot;

10.1. 配置haproxy三台master节点，配置haproxy，它们的配置都相同
1、创建haproxy.cfg文件
mkdir -p etc/haproxy/vim etc/haproxy/haproxy.cfg

内容为：
global    log /dev/log  local0 warning    chroot      /var/lib/haproxy    pidfile     /var/run/haproxy.pid    maxconn     4000    user        haproxy    group       haproxy    daemon       stats socket /var/lib/haproxy/stats   defaults    log global    option  httplog    option  dontlognull    timeout connect 5000    timeout client 50000    timeout server 50000   frontend kube-apiserver    bind *:9443 # 监听9443端口    mode tcp    option tcplog    default_backend kube-apiserver   backend kube-apiserver    mode tcp    option tcplog    option tcp-check    balance roundrobin    default-server inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100    server kube-apiserver-1 192.168.56.101:6443 check # Replace the IP address with your own.    server kube-apiserver-2 192.168.56.102:6443 check # Replace the IP address with your own.    server kube-apiserver-3 192.168.56.103:6443 check # Replace the IP address with your own.

2、备份&amp;替换haproxy.cfg
ansible master -i hosts -m command -a &quot;mkdir -p etc/haproxy/&quot;ansible master -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/etc/haproxy/haproxy.cfg ~/etc/haproxy/&quot;ansible master -i hosts -m command -a &quot;sudo mv /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak&quot;ansible master -i hosts -m command -a &quot;sudo cp etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg&quot;

3、重启haproxy
ansible master -i hosts -m command -a &quot;sudo systemctl restart haproxy&quot;ansible master -i hosts -m command -a &quot;sudo systemctl enable haproxy&quot;ansible master -i hosts -m command -a &quot;sudo systemctl status haproxy&quot;

10.2. 找一个VIP在配置keepalived之前，需要找一个没有被占用的IP作为VIP，给keepalived使用。
nmap -sP 192.168.56.0/24cat /proc/net/arp | awk &#x27;&#123;print $1&#125;&#x27; | sort
通过上面的命令可以筛选出没有被占用的IP，选择其中一个作为VIP，这里选择192.169.56.200
10.3. 配置keepalived三台master节点，都配置keepalived，它们的配置有所不同
1、创建keepalived.conf文件
mkdir -p etc/keepalived/vim etc/keepalived/keepalived.conf

101机器，keepalived.conf文件内容应该为：
global_defs &#123;  notification_email &#123;  &#125;  router_id LVS_DEVEL  vrrp_skip_check_adv_addr  vrrp_garp_interval 0  vrrp_gna_interval 0&#125;   vrrp_script chk_haproxy &#123;  script &quot;killall -0 haproxy&quot;  interval 2  weight 2&#125;   vrrp_instance haproxy-vip &#123;  state BACKUP  priority 100  interface eth0                       # Network card  virtual_router_id 60  advert_int 1  authentication &#123;    auth_type PASS    auth_pass 1111  &#125;  unicast_src_ip 192.169.56.101     # The IP address of this machine  unicast_peer &#123;    192.169.56.102                         # The IP address of peer machines    192.169.56.103  &#125;     virtual_ipaddress &#123;    192.169.56.200/24                  # The VIP address  &#125;     track_script &#123;    chk_haproxy  &#125;&#125;

考虑到通用性，需要创建一个通用的配置文件，keepalived.conf内容为：
global_defs &#123;  notification_email &#123;  &#125;  router_id LVS_DEVEL  vrrp_skip_check_adv_addr  vrrp_garp_interval 0  vrrp_gna_interval 0&#125;   vrrp_script chk_haproxy &#123;  script &quot;killall -0 haproxy&quot;  interval 2  weight 2&#125;   vrrp_instance haproxy-vip &#123;  state BACKUP  priority 100  interface eth0                      # Network card  virtual_router_id 60  advert_int 1  authentication &#123;    auth_type PASS    auth_pass 1111  &#125;  unicast_src_ip &lt;unicast_src_ip&gt;     # The IP address of this machine  unicast_peer &#123;    192.169.56.101                    # The IP address of peer machines    192.169.56.102    192.169.56.103  &#125;     virtual_ipaddress &#123;    192.169.56.200/24                 # The VIP address  &#125;     track_script &#123;    chk_haproxy  &#125;&#125;

2、创建替换配置的脚本keepalived.sh
vim etc/keepalived/keepalived.sh

keepalived.sh内容为：
#!/bin/bashsource /etc/profileip=$(ifconfig eth0 | awk &#x27;NR==2&#123;print $2&#125;&#x27;)sed -i &quot;s/  $&#123;ip&#125;/  #$&#123;ip&#125;/g&quot; etc/keepalived/keepalived.confsed -i &quot;s/&lt;unicast_src_ip&gt;/$&#123;ip&#125;/g&quot; etc/keepalived/keepalived.conf

3、备份&amp;替换keepalived.conf
ansible master -i hosts -m command -a &quot;mkdir -p etc/keepalived/&quot;ansible master -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/etc/keepalived/keepalived.conf ~/etc/keepalived/&quot;ansible master -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/etc/keepalived/keepalived.sh ~/etc/keepalived/&quot;ansible master -i hosts -m command -a &quot;bash etc/keepalived/keepalived.sh&quot;ansible master -i hosts -m command -a &quot;sudo mv /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.bak&quot;ansible master -i hosts -m command -a &quot;sudo cp etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf&quot;

5、重启keepalived
ansible master -i hosts -m command -a &quot;sudo systemctl restart keepalived&quot;ansible master -i hosts -m command -a &quot;sudo systemctl enable keepalived&quot;ansible master -i hosts -m command -a &quot;sudo systemctl status keepalived&quot;ansible master -i hosts -m command -a &quot;sudo ip a&quot;

配置成功后，ip a查看节点IP时，会发现其中一个节点的eth0网卡有两个IP，其中eth0:0的IP是VIP 192.169.56.200
11. k8s集群部署11.1. 下载镜像文件1、导出kubeadm默认配置
sudo kubeadm config print init-defaults &gt; kubeadm.conf

kubeadm.conf内容为：
apiVersion: kubeadm.k8s.io/v1beta3bootstrapTokens:- groups:  - system:bootstrappers:kubeadm:default-node-token  token: abcdef.0123456789abcdef  ttl: 24h0m0s  usages:  - signing  - authenticationkind: InitConfigurationlocalAPIEndpoint:  advertiseAddress: 1.2.3.4  bindPort: 6443nodeRegistration:  criSocket: /var/run/dockershim.sock  imagePullPolicy: IfNotPresent  name: node  taints: null---apiServer:  timeoutForControlPlane: 4m0sapiVersion: kubeadm.k8s.io/v1beta3certificatesDir: /etc/kubernetes/pkiclusterName: kubernetescontrollerManager: &#123;&#125;dns: &#123;&#125;etcd:  local:    dataDir: /var/lib/etcdimageRepository: k8s.gcr.iokind: ClusterConfigurationkubernetesVersion: 1.22.0networking:  dnsDomain: cluster.local  serviceSubnet: 10.96.0.0/12scheduler: &#123;&#125;

PS：导出所有默认配置
kubeadm config print init-defaults --component-configs KubeletConfiguration &gt; kubeadm.conf


2、编辑kubeadm.conf，imageRepository改为国内镜像源，kubernetesVersion改为期望的版本
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containerskubernetesVersion: 1.22.15

3、下载镜像
sudo kubeadm config images pull --config kubeadm.conf

4、打包镜像
sudo docker save $(sudo docker images | grep -v REPOSITORY | awk &#x27;BEGIN&#123;OFS=&quot;:&quot;;ORS=&quot; &quot;&#125;&#123;print $1,$2&#125;&#x27;) -o all.tar

5、拷贝镜像到所有master节点
sudo chown voidking:voidking all.taransible master -i hosts -m command -a &quot;scp -o StrictHostKeyChecking=no voidking@192.168.56.101:~/all.tar ~/&quot;ansible master -i hosts -m command -a &quot;sudo docker load -i ~/all.tar&quot;

11.2. k8s集群配置选择一个master节点（master-0），作为第一个控制平面节点。编辑kubeadm.conf，指定k8s集群配置，详情参考利用 kubeadm 创建高可用集群
11.2.1. controlPlaneEndpoint添加controlPlaneEndpoint相关配置，keepalived的VIP + haproxy的端口
---apiVersion: kubeadm.k8s.io/v1beta3kind: ClusterConfigurationcontrolPlaneEndpoint: &quot;192.168.56.200:9443&quot;

11.2.2. localAPIEndpointapiserver地址改为master本机的IP和端口
localAPIEndpoint:  advertiseAddress: 192.168.56.101  bindPort: 6443

11.2.3. 指定节点名称修改 nodeRegistration.name ，把节点名称改为执行初始化的master主机的主机名
nodeRegistration:  criSocket: /var/run/dockershim.sock  name: master-0  imagePullPolicy: IfNotPresent  taints: null

11.2.4. 配置pod和serviceip范围ip route查看当前配置使用的ip范围，避开这些范围。
networking:  dnsDomain: cluster.local  serviceSubnet: 172.31.0.0/16  podSubnet: 10.96.0.0/12

关于pod ip范围和servcie ip范围的设置，可以参考子网划分详解与子网划分实例精析和详解网络分类ABC
11.3. master-0节点初始化1、master-0节点执行初始化
sudo kubeadm init --config ~/kubeadm.conf --upload-certs

看到类似如下的结果：
You can now join any number of the control-plane node running the following command on each as root:  kubeadm join 192.168.56.200:9443 --token abcdef.0123456789abcdef \    --discovery-token-ca-cert-hash sha256:xxxxx \    --control-plane --certificate-key xxxxxThen you can join any number of worker nodes by running the following on each as root:  kubeadm join 192.168.56.200:9443 --token abcdef.0123456789abcdef \    --discovery-token-ca-cert-hash sha256:xxxxx

2、配置kubeconfig
mkdir -p $HOME/.kubesudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configsudo chown $(id -u):$(id -g) $HOME/.kube/config

3、查看集群状态
kubectl get nodeskubectl get pods --all-namespaces

11.4. 其他master节点加入集群1、修改hosts文件，注释掉master-0节点
2、其他master节点加入集群
ansible master -i hosts -m command -a &quot;sudo kubeadm join 192.168.56.200:9443 --token abcdef.0123456789abcdef \    --discovery-token-ca-cert-hash sha256:xxxxx \    --control-plane --certificate-key xxxxx&quot;

11.5. worker节点加入集群ansible worker -i hosts -m command -a &quot;sudo kubeadm join 192.168.56.200:9443 --token abcdef.0123456789abcdef \    --discovery-token-ca-cert-hash sha256:xxxxx&quot;

11.6. 安装CNI插件参考集群网络系统选择一个cni插件，这里我们选择flannel。
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

11.7. 查看集群状态kubectl get nodeskubectl get pods --all-namespaces




]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>网络</tag>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>ansible</tag>
        <tag>keepalived</tag>
        <tag>haproxy</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：kubeadm安装的K8S集群证书过期问题</title>
    <url>/dev-kubeadm-k8s-cert-expired/</url>
    <content><![CDATA[1. 问题描述使用kubectl访问k8s集群，报错证书过期：Unable to connect to the server: x509: certificate has expired or is not yet valid: current time 2022-09-26T01:33:25-04:00 is after 2022-09-26T03:45:02Z


2. 解决思路1、确认证书过期2、更新证书3、更新相关配置
参考文档：

解决kubernetes证书过期问题
Kubeadm集群证书过期后的处理

3. 具体操作3.1. 确认证书过期1、登录证书过期集群的任意master节点
2、查看证书过期时间
kubeadm certs check-expiration

看到 RESIDUAL TIME 为 &lt;invalid&gt;，表明证书已经过期。
3.2. 更新证书注意：更新证书时，所有master节点都需要操作
1、备份k8s配置
cp -r /etc/kubernetes&#123;,.old&#125;#cp -r /var/lib/etcd&#123;,.old&#125;

2、更新证书
kubeadm certs renew all

更新成功后，会提示重启一些pod：

Done renewing certificates. You must restart the kube-apiserver, kube-controller-manager, kube-scheduler and etcd, so that they can use the new certificates.

3、重启kubelet和容器
systemctl restart kubeletdocker ps | grep -v pause | grep -E &quot;etcd|scheduler|controller|apiserver&quot; | awk &#x27;&#123;print $1&#125;&#x27; | awk &#x27;&#123;print &quot;docker&quot;,&quot;restart&quot;,$1&#125;&#x27; | bash

4、验证
cd /etc/kubernetes/pkillkubeadm certs check-expiration

3.3. 更新kubectl配置1、更新kubeconfig配置
cp /etc/kubernetes/admin.conf .kube/config

2、测试
kubectl get nodes

3.4. 更新kubesphere配置以上，k8s集群的证书过期问题已经解决。但是，使用通过kubesphere的管理页面访问k8s集群时，会报错：

会话超时或此帐户在其它地方登录，请重新登录

ks官方给的解决方案是在页面上更新 kubeconfig，但是这个方案只适用于v3.3以上的版本（页面上会提示kubeconfig已过期），详情参考更新 Kubeconfig
如果是v3.2或者更低版本，可以通过kubectl修改CRD clusters.cluster.kubesphere.io，详情参考member节点证书到期，续约证书后，kubesphere如何更新kubeconfig？
为方便描述，假设证书过期的k8s集群名称为 dev。证书更新后，在kubesphere主集群执行如下操作：
1、查看所有集群配置
kubectl get clusters.cluster.kubesphere.io -n kubesphere-system

2、备份dev集群配置
kubectl get clusters.cluster.kubesphere.io/dev -n kubesphere-system -o yaml &gt; dev.yaml

3、base64加密新的dev集群的kubeconfig
cat .kube/config | base64 | tr -d &#x27;\n&#x27;

4、更新dev集群配置
kubectl edit clusters.cluster.kubesphere.io/dev -n kubesphere-system

spec:  connection:    kubeconfig: xxx

把 kubeconfig 的value替换为base64加密后的kubeconfig内容。
5、验证通过ks的管理页面再次访问k8s集群，已经可以正常访问。
3.5. 更新argocd配置查看argocd和k8s dev集群的连接，发现CONNECTION STATUS已经失联。
参考Argocd cluster add，通过--upsert参数更新集群配置。注意：更新集群配置时，集群的名称一定要和现有名称保持一致。
假设：

argocd所在k8s集群为manager
manager的master节点上包含dev集群的新的kubeconfig，路径为~/.kube/newconfig
dev集群在argocd中的名称为dev(开发)
argo-cd-argocd-server pod ip为 10.x.x.x

那么，在manager集群的master节点上执行如下操作。
kubectl get pod -n argocd -owideargocd login 10.x.x.x:8080 --grpc-webargocd cluster listargocd cluster add kubernetes-admin@kubernetes --kubeconfig ~/.kube/newconfig --name &quot;dev(开发)&quot; --upsert 

如果master节点没有argocd client，可以登录到argo-cd-argocd-server pod中进行操作。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>cicd</tag>
        <tag>好好学K8S</tag>
        <tag>kubesphere</tag>
        <tag>kubeadm</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中安装配置Spark</title>
    <url>/dev-k8s-spark/</url>
    <content><![CDATA[1. spark-on-k8s-operator简介
spark-on-k8s-operator: Kubernetes operator for managing the lifecycle of Apache Spark applications on Kubernetes.The Kubernetes Operator for Apache Spark aims to make specifying and running Spark applications as easy and idiomatic as running other workloads on Kubernetes. It uses Kubernetes custom resources for specifying, running, and surfacing status of Spark applications. 

spark-on-k8s-operator工作流程：1、提交sparkApplication的请求到api-server2、把sparkApplication的CRD持久化到etcd3、operator订阅发现有sparkApplication，获取后通过submission   4、runner提交spark-submit过程，请求给到api-server后生成对应的driver/executor pod5、spark pod monitor会监控到application执行的状态（所以通过sparkctl可以通过list、status看到）mutating adminission webhook建svc，可以查看spark web ui
简而言之，spark-on-k8s-operator改变了传统的spark任务运行方式，能够提高资源利用率和节省资源。用户提交CRD之后，k8s才会创建运行spark任务需要的pod，从而能够利用整个k8s集群的资源。任务跑完之后，pod会被回收，从而节省资源。
参考文档：

spark-on-k8s-operator
spark operator部署安装
spark-on-kubernetes-operator环境搭建
Apache Spark on K8S Best Practice and Performance in the Cloud
苹果工程师分享如何运维超大规模Spark on Kubernetes集群



2. 安装spark-on-k8s-operator1、安装operator
helm repo add spark-operator https://googlecloudplatform.github.io/spark-on-k8s-operatorhelm install my-release spark-operator/spark-operator --namespace spark-operator --create-namespace

或者
git clone https://github.com/GoogleCloudPlatform/spark-on-k8s-operator.gitcd spark-on-k8s-operator/charts/spark-operator-charthelm install my-release . --namespace spark-operator --create-namespace

2、镜像拉取报错处理拉取镜像 ghcr.io/googlecloudplatform/spark-operator:v1beta2-1.3.7-3.1.1 时报错ErrImagePull，这是因为ghcr.io被墙了，需要一些技巧进行下载。方法一：科学上网下载镜像到本地，然后上传镜像到内部镜像仓库，修改yaml使用内部镜像仓库。方法二：利用GitHub Actions下载ghcr.io的镜像，上传到Docker Hub，详情参考togettoyou/hub-mirror
这里使用方法一，修改镜像为内部镜像：
docker pull ghcr.io/googlecloudplatform/spark-operator:v1beta2-1.3.7-3.1.1docker tag ghcr.io/googlecloudplatform/spark-operator:v1beta2-1.3.7-3.1.1 harbor.voidking.com/ghcr.io/googlecloudplatform/spark-operator:v1beta2-1.3.7-3.1.1docker login harbor.voidking.comdocker push harbor.voidking.com/ghcr.io/googlecloudplatform/spark-operator:v1beta2-1.3.7-3.1.1

3、替换镜像
kubectl edit deployment.apps/my-release-spark-operator -n spark-operator
修改镜像为内部镜像地址。
3. 使用spark-on-k8s-operator1、下载spark-on-k8s-operator/examples/spark-pi.yaml
2、修改镜像为内部镜像
docker pull gcr.io/spark-operator/spark:v3.1.1docker tag gcr.io/spark-operator/spark:v3.1.1 harbor.voidking.com/gcr.io/spark-operator/spark:v3.1.1docker push harbor.voidking.com/gcr.io/spark-operator/spark:v3.1.1

3、替换镜像修改spark-pi.yaml中的镜像为内部镜像地址
4、生效yaml文件
kubectl apply -f spark-pi.yaml

5、查看spark应用
kubectl get sparkapplications spark-pi -o=yaml -n defaultkubectl describe sparkapplication spark-pi -n default
报错：error looking up service account default/spark: serviceaccount “spark” not found.
官方文档中也给了说明，spark这个serviceaccount需要替换成有权限的serviceaccount，该serviceaccount有权创建、获取、列出和删除执行程序 pod，并为驱动程序创建 Kubernetes 无头服务。
注意：不能直接替换成default，否则spark-pi-driver执行时会报错权限不够。可以选择给default授权，或者自己创建一个新的sa，这里我们选择后者。
6、创建sa参考spark-application-rbac.yaml，修改为：
apiVersion: v1kind: ServiceAccountmetadata:  name: spark  namespace: default---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:  namespace: default  name: spark-rolerules:- apiGroups: [&quot;&quot;]  resources: [&quot;pods&quot;]  verbs: [&quot;*&quot;]- apiGroups: [&quot;&quot;]  resources: [&quot;services&quot;]  verbs: [&quot;*&quot;]- apiGroups: [&quot;&quot;]  resources: [&quot;configmaps&quot;]  verbs: [&quot;*&quot;]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata:  name: spark-role-binding  namespace: defaultsubjects:- kind: ServiceAccount  name: spark  namespace: defaultroleRef:  kind: Role  name: spark-role  apiGroup: rbac.authorization.k8s.io

注意： configmaps 的管理权限也是必要的。
执行创建sa
kubectl apply -f spark-application-rbac.yaml

如果创建的不是名为spark的sa，那么还需要修改spark-pi的spec yaml
kubectl edit sparkapplication spark-pi -n default

7、查看spark资源情况
kubectl get all -n default | grep spark
以上，符合预期。如果出现Error，那么通过日志进行排查。
8、查看执行结果（查看日志）
kubectl logs pod/spark-pi-driver -n default
日志中会输出：Pi is roughly 3.145515727578638
4. sparkapplication yaml编写参考文档：

spark-on-k8s-operator/examples
User Guide

5. 调用hive5.1. hive基本操作1、登录hiveserverkubectl exec进入到hive pod中，执行登录命令
hive # orbeeline -u jdbc:hive2://localhost:10000 username password

2、查看hive_client_test表内容
show databases;use default;show tables;select * from hive_client_test;

5.2. spark调用hivespark调用hive时，关于hive的配置，有两种配置方式：一种是写在代码中，一种是配置在环境变量中。写在代码中的demo：
spark = SparkSession.builder \        .config(&quot;hive.metastore.uris&quot;,&quot;thrift://hive-metastore.bigdata.svc:9083&quot;) \        .appName(&quot;test&quot;) \        .enableHiveSupport() \        .getOrCreate()read_df = spark.sql(&quot;select * from default.hive_client_test limit 1&quot;)read_df.show()

如果不使用config，那么默认读取环境变量中的配置：
spark = SparkSession.builder\        .master(&quot;local[*]&quot;) \        .appName(&quot;test&quot;) \        .enableHiveSupport() \        .getOrCreate()read_df=spark.sql(&quot;select * from default.hive_client_test limit 1&quot;)read_df.show()

hive.metastore.uris对应的环境变量是啥？
5.3. sparkapplication定义参考文档：

spark examples - hive.py
spark-on-k8s-operator/examples/spark-py-pi.yaml

这里比较友好的是，gcr.io/spark-operator/spark-py:v3.1.1镜像中包含所有的spark examples，可以比较方便地进行测试。
已知k8s集群中存在hive，hive-metastore的svc为hive-metastore.bigdata.svc。
1、修改hive.py，内容为：
&quot;&quot;&quot;A simple example demonstrating Spark SQL Hive integration.Run with:  ./bin/spark-submit examples/src/main/python/sql/hive.py&quot;&quot;&quot;# $example on:spark_hive$from os.path import abspathfrom pyspark.sql import SparkSessionfrom pyspark.sql import Row# $example off:spark_hive$if __name__ == &quot;__main__&quot;:    # $example on:spark_hive$    # warehouse_location points to the default location for managed databases and tables    warehouse_location = abspath(&#x27;spark-warehouse&#x27;)    spark = SparkSession \        .builder \        .config(&quot;hive.metastore.uris&quot;,&quot;thrift://hive-metastore.bigdata.svc:9083&quot;) \        .appName(&quot;Python Spark SQL Hive integration example&quot;) \        .config(&quot;spark.sql.warehouse.dir&quot;, warehouse_location) \        .enableHiveSupport() \        .getOrCreate()    # spark is an existing SparkSession    spark.sql(&quot;CREATE TABLE IF NOT EXISTS src (key INT, value STRING) USING hive&quot;)    spark.sql(&quot;LOAD DATA LOCAL INPATH &#x27;/opt/spark/examples/src/main/resources/kv1.txt&#x27; INTO TABLE src&quot;)    # Queries are expressed in HiveQL    spark.sql(&quot;SELECT * FROM src&quot;).show()    # +---+-------+    # |key|  value|    # +---+-------+    # |238|val_238|    # | 86| val_86|    # |311|val_311|    # ...    # Aggregation queries are also supported.    spark.sql(&quot;SELECT COUNT(*) FROM src&quot;).show()    # +--------+    # |count(1)|    # +--------+    # |    500 |    # +--------+    # The results of SQL queries are themselves DataFrames and support all normal functions.    sqlDF = spark.sql(&quot;SELECT key, value FROM src WHERE key &lt; 10 ORDER BY key&quot;)    # The items in DataFrames are of type Row, which allows you to access each column by ordinal.    stringsDS = sqlDF.rdd.map(lambda row: &quot;Key: %d, Value: %s&quot; % (row.key, row.value))    for record in stringsDS.collect():        print(record)    # Key: 0, Value: val_0    # Key: 0, Value: val_0    # Key: 0, Value: val_0    # ...    # You can also use DataFrames to create temporary views within a SparkSession.    Record = Row(&quot;key&quot;, &quot;value&quot;)    recordsDF = spark.createDataFrame([Record(i, &quot;val_&quot; + str(i)) for i in range(1, 101)])    recordsDF.createOrReplaceTempView(&quot;records&quot;)    # Queries can then join DataFrame data with data stored in Hive.    spark.sql(&quot;SELECT * FROM records r JOIN src s ON r.key = s.key&quot;).show()    # +---+------+---+------+    # |key| value|key| value|    # +---+------+---+------+    # |  2| val_2|  2| val_2|    # |  4| val_4|  4| val_4|    # |  5| val_5|  5| val_5|    # ...    # $example off:spark_hive$    spark.stop()

2、docker commit新的镜像harbor.voidking.com/gcrmirror/spark-operator/spark-py:v3.1.1.0
3、创建spark-hive.yaml，内容为：
apiVersion: &quot;sparkoperator.k8s.io/v1beta2&quot;kind: SparkApplicationmetadata:  name: pyspark-hive  namespace: defaultspec:  type: Python  pythonVersion: &quot;3&quot;  mode: cluster  image: &quot;harbor.voidking.com/gcrmirror/spark-operator/spark-py:v3.1.1.0&quot;  imagePullPolicy: Always  mainApplicationFile: local:///opt/spark/examples/src/main/python/sql/hive.py  sparkVersion: &quot;3.1.1&quot;  restartPolicy:    type: OnFailure    onFailureRetries: 3    onFailureRetryInterval: 10    onSubmissionFailureRetries: 5    onSubmissionFailureRetryInterval: 20  driver:    cores: 1    coreLimit: &quot;1200m&quot;    memory: &quot;512m&quot;    labels:      version: 3.1.1.0    serviceAccount: spark  executor:    cores: 1    instances: 1    memory: &quot;512m&quot;    labels:      version: 3.1.1.0

4、创建sparkapplication
kubectl apply -f spark-hive.yaml

5、查看结果
kubectl get all | grep hivekubectl logs pod/pyspark-hive-driver

]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>好好学K8S</tag>
        <tag>spark</tag>
        <tag>hive</tag>
        <tag>operator</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux配置网络代理</title>
    <url>/dev-linux-network-proxy/</url>
    <content><![CDATA[1. 网络代理《好好学Linux：Linux配置SNAT上网》一文中，我们了解到，通过SNAT的方式能够让局域网中所有主机都能访问外网。而网络代理，也能让局域网中所有主机都能访问外网。并且，如果网络代理支持科学上网，那么所有使用这个代理的主机也可以科学上网。
本文我们就来学习一下Linux中常见的网络代理配置方法，参考文档：

linux设置代理上网
docker pull 配置代理
如何为 APT 命令设置代理



2. 测试代理是否正常测试代理是否正常的方法：
curl -x http://proxy-server:port -I https://www.example.com

已知网络代理地址为：http://192.168.56.1:7890 和 socks5://192.168.56.2:10808
测试示例：
curl -x http://192.168.56.1:7890 -I https://www.google.comcurl -x http://192.168.56.1:7890 https://www.google.comcurl -x socks5://192.168.56.2:10808 https://www.google.com

如果代理配置了密码：
curl -x http://username:password@192.168.56.1:7890 -I https://www.google.comcurl -x socks5://username:password@192.168.56.2:10808 https://www.google.com

3. bash环境网络代理bash环境网络代理设置，是适用于全局的，因为绝大部分软件都会从环境变量中读取数据，比如curl命令、yum命令、wget命令等等。
3.1. 临时配置export https_proxy=http://192.168.56.1:7890 http_proxy=http://192.168.56.1:7890 all_proxy=socks5://192.168.56.1:7891 ftp_proxy=http://192.168.56.1:7890 no_proxy=localhost,127.0.0.1,192.168.56.0/24

这里的no_proxy需要注意，不同的软件对于它有不同的处理。最典型的例子：

example.com：大部分软件都支持精确匹配和下一级域名匹配，例如还可以匹配subdomain.example.com
.example.com：curl剥离前缀.，对于example.com不会使用代理；wget不会剥离前缀.，对于example.com会使用代理
192.168.56.0/24：只有Go和Ruby支持这种CIDR块

详情参考We need to talk: Can we standardize NO_PROXY?
PS：取消代理的方法如下
unset http_proxy https_proxy all_proxy

3.2. 永久生效1、写入配置内容到.bashrc文件中
export https_proxy=http://192.168.56.1:7890 export http_proxy=http://192.168.56.1:7890 export all_proxy=socks5://192.168.56.1:7891export ftp_proxy=http://192.168.56.1:7890 export no_proxy=localhost,127.0.0.1,192.168.56.0/24

写到.bashrc，无论是登录session还是非登录session，都可以使用这些变量，详情参考bash_profile和bashrc的区别。
2、使生效
source .bashrc

4. wget代理编辑文件/etc/wgetrc，添加内容：
http_proxy = http://192.168.56.1:7890  https_proxy = http://192.168.56.1:7890ftp_proxy = http://192.168.56.1:7890

注意：wget是不支持 socks5 代理的，如果只有 socks5 代理，建议使用 curl 代替 wget。
wget https://example.com/test.zip# 等价于curl -L -o test.zip https://example.com/test.zip

5. yum代理编辑文件/etc/yum.conf，添加内容：
proxy=http://192.168.56.1:7890

6. apt代理6.1. 临时生效方法一：
export https_proxy=http://192.168.56.1:7890 http_proxy=http://192.168.56.1:7890 all_proxy=socks5://192.168.56.1:7891apt update


方法二：
apt-get -o Acquire::http::proxy=&quot;http://192.168.56.1:7890/&quot; update

6.2. 永久生效创建文件 /etc/apt/apt.conf.d/proxy.conf ，添加内容：
Acquire::http::Proxy &quot;http://192.168.56.1:7890/&quot;;Acquire::https::Proxy &quot;http://192.168.56.1:7890/&quot;;

7. 浏览器上网代理以Firefox浏览器为例：Edit -&gt; Preferences -&gt; Advanced -&gt; Network在Connection下点击Settings，manual proxy configuration里设置IP和PORT。
8. docker pull代理1、创建docker配置目录
mkdir /etc/systemd/system/docker.service.d

2、添加代理配置
vim /etc/systemd/system/docker.service.d/http-proxy.conf

写入内容为：
[Service]Environment=&quot;HTTP_PROXY=http://192.168.56.1:7890&quot;Environment=&quot;HTTPS_PROXY=http://192.168.56.1:7890&quot;Environment=&quot;NO_PROXY=localhost,127.0.0.1,192.168.56.200,harbor.voidking.com&quot;

需要注意的是，20.10.18和更早的docker版本，NO_PROXY不支持CIDR，详情参考NO_PROXY does not support CIDR notation
3、重启docker
systemctl daemon-reloadsystemctl restart docker

4、查看docker代理
systemctl show docker --property Environmentdocker info | grep Proxy

PS：Docker还支持使用 daemon.json 文件配置代理，详情参考Daemon proxy configuration
9. docker build代理docker build时，容器内需要使用pip下载依赖。
docker build \--network=host \--build-arg HTTP_PROXY=http://192.168.56.1:7890 \--build-arg HTTPS_PROXY=http://192.168.56.1:7890 \-t voidking/demo:v1.0.0 . \-f Dockerfile_test

10. pip代理执行pip命令时直接指定代理
pip install xxx --proxy http://192.168.56.1:7890

11. git代理git代理，一般不用单独配置，它会使用bash环境网络代理。但是，如果bash环境网络代理使用的是squid，那么需要单独配置下git代理。
git config --global http.proxy http://192.168.56.101:3128git config --global https.proxy http://192.168.56.101:3128

不需要git代理时，可以取消。
git config --global --unset http.proxygit config --global --unset https.proxy





]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>network</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>网络</tag>
        <tag>docker</tag>
        <tag>git</tag>
      </tags>
  </entry>
  <entry>
    <title>Linux中搭建Spark集群</title>
    <url>/dev-install-spark-on-linux/</url>
    <content><![CDATA[1. 前言计划在三台Linux主机中搭建Spark 3.3.1集群，主机配置为4C8G，操作系统为CentOS7。三台主机的IP为：
192.168.56.101192.168.56.102192.168.56.103
选择101作为master节点，另外两个作为worker节点。
参考文档：

spark3.3.0安装&amp;部署过程
Spark Overview
Spark Standalone Mode
Submitting Applications



2. 安装Java参考文档《全平台安装JDK》
3. 安装Spark3.1. master节点配置1、下载spark并解压
wget https://dlcdn.apache.org/spark/spark-3.3.1/spark-3.3.1-bin-hadoop3-scala2.13.tgzmkdir -p /usr/local/sparktar -xzvf spark-3.3.1-bin-hadoop3-scala2.13.tgz -C /usr/local/spark

这里选择spark-3.3.1-bin-hadoop3-scala2.13.tgz版本，带着scala一起。spark-3.3.1-bin-hadoop3.tgz也包含scala，但是版本好像有些问题，因此不选择它。
更多版本的spark，可以在 Download Apache Spark 和 Spark release archives 页面找到。
2、创建配置文件
cd /usr/local/spark/spark-3.3.1-bin-hadoop3-scala2.13/confcp workers.template workerscp spark-defaults.conf.template spark-defaults.confcp spark-env.sh.template spark-env.sh

3、修改配置（1）workers中删除localhost，添加
#192.168.56.101192.168.56.102192.168.56.103

（2）spark-defaults.conf暂时不变
（3）spark-env.sh中添加
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161export SPARK_MASTER_HOST=192.168.56.101export SPARK_MASTER_PORT=7077

4、/etc/profile中添加环境变量
export SPARK_HOME=/usr/local/spark/spark-3.3.1-bin-hadoop3-scala2.13export PATH=$SPARK_HOME/bin:$PATH

5、使配置生效
source /etc/profile

3.2. master配置同步到worker1、打包spark程序和配置
cd /usr/local/sparktar -czvf spark-3.3.1-bin-hadoop3-scala2.13.tgz spark-3.3.1-bin-hadoop3-scala2.13

2、拷贝程序和配置到worker节点
scp spark-3.3.1-bin-hadoop3-scala2.13.tgz 192.168.56.102:~scp /etc/profile 192.168.56.102:/etc/profilescp spark-3.3.1-bin-hadoop3-scala2.13.tgz 192.168.56.103:~scp /etc/profile 192.168.56.103:/etc/profile

3.3. worker节点配置1、解压spark
mkdir -p /usr/local/sparktar -xzvf spark-3.3.1-bin-hadoop3-scala2.13.tgz -C /usr/local/spark

3.4. 运行Spark1、启动spark（在master节点上执行）
cd /usr/local/spark/spark-3.3.1-bin-hadoop3-scala2.13/sbin./start-all.sh

根据提示，依次输入两台worker节点的密码。（这里最好配置上免密登录）这样，三个节点上的spark就都可以启动起来。
2、验证安装
jps

master节点看到Master和Jps两个进程，worker节点看到Worker和Jps两个进程。
4. 使用Spark4.1. 单机测试1、在master或者worker节点上执行
cd /usr/local/spark/spark-3.3.1-bin-hadoop3-scala2.13/./bin/run-example SparkPi 10

输出：
22/11/09 20:38:29 INFO DAGScheduler: ResultStage 0 (reduce at SparkPi.scala:38) finished in 1.118 s22/11/09 20:38:29 INFO DAGScheduler: Job 0 is finished. Cancelling potential speculative or zombie tasks for this job22/11/09 20:38:29 INFO TaskSchedulerImpl: Killing all running tasks in stage 0: Stage finished22/11/09 20:38:29 INFO DAGScheduler: Job 0 finished: reduce at SparkPi.scala:38, took 1.175116 sPi is roughly 3.13890713890713922/11/09 20:38:29 INFO SparkUI: Stopped Spark web UI at http://spark-master:4040


2、查看执行进度浏览器访问 http://192.168.56.101:8080
4.2. spark-submit提交任务./bin/spark-submit \  --class org.apache.spark.examples.SparkPi \  --master spark://192.168.56.101:7077 \  ./examples/jars/spark-examples_2.12-3.3.1.jar 1000

spark-submit提交的任务，执行速度非常非常慢，不知道为什么，留个坑吧。。。
4.3. 连接mysql1、创建测试文件 test_mysql.py
from pyspark.sql import SparkSessiondef test_sql():    spark = SparkSession.builder.appName(&#x27;testmysql&#x27;).master(&#x27;local&#x27;).getOrCreate()    df = spark.read.format(&quot;jdbc&quot;)\        .option(&quot;url&quot;, &quot;jdbc:mysql://192.168.56.101:3306/testdb&quot;)\        .option(&quot;driver&quot;, &quot;org.mariadb.jdbc.Driver&quot;)\        .option(&quot;dbtable&quot;, &quot;testtable&quot;)\        .option(&quot;user&quot;, &quot;root&quot;)\        .option(&quot;password&quot;, &quot;xxxxxx&quot;)\        .load()    df.printSchema()    df.select(df[&#x27;testcolumn&#x27;]).show()if __name__ == &quot;__main__&quot;:    test_sql()

2、提交测试
spark-submit mysql.py

4.4. 连接kafka1、创建测试文件 test_kafka.py
from pyspark.sql import SparkSessiondef read_data():    spark = SparkSession.builder.appName(&#x27;testkafka&#x27;).getOrCreate()    # Creating a Kafka Source for Streaming Queries    df = spark.readStream\        .format(&#x27;kafka&#x27;)\        .option(&quot;kafka.bootstrap.servers&quot;, &quot;192.168.56.101:9092&quot;)\        .option(&quot;subscribe&quot;, &quot;customer_volume&quot;)\        .load()    print(&quot;&gt;&gt;&gt;&gt; print schema:&quot;)    df.printSchema()    df.selectExpr(&quot;CAST(key AS STRING)&quot;, &quot;CAST(value AS STRING)&quot;)if __name__ == &quot;__main__&quot;:    read_data()


2、提交测试
spark-submit test_kafka.py
报错：py4j.protocol.Py4JJavaError: An error occurred while calling o29.load.java.lang.NoClassDefFoundError: org/apache/kafka/common/serialization/ByteArraySerializer看提示是因为缺少kafka序列化类，根本原因是因为缺少kafka client jar包。
解决办法，下载kafka-clients jar，放入到 /usr/local/spark/spark-3.3.1-bin-hadoop3-scala2.13/jars 目录中
类似的，spark连接kafka相关的jar包还有：

spark-sql-kafka jar
spark-token-provider-kafka jar
commons-pool2 jar

参考文档：Spark 3.0.1 Structured Streaming 提交程序异常解决
5. 后记对于缺少jar包问题，都可以到 MVNREPOSITORY 这个网站查找需要的jar包，很全。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>troubleshooting</category>
        <category>java</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>问题排查</tag>
        <tag>java</tag>
        <tag>maven</tag>
        <tag>spark</tag>
      </tags>
  </entry>
  <entry>
    <title>Spark入门篇</title>
    <url>/dev-spark-start/</url>
    <content><![CDATA[1. Spark简介Spark是一个大数据处理框架，可以进行分片计算和并行计算。和MapReduce相比，计算速度更快，编程模型更简单易用。当需要处理的数据量超过了单机尺度（比如我们的计算机有4GB的内存，而我们需要处理100GB以上的数据），这时我们可以选择spark集群进行计算。有时我们可能需要处理的数据量并不大，但是计算很复杂，需要大量的时间，这时我们也可以选择利用spark集群强大的计算资源，并行化地计算。
详情参考：

一文详解Spark基本架构原理
一文带你了解 Spark 架构设计与原理思想



2. Spark架构

Spark Core：包含Spark的基本功能；尤其是定义RDD的API、操作以及这两者上的动作。其他Spark的库都是构建在RDD和Spark Core之上的。
Spark SQL：提供通过Apache Hive的SQL变体Hive查询语言（HiveQL）与Spark进行交互的API。每个数据库表被当做一个RDD，Spark SQL查询被转换为Spark操作。
Spark Streaming：对实时数据流进行处理和控制。Spark Streaming允许程序能够像普通RDD一样处理实时数据。
MLlib：一个常用机器学习算法库，算法被实现为对RDD的Spark操作。这个库包含可扩展的学习算法，比如分类、回归等需要对大量数据集进行迭代的操作。
GraphX：控制图、并行图操作和计算的一组算法和工具的集合。GraphX扩展了RDD API，包含控制图、创建子图、访问路径上所有顶点的操作。
HDFS：分布式文件系统。spark本身并没有提供分布式文件系统，因此spark的分析大多依赖于Hadoop的分布式文件系统HDFS。

3. Spark执行流程Spark 支持 Standalone、Yarn、Mesos、Kubernetes 等多种部署方案，几种部署方案原理也都一样，只是不同组件角色命名不同，但是核心功能和运行流程都差不多。
首先，Spark 应用程序启动在自己的 JVM 进程里，即 Driver 进程，启动后调用 SparkContext 初始化执行配置和输入数据。SparkContext 启动 DAGScheduler 构造执行的 DAG 图，切分成最小的执行单位也就是计算任务。
然后 Driver 向 Cluster Manager 请求计算资源，用于 DAG 的分布式计算。Cluster Manager 收到请求以后，将 Driver 的主机地址等信息通知给集群的所有计算节点 Worker。
Worker 收到信息以后，根据 Driver 的主机地址，跟 Driver 通信并注册，然后根据自己的空闲资源向 Driver 通报自己可以领用的任务数。Driver 根据 DAG 图开始向注册的 Worker 分配任务。
Worker 收到任务后，启动 Executor 进程开始执行任务。Executor 先检查自己是否有 Driver 的执行代码，如果没有，从 Driver 下载执行代码，通过 Java 反射加载后开始执行。
4. Spark部署模式本节内容来自chatgpt。
Apache Spark 可以运行在不同的部署模式下，以满足不同的需求。以下是一些常见的 Spark 部署模式：

Local 模式：在单个 JVM 进程中运行，主要用于开发和测试。
Standalone 模式：在 Spark 自带的资源管理器下运行，适用于中小规模集群。
Apache Mesos 模式：在 Mesos 集群上运行，可以与其他框架共享硬件资源。
Hadoop YARN 模式：在 Hadoop YARN 集群上运行，可以与其他 Hadoop 应用程序共享资源。
Kubernetes 模式：在 Kubernetes 集群上运行，可以与其他容器化应用程序共享硬件资源。

Spark 还支持各种云环境中的部署，例如 Amazon EMR、Microsoft Azure 和 Google Cloud Platform 等。在这些环境中，Spark 集成了相应的管理工具，使得部署更加简单。
]]></content>
      <categories>
        <category>engineering</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>chatgpt</tag>
        <tag>spark</tag>
      </tags>
  </entry>
  <entry>
    <title>Argo CD入门篇</title>
    <url>/dev-argocd-start/</url>
    <content><![CDATA[1. Argo CD是啥？
What Is Argo CD? Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.Why Argo CD? Application definitions, configurations, and environments should be declarative and version controlled. Application deployment and lifecycle management should be automated, auditable, and easy to understand.

Argo CD是什么？Argo CD是一个声明式的基于GitOps的用于K8S的持续交付工具。为什么要使用Argo CD？应用定义、配置和环境都应该被声明和版本控制。应用部署和生命周期管理都应该是自动的、可审计的、易理解的。
Argo CD 遵循 GitOps 模式，使用 Git 仓库作为定义所需应用程序状态的真实来源，Argo CD 支持多种 Kubernetes 清单：

kustomize
helm charts
ksonnet applications
jsonnet files
Plain directory of YAML/json manifests
Any custom config management tool configured as a config management pluginArgo CD 可在指定的目标环境中自动部署所需的应用程序状态，应用程序部署可以在 Git 提交时跟踪对分支、标签的更新，或固定到清单的指定版本。

参考文档：

Argo CD官方文档
Argo CD



2. GitOps是啥？上一节中提到了GitOps，这是个啥？在聊GitOps之前，先聊一下DevOps。

DevOps（Development和Operations的组合词）是一种重视“软件开发人员（Dev）”和“IT运维技术人员（Ops）”之间沟通合作的文化、运动或惯例。通过自动化“软件交付”和“架构变更”的流程，来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

DevOps简单来说，就是敏捷思维（版本更新又快又好）！为了实现敏捷，需要标准和规范，需要开发、测试、运维合作，需要自动化。
而GitOps，正是基于敏捷思维而诞生的一种持续交付方式，能够用声明的方式优雅地进行CICD。1、源码存储于Git源码仓库，开发人员推送提交包含新功能的代码到代码仓库的对应分支中；代码审核通过后将被合并至对应分支。2、合并请求通过后会触发构建并进行测试，构建好的镜像将被推送至镜像仓库中。3、GitOps检测到有新的镜像，会提取最新的镜像标记，然后同步到Git配置仓库（Config）的清单中。4、GitOps检测到集群状态过期，会从配置仓库中拉取更新后的清单，并将包含新功能的镜像通过部署到集群里。
对于不同环境而言，可以在Config仓中创建多个子目录或者子分支管理不同环境对应的多个集群，从而实现多环境的GitOps。
更多内容参考浅谈GitOps
3. Argo CD架构

API Server是一个 gRPC/REST server，它开放了 Web UI、CLI 和 CI/CD 系统使用的 API。
Repository Server是一个内部服务，它维护应用清单的Git仓库的本地缓存，负责生成和返回 Kubernetes 清单。
Application Controller是一个 Kubernetes 控制器，它持续监控正在运行的应用程序并将当前的活动状态与所需的目标状态（如 repo 中指定的）进行比较，检测 OutOfSync 应用程序状态并可选择采取纠正措施。负责为生命周期事件（PreSync、Sync、PostSync）调用用户定义的钩子。

以上是官方给的argocd架构图和说明，详情参考Architectural Overview。这个架构图不太友好，没有明显体现出argocd监听git仓库。
下面再看一个比较友好的argocd架构图：CI流水线触发更新Git仓库中的K8S应用清单，或者工程师直接修改Git仓库中的K8S应用清单，Argo CD都会自动拉取最新的配置并应用到K8S集群中。详情参考Argo CD 入门教程
4. 触发Argo CD同步的方式触发Argo CD同步有三种方式：

手动：在页面上点击触发同步，或者使用CLI触发同步
自动：配置后自动触发同步
Webhook：Git仓库更新后，通过Webhook告知Argo CD触发同步。详情参考文档Git Webhook Configuration

5. Argo CD使用方法5.1. Argo CD使用概述1、准备一个git仓库，作为存放k8s yaml资源清单的仓库2、创建一个argocd应用，关联git仓库的分支和路径，关联k8s集群3、触发同步，把git仓库中的资源清单apply到k8s集群
参考文档：

Argo CD - Getting Started
Argo CD 入门教程
Argo CD 使用
Kustomize + Argo CD 优化发布流程
Argo CD - Automated Sync Policy
Argo CD - Sync Options

5.2. 准备git仓库创建一个git仓库，包含k8s资源清单（一般是kustomization文件）。使用这些资源清单可以在k8s中创建和变更deployment、service等资源对象（不使用argocd，直接使用kubectl也可以）。
git仓库参考：voidking/argocd-demo
kustomize相关知识点参考：《Kustomize工具》
5.3. 配置argocd应用通过argocd的client或者UI界面，配置一个argocd应用。应用会指定git仓库、路径，这样就知道使用哪些资源清单了。应用还会指定k8s集群，这样就知道在哪个集群中创建和变更资源对象了。
Automated SYNC POLICY​​​：

RRUNE RESOURCES​​：自动修剪。集群上某个资源在 GitRepo 中找不到对应的配置时，自动删除集群上的该资源。
SELF HEAL​​：自愈。因各种原因（如手动修改）集群上资源的实时状态而导致与 GitRepo 不匹配时，自动将实际状态与 GitRepo 的期望状态同步。不勾选时，虽然也会进行自动同步的探测，但是只有当GitRepo中清单发生变化时，才会触发k8s资源变更。
自动同步时间间隔：默认180s。详情参考文档Automated Sync Policy default interval value

SYNC OPTIONS：​- SKIP SCHEMA VALIDATION​​：是否执行资源规范格式的校验。​​- AUTO-CREATE NAMESPACE​​：自动创建命名空间。​​- PRUNE LAST​​：在同步操作的最后在执行修剪操作，即其他资源已经部署且转为健康状态后再进行 prune​​- APPLY OUT OF SYNC ONLY​​​：仅对那些处于 ​​OutOfSync​​​ 状态的资源执行同步操作。​​- REPLACE​​：将使用 kubectl replace/create 命令同步资源，而非默认的 apply​​- PRUNE PROPAGATION POLICY​​：资源修剪传播策略，默认值使用 foreground 策略，还有 background 和 orphan​​- RESPECT IGNORE DIFFERENCES​​​：在同步阶段忽略期望状态的字段。例如我们有个 deployment，里面的 replicas 为 20，代表我们期望的 pod 数量为 20 个，但如果我们进行灰度发布的时候，可能多，也可能少。这个时候，如果不勾选 ​​RESPECT IGNORE DIFFERENCES​​ ，就会导致灰度发布出现问题，所以这时候我们最好是勾选上该参数。
5.4. 资源创建/变更配置完成，点击同步，argocd就会从git仓库获取最新的配置清单，apply到k8s集群。如果配置了自动同步，那么默认3分钟自动同步一次，可以修改timeout.reconciliation字段修改同步间隔。
6. 添加新集群参考文档：argocd cluster Command Referenc
一个argocd server，可以管理多个k8s集群。添加新集群的流程如下：
1、准备新集群的kubeconfig文件

kubeconfig 文件名随意，例如 newkubeconfig
修改 contexts.name 和 current-context 为想要在argocd中展示的名称，例如 test-k8s

2、验证 newkubeconfig
KUBECONFIG=newkubeconfig kubectl config get-contexts -o nameKUBECONFIG=newkubeconfig kubectl get nodes

3、执行添加新集群命令
KUBECONFIG=newkubeconfig argocd cluster add test

4、验证新集群是否添加成功
argocd cluster list

访问argocd server，查看集群列表https://argocd.voidking.com/settings/clusters
7. 问题记录7.1. Token is expired问题描述：执行argocd app list命令，报错：
FATA[0000] rpc error: code = Unauthenticated desc = invalid session: Token is expired

解决办法：重新登录argocd server
argocd login argocd.voidking.com --grpc-web# orargocd login localhost:31080 --grpc-web

7.2. K8S集群证书过期问题描述：正常使用argocd一段时间后，argocd在执行同步时报错
the server has asked for the client to provide credentials

解决办法：更新k8s集群证书更新k8s集群证书的办法，是重新执行添加集群命令，更新时使用 --upsert 参数。
argocd cluster listKUBECONFIG=newkubeconfig argocd cluster add test --upsert


]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>argocd</tag>
        <tag>cicd</tag>
        <tag>devops</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：GitLab CI报错no space left on device</title>
    <url>/dev-gitlab-ci-no-space-left-on-device/</url>
    <content><![CDATA[1. 问题描述gitlab CI任务，拉基础镜像的时候报错no space left on device：
failed to register layer: Error processing tar file(exit status 1): write /root/miniconda3/envs/cv/lib/python3.7/site-packages/xxx.so: no space left on device



2. 排查思路问题原因猜测：

偶发问题？确认是否能稳定复现
磁盘空间问题？确认runner所在主机的磁盘空间
CICD配置问题？确认CICD配置，检查用法是否正确
runner问题？重启runner，再次尝试复现

排查确认：

问题偶发复现，都是在同一个runner所在主机
runner所在主机的磁盘空间充足
CICD配置正确
重启runner后无法复现，问题解决了，说明是runner问题。但是过几天会再次复现，需要继续定位根因。

3. runner问题排查出问题的runner，使用的执行器是 docker machine executordocker machine类型的执行器，特点是先创建VM，然后在VM中执行作业。了解了这个特点，我们就能知道报错磁盘满的并不是runner所在宿主机，而是实际执行作业的VM。
1、登录到执行作业的VM进行确认
docker-machine lsdocker-machine ssh xxxdf -h
经确认，确实是执行作业的VM磁盘满了。因为runner所在宿主机磁盘充足，因此解决办法是给VM扩磁盘。
2、VM磁盘扩容因此VM是受runner管理的，所以最好不要通过docker-machine命令手动扩容，而是通过修改runner配置来修改。
sudo vim /etc/gitlab-runner/config.toml

config.toml中的virtualbox-disk-size修改到期望的值：
...  [runners.machine]    IdleCount = 5    MaxGrowthRate = 1    IdleTime = 1800    MachineDriver = &quot;virtualbox&quot;    MachineName = &quot;auto-scale-%s&quot;    MachineOptions = [      &quot;engine-registry-mirror=http://xxxxxx:5000&quot;,      &quot;virtualbox-memory=4048&quot;,      &quot;virtualbox-disk-size=204800&quot;,      &quot;virtualbox-cpu-count=2&quot;    ]

3、重启runner
gitlab-runner restartdocker-machine ls
重启runner后，我们可以看到VM被重建了。
4、确认磁盘大小
docker-machine ssh xxxdf -h

以上，问题解决。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>troubleshooting</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>问题排查</tag>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：GitLab CI/CD入门篇</title>
    <url>/dev-gitlab-cicd/</url>
    <content><![CDATA[1. GitLab CI/CD是啥？CI，CONTINUOUS INTEGRATION，持续集成。简单来说就是自动化构建和测试。一个应用程序的代码存储在Git仓库中。开发人员推送的每个更改，甚至是开发分支，都可以通过一组脚本来自动地构建和测试。这些测试可确保更改通过您为应用程序建立的所有测试、指南和代码合规性标准。
CD，CONTINUOUS DELIVERY，持续交付。简单来说就是自动化构建和测试+支持手动触发部署。每次将代码更改推送到代码库时，不仅会自动构建和测试应用程序，还支持一键部署应用程序，这里的部署需要手动触发。
CD，CONTINUOUS DEPLOYMENT，持续部署。简单来说就是自动化构建和测试+自动部署。持续部署类似于持续交付，不同之处在于，不是手动触发部署应用程序，而是将其设置为自动部署。
而GitLab CI/CD，就是一种支持在GitLab中配置使用持续集成、持续交付和持续部署的工具。GitLab CI/CD通过使用YAML文件定义作业流程和流水线（pipelines），可实现复杂的应用程序的自动化构建和部署。
参考文档：

GitLab CI/CD Document
GitLab CI/CD 中文文档
CI/CD 概念
详解CI、CD &amp; CD
什么是 CI/CD？
Deployment environment



2. GitLab CI/CD基本概念
pipeline：在每个仓库中，使用名为.gitlab-ci.yml的yaml文件配置gitlab ci/cd流水线（pipeline）
stage：一条流水线可以包含多个阶段（stage），一个阶段可以包含多个作业
job：作业（job）是具体要执行的任务，是命令脚本语句的集合
runner：runner是每个作业的执行节点；每个作业可以根据标签选择不同的执行节点

3. Gitlab CI/CD工作机制1、在gitlab仓库中的根目录下添加.gitlab-ci.yml文件，定义流水线  
2、当仓库中发生commit、push、merge等事件时，根据.gitlab-ci.yml的定义触发流水线  
3、流水线会调用gitlab runner，在gitlab runner中按照.gitlab-ci.yml的定义跑作业
4. 配置第一个流水线4.1. 配置.gitlab-ci.yml为了配置GitLab CI/CD流水线，首先需要在项目根目录创建一个.gitlab-ci.yml文件，并定义需要执行的作业和阶段。
stages:  - build  - test  - deploybuild_job:  stage: build  script:    - echo &quot;Building the project...&quot;    - ./build-script.shtest_job:  stage: test  script:    - echo &quot;Running tests...&quot;    - ./run-tests.shdeploy_job:  stage: deploy  script:    - echo &quot;Deploying application...&quot;    - ./deploy-script.sh

这个简单的例子定义了三个作业，每个作业分别执行构建、测试和部署脚本。
4.2. 注册Runner为了让流水线工作，我们需要至少一个注册的Runner。可以使用GitLab提供的共享Runner，或者自己安装和注册一个专用的Runner。
注册Runner步骤如下：
1、在GitLab页面上，进入我们的项目设置。2、寻找CI/CD设置，找到Runners部分。3、我们会看到用于注册Runner的URL和Token。4、在具有网络访问权限的机器上安装Runner并运行注册命令，使用提供的URL和Token。
更多Runner相关内容，参考文档《好好学GitLab：GitLab Runner入门篇》
5. 流水线典型示例stages: #对stages的编排  - build  - test  - deployvariables:  DEPLOY_ENV: &quot;dev&quot;ciinit:  tags:    - build  stage: .pre  script:    - echo &quot;Pipeline init first job&quot;ciend:  tags:    - build  stage: .post  script:    - echo &quot;Pipeline end job&quot;before_script:  - echo &quot;Before script section&quot;  - echo &quot;For example you might run an update here or install a build dependency&quot;  - echo &quot;Or perhaps you might print out some debugging details&quot;after_script:  - echo &quot;After script section&quot;  - echo &quot;For example you might do some cleanup here&quot;build:  tags:    - k8s  stage: build  script:    - echo &quot;Do your build here&quot;test1:  tags:    - k8s    stage: test  script:    - echo &quot;Do a test here&quot;    - echo &quot;For example run a test suite&quot;test2:  tags:    - k8s    stage: test  script:    - echo &quot;Do a test here&quot;    - echo &quot;For example run a test suite&quot;deploy:  tags:    - k8s    stage: deploy  variables:    DEPLOY_ENV: &quot;test&quot;  script:    - echo &quot;$&#123;DEPLOY_ENV&#125;&quot;    - echo &quot;Do your deploy here&quot;

6. 流水线语法6.1. 流水线语法概述GitLab CI/CD流水线是由 .gitlab-ci.yml 文件中定义的一系列规则和指令组成。这个文件使用YAML语法来描述CI/CD的过程。
GitLab CI/CD 流水线配置包括：

配置流水线行为的全局关键字
配置作业行为作业关键字

参考文档：

.gitlab-ci.yml 关键字参考
The .gitlab-ci.yml file
.gitlab-ci.yml keyword reference
GitLabCI-CD流水线语法

6.2. 全局关键字
default 作业关键字的自定义默认值。
stages 流水线阶段的名称和顺序。
workflow 控制运行的流水线类型。
include 从其他 YAML 文件导入配置。

6.3. 作业关键字6.3.1. 执行命令
before_script：作业在运行前执行的Shell命令行
script：作业在运行中执行的Shell命令行，每个作业至少要包含一个script
after_script：作业在运行后执行的Shell命令行

有了before_script、script、和after_script，可以方便我们把通用的脚本抽象出来。

before_script：有命令执行结果非0，job判定失败，不再执行before_script的后续命令，跳过script，继续执行after_script
script：有命令执行结果为0，job判定失败，不再执行script的后续命令，继续执行after_script
after_script：有命令执行结果非0，job判定不受影响，不再执行after_script后续命令

6.3.2. 环境变量
variables：在作业级别定义作业变量。
environment：作业部署到的环境的名称。
secrets：作业所需的 CI/CD secret 信息。secret属于enviroment。

6.3.3. 作业条件
stage：定义作业阶段，指定当前作业所属的阶段名称，从而确定作业顺序
only：满足条件时执行作业
except：满足条件时不执行作业
rules：12.3之后引入的特性，不能与only/except混用，具体用法参考【jobs:rules作业控制】一节
when：何时运行作业。
needs：在 stage 顺序之前执行的作业。
tags：根据标签选择运行作业的Runner节点，如果有多个标签，则匹配具有所有标签的Runner节点
image：指定执行作业的脚本时应该启动哪个 Docker 镜像作为执行环境，成为主容器。这个 Docker 镜像应包含所有必要的依赖和工具，以便能够运行定义在 script 部分的命令。需要Runner支持Docker。
services：指定与主容器相连的额外容器服务。这些服务可以是数据库、缓存等。
allow_failure：允许作业失败。失败的作业不会导致流水线失败。
parallel：应该并行运行多少个作业实例。
resource_group：限制作业并发。
trigger：定义下游流水线触发器。

6.3.4. 超时与重试
retry：在失败的情况下可以自动重试作业的时间和次数。
timeout：定义优先于项目范围设置的自定义作业级别超时。

6.3.5. 继承
extends：此作业继承自的配置条目。
inherit：选择所有作业继承的全局默认值。

6.3.6. 其他
artifacts：成功时附加到作业的文件和目录列表。
cache：应在后续运行之间缓存的文件列表。
coverage：给定作业的代码覆盖率设置。
dast_configuration：在作业级别使用来自 DAST 配置文件的配置。
dependencies：通过提供要从中获取产物的作业列表，来限制将哪些产物传递给特定作业。
interruptible：定义当新运行使作业变得多余时，是否可以取消作业。
pages：上传作业的结果，与 GitLab Pages 一起使用。
release：指示运行器生成 release 对象。

7. 流水线部分关键字详解7.1. variables环境变量环境变量可以分为两类：全局变量和局部变量，局部变量优先级高于全局变量

全局变量是整个流水线生效的，局部变量是仅在作业中生效的
全局变量可以使用CI自带的预定义变量，也可以自定义变量
全局变量定义在全局 variables 中，也可以定义在 workflow:rules:variables 中
局部变量定义在 job:variables 中，也可以定义在 job:rules:variables 中

.gitlab-ci.yml解析顺序为：全局变量 -&gt; workflow规则 -&gt; job规则因此优先级顺序为：job中变量优先级 &gt; workflow中变量优先级 &gt; 全局变量优先级
常用全局预定义变量：



变量名称
GitLab
GitLab Runner
描述



CI
all
0.4
对CI/CD中的所有作业可见，值为true


CI_BUILDS_DIR
all
11.10
构建时的最顶层目录


CI_COMMIT_AUTHOR
13.11
all
提交的作者，格式为：名称&lt;邮箱&gt;


CI_COMMIT_BEFORE_SHA
11.2
all
当前分支的上一个提交哈希值


CI_COMMIT_BRANCH
12.6
0.5
提交的分支名，在合并流水线和tag流水线时不可见


CI_COMMIT_DESCRIPTION
10.8
all
提交的描述


CI_COMMIT_MESSAGE
10.8
all
完整的提交信息


CI_COMMIT_REF_NAME
9.0
all
项目的分支名或tag名


CI_COMMIT_REF_PROTECTED
11.11
all
如果作业正在构建的是被保护的分支或tag，值为true


CI_COMMIT_REF_SLUG
9.0
all
CI_COMMIT_REF_NAME的小写形式。


CI_COMMIT_SHA
9.0
all
提交的完整哈希值


CI_COMMIT_SHORT_SHA
11.7
all
8个字符的提交哈希值


CI_COMMIT_TAG
9.0
0.5
提交的tag，仅在tag流水线可见


CI_COMMIT_TIMESTAMP
13.4
all
提交时的时间戳


CI_COMMIT_TITLE
10.8
all
提交的标题


CI_DEFAULT_BRANCH
12.4
all
项目的默认分支


CI_DEPLOY_FREEZE
13.2
all
当流水运行是处于部署冻结阶段时可见，值为true。


CI_ENVIRONMENT_NAME
8.15
all
当前作业的部署环境名，当设置了environment:name 时可见


CI_ENVIRONMENT_URL
9.3
all
当前作业的部署环境地址，只有设置了environment:url可见


CI_JOB_ID
9.0
all
当前作业的ID，系统内唯一


CI_JOB_IMAGE
12.9
12.9
当前作业使用的Docker镜像名


CI_JOB_NAME
9.0
0.5
当前作业名称


CI_JOB_STAGE
9.0
0.5
当前作业所属的阶段名


CI_PIPELINE_ID
8.10
all
当前流水线ID（实例级），系统内唯一


CI_PIPELINE_SOURCE
10.0
all
流水线触发方式，枚举值为push,web, schedule, api, external, chat, webide,merge_request_event, external_pull_request_event, parent_pipeline, trigger, 或者 pipeline


CI_PIPELINE_TRIGGERED
all
all
当作业是使用trigger触发的时为true


CI_PIPELINE_URL
11.1
0.5
流水线详情的地址


CI_PIPELINE_CREATED_AT
13.10
all
流水线创建时间


CI_PROJECT_DIR
all
all
存放克隆项目的完整路径，作业运行的目录。


CI_PROJECT_NAME
8.10
0.5
当前项目名称，不包含组名


CI_PROJECT_NAMESPACE
8.10
0.5
项目的命名空间（组名或用户名）


CI_PROJECT_PATH
8.10
0.5
包含项目名称的命名空间


CI_PROJECT_TITLE
12.4
all
项目名称（网页上显示的）


CI_PROJECT_URL
8.10
0.5
项目HTTP(S)地址


CI_RUNNER_TAGS
8.10
0.5
逗号分割的runner标签列表


GITLAB_USER_EMAIL
8.12
all
开始当前作业的用户邮箱


GITLAB_USER_LOGIN
10.0
all
开始当前作业的登录用户名


GITLAB_USER_NAME
10.0
all
开始当前作业的用户名


CI_MERGE_REQUEST_APPROVED （仅合并流水线）
14.1
all
当合并流水线的MR被通过时值为true


CI_MERGE_REQUEST_ASSIGNEES （仅合并流水线）
11.9
all
逗号分割的合并请求指派人列表


CI_MERGE_REQUEST_SOURCE_BRANCH_NAME（仅合并流水线）
11.6
all
合并请求中的源分支名称


CI_MERGE_REQUEST_TARGET_BRANCH_NAME（仅合并流水线）
11.6
all
合并请求中的目标分支名称


CI_MERGE_REQUEST_TITLE（仅合并流水线）
11.9
all
合并请求的标题


预定义变量详情参考文档：

Predefined variables reference
GitLab CI/CD中的常用预设变量

7.2. 全局关键字7.2.1. workflow:rules流水线控制使用 workflow:rules 关键字来控制何时创建流水线。workflow:rules 关键字在作业之前进行评估。例如，将作业配置为针对标签运行，但工作流阻止标签流水线，则该作业永远不会运行。
rules包含以下关键字：

if：条件判断，为true时再看对应的when规则。
when：取值never表示流水线不运行，取值always或者缺省表示流水线运行。
最后一个规则的if缺省，when: always表示其他所有流水线类型都运行。
最后一个规则不是when: always，表示其他所有流水线类型都不运行。


variables：定义特定流水线条件的变量（引入于13.11版本）。例如不同分支，同一个变量可以定义不同的值。

参考文档：

GitLab CI/CD workflow 关键字
.gitlab-ci.yml 关键字参考 - workflow
Interaction between workflow:rules and rules

7.2.2. stages阶段控制
stages里可以定义各个stage执行的前后顺序，但是.pre和.post阶段不受其控制
.pre阶段的作业总是在流水线开始时执行
.post阶段的作业总是在流水线结束时执行
如果两个或者多个作业，指向同一个阶段名称，则该阶段下的所有作业都并行运行；如果不能并行运行，需要检查runner的配置文件中的concurrent

7.2.3. include使用 include 在 CI/CD 配置中包含外部 YAML 文件。 您可以将一个长的 .gitlab-ci.yml 文件拆分为多个文件以提高可读性，或减少同一配置在多个位置的重复。
您还可以将模板文件存储在中央仓库中并将它们包含在项目中。
include 文件说明：

与 .gitlab-ci.yml 文件中的那些合并。
无论 include 关键字的位置如何，始终先求值，然后与 .gitlab-ci.yml 文件的内容合并。

include 子键：

include:local
include:project，include:file，include:ref
include:remote
include:template

注意：如果include了多个文件中都包含stages，那么最后一个文件中的stages会生效。
参考文档：gitlab-ci - include
7.3. 作业关键字7.3.1. job:rules作业控制使用 job:rules 关键字来控制何时创建作业。
rules包含以下子关键字：

if：条件判断，为true时再看对应的其他规则。
if的语法和bash中的if语法很像
不同1：模式匹配时添加了两个斜杠
不同2：变量不能加花括号（版本14.0.5）


when：前面的作业成功或者失败时运行。on_success（默认值）前面作业成功时执行；on_failure 前面作业失败时执行；always 总是执行；manual 手动执行；delayed&amp;start_in 延迟执行；never 永不执行。
allow_failure：是否允许作业失败，默认值为false。启用后，作业失败不会阻塞接下来的任务。
retry：作业遇到错误重新运行的次数。
timeout：作业运行超时时间。
needs：作业依赖控制。当前作业可能只依赖前一个stage的其中一个作业，就不用等前一个stage的作业全部完成。
parallel：生成多个作业，并行运行。值在2-50之间。
variables：定义特定作业条件下的变量。

参考文档：

GitLabCI系列之流水线语法第三部分
Choose when to run jobs
选择何时运行作业
.gitlab-ci.yml 关键字参考 - rules

7.3.2. extends使用 extends 来重用配置 section。它是 YAML 锚点 的替代方案，并且更加灵活和可读。
关键字类型：作业关键字。您只能将其用作作业的一部分。
可能的输入：

流水线中另一个作业的名称。
流水线中其他作业的名称列表（数组）。

注意：extends父作业时，如果不想执行父作业，那么父作业的名称应该以点.开头。
参考文档：gitlab-ci - extends
7.3.3. job:services使用services可以指定job运行所依赖的服务镜像。GitLab CI/CD会为每个任务创建一个Docker网络，并且把任务容器（主容器）和服务容器都连接到这个网络上。服务容器的别名就是它在这个网络上的主机名，所以任务容器可以通过别名来访问服务容器提供的端口。
使用Docker执行器的话，任务和服务都跑在容器中。使用Shell执行器的话，任务跑在执行器所在的机器上，服务跑在容器中。同时，执行器所在机器需要安装Docker。
参考文档：

gitlab-ci - services
Available settings for services
Define services in the .gitlab-ci.yml file

7.3.4. job:artifactsjob:artifacts的作用是将Job执行后生成的文件或目录存储在GitLab服务器上，供后续的Job或用户下载和使用。我们可以通过UI或API来下载Job artifacts。需要注意的是，Job artifacts的大小不能超过配置的最大值。
artifacts常用关键字：

paths：所有指定的路径中的文件，都会被放入artifacts目录。用于指定任意类型的Job artifact，例如编译后的二进制文件、日志文件、图片文件等，这些类型的Job artifact可以被后续的Job或用户下载和使用。只有在Job执行成功时才会上传，除非指定了when:always参数。
name：指定制品名称，默认制品名称是artifacts，下载时对应artifacts.zip。
exclude：排除不要放入artifacts目录的文件。
expire_in：制品的过期时间，默认30天。每小时会自动删除一次过期制品。
expose_as：要在制品下载链接的UI中显示的名称。
public：制品是否可以公开下载。
reports：用于指定一些特定类型的Job artifact，例如JUnit测试报告、代码覆盖率报告、性能测试报告等，这些报告是可以在GitLab的流水线视图、性能仪表盘和安全仪表盘中查看的。总是上传制品，无论Job执行成功还是失败。
untracked：是否把.gitignore中忽略的文件也作为制品，默认false，不包含被忽略的文件。
when：什么时候上传制品。取值on_success、on_failure和always，只对paths生效，reports一定会上传。

下载制品的方法：进入CI/CD Pipelines页面，找到Job对应的Pipeline，最右边三个点，点击下载artifacts。
参考文档：

gitlab-ci - artifacts

7.3.5. job:coverage将覆盖率与自定义正则表达式一起使用，可以配置如何从作业输出中提取代码覆盖率。如果作业输出中至少有一行与正则表达式匹配，则覆盖率将显示在UI中。
参考文档：

gitlab-ci - coverage
Test coverage visualization

8. 常用配置8.1. 当流水线成功时合并分支1、项目配置Project -&gt; Settings -&gt; General -&gt; Merge requests -&gt; Expand -&gt; Merge checks -&gt; 勾选 Pipelines must succeed
2、gitlab-ci配置gitlab-ci配置时，要保证workflow和job中都允许merge requests触发pipeline。
3、触发合并请求gitlab页面上发起一次合并请求，或者使用glab发起合并请求
4、流水线成功时自动合并打开合并代码的页面，当pipeline运行时，点击 Merge when pipeline succeeds
参考文档：

当流水线成功时合并
Configuring pipelines for merge requests
配置Gitlab合并流水线
Gitlab 发起合并请求，一行命令就搞定！

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
      </tags>
  </entry>
  <entry>
    <title>ClickHouse入门篇</title>
    <url>/dev-clickhouse-start/</url>
    <content><![CDATA[1. ClickHouse简介
ClickHouse 是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。

参考文档： 

GitHub - ClickHouse
什么是ClickHouse？
ClickHouse深度揭秘



2. ClickHouse基础概念在clickhouse集群中，clickhouse的表有两种类型：一种是本地表，一种是分布式表。在单机情况下，使用本地表就可以了；在集群情况下，一般使用分布式表。ch的单机和分布式的增删查改差异很大，使用的时候需要注意。分布式表首先需要在集群所有节点上创建本地表，然后在本地表的基础上创建分布式表（本质是分布式视图），通过分布式表引擎聚合本地表引擎的数据。
一般写入使用本地表，查询使用分布式表。
写分布式表的优点：可以让ClickHouse控制数据到分片的路由。写分布式表的缺点：

数据是先写到一个分布式表的实例中并缓存起来，再逐渐分发到各个分片上去，实际是双写了数据（写入放大），浪费资源；
数据写入默认是异步的，短时间内可能造成不一致；
目标表中会产生较多的小parts，使merge（即compaction）过程压力增大。

写本地表优点：同步操作，更快，parts的大小也比较合适。写本地表缺点：要求应用层额外实现sharding和路由逻辑，如轮询或者随机等。
表引擎特殊说明：

20版本的ch只有使用了replicated开头的engine的引擎的表，才能够在拥有on cluster xxx条件的ddl语句中进行集群更新；其他engine的表，只能够每个node进行update；21版本修复了这个bug。
目前阿里云20版本的ch的mergeTree引擎是支持on cluster xxx这样的ddl语句的。

参考文档：

ClickHouse - 基本概念
ClickHouse创建分布式表
ClickHouse常见问题
「Clickhouse系列」分布式表&amp;本地表详解
CK 分布式表和本地表
clickhouse单机的增删查询实现方案和clickhouse分布式部署的增删查改实现方案
ClickHouse - Data Replication
clickhouse删除ReplicatedMergeTree复制表后，新建同名表失败原因分析

3. 安装ClickHouse服务端参考文档：

《好好学Docker：使用Docker安装配置ClickHouse》
《好好学K8S：K8S中安装配置ClickHouse》

4. 安装ClickHouse客户端4.1. dbeaver使用dbeaver，可以图形化连接clickhouse。安装方法：下载安装包，双击安装。
4.2. clickhouse-client使用clickhouse-client，可以命令行连接clickhouse。
安装方法参考ClickHouse - 安装
4.2.1. centos7中安装安装：
wget https://packages.clickhouse.com/rpm/stable/clickhouse-common-static-21.8.15.7-2.x86_64.rpmwget https://packages.clickhouse.com/rpm/stable/clickhouse-client-21.8.15.7-2.noarch.rpmrpm -ivh clickhouse-common-static-21.8.15.7-2.x86_64.rpmrpm -ivh clickhouse-client-21.8.15.7-2.noarch.rpm

测试：
clickhouse-client -h 192.168.56.101 --port 9000 \-u default --password xxx \--query &quot;select hostName()&quot;

如果执行报错：Code: 102. DB::NetException: Unexpected packet from server 192.168.56.101:9000 (expected Hello or Exception, got Unknown packet). (UNEXPECTED_PACKET_FROM_SERVER)
请检查clickhouse服务端端口是否正确，改成正确的端口。
4.2.2. macos中安装安装：
curl -O &#x27;https://builds.clickhouse.com/master/macos/clickhouse&#x27; &amp;&amp; chmod a+x ./clickhouse

测试：
./clickhouse client -h 192.168.56.101 --port 9000 \-u default --password xxx \--query &quot;select hostName()&quot;


5. 使用ClickHouse5.1. 使用分布式表1、新建分布式表
-- 查看宏变量select * from `system`.macros;-- 查看clusterselect * from `system`.clusters;-- 创建本地表-- create table `default`.optest on cluster cluster (a UInt64) Engine = MergeTree() order by tuple();create table `default`.optest on cluster cluster (a UInt64) Engine = ReplicatedMergeTree(&#x27;/clickhouse/tables/&#123;shard&#125;/default.optest&#x27;,&#x27;&#123;replica&#125;&#x27;) order by tuple();-- 创建本地表对应的分布式表create table `default`.optest_dis on cluster cluster as optest Engine = Distributed(cluster, default, optest, cityHash64(a));-- 删除本地表drop table `default`.optest on cluster cluster;-- 要等待480s才会删除zk中的数据

order by指定的字段是对分区内数据进行排序，在ReplicatedMergeTree引擎下，会对相邻的重复数据进行删除。
2、增删查改
INSERT INTO `default`.optest(a) values(10);SELECT * FROM `default`.optest_dis;ALTER TABLE `default`.optest  on cluster cluster UPDATE `a` = &#x27;11&#x27; WHERE `a` = &#x27;10&#x27;;SELECT * FROM `default`.optest_dis;ALTER TABLE `default`.optest on cluster cluster DELETE WHERE `a`=&#x27;11&#x27;;SELECT * FROM `default`.optest_dis;

6. 更新延迟处理从使用场景来说，Clickhouse是个分析型数据库。这种场景下，数据一般是不变的，因此Clickhouse对update、delete的支持是比较弱的，实际上并不支持标准的update、delete操作。
Clickhouse通过alter方式实现更新、删除，它把update、delete操作叫做mutation(突变)。
标准SQL的更新、删除操作是同步的，即客户端要等服务端反回执行结果（通常是int值）；而Clickhouse的update、delete是通过异步方式实现的，当执行update语句时，服务端立即反回，但是实际上此时数据还没变，而是排队等着。
ClickHouse本身对update的执行是低效的，因为ClickHouse的MergeTree存储一旦生成一个Data Part，这个Part就不支持更改，而是需要删除旧Part, 重写整个Part。所以从MergeTree存储内核层面，ClickHouse就不擅长做数据更新删除操作。
参考文档：

ClickHouse多种实时更新方法总结
clickhouse数据实时更新实现的三种方式
Clickhouse中update/delete的使用

6.1. mutations_sync使用mutations_sync参数，是最简单的方法。方法一：客户端和clickhouse建立连接时，添加参数mutations_sync=2方法二：执行sql时，添加参数settings mutations_sync=2方法三：修改clickhouse服务端配置，设置mutations_sync=2
python客户端连接参数：
ck_config = &#123;  &quot;host&quot;: ck_host,   &quot;port&quot;: ck_port,   &quot;user&quot;: ck_user,   &quot;password&quot;: ck_pwd,   &quot;settings&quot;: &#123;&quot;mutations_sync&quot;: &quot;2&quot;&#125;&#125;

但是，如果存在大量更新，这种方法会大量重建Part，效率会很低。
6.2. ReplacingMergeTreeReplacingMergeTree + INSERT + Final 可以实现虚拟更新，以insert代替alter操作，每次select时都拉取最新一条数据。当执行optimize时，老的数据才会被删除。
CREATE TABLE tb_test_replacing(ts DateTime,uid String,biz String) ENGINE = ReplacingMergeTree(ts) ORDER BY (ts) SETTINGS index_granularity = 8192;INSERT INTO tb_test_replacing VALUES (&#x27;2019-06-07 20:01:01&#x27;, &#x27;c&#x27;, &#x27;c1&#x27;);INSERT INTO tb_test_replacing VALUES (&#x27;2019-06-07 20:01:01&#x27;, &#x27;c&#x27;, &#x27;c2&#x27;);SELECT * FROM tb_test_replacing;SELECT * FROM tb_test_replacing FINAL;optimize table tb_test_replacing;SELECT * FROM tb_test_replacing;

7. 从MySQL同步数据到ClickHouse7.1. ClickHouse官方工具set allow_experimental_database_materialize_mysql = 1;select value from system.settings where name = &#x27;allow_experimental_database_materialize_mysql&#x27;;CREATE DATABASE db1_mysqlENGINE = MaterializeMySQL(  &#x27;mysql-host.domain.com:3306&#x27;,  &#x27;db1&#x27;,  &#x27;clickhouse_user&#x27;,  &#x27;ClickHouse_123&#x27;);

参考文档：

Replicate a MySQL Database in ClickHouse
Integrating MySQL with ClickHouse
[experimental] MaterializedMySQL
MaterializeMySQL引擎
ClickHouse和他的朋友们（9）MySQL实时复制与实现

注意：官方文档中的materialized，需要改成materialize。
7.2. MaterializeMySQL同步原理（ChatGPT）使用 MaterializeMySQL 引擎同步数据时，首先会进行一次全量同步，然后再进行增量同步。
7.2.1. 全量同步全量同步的过程：
1、建立连接：首先需要建立到 MySQL 服务器的连接，提供主机名、端口、用户名和密码。
2、创建数据库：在 ClickHouse 中创建一个使用 MaterializeMySQL 引擎的数据库。这将自动触发全量同步过程。
3、获取数据表结构：ClickHouse 会从 MySQL 数据库获取表的结构信息，包括列名、数据类型等，并在 ClickHouse 中创建相应的表结构。
4、导出数据：ClickHouse 会从 MySQL 数据库导出所有表的数据。这一过程通常使用 mysqldump 工具或类似方法来完成。
5、导入数据：将导出的数据导入到 ClickHouse 中相应的表中。在这个过程中，数据可能需要进行转换，以便与 ClickHouse 的数据格式相匹配。
6、启动增量同步：完成全量同步后，ClickHouse 会自动开始监听 MySQL 服务器的二进制日志，以实时捕获和应用数据更改。
7.2.2. 增量同步使用ClickHouse的MaterializeMySQL引擎增量同步数据的原理主要基于MySQL的二进制日志（Binary Log，简称binlog）。二进制日志是MySQL用于记录所有更改（如数据修改和表结构变更）的日志文件。binlog是一种行级别的日志记录格式，包含足够的信息来重放所有已执行的数据更改。
以下是MaterializeMySQL引擎同步数据的主要步骤：
1、连接到 MySQL：首先，ClickHouse 需要连接到 MySQL 服务器。为此，需要提供 MySQL 服务器的连接信息，如主机名、端口、用户名和密码。
2、读取 binlog：ClickHouse 从 MySQL 服务器读取二进制日志，这些日志记录了数据库中的所有更改。ClickHouse 使用 mysqlbinlog 工具解析二进制日志文件。
3、解析更改：ClickHouse 解析二进制日志中的事件，如插入、更新、删除操作。这些事件将被转换为 ClickHouse 可以理解的格式。
4、应用更改：根据解析的更改事件，ClickHouse 更新其数据库。此过程涉及将更改应用到相应的表和列中。
5、实时同步：MaterializeMySQL 引擎会持续监听 MySQL 服务器的二进制日志，当 MySQL 数据库中发生数据更改事件时，ClickHouse 会立即捕获这些事件并应用相应的更改。
7.3. clickhouse-clientclickhouse-client支持通过输入数据文件、管道、JDBC等方式导入数据。可以将MySQL中的数据导出成csv文件，然后使用clickhouse-client导入到ClickHouse中。
参考文档：

Command-Line Client

7.4. 阿里云工具参考文档：

阿里云ClickHouse - 从MySQL导入数据
阿里云ClickHouse - 从RDS MySQL同步

]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>chatgpt</tag>
        <tag>数据库</tag>
        <tag>macos</tag>
        <tag>clickhouse</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置ClickHouse</title>
    <url>/dev-docker-clickhouse/</url>
    <content><![CDATA[1. 前言本文中，我们学习使用Docker安装配置ClickHouse。
参考文档： 

ClickHouse官方文档 - 安装
dockerhub - clickhouse



2. 安装ClickHouse1、下载ClickHouse镜像（这里选择 24.9.2.42）
docker pull clickhouse:24.9.2.42

2、准备目录和配置文件
mkdir -p /opt/clickhouse/&#123;config.d,users.d,docker-entrypoint-initdb.d,data,logs&#125;chmod a+rw -R /data/clickhouse/&#123;data,logs&#125;docker run -d --name tmp \  --ulimit nofile=262144:262144 \  clickhouse:24.9.2.42docker cp tmp:/etc/clickhouse-server/config.d/. /opt/clickhouse/config.ddocker cp tmp:/etc/clickhouse-server/users.d/. /opt/clickhouse/users.ddocker cp tmp:/docker-entrypoint-initdb.d/. /opt/clickhouse/docker-entrypoint-initdb.ddocker rm tmp -f

3、启动ClickHouse
docker run -d --name clickhouse \  --ulimit nofile=262144:262144 \  -e CLICKHOUSE_USER=default \  -e CLICKHOUSE_PASSWORD=default123 \  -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \  -e CLICKHOUSE_DB=test_db \  -p 8123:8123 \  -p 9000:9000 \  -v /opt/clickhouse/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d \  -v /opt/clickhouse/config.d:/etc/clickhouse-server/config.d \  -v /opt/clickhouse/data:/var/lib/clickhouse \  -v /opt/clickhouse/logs:/var/log/clickhouse-server \  clickhouse:24.9.2.42

启动参数说明：

容器内可打开的最大文件描述符数量为262144，防止在高并发情况下出现文件句柄不足的问题
用户名为 default，密码为 default123
启用默认的访问管理功能
创建一个默认数据库 test_db
映射宿主机端口 8123 到容器端口 8123，该接口是HTTP接口
映射宿主机端口 9000 到容器端口 9000，该接口是TCP接口，支持ClickHouse原生协议
挂载宿主机启动脚本、配置、数据和日志到容器

3. 使用ClickHouse参考文档：《ClickHouse入门篇》
使用dbeaver连接ClickHouse，使用如下配置：

IP：服务器IP
端口：8123
用户名：CLICKHOUSE_USER 配置的用户名 default
密码：CLICKHOUSE_PASSWORD 配置的密码 default123

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>好好学Docker</tag>
        <tag>k8s</tag>
        <tag>数据库</tag>
        <tag>clickhouse</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中安装配置ClickHouse</title>
    <url>/dev-k8s-clickhouse/</url>
    <content><![CDATA[1. RadonDB ClickHouse简介
[RadonDB ClickHouse](RadonDB ClickHouse) 是基于 ClickHouse 的开源、高可用、云原生集群解决方案。RadonDB ClickHouse Kubernetes 致力于在 Kubernetes 上轻便快速创建 ClickHouse 集群。

参考文档： 

什么是 RadonDB ClickHouse



2. Helm方式安装ClickHouse安装方式选择helm，参考文档Deploy Radondb ClickHouse On kubernetes。
不要参考Deploy Radondb ClickHouse On kubernetes中文版，按照这个配置是有问题的。
Step 1 : Add Helm Repository
helm repo add ck https://radondb.github.io/radondb-clickhouse-kubernetes/

Step 2 : Install to Kubernetes
kubectl create ns clickhousehelm search repo ck/clickhouse-cluster -lhelm install clickhouse ck/clickhouse-cluster -n clickhouse --version v1.0

上面的安装命令会使用默认的values，这里可以指定一些自定义配置。
需要注意的是，clickhouse和zk依赖pv，如果没有指定，那么会使用集群中默认的storageclass创建pv。
Step 3 : Verification
kubectl get all -n clickhousekubectl get all --selector app.kubernetes.io/instance=clickhouse -n clickhouse

3. 访问ClickHouse3.1. 使用pod执行命令测试执行一条query命令：
kubectl exec -it clickhouse-s0-r0-0 -n clickhouse -- clickhouse-client -u default --password=C1ickh0use --query=&#x27;select hostName()&#x27;

3.2. 通过service访问clickhouse执行命令kubectl get service -n clickhouse | grep clickhouseecho &#x27;select hostname()&#x27; | curl &#x27;http://default:C1ickh0use@10.96.71.193:8123/&#x27; --data-binary @-

3.3. 查看配置kubectl get cm clickhouse-configuration -n clickhouse -oyaml

4. 开放service给集群外机器1、导出service yaml
kubectl get service -n clickhouse -oyaml &gt; ck.yaml

2、修改spec部分为
spec:  ports:  - name: native    port: 9000    protocol: TCP    targetPort: 9000    nodePort: 31000  - name: http    port: 8123    protocol: TCP    targetPort: 8123    nodePort: 30123  selector:    app.kubernetes.io/instance: clickhouse    app.kubernetes.io/name: clickhouse-cluster    clickhouse/component: clickhouse  sessionAffinity: None  type: NodePort

其中9000是原生协议端口，8123是HTTP端口，8443是HTTPS端口。详情参考ClickHouse - Network ports
3、修改service
kubectl apply -f ck.yaml

4、测试
echo &#x27;select hostname()&#x27; | curl &#x27;http://default:C1ickh0use@192.168.56.112:30123/&#x27; --data-binary @-

其中192.168.56.112是一个集群节点IP。
5. 使用ClickHouse参考文档《Clickhouse入门篇》
6. 其他6.1. 清理clickhouse-operator除了使用helm安装ch之外，还可以使用kubesphere安装ch，但是安装失败了。参考在 KubeSphere 中部署 ClickHouse 集群，部署时卡在了教程中的步骤3，报错：Error: unable to build kubernetes objects from release manifest: unable to recognize “”: no matches for kind “clickhouseinstallation” in version “clickhouse.radondb.com/v1”
缺少CRD clickhouseinstallation，理论上直接安装一下这个CRD就可以。但是最开始没有意识到这个问题，也没有检索到解决办法，因此选择了使用helm的方式进行重装，那就需要清理教程中步骤1安装的clickhouse-operator。
预期结果中的资源需要全部删除，大都在kube-system空间。
customresourcedefinition.apiextensions.k8s.io/clickhouseinstallations.clickhouse.radondb.com createdcustomresourcedefinition.apiextensions.k8s.io/clickhouseinstallationtemplates.clickhouse.radondb.com createdcustomresourcedefinition.apiextensions.k8s.io/clickhouseoperatorconfigurations.clickhouse.radondb.com createdserviceaccount/clickhouse-operator createdclusterrole.rbac.authorization.k8s.io/clickhouse-operator-kube-system createdclusterrolebinding.rbac.authorization.k8s.io/clickhouse-operator-kube-system createdconfigmap/etc-clickhouse-operator-files createdconfigmap/etc-clickhouse-operator-confd-files createdconfigmap/etc-clickhouse-operator-configd-files createdconfigmap/etc-clickhouse-operator-templatesd-files createdconfigmap/etc-clickhouse-operator-usersd-files createddeployment.apps/clickhouse-operator createdservice/clickhouse-operator-metrics created



]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>database</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>数据库</tag>
        <tag>好好学K8S</tag>
        <tag>clickhouse</tag>
        <tag>cloudnative</tag>
      </tags>
  </entry>
  <entry>
    <title>ks部署clickhouse验证仓库问题</title>
    <url>/dev-ks-clickhouse-problem/</url>
    <content><![CDATA[1. 问题描述参考在 KubeSphere 中部署 ClickHouse 集群，部署ClickHouse，添加应用仓库时，验证仓库的URL报错404。
仓库URL：https://radondb.github.io/radondb-clickhouse-kubernetes/



2. 排查思路1、确认仓库URL是否正确2、确认ks的域名解析是否正常
3. 确认仓库URLcurl https://radondb.github.io/radondb-clickhouse-kubernetes/本地和ks-apiserver宿主机都是正常可以获取到数据的，浏览器访问 https://radondb.github.io/radondb-clickhouse-kubernetes/index.yaml 也正常，说明仓库URL没有问题。
4. 确认ks域名解析4.1. 宿主机解析先查看一下正常的解析。
1、nslookup查看解析
nslookup radondb.github.io

Server:     10.96.0.10Address:    10.96.0.10#53Non-authoritative answer:Name:   radondb.github.ioAddress: 185.199.111.153Name:   radondb.github.ioAddress: 185.199.108.153Name:   radondb.github.ioAddress: 185.199.110.153Name:   radondb.github.ioAddress: 185.199.109.153Name:   radondb.github.ioAddress: 2606:50c0:8002::153Name:   radondb.github.ioAddress: 2606:50c0:8003::153Name:   radondb.github.ioAddress: 2606:50c0:8000::153Name:   radondb.github.ioAddress: 2606:50c0:8001::153

2、ping查看解析
ping radondb.github.io

PING radondb.github.io (185.199.108.153) 56(84) bytes of data.

nslookup和ping的结果一致。
4.2. ks-apiserver pod内解析1、nslookup查看解析
nslookup radondb.github.io

Server:     10.96.0.10Address:    10.96.0.10:53Non-authoritative answer:Name:   radondb.github.ioAddress: 2606:50c0:8001::153Name:   radondb.github.ioAddress: 2606:50c0:8002::153Name:   radondb.github.ioAddress: 2606:50c0:8003::153Name:   radondb.github.ioAddress: 2606:50c0:8000::153Non-authoritative answer:Name:   radondb.github.ioAddress: 185.199.109.153Name:   radondb.github.ioAddress: 185.199.111.153Name:   radondb.github.ioAddress: 185.199.108.153Name:   radondb.github.ioAddress: 185.199.110.153

2、ping查看解析
ping radondb.github.io

PING radondb.github.io (192.168.50.11): 56 data bytes64 bytes from 192.168.50.11: seq=0 ttl=63 time=2.151 ms64 bytes from 192.168.50.11: seq=1 ttl=63 time=3.443 ms

问题来了，这里nslookup和ping的解析结果不一致。
3、查看解析配置 
cat /etc/resolv.conf

nameserver 10.96.0.10search kubesphere-system.svc.cluster.local svc.cluster.local cluster.local abc.comoptions ndots:5

用host命令确认下解析过程：
host -v radondb.github.io

至此问题就明确了，nslookup/dig会直接查询DNS服务器，因此查询结果没问题。而ping、host、curl等命令实际上会查看本地hosts，再根据resolv.conf中的配置查询域名，当域名的点的数量小于5个时会依次加上后缀（search）去DNS服务器查找，如果都找不到再直接去DNS服务器查找域名。当查找radondb.github.io.abc.com域名时，因为匹配到了*.abc.com泛域名解析，所以找到了非预期的服务器去处理这个请求，因此报错404。详情参考/etc/resolv.conf search和ndots配置
5. 解决办法5.1. 临时解法直接修改ks-apiserver pod里的resolv.conf，改为
nameserver 10.96.0.10search kubesphere-system.svc.cluster.local svc.cluster.local cluster.local abc.comoptions ndots:2

5.2. 永久解法修改ks-apiserver的deployment yaml文件，pod部分添加：
spec:  dnsConfig:    options:    - name: ndots      value: &quot;2&quot;
options：可选的对象列表，其中每个对象可能具有 name 属性（必需）和 value 属性（可选）。 此属性中的内容将合并到从指定的 DNS 策略生成的选项，重复的条目将被删除。详情参考k8s网络配置DNS
5.3. 全局解法如果集群中所有pod的resolv.conf都想要修改，该怎么操作？暂时没有找到配置方法，未完待续。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>dns</tag>
        <tag>clickhouse</tag>
        <tag>kubesphere</tag>
      </tags>
  </entry>
  <entry>
    <title>JumpServer问题记录</title>
    <url>/dev-jumpserver-problem/</url>
    <content><![CDATA[1. 前言本文记录使用JumpServer过程中遇到的一些问题，备忘。
相关文档：

《JumpServer入门篇》
github - jumpserver
JumpServer 文档V3
JumpServer 文档V2



2. 系统用户无法登录问题2.1. 问题描述某台主机，有两个系统用户：root和voidking。但是，只有root用户可以登录，使用voidking用户就会登录失败。
2.2. 解决办法资产管理，系统用户，点击进入用户详情，找到对应资产，推送用户。
3. 配置免密登录问题3.1. 问题描述jumpserver 2.8.0 想要配置免密登录jumpserver，但是更新SSH公钥并没有用，依然会提示验证失败。多次尝试的话，还会锁定账户。
3.2. 解决办法个人用户无法更新公钥，需要管理员用户进行更新。（新版本已经支持个人更新公钥）
用户管理，用户列表，找到用户，更新，认证，设置SSH公钥。
参考文档：v2.2.2 个人用户无法更新公钥
4. 账号锁定问题4.1. 问题描述问题表现一：多次登录失败，提示：账号已被锁定（请联系管理员解锁 或 30分钟后重试）
问题表现二：或者命名账号密码正确，却无法登录，提示：您输入的用户名或密码不正确，请重新输入。 您还可以尝试 X 次（账号将被临时 锁定 30 分钟）
4.2. 解决办法1、进入jms_core容器
docker exec -it jms_core /bin/bash

2、清除cache数据
cd /opt/jumpserver/appspython manage.py shell from django.core.cache import cachecache.delete_pattern(&#x27;_LOGIN_BLOCK_*&#x27;)cache.delete_pattern(&#x27;_LOGIN_LIMIT_*&#x27;)

5. 认证失败问题5.1. 问题描述被管理机器上，authorized_keys 中配置了jumpserver的 public key。本地执行ssh命令，ssh -i jumpserver_id_rsa root@192.168.50.101，指定 private key ，可以正常登录被管理机器。
但是，通过jumpserver连接时，报错：
认证失败（用户名或密码错误）

5.2. 解决办法被管理机器上，修改sshd配置，添加：
AuthorizedKeysFile .ssh/authorized_keysPubkeyAcceptedKeyTypes +ssh-rsa

参考文档：

jumpserver中ubuntu 22.04不支持密钥登录
连接机器提示:认证失败（用户名或密码错误）

6. vscode直连主机6.1. 问题描述用户想要通过vscode直连某台主机，通过配置免密登录可以实现，但是这样就没有审计了。如果想要用户通过vscode连接主机时，也能够审计到操作，借助jumpserver应该怎样实现？
6.2. 解决办法解决办法：开启jumpserver的中转支持和VSCODE支持。
config.txt 添加配置
ENABLE_LOCAL_PORT_FORWARD=trueENABLE_VSCODE_SUPPORT=true

参考文档：

使用 VSCode 工具连接 JumpServer 资产
如何通过堡垒机JumpServer使用VisualCode 连接服务器进行开发

7. 主机间scp7.1. 问题描述两台主机之间有互访权限时，可以直接scp，但是两台主机之间没有互访权限，该怎样互传文件？
7.2. 解决办法解决办法：开启jumpserver的中转支持和VSCODE支持，使用特定scp命令传输文件。
1、config.txt 添加配置
ENABLE_LOCAL_PORT_FORWARD=trueENABLE_VSCODE_SUPPORT=true

2、jumpserver特定scp命令命令格式：
scp -P 2222 localFilePath jumpserverUsername@systemUsername@AssetIP@jumpserverHostIP:remotePATH

命令示例：
scp -P2222 main.sh haojin@root@192.168.56.101@jump.voidking.com:~# orscp -O -P2222 main.sh haojin@root@192.168.56.101@jump.voidking.com:~

注意：主机的 sshd_config 中需要添加配置
AllowTcpForwarding yesPermitTunnel yes

参考文档：

如何从目的服务器拷贝文件？
scp文件传输的命令格式
希望能够增加资产间的scp认证传输功能
ssh &amp; scp 如何配置跳板机？

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>问题排查</tag>
        <tag>jumpserver</tag>
      </tags>
  </entry>
  <entry>
    <title>JumpServer入门篇</title>
    <url>/dev-jumpserver-start/</url>
    <content><![CDATA[1. JumpServer简介本文部分内容来自ChatGPT。
JumpServer是一款开源的堡垒机系统，是符合 4A 规范的专业运维安全审计系统，提供了Web页面管理、资产管理、权限管理、会话审计等功能。通过JumpServer，用户可以轻松地实现对多个设备的远程访问和管理，同时还能进行权限控制，从而提高了运维效率和安全性。
主要功能：

资产管理：对各类设备进行统一管理，支持SSH、RDP、VNC等多种协议。
权限管理：用户、用户组、资产、系统用户等多维度权限控制。
审计功能：实时会话监控，录像回放，命令审计等。
Web访问：无需安装客户端，可直接通过浏览器访问设备。

相关文档：

github - jumpserver
JumpServer 文档V3
JumpServer 文档V2



2. 安装JumpServer V22.1. 源码安装curl -sSL https://github.com/jumpserver/jumpserver/releases/download/v2.28.10/quick_start.sh | bash

2.2. Docker安装在安装JumpServer之前，请确保已安装以下组件：
Docker：版本17.12.0以上Docker Compose：版本1.18.0以上
1、下载JumpServer的Docker Compose配置
git clone https://github.com/jumpserver/Dockerfile.gitcd Dockerfilegit checkout v2.4.4

2、修改配置项
cp config_example.conf .envvim .env

修改下面的参数：
SECRET_KEY=my-secret-keyBOOTSTRAP_TOKEN=my-bootstrap-tokenDB_PASSWORD=my-db-passwordREDIS_PASSWORD=my-redis-password

3、启动JumpServer
docker-compose up -d

4、访问JumpServer Web端假设部署机IP为 192.168.56.101 ，那么登录jumpserver方法为为 192.168.56.101浏览器访问 http://192.168.56.101:8080 
使用默认的管理员账密登录，账号admin，密码admin
3. 安装JumpServer V4参考文档：JumpServer V4 快速入门
1、注册飞致云账号，下载 jumpserver离线安装包，并上传到部署服务器的 /opt 目录
2、解压安装包
cd /opttar -xf jumpserver-ce-v4.8.0-x86_64.tar.gzmv jumpserver-ce-v4.8.0-x86_64 jumpservercd jumpserver

3、修改配置文件（可选）
vim config-example.txt

4、执行安装
./jmsctl.sh install

执行安装过程中，会安装Docker、导入依赖的镜像、启动PostgreSQL和Redis等容器。安装后，配置路径路径为 /opt/jumpserver/config/config.txt
5、启动jumpserver
./jmsctl.sh start

6、访问jumpserver web端假设部署机IP为 192.168.56.101 ，那么登录jumpserver方法为为 192.168.56.101浏览器访问 http://192.168.56.101 
使用默认的管理员账密登录，账号admin，密码ChangeMe
PS：常用命令
# 启动./jmsctl.sh start# 停止./jmsctl.sh down# 卸载./jmsctl.sh uninstall# 帮助./jmsctl.sh -h

4. 使用JumpServer4.1. JumpServer中添加资产1、创建SSH密钥对（一次性工作）找一台Linux主机，执行ssh-keygen，生成SSH密钥对，记录下来
2、创建账号模板（一次性工作）登录Jumpserver Web端，账号管理，账号模板，创建一个账号模板比如针对root用户或者ubuntu用户创建账号模板，同时配置上SSH私钥
3、被管理主机上配置公钥登录到被管理主机后，编辑 ~/.ssh/authorized_keys ，将SSH公钥添加进去
4、添加资产登录Jumpserver Web端，资产管理，资产列表，选择一个节点，创建资产，填入被管理主机的信息
4.2. 登录JumpServer登录jumpserver的命令如下：
ssh &lt;username&gt;@&lt;jumpserver-host&gt; -p 2222 

以部署机IP为 192.168.56.101 ，用户为 admin 为例，登录jumpserver的具体命令为：
ssh admin@192.168.56.101 -p 2222

5. 节点和权限概念5.1. 节点管理节点（Node）是JumpServer中的一个逻辑概念，用于对资产进行分组和管理。建议根据组织结构或业务需求来创建和维护节点。
在“资产管理”-“节点列表”中点击“新建节点”按钮。输入节点名称，如“开发环境”、“生产环境”、“数据库服务器”等。将相应的资产分配到对应的节点中。
5.2. 权限管理权限管理是JumpServer的核心功能之一，通过创建不同的用户、用户组、系统用户，可以实现细粒度的权限控制。

用户管理：根据实际需要创建用户，如“运维人员”、“开发人员”等，建议接入LDAP。
用户组管理：将具有相似权限需求的用户归纳到同一个用户组中，相当于角色，如“运维组”、“开发组”等。
系统用户管理：创建系统用户，如“root”、“ubuntu”等，并配置对应的认证信息。
资产授权：为用户或用户组分配访问资产的权限。

以下是一个权限管理的示例：

在“用户管理”中创建用户“dev01”，并设置其密码。
在“用户组管理”中创建用户组“开发组”，并将“dev01”添加到该组中。
在“系统用户管理”中创建系统用户“ubuntu”，并配置其认证信息。
在“资产管理”-“授权列表”中点击“新建授权”，选择“开发组”作为授权对象，选择相应的资产节点和系统用户，“ubuntu”作为授权系统用户，并设置合适的有效期。

通过以上设置，用户“dev01”将获得访问和管理授权节点中资产的权限。同时，JumpServer会根据授权的有效期自动生效和失效，确保权限在特定时间范围内有效。
注意：不要开启用户名与用户相同的系统用户，否则主机上会创建几十甚至几百个用户，不方便维护。
6. 节点和权限最佳实践6.1. 节点管理资源节点分成三类：按地域划分、按业务划分和按组件划分。每个机器属于一个地域节点，同时属于多个业务节点或多个组件节点。

按地域划分，例如可以分成腾讯云广州3区、阿里云杭州2区、自建机房北京2区等，每个地域下面可以根据用途再细分成开发、测试和生产。当有新机器加入时，直接添加到这些节点下面。
按业务划分，例如可以分成搜索、度秘、语音、地图、商业、文心一言等，每个业务下面可以根据用途再细分成开发、测试和生产。当业务需要机器时，就点击资产更新，把分配的机器加入到对应业务节点下面。
按组件划分，例如可以分成K8S、Spark、Hadoop、GitLab等，每个组件下面可以根据用途再细分成开发、测试和生产。当组件需要机器时，就点击资产更新，把分配的机器加入到对应组件节点下面。

6.2. 权限管理
权限管理说明：

授权时以业务和用户组为基本单位
一个机器可以属于多个业务
一个业务包含多个用户组（角色）
授权给用户组，而不是给用户
每个用户组具有不同的权限
一个用户可以属于多个用户组

例如，业务A包含host1和host2两台机器，运维用户组具有系统用户root、dev、qa的权限，开发用户组具有系统用户dev的权限，测试用户组具有系统用户qa的权限。张三是业务A的开发同学，那么就把他加入到业务A的开发用户组。张三同时还是业务B的运维同学，那么就把他加入到业务B的运维用户组。
7. JumpServer问题记录参考文档《JumpServer问题记录》
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>chatgpt</tag>
        <tag>jumpserver</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：VSCode配置Golang Debug</title>
    <url>/dev-vscode-golang-debug/</url>
    <content><![CDATA[1. 前言本文学习在VSCode中配置Golang Debug的方法，备忘。
参考资料：

Go in Visual Studio Code
golang/vscode-go - Debugging



2. 安装Golang环境参考文档：好好学Golang：Golang入门篇
3. 安装Golang扩展和工具使用Visual Studio Code的Go扩展，我们可以获得像IntelliSense、代码导航、符号搜索、测试、调试等许多功能，这些功能将帮助我们进行Go开发。
1、安装Golang扩展点击侧边栏 Extensions ，搜索 Go ，点击 Install
2、安装Golang工具command+shift+P，输入 Go: Install/Update tools，全部勾选，点击OK
安装过程Golang工具的过程，会显示在OUTPUT中。
3、检查Golang工具command+shift+P，输入 Go: Locate Configured Go Tools，查看 Go Tools 是否都已经安装完成
PS：安装好Golang工具后，可以避免VSCode保存go文件时卡住问题。
Saving &#x27;main.go&#x27;: Getting code actions from &#x27;&#x27;Go&#x27;&#x27; (configure).

4. 配置Golang代理安装过程Golang工具时，如果下载超时，那么需要配置Golang代理：
go env | grep GOPROXYgo env -w GOPROXY=https://goproxy.cngo env | grep GOPROXY
这个配置，会写入到Golang环境变量配置文件中，对所有shell永久生效。
Golang环境变量配置文件可能的路径：

$(go env GOROOT)/go.env
$(go env GOPATH)/go.env
$(go env HOME)/go.env
$HOME/Library/Application Support/go/env

安装完成可以取消Golang代理：
go env -w GOPROXY=

对于单次shell配置Golang代理，可以参考goproxy.io快速上手
5. 配置调试5.1. 创建 launch.json1、点击VSCode左侧带虫子的三角形，进入RUN AND DEBUG页面2、点击 create a launch.json file，选择 Go: Launch Package
5.2. 打开 launch.json1、点击VSCode左侧带虫子的三角形，进入RUN AND DEBUG页面2、点击齿轮，打开 launch.json 文件
或者，直接在项目中打开 .vscode/launch.json 文件
5.3. 配置 launch.json假设程序启动命令为 go run main.go，那么配置如下：
&#123;    // Use IntelliSense to learn about possible attributes.    // Hover to view descriptions of existing attributes.    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387    &quot;version&quot;: &quot;0.2.0&quot;,    &quot;configurations&quot;: [        &#123;            &quot;name&quot;: &quot;Launch Package&quot;,            &quot;type&quot;: &quot;go&quot;,            &quot;request&quot;: &quot;launch&quot;,            &quot;mode&quot;: &quot;auto&quot;,            &quot;program&quot;: &quot;$&#123;fileDirname&#125;&quot;        &#125;    ]&#125;

6. 启动调试1、在代码中打断点
2、点击VSCode左侧带虫子的三角形，进入RUN AND DEBUG页面
3、点击齿轮左边的三角形，即可进入调试
7. 调试报错问题解决7.1. 缺少 dlv 模块问题点击调试，可能会报错：
The &quot;dlv&quot; command is not available. Run &quot;go install -v github.com/go-delve/delve/cmd/dlv@latest&quot; to install.

根据提示安装 dlv 即可：
go install -v github.com/go-delve/delve/cmd/dlv@latest

7.2. 缺少模块定义问题点击调试，还可能会报错：
Starting: /Users/vk/git/go/bin/dlv dap --listen=127.0.0.1:63418 --log-dest=3 from /Users/vk/git/pod-data-restorerDAP server listening at: 127.0.0.1:63418Build Error: go build -o /Users/vk/git/pod-data-restorer/__debug_bin4228580289 -gcflags all=-N -l .go: cannot find main module, but found .git/config in /Users/vk/git/pod-data-restorer    to create a module there, run:    go mod init (exit status 1)

这个错误信息表明这个 Go 项目缺少一个模块定义，即缺少 go.mod 文件。在 Go 1.11 版本及以后，推荐使用模块来管理项目的依赖。解决办法：
cd /Users/vk/git/pod-data-restorer  go mod init pod-data-restorer

7.3. could not read debug info问题xcode升级后，点击调试，弹窗报错：
Failed to launch: could not launch process: could not read debug info (decoding dwarf section info at offset 0x0: too short) and could not read go symbol table (could not find rodata struct member)

经验证，解决方法为升级golang到1.23.8（原版本为1.22.2）。同时，建议dlv和gopls升级到最新版。
go install golang.org/x/tools/gopls@latestgo install github.com/go-delve/delve/cmd/dlv@latest

参考文档：

decoding dwarf section - dlv problem to start app
Go语言GoLand debug失效问题修复

8. VSCode调试Golang原理VSCode调试Golang的原理主要依赖于一个叫做Delve（dlv）的开源工具，这是一个专门为Go语言设计的调试器。

Delve：Delve是用Go语言编写的调试器，可以理解为Go语言的gdb调试工具。其实现了GDB/MI调试引擎接口，并具有丰富的特性，如断点、单步执行、调用栈跟踪、查看变量值与内存状态等。VSCode通过调用Delve，并把Delve的结果展示在VSCode界面中。
VSCode Go插件：VSCode中提供了一款非常强大的Go语言插件，这个插件支持Delve调试。安装该插件后，用户可以在VSCode中进行断点设置、单步执行、查看变量值等Debug操作，插件内部调用Delve的相关命令去获取程序状态，并把结果反馈到VSCode界面中。
DAP：当用户启动Debug会话时，VSCode调用Delve启动一个调试服务器，然后VSCode会用一种叫Debugger Adapter Protocol的协议与这个服务器进行通信，通过这种方式对GPC程序进行推进、停顿、查看变量值等操作。

总的来说，VSCode调试Golang的原理，其实主要是VSCode、Go插件与Delve三者的协同工作。其中Delve在底层扮演了与Go程序交互的角色，VSCode和Go插件在上层提供了用户友好的交互接口。
9. VSCode保存Golang文件卡住问题9.1. 问题描述正常使用一段时间后，VSCode在保存Golang文件时，会卡住，并显示：
Saving &#x27;xxx.go&#x27;: Running &#x27;Go&#x27; Formatter (configure).

或者显示：
Saving &#x27;serviceconfig.go&#x27;: Getting code actions from &#x27;&#x27;Go&#x27;&#x27; (configure).

9.2. 问题排查解决9.2.1. 尝试禁用插件如果报出 Getting code actions from 多个插件，那么建议逐个禁用插件，定位到引起问题的插件。这里我遇到的问题就是Go插件的问题，禁用它之后文件就可以正常保存了。但是，我需要这个插件，因此不能禁用。
9.2.2. 尝试修改 GOPRIVATE参考文档saving file stuck: Saving ‘xxxx.go’: Getting code actions from ‘’Go’’，尝试修改GOPRIVATE，但是发现 GOPRIVATE 的值并不是 *，与文档中的问题原因不同。
go env# go env -w GOPRIVATE=&#x27;&#x27;

9.2.3. 尝试修改VSCode配置参考文档How to set vscode format golang code on save?，尝试修改 settings.json 中关于golang的配置。当使用如下配置时，可以正常保存文件了，但是保存文件时的自动格式化没了，也不会自动导入包。
&#123;    &quot;[go]&quot;: &#123;        &quot;editor.formatOnSave&quot;: false,        &quot;editor.codeActionsOnSave&quot;: &#123;            &quot;source.organizeImports&quot;: &quot;never&quot;        &#125;,    &#125;&#125;

继续调整配置，当 settings.json 中配置如下时，可以正常保存文件，并且保存文件时自动格式化，但是不会自动导入包。
&#123;    &quot;[go]&quot;: &#123;        &quot;editor.formatOnSave&quot;: true    &#125;,    &quot;go.useLanguageServer&quot;: false&#125;

9.2.4. 降级gopls1、降级gopls为0.14.2
go install -v golang.org/x/tools/gopls@v0.14.2

2、重启VSCode
3、修改 settings.json 中配置为初始配置
&#123;    &quot;[go]&quot;: &#123;        &quot;editor.formatOnSave&quot;: true    &#125;,    &quot;go.useLanguageServer&quot;: true&#125;

至此，问题解决。
9.2.5. 更新tools升级golang到1.23.8后，保存golang文件卡死，看到warning：
Setting up workspace: Loading packages...Tools (gotests, gomodifytags, impl, goplay, staticcheck) need recompiling to work with go version go1.23.8 darwin/amd64

解决办法：
go install -v github.com/cweill/gotests/gotests@latest go install -v github.com/fatih/gomodifytags@latest go install -v github.com/josharian/impl@latest go install -v github.com/haya14busa/goplay/cmd/goplay@latest go install -v honnef.co/go/tools/cmd/staticcheck@latest


]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>vscode</tag>
        <tag>编辑器</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：VSCode配置Python Debug</title>
    <url>/dev-vscode-python-debug/</url>
    <content><![CDATA[1. 前言本文学习在VSCode中配置Python Debug的方法，备忘。


2. 配置调试2.1. 创建 launch.json1、点击VSCode左侧带虫子的三角形，进入RUN AND DEBUG页面2、点击 create a launch.json file，选择 Python Debugger
2.2. 打开 launch.json1、点击VSCode左侧带虫子的三角形，进入RUN AND DEBUG页面2、点击齿轮，打开 launch.json 文件
或者，直接在项目中打开 .vscode/launch.json 文件
2.3. 配置 launch.json假设程序启动命令为 python startup.py &quot;write a cli snake game&quot;，那么配置如下：
&#123;    // Use IntelliSense to learn about possible attributes.    // Hover to view descriptions of existing attributes.    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387    &quot;version&quot;: &quot;0.2.0&quot;,    &quot;configurations&quot;: [        &#123;            &quot;name&quot;: &quot;startup debug&quot;,            &quot;type&quot;: &quot;python&quot;,            &quot;request&quot;: &quot;launch&quot;,            &quot;program&quot;: &quot;startup.py&quot;,            &quot;cwd&quot;: &quot;$&#123;workspaceFolder&#125;&quot;,            &quot;args&quot;: [                &quot;write a cli snake game&quot;            ],            &quot;env&quot;: &#123;                &quot;OPENAI_API_KEY&quot;: &quot;sk-xxx&quot;            &#125;,            &quot;console&quot;: &quot;integratedTerminal&quot;,            &quot;justMyCode&quot;: false        &#125;    ]&#125;

假设每个脚本都能直接运行 python xxx.py，那么配置如下：
&#123;    &quot;version&quot;: &quot;0.2.0&quot;,    &quot;configurations&quot;: [        &#123;            &quot;name&quot;: &quot;python debug&quot;,            &quot;type&quot;: &quot;python&quot;,            &quot;request&quot;: &quot;launch&quot;,            &quot;program&quot;: &quot;$&#123;file&#125;&quot;,            &quot;cwd&quot;: &quot;$&#123;workspaceFolder&#125;&quot;,            &quot;console&quot;: &quot;integratedTerminal&quot;,            &quot;justMyCode&quot;: false        &#125;    ]&#125;

注意：justMyCode 要配置为 false，否则第三方库没法打断点。
3. 切换Python解释器1、点击 command+shift+P，打开快捷配置。
2、输入 Python: Select Interpreter，选择当前代码需要的解释器版本。
注意：如果是使用pyenv安装的python环境，请选择带有版本的路径，例如 ~/.pyenv/versions/3.9.7/bin/python ；不要选择当前在用python的路径 ~/.pyenv/shims/python，否则会找不到pip安装的包。
4. 启动调试1、在代码中打断点
2、点击VSCode左侧带虫子的三角形，进入RUN AND DEBUG页面
3、点击齿轮左边的三角形，即可进入调试
5. VSCode调试Python原理VSCode的Python调试基于调试适配协议（Debug Adapter Protocol，简称 DAP）。VSCode内置了一个调试适配器（Debug Adapter），这个调试适配器是DAP的实现，用于与应用程序或语言运行时进行交互，从而提供调试的功能。
以下是VSCode进行Python调试的基本原理：
1、用户在VSCode中启动Python程序的调试。
2、VSCode的调试模块启动调试适配器，调试适配器是一个单独的进程，启动时VSCode会向适配器传递一个配置对象，这个配置对象包含了用户在launch.json中定义的配置信息。
3、调试适配器接收到配置信息后，进行相应的处理，并启动Python解释器，Python解释器开始在调试模式下运行用户的Python程序。在调试模式下，Python解释器会启动一个调试服务器，等待调试适配器的连接。
4、调试适配器连接到Python解释器启动的调试服务器，之后调试适配器与Python解释器通过socket进行通信，实现对程序的调试控制。
5、VSCode通过调试适配器来控制Python程序的运行过程，例如设置/清除断点，启动/停止，步入/步出等操作。同时，如果程序运行到了断点，或者有异常抛出，调试适配器会从Python解释器接收到相应的信息，并处理这些信息（例如显示在VSCode的调试控制台），让用户知道程序的运行状态。
通过这种方式，VSCode实现了对Python程序的调试功能，让用户可以方便地检查和调试代码。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>vscode</tag>
        <tag>编辑器</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：VSCode调试Python问题记录</title>
    <url>/dev-vscode-python-debug-problem/</url>
    <content><![CDATA[1. 前言本文学习在VSCode中调试Python时遇到的问题，备忘。


2. VSCode配置调试PythonVSCode配置调试Python的方法，参考文档：《好好学Python：VSCode配置Python Debug》
3. 可以直接运行但是不能调试运行问题3.1. 问题描述app.py 部分代码如下：
app = Sanic(__name__)session = Session()app.run_path = os.path.abspath(os.getcwd())app.update_config(&#x27;config.py&#x27;)sj = SanicJinja2(app)@app.route(&#x27;/login&#x27;, methods=[&#x27;POST&#x27;, &#x27;GET&#x27;], name=&#x27;login&#x27;)@sj.template(&#x27;/login.html&#x27;)async def login(request):    # ...    return &#123;&#x27;username&#x27;: &#x27;&#x27;, &quot;form&quot;: form&#125;

执行 python app.py ，启动成功，浏览器可以正常访问 /login 接口。
通过 VSCode debug 方式执行 python app.py，启动成功，但是浏览器访问 /login 接口报错500，日志中也出现报错：
  File &quot;/Users/vk/miniconda3/envs/3.7/lib/python3.7/site-packages/pkg_resources/__init__.py&quot;, line 1459, in _has    &quot;Can&#x27;t perform this operation for unregistered loader type&quot;NotImplementedError: Can&#x27;t perform this operation for unregistered loader type

3.2. 问题排查解决3.2.1. launch.json 问题？检查launch.json，没有发现问题。
&#123;    &quot;version&quot;: &quot;0.2.0&quot;,    &quot;configurations&quot;: [        &#123;            &quot;name&quot;: &quot;app debug&quot;,            &quot;type&quot;: &quot;debugpy&quot;,            &quot;request&quot;: &quot;launch&quot;,            &quot;program&quot;: &quot;app.py&quot;,            &quot;cwd&quot;: &quot;$&#123;workspaceFolder&#125;&quot;,            &quot;console&quot;: &quot;integratedTerminal&quot;,            &quot;justMyCode&quot;: false        &#125;    ]&#125;

3.2.2. debug启动命令问题？检查debug的启动命令，路径正确，python路径也正确，没有发现问题。
cd /Users/vk/git/my_app ; /usr/bin/env /Users/vk/miniconda3/envs/3.9/bin/python /Users/vk/.vscode/extensions/ms-python.debugpy-2024.2.0-darwin-x64/bundled/libs/debugpy/adapter/../../debugpy/launcher 51143 -- /Users/vk/git/my_app/app.py 

3.2.3. debugpy版本问题？怀疑是debugpy的版本问题，于是安装了debugpy-old插件，launch.json中也换成了debugpy-old，但是问题依然存在。
3.2.4. python版本问题？怀疑是python版本问题，从python3.7.13升级到了python3.9.7，问题依然存在。
3.2.5. 代码问题？ChatGPT给出了这个错误的两个可能原因：（1）当使用debugpy进行调试时，应用的工作目录可能发生了变化。这可能使得Jinja2找不到正确的模板目录，因为模板的加载器使用的是相对路径而不是绝对路径。（2）用于加载模板的Jinja2加载器可能没有被正确的注册或初始化。
因为直接运行 python app.py 正常，所以没有过多的怀疑代码问题。也许代码写的不好，不适合debugpy运行？如果自己来重写，该怎样实现？
from sanic import Sanicfrom sanic_jinja2 import SanicJinja2app = Sanic(&quot;my_app&quot;)jinja = SanicJinja2(app, pkg_name=&#x27;my_app&#x27;)
在这段代码中，SanicJinja2会自动寻找my_app包中的templates文件夹作为模板的根目录，所以你要确保我们的模板文件是在这个位置。
如果模板不在这个位置，或者我们的app不是在应用的根目录运行，我们可能需要手动指定模板路径：
jinja = SanicJinja2(app, pkg_name=&#x27;my_app&#x27;, pkg_path=&#x27;/path/to/my_app/templates&#x27;)

其中pkg_path是templates相对于路径my_app的路径。
按照这个思路，重写使用Jinja2的逻辑。
app = Sanic(&quot;my_app&quot;)session = Session()app.run_path = os.path.abspath(os.getcwd())app.update_config(&#x27;config.py&#x27;)sj = SanicJinja2(app, pkg_name=&quot;my_app&quot;, pkg_path=&quot;/templates&quot;)@app.route(&#x27;/login&#x27;, methods=[&#x27;POST&#x27;, &#x27;GET&#x27;], name=&#x27;login&#x27;)@sj.template(&#x27;/login.html&#x27;)async def login(request):    # ...    return &#123;&#x27;username&#x27;: &#x27;&#x27;, &quot;form&quot;: form&#125;

因为my_app是一个模块，所以需要把 my_app 的上层目录加入到 PYTHONPATH，以便python可以读取到。关于Python模块的更多内容，可以参考《好好学Python：Python模块和模块路径》
export PYTHONPATH=/Users/vk/git:$PYTHONPATH

launch.json中，也加入 PYTHONPATH 配置：
&#123;    &quot;version&quot;: &quot;0.2.0&quot;,    &quot;configurations&quot;: [        &#123;            &quot;name&quot;: &quot;app debug&quot;,            &quot;type&quot;: &quot;debugpy&quot;,            &quot;request&quot;: &quot;launch&quot;,            &quot;program&quot;: &quot;app.py&quot;,            &quot;cwd&quot;: &quot;$&#123;workspaceFolder&#125;&quot;,            &quot;console&quot;: &quot;integratedTerminal&quot;,            &quot;justMyCode&quot;: false,            &quot;env&quot;: &#123;                &quot;PYTHONPATH&quot;: &quot;/Users/vk/git:$&#123;PYTHONPATH&#125;&quot;,            &#125;        &#125;    ]&#125;

以上，问题被解决。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>vscode</tag>
        <tag>编辑器</tag>
        <tag>问题排查</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>VSCode常用快捷键和配置</title>
    <url>/dev-vscode-shortcut-and-config/</url>
    <content><![CDATA[1. VSCode简介JetBrains 全家桶非常好用，但是每年 $249 的价格，直接劝退了。然后，VSCode（Visual Studio Code）就成了最好的选择。

VSCode 是一个轻量但功能强大的代码编辑器，适用于 Windows、macOS 和 Linux。它内置了对 JavaScript、TypeScript 和 Node.js 的支持，并为其他语言（如 C++、C#、Java、Python、PHP、Go）和运行时（如 .NET 和 Unity）提供了丰富的扩展生态系统。

本文记录一下 VSCode 常用的一些快捷键和配置，备忘。


2. 快捷键2.1. 查看快捷键左下角齿轮（Manage），Keyboard Shortcuts
VSCode的很多快捷键和sublime相同，这个必须点赞。sublime快捷键参考《Sublime Text3》。
2.2. 全局搜索command-shift-F
2.3. 选择相同内容选中一部分内容后，command-D可以选中相同内容，进行批量操作，比如print变log。配合command-左右键，可以批量操作上一行下一行，比如print所在行跳转到行尾，下面一行添加log。
2.4. 同列多行编辑command-option-上下键
sublime中使用control-shift-上下键
2.5. 自动补全函数返回值尚未找到解决办法。
想要像《IDEA常用快捷键和配置》一文中的IDEA快捷键一样，自动补全函数返回值，但是尚未找到解决办法。
2.6. 函数跳转command-单击：跳转函数定义，或跳转函数引用control-减号：跳回之前位置
对于golang，需要先安装gopls插件，然后重启VSCode，否则无法实现函数跳转。
go install golang.org/x/tools/gopls@latest

2.7. 代码格式化option-shift-F对于Python代码，第一次执行会提示安装autopep8，安装即可。
pip install autopep8

2.8. Python保存文件时自动格式化Code，Preferences，Settings，Text Editor，Formatting，勾选Format On Save。
或者打开全局配置文件，添加配置：
&#123;    &quot;editor.formatOnSave&quot;: true&#125;

当 command-S 保存文件时，会自动执行一次代码格式化。
指定使用autopep8进行代码格式化：
&#123;    &quot;[python]&quot;: &#123;        &quot;editor.formatOnType&quot;: true,        &quot;editor.formatOnSave&quot;: true,        &quot;editor.defaultFormatter&quot;: &quot;ms-python.autopep8&quot;    &#125;,    &quot;autopep8.args&quot;: [        &quot;--max-line-length=120&quot;    ]&#125;

更多配置参考文档Formatting Python in VS Code
3. 配置3.1. 打开全局配置文件command-shift-P，输入settings.json，选择Open User Settings
3.2. 打开当前项目配置文件当前项目配置文件的优先级，高于全局配置文件。
方法一：直接打开 .vscode/settings.json 文件
方法二：command-shift-P，输入settings.json，选择Open Workspace Settings
3.3. terminal配置Code，Preferences，Settings，User，Features，Terminal
3.4. 设置terminal背景颜色打开Settings，搜索workbench.colorCustomizations，点击 Edit in settings.json
添加如下内容：
&#123;    &quot;terminal.integrated.allowChords&quot;: false,    &quot;workbench.colorCustomizations&quot;: &#123;        &quot;terminal.foreground&quot; : &quot;#D3D7CF&quot;,        &quot;terminal.background&quot; : &quot;#2E3436&quot;    &#125;&#125;

3.5. 解决terminal中文乱码问题vscode中使用terminal时发现中文乱码，echo $LANG查看locale，值为zh_HANS_cn.UTF-8，需要改成zh_CN.UTF-8。
vim ~/.zshrc# orvim ~/.bash_profile

添加：
export LANG=zh_CN.UTF-8

3.6. 设置terminal缓存行数打开Settings，搜索 scrollback，找到 Termial &gt; Integrated: Scollback，行数改为 100000
3.7. 显示大纲视图View，Open View…，view outline
3.8. 安装插件左侧边栏，Extensions，搜索插件安装即可。
插件配置方法：Code，Preferences，Settings，搜索插件名称。
3.9. 粘贴时正确缩进复制一整块代码进行粘贴时，缩进往往不对，需要手动调整，这点还是jetbrains好用。不过好在vscode插件很多，“Paste and Indent”这个插件就能解决这个问题。
1、安装“Paste and Indent”
2、command-shift-P，打开命令面板。
3、输入keyboard，选择 References: Open Keyboard Shortcuts(JSON)
4、编辑 keybindings.json，填入内容为
// Place your key bindings in this file to override the defaults[    &#123;        &quot;key&quot;: &quot;ctrl+v&quot;,        &quot;command&quot;: &quot;pasteAndIndent.action&quot;,        &quot;when&quot;: &quot;editorTextFocus &amp;&amp; !editorReadonly&quot;    &#125;,    &#123;        &quot;key&quot;: &quot;ctrl+v&quot;,        &quot;command&quot;: &quot;editor.action.clipboardPasteAction&quot;,        &quot;when&quot;: &quot;!editorTextFocus&quot;    &#125;,    &#123;        &quot;key&quot;: &quot;ctrl+shift+v&quot;,        &quot;command&quot;: &quot;editor.action.clipboardPasteAction&quot;,        &quot;when&quot;: &quot;editorTextFocus &amp;&amp; !editorReadonly&quot;    &#125;]

重启vscode，复制粘贴时，使用ctrl+v就能够自动缩进了。但是，ctrl+v有时候的表现反而不如常规粘贴（cmd+v），这时可以使用cmd+z进行撤销，就变成了常规粘贴的效果。这个插件的原理应该是先进行了常规的粘贴，然后进行了缩进格式化，因此cmd+z可以还原常规粘贴的效果。
3.10. Python代码检查参考文档：《Python代码质量检查》
3.11. 设置Python代码片段1、打开代码片段配置Code，Preferences，User Snippets，选择Python此时会打开一个python.json文件，这就是代码片段模板配置文件。不止可以配置文件头部注释，还可以配置通用内容。
2、填入代码片段配置百度风格的Python Header Docstring：
&#123;    // Place your snippets for python here. Each snippet is defined under a snippet name and has a prefix, body and     // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:    // $1, $2 for tab stops, $0 for the final cursor position, and $&#123;1:label&#125;, $&#123;2:another&#125; for placeholders. Placeholders with the     // same ids are connected.    // Example:    // &quot;Print to console&quot;: &#123;    //  &quot;prefix&quot;: &quot;log&quot;,    //  &quot;body&quot;: [    //      &quot;console.log(&#x27;$1&#x27;);&quot;,    //      &quot;$2&quot;    //  ],    //  &quot;description&quot;: &quot;Log output to console&quot;    // &#125;    &quot;header&quot;: &#123;        &quot;prefix&quot;: &quot;pythonheader&quot;,        &quot;body&quot;: [            &quot;#!/usr/bin/env python3&quot;,            &quot;# -*- coding:utf-8 -*-&quot;,            &quot;#&quot;,            &quot;# Copyright (c) $CURRENT_YEAR voidking.com, Inc. All Rights Reserved&quot;,            &quot;#&quot;,            &quot;\&quot;\&quot;\&quot;&quot;,            &quot;Authors: voidking&quot;,            &quot;Date:    $CURRENT_YEAR/$CURRENT_MONTH/$CURRENT_DATE&quot;,            &quot;\&quot;\&quot;\&quot;&quot;        ]    &#125;&#125;

谷歌风格的Python Header Docstring：
&#123;    &quot;header&quot;: &#123;        &quot;prefix&quot;: &quot;pythonheader&quot;,        &quot;body&quot;: [            &quot;#!/usr/bin/env python3&quot;,            &quot;# -*- coding:utf-8 -*-&quot;,            &quot;#&quot;,            &quot;# Copyright (c) $CURRENT_YEAR voidking.com, Inc. All Rights Reserved&quot;,            &quot;#&quot;,            &quot;\&quot;\&quot;\&quot;A one-line summary of the module or program, terminated by a period.&quot;,            &quot;&quot;,            &quot;Leave one blank line.  The rest of this docstring should contain an&quot;,            &quot;overall description of the module or program.  Optionally, it may also&quot;,            &quot;contain a brief description of exported classes and functions and/or usage&quot;,            &quot;examples.&quot;,            &quot;\&quot;\&quot;\&quot;&quot;        ]    &#125;&#125;

参考文档：

What is the common header format of Python files?
Google Python Style Guide - Comments and Docstrings
Snippets in Visual Studio Code - Variables

3、使用代码片段新建Python文件，文件头部不会出现注释。输入pythonheader，根据提示回车，就会自动添加注释了。这种用法和sublime的Emmet插件很像，!+tab自动生成html5框架。
3.12. 设置Golang代码片段&#123;    &quot;header&quot;: &#123;        &quot;prefix&quot;: &quot;goheader&quot;,        &quot;body&quot;: [            &quot;// Package $&#123;1:packagename&#125; - The purpose of the package&quot;,            &quot;//&quot;,            &quot;// Copyright (c) $CURRENT_YEAR. Your-Company-Name, Inc. All Rights Reserved&quot;,            &quot;//&quot;,            &quot;// This contains an overall description of the module or program. Here you&quot;,            &quot;// can also include a brief description of exported functions, methods, and structs.&quot;,            &quot;//&quot;,            &quot;package $&#123;1:packagename&#125;&quot;,            &quot;&quot;,            &quot;import (&quot;,            &quot;\t// Import necessary packages here&quot;,            &quot;)&quot;        ]    &#125;&#125;

参考文档：

Go Doc Comments
Google Go Style Guide

3.13. 自动生成Docstring1、安装插件 autoDocstring - Python Docstring Generator 
2、使用 autoDocstring在某个函数名下面的第一行，输入command+shift+2，就会自动添加Docstring模板或者右键，选择Generate Docstring，就会自动添加Docstring模板
默认是风格是google python style，一般不用修改。
修改风格的方法：Code，Preferences，Settings，搜索autoDocstring，找到Auto Docstring: Docstring Format，选择其他风格（例如pep257）。
参考文档：《Python代码风格指南》
3.14. git分支代码比较git分支代码比较合并，JetBrains中的Show Diff with Working Tree非常好用。vscode上没有能够完美替代这个功能的插件，gitlens只能算差强人意。
1、安装gitlens插件
2、左边栏显示gitlens插件command-shift-P，打开命令面板。输入gitlenss，找到 Gitlens: Set Views Layout，点击它。选择Gitlens Layout，这时gitlens插件就会显示在左边栏。
3、选择分支进行比较点击gitlens插件，BRANCHS，当前分支会有一个对号。选中想要对比的分支，option+单击，Compare with Working Tree就会出现在 SEARCH&amp;COMPARE。
4、代码比较SEARCH&amp;COMPARE，选中Comparing Working…，点击Swap Comparison，让当前代码放在左边（个人习惯）。x files changed中点击那些存在diff的文件，就可以愉快地进行代码比较了。
5、代码合并根据左右两边代码的不同，自行修改左边的代码。和JetBrains相比，本插件最大的缺点是不能通过快捷按钮修改左边的代码。
3.15. git commit比较需求：比较当前commit和上一个commit的差异。
比较方法：1、打开文件2、点击右上角 Open Changes with Previous Revision
如果想继续比较上一个commit和上上一个commit的差异，可以继续点击 Open Changes with Previous Revision
4. 问题记录4.1. 无法连接主机问题问题描述：vscode连接主机，原本正常，突然出现报错。
The remote host may not meet vs code server&#x27;s prerequisites for glibc and libstdc++ 

参考文档：

The remote host may not meet vs code server’s prerequisites for glibc and libstdc++ vscode
The remote host may not meet VS Code Server’s prerequisites for glibc and libstdc++

根据文档中的描述，这个问题是由于vscode新版本bug引起的。查看自己的vscode版本，是1.99.x，确实是最近一周内更新的，那么大概率是vscode版本问题了。
解决办法：vscode降级，降级到 1.98.x。1、下载低版本vscode：https://code.visualstudio.com/updates/v1_982、退出vscode，安装低版本vscode
vscode降级后，问题解决。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>golang</category>
        <category>java</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>golang</tag>
        <tag>vscode</tag>
        <tag>编辑器</tag>
        <tag>git</tag>
        <tag>java</tag>
      </tags>
  </entry>
  <entry>
    <title>macos挂载阿里云盘当做本地盘</title>
    <url>/hobby-macos-mount-aliyundrive/</url>
    <content><![CDATA[1. 前言相比于百度云盘，阿里云盘还不够成熟，很多功能还不完善，但是最大的优点是不限速，不限速，不限速！为了更方便地使用阿里云盘，本文研究一下阿里云盘挂载到本地macos系统的方法。参考文档：

webdav-aliyundriver
mac挂载阿里云盘



2. 思路首先通过webdav协议把阿里云盘变成本地文件服务器，然后通过mac挂载工具挂载本地文件服务器，当做本地磁盘使用。
其中webdav协议是基于Web的分布式创作和版本控制协议（Web-based Distributed Authoring and Versioning），它扩展了HTTP 1.1，在GET、POST、HEAD等几个HTTP标准方法以外添加了一些新的方法，使应用程序可对Web Server直接读写，还可以支持文件的版本控制。
webdav-aliyundriver这个工具实现了阿里云盘的webdav协议，可以用来把阿里云盘变成本地文件服务器。
CloudMounter这个工具可以用来挂载文件服务器当做本地磁盘。
3. 操作3.1. 获取阿里云token1、登录阿里云盘网页版
2、F12 -&gt; Application -&gt; Storage -&gt; Local Storage -&gt; token -&gt; refresh_token
3.2. 启动本地文件服务器1、下载webdav-aliyundriver.jar。
2、启动阿里云盘为本地文件服务器java -jar webdav-aliyundriver.jar --aliyundrive.refresh-token=&quot;your refreshToken&quot;
3、测试访问浏览器访问 http://127.0.0.1:8080 ，输入用户名和密码就可以看到云盘里的文件了。其中用户名和密码都是admin。
3.3. 挂载本地文件服务器1、下载安装CloudMounter破解版（有条件的同学还是要支持正版啊）
2、打开CloudMounter，选择WebDAV。填入名称、WebDAV URL、用户名和密码。
3、然后，就可以在macos系统中愉快地使用阿里云盘了。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>macos</tag>
      </tags>
  </entry>
  <entry>
    <title>Airflow入门篇</title>
    <url>/dev-airflow-start/</url>
    <content><![CDATA[1. Airflow是啥？
Airflow is a platform to programmatically author, schedule and monitor workflows.Airflow 是一个以编程方式创作、安排和监控工作流的平台。


Airflow 可以将工作流编排为任务的有向无环图 (DAG)。 
Airflow 调度器调度任务在一组work上执行，同时满足指定的依赖项。
Airflow 拥有大量的命令行工具，可以轻松在DAG上执行复杂的操作。
Airflow 界面友好，管道、进度、问题一目了然。

学习资料：

Airflow官方文档
Airflow从零到神
没看过这篇文章，别说你会用Airflow
你不可不知的任务调度神器-AirFlow
海外的工程师是如何使用Airflow的？



2. 安装和初始化安装配置airflow，主要参考Airflow本地快速运行和Airflow Installation。
前置条件：安装配置好python3.6，详情参考《MacOS上软件配置》。
1、安装airflow
pip install &quot;apache-airflow[celery]==2.0.0&quot; --constraint &quot;https://raw.githubusercontent.com/apache/airflow/constraints-2.0.0/constraints-3.6.txt&quot;

2、初始化
airflow db initairflow users create \    --username admin \    --firstname Peter \    --lastname Parker \    --role Admin \    --email spiderman@superhero.org

默认安装目录为 /Users/voidking/airflow，如果要指定安装目录的话，可以在执行初始化命令前配置 AIRFLOW_HOME 变量。
3. 启停Airflow1、启动airflow
airflow webserver -D  # 启动webairflow scheduler -D  # 启动调度器

2、停止airflow，参考Airflow任务调度系统
# 停止airflow webserverps -ef | grep &#x27;airflow&#x27; | grep &#x27;webserver&#x27; | awk &#x27;&#123;print $2&#125;&#x27; | xargs kill -9 cd $AIRFLOW_HOME  rm -rf airflow-webserver.pidrm -rf airflow-webserver-monitor.pid# 停止airflow scheduler  ps -ef | grep &#x27;airflow&#x27; | grep &#x27;scheduler&#x27; | awk &#x27;&#123;print $2&#125;&#x27; | xargs kill -9  cd $AIRFLOW_HOME  rm -rf airflow-scheduler.pid

3、封装一个启停脚本脚本链接
4. 常用命令airflow dags list

5. 实用配置修改airflow.cfg：
# 指定dags目录dags_folder = /Users/voidking/airflow/dags# dags目录扫描时间间隔dag_dir_list_interval = 30# 启用api请求auth_backend = airflow.api.auth.backend.basic_auth# 日志路径和日志文件名log_filename_template = &#123;&#123; ti.dag_id &#125;&#125;/&#123;&#123; ti.task_id &#125;&#125;.log

更多内容参考Configuration Reference。
6. Hello DAGs1、启动airflow启动airflow webserver和airflow scheduler，然后访问本地8080端口，看到airflow管理页面
2、编写dags脚本编写第一个dags脚本，命名为 hello.py
#!/usr/bin/env python3# -*- coding: utf-8 -*-import datetimefrom airflow.models import DAGfrom airflow.operators.bash import BashOperatordefault_args = &#123;    &#x27;owner&#x27;: &#x27;airflow&#x27;,    &#x27;depends_on_past&#x27;: False,    &#x27;email&#x27;: [&#x27;airflow@example.com&#x27;],    &#x27;email_on_failure&#x27;: False,    &#x27;email_on_retry&#x27;: False,    &#x27;retries&#x27;: 1,    &#x27;retry_delay&#x27;: datetime.timedelta(minutes=5),&#125;dag = DAG(    dag_id=&#x27;hello&#x27;,    default_args=default_args,    description=&#x27;The first DAG&#x27;,    catchup=False,    schedule_interval=None,    start_date=datetime.datetime(2021, 1, 1),    tags=[&#x27;example&#x27;],)t1 = BashOperator(    task_id=&#x27;print_hello&#x27;,    bash_command=&#x27;echo &quot;Hello dags!&quot;&#x27;,    dag=dag)t2 = BashOperator(    task_id=&#x27;print_hello_again&#x27;,    bash_command=&#x27;echo &quot;Hello dags again!&quot;&#x27;,    dag=dag)t1 &gt;&gt; t2
简单测试dags脚本：python hello.py
3、dags脚本放入到airflow dags目录hello.py 放入 /Users/voidking/airflow/dags 目录（或者创建软链），使airflow可以扫描读取到它（前提是启动scheduler）。注意，新建的dags任务并不会马上出现在界面上，默认需要5分钟。如果页面头部出现报错信息，请按照提示修改dags脚本。
另外，dags脚本也可以放入到 /Users/voidking/.pyenv/versions/3.6.4/lib/python3.6/site-packages/airflow/example_dags/ 目录，和示例脚本放到一起。
4、触发运行这里的传参为空，因为不需要额外参数。
5、查看执行日志点击查看hello的dags详情，查看执行日志。以上，第一个dags脚本运行完成。
7. 重跑任务1、进入dags详情页面
2、点击需要重跑的task的小方块
3、点击Clear或者Run，重跑任务
8. TODOtutorial.py详解、传参、接口调用airflow + django
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：基于Pyarmor加密Python代码</title>
    <url>/dev-obfuscate-python-based-on-pyarmor/</url>
    <content><![CDATA[1. 前言Python是一种解释型语言，相较于编译型语言，代码更容易泄露。为了保护知识产权、防止代码被未授权访问或修改、以及保护商业机密，有时候我们需要进行Python代码加密（混淆）。
目前比较好的Python代码加密产品是Pyarmor，它具有免费版本和付费版本。Pyarmor 加密后的脚本输出的是同名的 .py 文件和一个运行辅助包。它们和普通 Python 模块一样，可以被Python 解释器调用执行，这也是 Pyarmor 的一个加密特点，可以使用加密后的脚本无缝替换原来的脚本。本文中，我们来学习一下Pyarmor的使用。
参考文档：

Pyarmor  Documentation
Pyarmor 8.5 用户文档
Pyarmor  Documentation PDF
Pyarmor官网
github - dashingsoft/pyarmor
使用PyArmor保护Python代码



2. 免费版与付费版区别参考文档：License Types
下载和安装本软件表示自动接受试用许可协议，试用版本（免费版本）有如下的限制：

加密功能对脚本大小有限制，不能加密超过限制的大脚本。
混淆字符串功能在试用版中无法使用。
RFT 加密模式，BCC 加密模式在试用版无法使用。
不可以用于加密商用产品。特别的，如果商用产品的累计销售额小于基础版许可证费用乘以30，可以暂时使用；但是一旦累计销售额超过阀值，就不可以在继续使用。
运行辅助包的名称 “pyarmor_runtime_000000” 不可以被设置和修改
不可以使用本软件提供任何形式的加密服务，不管是通过应用程序还是网络服务。总之在任何情况下都不允许使用本软件加密其他人的脚本。
不支持 obf-code 大于 1 的任何加密模式

试用版本中功能限制，需要通过许可授权来解锁相关功能。但是第 (6) 条限制没有任何许可证可以解锁。
3. 使用免费版Pyarmor3.1. 加密单个文件1、准备一个测试文件 demo.py
print(&quot;hello&quot;)

2、安装pyarmor
pip install pyarmor

3、加密文件
pyarmor gen demo.py

4、运行加密后文件
cat dist/demo.pypython dist/demo.py

3.2. 加密代码包1、准备一个代码包 mypkg
mypkg├── __init__.py└── demo.py

2、加密代码包
pyarmor gen -O dist2 mypkg

3、测试运行代码包
cd dist2python -c &#x27;import mypkg&#x27;

3.3. 设置过期时间设置30天之后过期
pyarmor gen -O dist4 -e 30 foo.py

设置指定日期过期
pyarmor gen -O dist4 -e 2020-12-31 foo.py

3.4. 绑定设备1、获取设备信息
python -m pyarmor.cli.hdinfo# pyarmor-7 hdinfo # Before Pyarmor 8.4.6, using pyarmor-7 hdinfo to get hardware information.

得到输出：
Default Harddisk Serial Number: &#x27;HXS2000CN2A&#x27;Default Mac address: &#x27;00:16:3e:35:19:3d&#x27;Default IPv4 address: &#x27;128.16.4.10&#x27;

2、绑定设备
pyarmor gen -O dist5 -b 00:16:3e:35:19:3d foo.pypyarmor gen -O dist5 -b 128.16.4.10 foo.pypyarmor gen -O dist5 -b HXS2000CN2A foo.pypyarmor gen -O dist5 -b &quot;00:16:3e:35:19:3d HXS2000CN2A&quot; foo.py

4. 使用付费版Pyarmor1、根据购买Licence时获取到的链接，下载 pyarmor-regfile-xxx.zip
2、注册Licence
pyarmor register /path/to/pyarmor-regfile-xxx.zip

3、验证注册信息
pyarmor register

4、使用Pyarmor付费版用法和免费版基本相同。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python代码质量检查</title>
    <url>/dev-python-code-quality-check/</url>
    <content><![CDATA[1. Pylint vs Flake8FROM ChatGPT：
Pylint和Flake8都是用于Python代码静态分析的工具，它们都可以帮助您发现代码中的潜在问题并提供一些代码质量方面的建议。但是，它们的设计目标和使用方法有所不同。
Pylint是一个非常全面的工具，它会对代码进行更严格的检查，包括PEP 8风格指南和一些语法规则。它还可以检查代码中的错误和漏洞，并提供复杂度和维护性方面的建议。它还可以进行类型检查和代码重构建议等高级功能。因此，Pylint适合大型项目和团队，可以帮助他们保持代码质量和一致性。
Flake8是一个更轻量级的工具，主要关注代码风格和语法规则。它通过结合多个工具，如PyFlakes和PEP 8风格指南检查工具，提供了更快速和简便的检查方式。Flake8对于小型项目和个人开发者非常适用。
因此，如果您需要一个更全面的代码分析工具并且有时间和资源，您可以选择Pylint。但如果您想要一个更快速，轻便的工具，则Flake8可能更适合您。无论哪种选择，它们都可以提供有价值的帮助来改进您的代码质量和可维护性。
参考文档：

pylint和flake8有什么区别，各适用于什么场景？



2. Pylintpip install pylintpylint test.py

************* Module testtest.py:62:0: C0305: Trailing newlines (trailing-newlines)test.py:1:0: C0114: Missing module docstring (missing-module-docstring)test.py:30:-1: W0105: String statement has no effect (pointless-string-statement)test.py:43:4: C0116: Missing function or method docstring (missing-function-docstring)test.py:43:4: C0103: Method name &quot;twoSum&quot; doesn&#x27;t conform to snake_case naming style (invalid-name)test.py:43:21: W0621: Redefining name &#x27;nums&#x27; from outer scope (line 57) (redefined-outer-name)test.py:43:38: W0621: Redefining name &#x27;target&#x27; from outer scope (line 58) (redefined-outer-name)test.py:44:20: R1735: Consider using &#x27;&#123;&#125;&#x27; instead of a call to &#x27;dict&#x27;. (use-dict-literal)test.py:32:0: R0903: Too few public methods (1/2) (too-few-public-methods)test.py:58:4: C0103: Constant name &quot;target&quot; doesn&#x27;t conform to UPPER_CASE naming style (invalid-name)------------------------------------------------------------------Your code has been rated at 4.12/10 (previous run: 4.12/10, +0.00)

3. Flake8pip install flake8flake8 test.py#flake8 --ignore=E231,E302 *.py

solution.py:32:1: E302 expected 2 blank lines, found 1solution.py:45:14: E231 missing whitespace after &#x27;,&#x27;solution.py:52:52: E231 missing whitespace after &#x27;,&#x27;solution.py:57:14: E231 missing whitespace after &#x27;,&#x27;solution.py:57:16: E231 missing whitespace after &#x27;,&#x27;solution.py:57:19: E231 missing whitespace after &#x27;,&#x27;solution.py:60:31: E231 missing whitespace after &#x27;,&#x27;solution.py:62:1: W391 blank line at end of file

4. 小结Pylint和Flake8的默认检查结果，完全不一致，这个是我没想到的。
综合来看，还是Pylint更优，因为它给出的检查结果更有意义。而Flake8给出的检查结果，都是些空格空行规范，这些可以通过配置autopep8自动进行格式化。
VSCode中autopep8配置方法参考《VSCode常用快捷键和配置》。
使用autopep8的方法：
pip install autopep8autopep8 test.pyautopep8 -i test.py

参考文档：

How can I configure Pylint to check all things PEP8 checks?

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>testing</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>好好学Python</tag>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python打印日志</title>
    <url>/dev-python-log/</url>
    <content><![CDATA[1. 输出日志到控制台import sysimport logginglog = logging.getLogger(__name__)log.setLevel(logging.DEBUG)log.addHandler(logging.StreamHandler(sys.stdout)) #默认sys.errorlog.info(&#x27;print info level log to console&#x27;)



2. 输出日志到文件import sysimport logginglog = logging.getLogger(__name__)log.setLevel(logging.DEBUG)log.addHandler(logging.FileHandler(&#x27;python.log&#x27;))log.info(&#x27;print info level log to file&#x27;)
日志文件默认存储到执行命令的路径下，可以通过使用绝对路径来指定日志文件路径。
3. 封装日志模块1、封装一个日志模块 log.py
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport loggingimport logging.handlersdef init_log(log_path, level=logging.INFO, when=&quot;D&quot;, backup=7,             format=&quot;%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d * %(thread)d %(message)s&quot;,             datefmt=&quot;%m-%d %H:%M:%S&quot;):    &quot;&quot;&quot;    init_log - initialize log module    Args:    log_path - Log file path prefix.    Log data will go to two files: log_path.log and log_path.log.wf    Any non-exist parent directories will be created automatically    level - msg above the level will be displayed    DEBUG &lt; INFO &lt; WARNING &lt; ERROR &lt; CRITICAL    the default value is logging.INFO    when - how to split the log file by time interval    &#x27;S&#x27; : Seconds    &#x27;M&#x27; : Minutes    &#x27;H&#x27; : Hours    &#x27;D&#x27; : Days    &#x27;W&#x27; : Week day    default value: &#x27;D&#x27;    format - format of the log    default format:    %(levelname)s: %(asctime)s: %(filename)s:%(lineno)d * %(thread)d %(message)s    INFO: 12-09 18:02:42: log.py:40 * 139814749787872 HELLO WORLD    backup - how many backup file to keep    default value: 7    Raises:    OSError: fail to create log directories    IOError: fail to open log file    &quot;&quot;&quot;    formatter = logging.Formatter(format, datefmt)    logger = logging.getLogger()    logger.setLevel(level)    dir = os.path.dirname(log_path)    if not os.path.isdir(dir):        os.makedirs(dir)    handler = logging.handlers.TimedRotatingFileHandler(log_path + &quot;.log&quot;,                                                        when=when,                                                        backupCount=backup)    handler.setLevel(level)    handler.setFormatter(formatter)    logger.addHandler(handler)    handler = logging.handlers.TimedRotatingFileHandler(log_path + &quot;.log.wf&quot;,                                                        when=when,                                                        backupCount=backup)    handler.setLevel(logging.WARNING)    handler.setFormatter(formatter)    logger.addHandler(handler)

2、使用日志模块
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport loggingimport logclass VKTest(object):    def __init__(self):        logging.info(&#x27;print info level log to file&#x27;)if __name__==&quot;__main__&quot;:    # 日志保存到./log/vk.log和./log/vk.log.wf，按天切割，保留7天    log.init_log(&quot;./log/vk&quot;)    logging.info(&quot;程序启动!!!&quot;)    vk = VKTest()    logging.info(&quot;程序结束!!!&quot;)

执行脚本后，日志内容会输出到 log/vk.log 中。
4. 封装日志模块2.01、封装一个日志模块 log.py
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport loggingimport logging.handlersDEFAULT_LOG_PATH = &#x27;./log/&#x27;class Log(object):    def __init__(self,                 logfile_name=&#x27;default&#x27;,                 log_path=DEFAULT_LOG_PATH,                 level=logging.INFO,                 when=&quot;D&quot;,                 backup=7,                 format=&quot;%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d * %(thread)d %(message)s&quot;,                 datefmt=&quot;%m-%d %H:%M:%S&quot;):        &quot;&quot;&quot;        init_log - initialize log module        Args:        log_path - Log file path prefix.        Log data will go to two files: log_path.log and log_path.log.wf        Any non-exist parent directories will be created automatically        level - msg above the level will be displayed        DEBUG &lt; INFO &lt; WARNING &lt; ERROR &lt; CRITICAL        the default value is logging.INFO        when - how to split the log file by time interval        &#x27;S&#x27; : Seconds        &#x27;M&#x27; : Minutes        &#x27;H&#x27; : Hours        &#x27;D&#x27; : Days        &#x27;W&#x27; : Week day        default value: &#x27;D&#x27;        format - format of the log        default format:        %(levelname)s: %(asctime)s: %(filename)s:%(lineno)d * %(thread)d %(message)s        INFO: 12-09 18:02:42: log.py:40 * 139814749787872 HELLO WORLD        backup - how many backup file to keep        default value: 7        Raises:        OSError: fail to create log directories        IOError: fail to open log file        &quot;&quot;&quot;        formatter = logging.Formatter(format, datefmt)        # getLogger一定要传参，否则多次调用对象创建，得到的会是同一个logger对象        # 后果就是相同的日志内容，会同时写到多个不同的日志文件中        self.logger = logging.getLogger(logfile_name)        self.logger.setLevel(level)        if not os.path.exists(log_path):            os.makedirs(log_path)                    common_log = os.path.join(log_path, logfile_name + &quot;.log&quot;)        handler = logging.handlers.TimedRotatingFileHandler(common_log,                                                            when=when,                                                            backupCount=backup)        handler.setLevel(level)        handler.setFormatter(formatter)        self.logger.addHandler(handler)        wf_log = os.path.join(log_path, logfile_name + &quot;.log.wf&quot;)        handler = logging.handlers.TimedRotatingFileHandler(wf_log,                                                            when=when,                                                            backupCount=backup)        handler.setLevel(logging.WARNING)        handler.setFormatter(formatter)        self.logger.addHandler(handler)    def get_logger(self):        return self.logger    # 同时输出内容到日志和控制台    def get_console_logger(self):        self.logger.addHandler(logging.StreamHandler(sys.stdout))        return self.logger

2、使用日志模块
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport log# 日志保存到./log/vk.log和./log/vk.log.wf，按天切割，保留7天logger = log.Log(logfile_name=&#x27;vk&#x27;).get_logger()# logger = log.Log(logfile_name=&#x27;vk&#x27;).get_console_logger()class VKTest(object):    def __init__(self):        logger.info(&#x27;print info level log to file&#x27;)if __name__==&quot;__main__&quot;:    logger.info(&quot;程序启动!!!&quot;)    vk = VKTest()    logger.info(&quot;程序结束!!!&quot;)

执行脚本后，日志内容会输出到 log/vk.log 中。
5. 参考文档
Python黑魔法手册
Python Cookbook 3rd Edition Documentation
Google Python风格指南
unittest - 单元测试框架

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python操作MySQL</title>
    <url>/dev-python-mysql/</url>
    <content><![CDATA[1. 前言python操作mysql数据库进行增删查改，是经常遇到的需求，下面整理一下具体操作方法。
2. 数据库准备1、安装配置好mysql参考《使用Docker安装配置Mysql》
2、创建一个测试用的数据库
create database vkphp default character set utf8 collate utf8_general_ci;use vkphp;CREATE TABLE IF NOT EXISTS `user` (  `id` int(8) NOT NULL AUTO_INCREMENT,  `name` varchar(32) NOT NULL,  `password` varchar(32) NOT NULL DEFAULT &#x27;&#x27;,  PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

更多命令参考 《MySQL常用命令》


3. 安装依赖pip3 install mysqlclient==1.3.13
4. 插入数据#!/usr/bin/python3# -*- coding: UTF-8 -*-import MySQLdb# 打开数据库连接db = MySQLdb.connect(host=&quot;localhost&quot;,                     port=&quot;3306&quot;,                    user=&quot;root&quot;,                     password=&quot;voidking&quot;,                     database=&quot;vkphp&quot;,                     charset=&#x27;utf8&#x27;)# 使用cursor()方法获取操作游标 cursor = db.cursor()# SQL 插入语句sql = &quot;insert into `user` (`name`,`password`) values(&#x27;haojin&#x27;,&#x27;voidking&#x27;);&quot;try:   # 执行sql语句   cursor.execute(sql)   # 提交到数据库执行   db.commit()except:   # 发生错误时回滚   db.rollback()# 关闭数据库连接db.close()

其他操作参考Python 操作 MySQL 数据库。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>mysql</tag>
        <tag>好好学Python</tag>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python包管理工具pip</title>
    <url>/dev-python-pip/</url>
    <content><![CDATA[1. pip简介
Pip is a package-management system written in Python and is used to install and manage software packages. The Python Software Foundation recommends using pip for installing Python applications and its dependencies during deployment. Pip connects to an online repository of public packages, called the Python Package Index. Pip can be configured to connect to other package repositories (local or remote), provided that they comply to Python Enhancement Proposal 503.


Most distributions of Python come with pip preinstalled. Python 2.7.9 and later (on the python2 series), and Python 3.4 and later include pip by default.

相关文档：

Wikipedia - pip
《Python软件仓库PyPI》
《Python包管理工具Conda》
《Python版本管理器pyenv》
《Python模块和模块路径》



2. 安装pip低版本的python没有默认安装pip，这时就需要手动安装。
方法一：
python -m ensurepip
方法二：
wget https://bootstrap.pypa.io/get-pip.pypython get-pip.py -i https://pypi.douban.com/simple/

3. 查看pip路径查找默认python的pip路径
which pipwhich python

查看特定python的pip路径，已知python路径为/path/to/python
ls -l /path/to/pip/path/to/python -m pip --version

pip bin一般和python bin在相同路径。
4. pip常用命令4.1. 安装软件包安装最新软件包
pip install requests

安装指定版本的软件包
pip install requests==2.25.1

更多版本控制方法，参考【pip控制软件版本】一节。
安装wheel包
pip install xxx.whl

4.2. 指定pypi加速pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple

4.3. 批量安装软件包1、依赖的软件包，全部写入 requirements.txt 文件中，例如
pylint=2.17.0requests==2.25.1

2、执行安装
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

4.4. 下载软件包下载软件包但不安装
pip download requests -d &quot;./&quot;

4.5. 查看软件包查看软件包
pip show requestspip show -f requests

查看需要升级的软件包
pip list -o

4.6. 升级软件包pip install --upgrade requestspip install -U requests

4.7. 卸载软件包pip uninstall requests

4.8. 查看兼容问题pip check package_namepip check # 检查所有包

5. pip控制软件版本pip安装软件时，控制软件版本的符号及其含义如下：
==: 精确版本。如 requests==2.25.1，表示只安装 requests 的 2.25.1 版本。!=: 不等于某个版本。如 requests!=2.25.1，表示安装的 requests 版本不能是 2.25.1。&gt;: 大于某个版本。如 requests&gt;2.25.1，表示安装的 requests 版本必须大于 2.25.1。&gt;=: 大于等于某个版本。如 requests&gt;=2.25.1，表示安装的 requests 版本必须大于等于 2.25.1。&lt;: 小于某个版本。如 requests&lt;2.26.0，表示安装的 requests 版本必须小于 2.26.0。&lt;=: 小于等于某个版本。如 requests&lt;=2.25.1，表示安装的 requests 版本必须小于等于 2.25.1。~=: 兼容版本。如 requests~=2.25.1，表示允许的 requests 版本范围与 2.25.1 兼容，即安装的版本在 2.25.1（包含）到 2.26.0（不包含）之间。安装这个范围内可获得的最高的版本。===: 精确匹配。如 requests===2.25.1，表示只安装与 2.25.1 完全匹配的 requests 版本。这与 == 类似，但更严格，要求版本标识符完全一致。我们还可以使用逗号 , 来组合多个条件。例如，requests&gt;=2.25.1,&lt;=2.26.0 表示安装的 requests 版本必须大于等于 2.25.1 且小于等于 2.26.0。这允许你定义更精确的版本范围。

6. python软件包的依赖Python 软件包中的依赖通常分为两种类型：必要依赖（也称为硬依赖）和可选依赖（也称为软依赖）。
必要依赖（硬依赖）：必要依赖是指一个 Python 包正常运行所必需的其他包。这些依赖通常包含在包的 setup.py 或 setup.cfg 文件中，或者在一个名为 requirements.txt 的文件中。当你使用 pip 安装一个包时，必要依赖会自动被安装。例如，如果一个包名为 foo 的必要依赖是 bar 和 baz，那么当你运行 pip install foo 时，pip 会自动安装 foo、bar 和 baz。
可选依赖（软依赖）：可选依赖是指那些对于一个 Python 包的核心功能不是必需的，但可能对某些额外功能或特性有用的其他包。可选依赖通常在 setup.py 或 setup.cfg 文件中的 “extras_require” 部分定义。当你使用 pip 安装一个包时，可选依赖默认不会被安装。但是，你可以通过在包名后面使用方括号（[]）并指定可选依赖的名称，来告诉 pip 安装某个或多个可选依赖。
例如，假设一个名为 foo 的包具有以下可选依赖：testing、docs
extras_require = &#123;    &quot;testing&quot;: [&quot;pytest==7.2.2&quot;, &quot;pytest-cov==4.0.0&quot;],    &quot;docs&quot;: [&quot;sphinx&quot;],&#125;

那么安装可选依赖的方法为：
pip install foo[testing]pip install foo[testing,docs]pip install foo[all]

并非所有包都定义了all可选依赖，all的定义方法为：
extras_require[&quot;all&quot;] = list(set(reduce(operator.add, [requires, test_requirements, *extras_require.values()])))

7. ModuleNotFoundError处理方法python 报错 ModuleNotFoundError: No module named ‘xxx’ ，该怎么找到module对应的package？
一般情况下，直接执行pip install安装package即可
pip install xxx

但是，某些情况下xxx这个module，对应的package名称并不是xxx，这时可以通过搜索引擎或者chatgpt查找xxx这个module对应的package。
module和package的对应关系，可以参考《Python基础》
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>pip</tag>
        <tag>好好学Python</tag>
        <tag>pypi</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python脚本路径</title>
    <url>/dev-python-script-path/</url>
    <content><![CDATA[1. 怎样获取一个脚本的路径？怎样获取执行脚本的绝对路径？怎样获取执行脚本的父绝对路径？怎样获取入口脚本的绝对路径？。。。带着这些问题，我们进行一个简单的实验，获知这些问题的答案。


2. 路径实验2.1. 路径设计path└── module_a    ├── getpath.py    └── module_b        └── getpath.py

2.2. 脚本内容getpath.py脚本内容为：
import osimport sysfrom module_b import getpathdef print_path():    print(f&#x27;os.getcwd() is &#123;os.getcwd()&#125;&#x27;)    print(f&#x27;sys.path[0] is &#123;sys.path[0]&#125;&#x27;)    print(f&#x27;sys.argv[0] is &#123;sys.argv[0]&#125;&#x27;)    print(f&#x27;__file__ is &#123;__file__&#125;&#x27;)    print(f&#x27;os.path.dirname(__file__) is &#123;os.path.dirname(__file__)&#125;&#x27;)    print(f&#x27;os.path.abspath(__file__) is &#123;os.path.abspath(__file__)&#125;&#x27;)    print(f&#x27;os.path.realpath(__file__) is &#123;os.path.realpath(__file__)&#125;&#x27;)    print(f&#x27;os.path.split(os.path.realpath(__file__))[0] is &#123;os.path.split(os.path.realpath(__file__))[0]&#125;&#x27;)print(&#x27;------ module_a/getpath.py ------&#x27;)print_path()print(&#x27;---------------------------------&#x27;)print(&#x27;------ module_a/module_b/getpath.py ------&#x27;)getpath.print_path()print(&#x27;------------------------------------------&#x27;)

2.3. 执行脚本在path目录中执行脚本python module_a/getpath.py ，得到结果为：
------ module_a/getpath.py ------os.getcwd() is /Users/vk/tmp/pathsys.path[0] is /Users/vk/tmp/path/module_asys.argv[0] is module_a/getpath.py__file__ is module_a/getpath.pyos.path.dirname(__file__) is module_aos.path.abspath(__file__) is /Users/vk/tmp/path/module_a/getpath.pyos.path.realpath(__file__) is /Users/vk/tmp/path/module_a/getpath.pyos.path.split(os.path.realpath(__file__))[0] is /Users/vk/tmp/path/module_a--------------------------------------- module_a/module_b/getpath.py ------os.getcwd() is /Users/vk/tmp/pathsys.path[0] is /Users/vk/tmp/path/module_asys.argv[0] is module_a/getpath.py__file__ is /Users/vk/tmp/path/module_a/module_b/getpath.pyos.path.dirname(__file__) is /Users/vk/tmp/path/module_a/module_bos.path.abspath(__file__) is /Users/vk/tmp/path/module_a/module_b/getpath.pyos.path.realpath(__file__) is /Users/vk/tmp/path/module_a/module_b/getpath.pyos.path.split(os.path.realpath(__file__))[0] is /Users/vk/tmp/path/module_a/module_b------------------------------------------

3. 实验结论由实验结果，可以得出如下结论。
3.1. 获取执行命令的绝对路径os.getcwd()

3.2. 获取入口脚本的父绝对路径sys.path[0]

3.3. 获取执行脚本的绝对路劲os.path.abspath(__file__)os.path.realpath(__file__)

3.4. 获取执行脚本的父绝对路径os.path.split(os.path.realpath(__file__))[0]

3.5. 获取文件/目录的父路径os.path.dirname(__file__)



]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python重试</title>
    <url>/dev-python-retry/</url>
    <content><![CDATA[1. 为什么需要重试？典型场景：程序的实现需要调用第三方的API，但是我们并不能保证第三方API一直好用，也不能保证网络一直畅通，所以在调用第三方API时需要加上错误重试。  
通用场景：程序的运行不符合预期，我们知道再次调用大概率可以使之符合预期，这时就需要重试。  
而重试逻辑我们可以通过循环多次调用实现，也可以使用封装好的重试模块。本文我们就学习一下Python中比较流行的 retrying 模块，实现一些重试Demo。


2. 安装retryingpip install retrying
3. 任意异常重试from retrying import retry@retry(stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=3000)def run():    print(&#x27;run&#x27;)    raise ValueErrorif __name__ == &#x27;__main__&#x27;:    run()

当函数run抛出任意异常时，则进行重试。

stop_max_attempt_number：最大重试次数
wait_random_min：最小重试间隔
wait_random_max：最大重试间隔

注意函数内不要使用try except，否则异常就被函数自己捕获处理了，不会抛给retry。
4. 指定异常重试from retrying import retryimport randomdef if_value_error(exception):    return isinstance(exception, ValueError)@retry(retry_on_exception=if_value_error, stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=3000)def run():    value = random.randint(0, 10)    msg = f&#x27;value is &#123;value&#125;&#x27;    print(msg)    if value &lt;= 5:        raise ValueError(&#x27;value is too small!&#x27;)    else:        raise Exception(&#x27;value is too large!&#x27;)if __name__ == &#x27;__main__&#x27;:    run()

当函数run抛出ValueError异常时，则进行重试；抛出Exception异常时，不会重试。
5. 指定返回值重试from retrying import retryimport randomdef if_return_small(value):    return value &lt;= 5@retry(retry_on_result=if_return_small, stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=3000)def run():    value = random.randint(0, 10)    msg = f&#x27;value is &#123;value&#125;&#x27;    print(msg)    return valueif __name__ == &#x27;__main__&#x27;:    run()

当函数run返回值小于5时，进行重试。
6. 多次重试后容忍异常from retrying import retry@retry(stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=3000)def run():    print(&#x27;run&#x27;)    raise ValueErrordef run2():    print(&#x27;run2&#x27;)if __name__ == &#x27;__main__&#x27;:    run()    run2()

当函数run抛出任意异常时，则进行重试。重试指定次数后，程序就异常结束了。这时，如果我们想要容忍异常，让run2可以正常运行，该怎么办呢？很简单，给run函数包裹一个try except即可。
from retrying import retry@retry(wrap_exception=True,stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=3000)def run():    print(&#x27;run&#x27;)    raise ValueErrordef wrap_run():    try:        run()    except:        passdef run2():    print(&#x27;run2&#x27;)if __name__ == &#x27;__main__&#x27;:    wrap_run()    run2()


]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python小技巧</title>
    <url>/dev-python-tricks/</url>
    <content><![CDATA[1. 前言本文记录使用Python时的一些小技巧，备忘。
参考文档：

Python官方文档
Python官方文档下载
Python官方文档下载-chm格式
Python黑魔法手册
Python Cookbook 3rd Edition Documentation
Google Python风格指南



2. 查找文件在指定目录中，根据文件前缀查找文件。
import globdef find_files_by_prefix(prefix, dir_path):    file_list = list()    file_pattern = os.path.join(dir_path, &#x27;&#123;&#125;*&#x27;.format(prefix))    for f in glob.glob(file_pattern):        file_list.append(f)    return file_list

3. 删除目录和文件封装几个简单函数，删除目录和文件。
import osimport shutil# 删除空目录def remove_dir(dir_path):    if os.path.exists(dir_path):        os.removedirs(dir_path)# 删除文件def remove_file(file_path):    if os.path.exists(file_path):        os.remove(file_path)# 删除目录和文件def remove_dir_and_file(dir_path):    if os.path.exists(dir_path):        shutil.rmtree(dir_path)

4. url特殊字符转义from urllib import parselabelselector = parse.quote(&#x27;app_id=voidking,env in (test,online)&#x27;)url = f&#x27;https://www.voidking.com?labelselector=&#123;labelselector&#125;&#x27;

特殊字符对应url编码表：
空格 : %20&quot; : %22# : %23% : %25&amp; : %26( : %28) : %29+ : %2B, : %2C/ : %2F: : %3A; : %3B&lt; : %3C= : %3D&gt; : %3E? : %3F@ : %40\ : %5C| : %7C 

5. 装饰器实现一个装饰器，功能：只要函数里传了name参数，且name参数的值为haojin，那么就把这个值替换为voidking。
from functools import wrapsdef haojin_to_voidking(func):    @wraps(func)    def decorated(*args, **kwargs):        if &#x27;name&#x27; in kwargs and kwargs[&#x27;name&#x27;] == &#x27;haojin&#x27;:            kwargs[&#x27;name&#x27;] = &#x27;voidking&#x27;        return func(*args, **kwargs)    return decorated@haojin_to_voidkingdef print_name(name):    print(name)print_name(name=&#x27;haojin&#x27;)

6. Python镜像选择我们倾向于选择更小的镜像，比如以Alpine为基础的Python镜像，但是这会导致更长的构建时间和模糊的错误，因此不建议。更推荐使用以Ubuntu和Debian为基础的Python镜像。
推荐的镜像：

ubuntu:18.04
ubuntu:20.04
ubuntu:22.04
python:3.7.10-slim-buster
python:3.9.10-slim-buster
python:3.10.10-slim-buster
python:3.11.10-slim-bookworm
python:3.12.10-slim-bookworm

Ubuntu/Debian的默认软件安装源都比较慢，因此最好先替换成国内软件安装源，详情参考《Ubuntu/Debian替换软件安装源》。
Debian buster软件包查找地址为Debian - buster版面列表，找到软件包后使用apt install安装即可，例如安装mysql client
apt install default-libmysqlclient-devapt install default-mysql-client

参考文档：

适用于您的 Python 应用程序的最佳 Docker 基础镜像
还在用Alpine作为你Docker的Python开发基础镜像？其实Ubuntu更好一点

7. 启动python web服务器使用sz传输大文件，有时候会被中断。这时我们可以启动一个web服务，把大文件作为静态资源下载。
python -m SimpleHTTPServer 9999python3 -m http.server 9999
然后浏览器输入ip:port，即可下载文件。
8. py编译为pyc1、批量编译整个项目
python -m compileall project_dir

2、移动位置和重命名编译后的 pyc 文件，放在 __pycache__ 目录中，需要移动到上一层目录。编译后的 pyc 文件名中，会带着解释器的版本（例如 cpython-37），会导致找不到模块，因此需要将所有 pyc 文件重命名。
find project_dir -type d -name &quot;__pycache__&quot; -exec sh -c &#x27;mv &quot;$0&quot;/* &quot;$(dirname &quot;$0&quot;)&quot;&#x27; &#123;&#125; \;find project_dir -name &quot;*.pyc&quot; -exec sh -c &#x27;mv &quot;$0&quot; &quot;$&#123;0%.cpython-37.pyc&#125;.pyc&quot;&#x27; &#123;&#125; \;#for file in *.cpython-37.pyc;do mv &quot;$file&quot; &quot;$&#123;file%.cpython-37.pyc&#125;.pyc&quot;;done

3、删除py文件
find project_dir -name &quot;*.py&quot; | xargs rm -fv

4、启动项目假设原启动命令为 python main.py ，那么编译后的启动命令为
python main.pyc

9. pyc反编译pypip install uncompyle6uncompyle6 -o filename.py filename.pyc

10. jupyter转pythonjupyter nbconvert --to python github_stars.ipynb

11. 自动生成python项目的依赖1、安装pipreqs工具
pip install pipreqs

2、生成依赖文件 requirements.txt
pipreqs /path/to/project

这种方法，不能保证找到所有的依赖，但是能找到大部分的依赖，已经够用了。
12. 打断点print(&quot;1&quot;)import pdb; pdb.set_trace()print(&quot;2&quot;)

13. 性能瓶颈排查参考文档：rkern/line_profiler
1、安装 line_profiler
pip install line_profiler

2、脚本中添加装饰器
@profiledef hello():  print(&#x27;hello&#x27;)if __name__ == &#x27;__main__&#x27;:  hello()

3、运行脚本
kernprof -l -v demo.py

4、查看上次的运行结果
python -m line_profiler demo.py.lprof

14. opencv-python 版本选择访问 Links for opencv-python，查看python与 opencv-python 对应的版本关系。安装与自己的python版本匹配的 opencv-python 包。
pip install opencv-python==3.4.16.57

15. pip安装依赖报错一pip install anyjson 报错：
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple/Collecting anyjson  Using cached https://pypi.tuna.tsinghua.edu.cn/packages/c3/4d/d4089e1a3dd25b46bebdb55a992b0797cff657b4477bc32ce28038fdecbc/anyjson-0.3.3.tar.gz (8.3 kB)  Preparing metadata (setup.py) ... error  error: subprocess-exited-with-error    × python setup.py egg_info did not run successfully.  │ exit code: 1  ╰─&gt; [1 lines of output]      error in anyjson setup command: use_2to3 is invalid.      [end of output]

这个错误是由于 Python 从 3.10 版本开始，移除了对 use_2to3 支持导致的。anyjson库在安装时候需要用到这个选项，而在新版本Python中该选项已经被移除，所以出现了此错误。
解决办法：换成老版本的Python，比如Python3.9。
16. pip安装依赖报错二pip install metagpt，报错：
Preparing metadata (setup.py) ... errorerror: subprocess-exited-with-error× python setup.py egg_info did not run successfully.│ exit code: 1╰─&gt; [1 lines of output]ERROR: Can not execute setup.py since setuptools is not available in the build environment.[end of output]

解决办法：使用 --use-pep517 参数。
pip install metagpt --use-pep517

参考文档Mac pip install error
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>docker</tag>
        <tag>mysql</tag>
        <tag>ubuntu</tag>
        <tag>好好学Python</tag>
        <tag>debian</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python单元测试</title>
    <url>/dev-python-unittest/</url>
    <content><![CDATA[1. unittest简介unittest是一个Python单元测试框架。它受到 JUnit 的启发，与其他语言中的主流单元测试框架有着相似的风格。其支持测试自动化，配置共享和关机代码测试。支持将测试样例聚合到测试集中，并将测试与报告框架独立。
主要参考 unittest - 单元测试框架。


2. 脚本编写新建一个 tests/test_demo.py 脚本，内容如下：
import unittestclass TestStringMethods(unittest.TestCase):    def test_upper(self):        self.assertEqual(&#x27;foo&#x27;.upper(), &#x27;FOO&#x27;)    def test_isupper(self):        self.assertTrue(&#x27;FOO&#x27;.isupper())        self.assertFalse(&#x27;Foo&#x27;.isupper())    def test_split(self):        s = &#x27;hello world&#x27;        self.assertEqual(s.split(), [&#x27;hello&#x27;, &#x27;world&#x27;])        # check that s.split fails when the separator is not a string        with self.assertRaises(TypeError):            s.split(2)if __name__ == &#x27;__main__&#x27;:    unittest.main()

继承 unittest.TestCase 就创建了一个测试样例。上述三个独立的测试是三个类的方法，这些方法的命名都以 test 开头。 这个命名约定告诉测试运行者类的哪些方法表示测试。
每个测试的关键是：调用 assertEqual() 来检查预期的输出； 调用 assertTrue() 或 assertFalse() 来验证一个条件；调用 assertRaises() 来验证抛出了一个特定的异常。
通过 setUp() 和 tearDown() 方法，可以设置测试开始前与完成后需要执行的指令。
最后的代码块中，演示了运行测试的一个简单的方法。 unittest.main() 提供了一个测试脚本的命令行接口。
3. 执行测试方法一：脚本级别调用（直接运行脚本）
python tests/test_demo.pypython tests/test_demo.py -vpython -m unittest tests/test_demo.py

方法二：包、模块、类和方法级别调用
# 包级别调用（进入tests目录查找所有test_*.py文件并运行）python -m unittest discover -s tests -p &quot;test_*.py&quot;# 模块级别调用python -m unittest tests.test_demo# 类级别调用python -m unittest tests.test_demo.TestStringMethods# 方法级别调用python -m unittest tests.test_demo.TestStringMethods.test_upper

4. 单元测试之mock单元测试时，如果涉及网络请求，建议使用mock模块来模拟。
import jsonimport unittestfrom unittest import mockimport requestsdef request_system(method: str, url: str, data=&#123;&#125;):    headers = &#123;        &#x27;Content-Type&#x27;: &#x27;application/json&#x27;    &#125;    response = requests.request(method=method, url=url, headers=headers, data=json.dumps(data))    return responsedef login(username: str, password: str):    # 以下接口暂时访问不通    LOGIN_URL=&#x27;http://127.0.0.1/login&#x27;    data = &#123;        &#x27;username&#x27;: username,        &#x27;password&#x27;: password    &#125;    response = request_system(&#x27;POST&#x27;, LOGIN_URL)    if response.status == 200:        return json.loads(response.text)    else:        return Noneclass TestMockMethods(unittest.TestCase):    def test_request_system(self):        mock_response = mock.Mock()        mock_response.status_code = 200        mock_response.text = json.dumps(&#123;&quot;code&quot;: 0, &quot;msg&quot;:&quot;success&quot;&#125;)        request_system = mock.Mock(return_value=mock_response)        response = request_system(method=&#x27;POST&#x27;,url=&#x27;http://127.0.0.1/&#x27;)        print(response.status_code)        print(response.text)        self.assertEqual(200, response.status_code)    def test_login(self):        login = mock.Mock(return_value=&#123;&quot;code&quot;: 0, &quot;msg&quot;:&quot;success&quot;&#125;)        data = login(username=&#x27;voidking&#x27;,password=&#x27;voidking&#x27;)        print(data)        self.assertEqual(0, data.get(&#x27;code&#x27;))if __name__ == &#x27;__main__&#x27;:    unittest.main()

这种模拟测试方式很巧妙，适合测试访问第三方接口，但是并不会真正发出请求。另一个问题来了，Python怎么测试自己的接口？不知道。想到以前使用Beego框架进行开发，它的单元测试就很巧妙，先在测试数据库插入数据，然后通过beego.BeeApp.Handlers.ServeHTTP(w, r)把自己临时启动起来，最后自己的单元测试调用自己的接口。其中的关键在于把自己启动起来，理论上Python也能做到。
5. 后记python语言中的单元测试，除了使用unittest，还可以使用pytest。
FROM ChatGPT：unittest和pytest是Python中两种流行的单元测试框架，它们有以下主要区别：

语法：unittest是Python标准库中的测试框架，而pytest是一个第三方测试框架。因此，unittest的语法更加正式和严格，而pytest的语法更加简洁和灵活。
自动化：pytest比unittest更加自动化和智能化。pytest可以自动发现和运行测试用例，而unittest需要手动编写测试用例的套件和运行代码。
插件和扩展性：pytest提供了大量的插件和扩展功能，例如测试报告、代码覆盖率、测试并行化、测试重试等。相比之下，unittest的扩展性较弱，需要编写更多的自定义代码才能实现类似的功能。
断言：unittest和pytest都提供了丰富的断言方法，用于检查测试结果是否符合预期。但是，pytest的断言方法更加灵活和易于使用，例如使用assert语句进行断言。
用例参数化：pytest支持用例参数化，可以通过数据驱动的方式多次运行同一个测试用例，以检查不同的输入和输出。相比之下，unittest需要手动编写多个测试用例，以实现类似的功能。

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>testing</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>好好学Python</tag>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>multipass入门篇</title>
    <url>/dev-multipass-start/</url>
    <content><![CDATA[1. multipass是啥？multipass 是一个轻量虚拟机管理器，是由Ubuntu运营公司Canonical所推出的开源项目。运行环境支持Linux、Windows、macOS，在Linux上使用的是KVM、Window上使用Hyper-V、macOS中使用HyperKit以最小开销运行VM。支持在笔记本模拟小型云。更多内容参考Multipass官网。


2. 安装使用multipass2.1. 安装multipassbrew install multipass

2.2. 创建虚拟机创建一个ubuntu 18.04虚拟机。
multipass findmultipass launch -n ubuntu18 -c 1 -m 2G -d 10G 18.04

2.3. 启停虚拟机multipass listmultipass stop ubuntu18multipass start ubuntu18

2.4. 删除虚拟机multipass delete ubuntu18multipass purge ubuntu18

2.5. 使用虚拟机multipass exec ubuntu18 -- lsb_release -amultipass shell ubuntu18

3. 配置远程登录上文安装好的ubuntu18虚拟机，默认是不支持远程登录的，可以按照如下步骤开启。1、切换到rootsudo su
2、修改sshd配置，开启支持远程登录vim /etc/ssh/sshd_config，如下修改：
Port 22ListenAddress 0.0.0.0ListenAddress ::PasswordAuthentication yes

3、重启sshdsystemctl restart sshd
4、给ubuntu用户设置一个密码passwd ubuntu
5、登录测试ssh ubuntu@localhost
以上，远程登录开启完成。
4. 安装使用Docker4.1. 安装Docker在虚拟机中，使用snap方式安装docker。
sudo susnap install dockerdocker info

snap方式安装的docker，只在用户目录下拥有读写权限。如果对docker挂载路径有要求，建议使用其他方式安装。
4.2. 安装mysqldocker run --name vk-mysql -d \-p 3306:3306 \-v /opt/data/mysql:/var/lib/mysql \-e MYSQL_ROOT_PASSWORD=voidking \mysql:5.6.50
报错为：docker: Error response from daemon: error while creating mount source path ‘/opt/data/mysql’: mkdir /opt/data: read-only file system.
这就是刚才说到的问题，snap方式安装的docker，只在用户目录下拥有读写权限。可以改成：
docker run --name vk-mysql -d \-p 3306:3306 \-v /home/work/data/mysql:/var/lib/mysql \-e MYSQL_ROOT_PASSWORD=voidking \mysql:5.6.50

4.3. 登录mysqlmysql -uroot -p登录报错：ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/var/run/mysqld/mysqld.sock’ (2)
应该是mysql的版本原因，需要指定host ip。
mysql -h127.0.0.1 -uroot -pmysql -h192.168.64.3 -P3306 -uroot -p



]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
      </tags>
  </entry>
  <entry>
    <title>MacOS安装Minikube</title>
    <url>/dev-macos-minikube/</url>
    <content><![CDATA[1. 前言Minikube是在个人计算机上的虚拟机中运行单节点Kubernetes集群的工具。《K8S入门篇》一文中，在Windows系统中安装过Minikube。本文中，我们来学习一下在MacOS系统中安装Minikube。主要参考再见Docker Desktop，你好 Minikube！。


2. 操作步骤2.1. 卸载Docker Desktop卸载Docker Desktop，包括Docker Client、Docker Daemon、Hyperkit等。
brew uninstall docker

PS：Docker Desktop安装方法参考《MacOS上软件配置》。
2.2. 安装Docker ClientDocker Client是一个客户端，用于和Docker Daemon进行交互。
brew install dockerdocker info

2.3. 安装KubectlKubectl是一个客户端，用于和K8S ApiServer进行交互。
brew install kubectl

2.4. 安装HyperkitHyperkit是一种MacOS上的虚拟化工具包，提供Hypervisor（虚拟机监视器，VMM）的能力，使虚拟机可以使用宿主机的资源进行计算。hyper是超级的意思，比super还要高级。
brew install hyperkithyperkid -v

2.5. 安装Minikubebrew install minikube

2.6. 设定minikube参数设置CPU和内存的限制。
minikube config set cpus 3minikube config set memory 6g
2.7. 启动Minikube启动Minikube，部署一个k8s集群。
minikube start --kubernetes-version=v1.19.14 --driver=hyperkit --container-runtime=docker

其中：

–kubernetes-version 指定K8S版本。
–driver 指定虚拟化驱动程序，比如hyperkit、multipass、virtualbox、parallels等。
–container-runtime 指定容器运行时，比如docker、containerd、cri-o等。

2.8. 查看k8s集群minikube kubectl get nodeskubectl get nodes

2.9. 设置docker环境变量eval $(minikube docker-env)

以上，Minikube安装完成，也可以正常使用Docker。但是，Docker的使用依赖Minikube的启动，这有点坑啊！能不能不启动Minikube，只启动Docker Daemon呢？可以的，使用multipass启动一个虚拟机，然后在虚拟机上安装Docker。详情参考《multipass入门篇》。之所以不使用virtualbox，是因为virtualbox使用virtualbox驱动，而不是hyperkit。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>macos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo环境搭建2021年8月版</title>
    <url>/dev-hexo-build-environment-2021-08/</url>
    <content><![CDATA[1. 前言15年的时候，写了一篇《Hexo环境搭建》，记录了hexo的详细安装部署步骤。18年的时候，写了一篇《Hexo环境搭建2018年5月版》，对安装方法和步骤进行了更新。转眼又过了三年，今天再整理一版hexo环境搭建的流程，与时俱进。


2. 环境准备系统环境：MACOS
安装brew、git、nvm、node，具体安装方法可以参考《MacOS上软件配置》。
其中，node版本选择 v12.22.5
3. 安装hexo1、切换国内源npm config set registry=&quot;https://registry.npm.taobao.org&quot;
2、安装hexo
npm install -g hexowhich hexo

3、初始化新建hexo目录，并安装依赖包。
hexo init hexocd hexonpm install --force

4、测试hexo g，hexo s，然后查看 http://localhost:4000
4. 安装依赖4.1. 添加RSSnpm install hexo-generator-feed --save，
注意，后面的参数--save绝对不能省，否则该插件信息不会写入package.json。hexo clean，hexo g，查看public文件夹，可以看到atom.xml文件。
4.2. 添加sitemapnpm install hexo-generator-sitemap --savenpm install hexo-generator-baidu-sitemap --save
如果报错：gyp: No Xcode or CLT version detected!可以忽略它，也可以通过重装xcode-select解决
xcode-select --print-pathsudo rm -r -f /Library/Developer/CommandLineToolssudo xcode-select --installxcode-select --installsudo xcode-select --reset

hexo clean，hexo g，查看public文件夹，可以看到sitemap.xml和baidusitemap.xml文件。sitemap的初衷是给搜索引擎看的，为了提高搜索引擎对自己站点的收录效果，我们最好手动到google和百度等搜索引擎提交sitemap.xml。具体参考《hexo生成sitemap》。
4.3. 支持本地搜索npm install hexo-generator-searchdb --save
需要配合_config.yml配置和主题配置，详情参考《Hexo更换主题为Next》。
4.4. 支持git部署npm install hexo-deployer-git --save

5. 安装主题进入hexo/themes目录，下载自己维护的next主题git clone https://github.com/voidking/hexo-theme-next.git next
该主题的更多个性化设置，可以参考《Hexo更换主题为Next》。
6. config.yml配置hexo目录中的_config.yml修改为：
# Hexo Configuration## Docs: http://hexo.io/docs/configuration.html## Source: https://github.com/hexojs/hexo/# Sitetitle: 好好学习的郝subtitle: 好好学习，天天向上！description: 学而不思则罔，思而不学则殆！author: 好好学习的郝language: zh-CNtimezone:# URL## If your site is put in a subdirectory, set url as &#x27;http://yoursite.com/child&#x27; and root as &#x27;/child/&#x27;url: https://www.voidking.comroot: /permalink: :title/permalink_defaults:# Directorysource_dir: sourcepublic_dir: publictag_dir: tagsarchive_dir: archivescategory_dir: categoriescode_dir: downloads/codei18n_dir: :langskip_render:  - &#x27;*.html&#x27;# Writingnew_post_name: :title.md # File name of new postsdefault_layout: posttitlecase: false # Transform title into titlecaseexternal_link: true # Open external links in new tabfilename_case: 0render_drafts: falsepost_asset_folder: falserelative_link: falsefuture: truehighlight:  enable: true  line_number: true  auto_detect: true  tab_replace:# Category &amp; Tagdefault_category: uncategorizedcategory_map:tag_map:# Date / Time format## Hexo uses Moment.js to parse and display date## You can customize the date format as defined in## http://momentjs.com/docs/#/displaying/format/date_format: YYYY-MM-DDtime_format: HH:mm:ss# Pagination## Set per_page to 0 to disable paginationper_page: 5pagination_dir: pageindex_generator:  per_page: 5archive_generator:  per_page: 100  yearly: true    monthly: true tag_generator:  per_page: 100 category_generator:   per_page: 100 # local searchsearch:  path: search.xml  field: post  format: striptags  limit: 10000# Extensions## Plugins: http://hexo.io/plugins/## Themes: http://hexo.io/themes/theme: next# Deployment## Docs: http://hexo.io/docs/deployment.html# github and giteedeploy:- type: git  repo: https://voidking.com/voidking/voidking.github.io.git  branch: master

7. 发布7.1. github1、在github上注册一个帐号。
2、新建一个项目，和帐号名相同。
3、项目启动Pages服务。
7.2. 本地操作1、生成本地代码hexo g
2、本地测试hexo s，然后访问 http://localhost:4000
3、发布到githubhexo d
4、线上测试http://voidking.github.io
7.3. 域名1、申请域名。
2、域名解析添加CNAME记录指向voidking.github.io。
3、voidking.github.io项目中添加CNAME文件，绑定域名。
4、测试访问。
8. 后记以上，hexo安装部署完成。有些步骤写的比较简单，不理解的可以自行谷歌百度。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Travis CI失效</title>
    <url>/dev-hexo-travis-ci-not-work-anymore/</url>
    <content><![CDATA[1. 问题描述2021年8月8日，趁着周末空闲修改了一些文章，然后发了新版。但是，发现网站没有更新。看看邮件，没有收到travis ci的邮件，再仔细想想，貌似好久没有收到travis ci的邮件了。登录Travis CI查看构建记录，好家伙，上次构建还是5月份的！问题大了！
到底是怎么回事？Travis CI有bug不能用了？GitHub封杀了Travis CI？Travis CI规则改了，开源项目也有构建次数限制？…不知道具体什么原因，本文我们就来排查一下。


2. 手动触发1、登录Travis CI
2、点击 voidking/hexo-deploy 项目，点击右上角 More options，点击 Trigger build 进行手动触发构建
3、Branch 保持默认 master ，CUSTOM COMMIT MESSAGE 输入随意内容，CUSTOM CONFIG 不填
4、点击 Trigger custom build，就看到了具体的报错了弹框提示 Oh no! You tried to trigger a build for voidking/hexo-deploy but the request was rejected.Requests Tab页提示 Could not authorize build request for voidking/hexo-deploy.从5月7日至今，一直是这个错误。
3. 猜想和验证3.1. Travis CI授权过期了？根据报错信息，很容易联想到授权过期，因为之前还好好的，突然不能用了。
1、进入GitHub Applications settings页面，配置Travis CI。2、检查了一遍，并没有授权过期一说，没有什么问题。3、保险起见，调整了一下 Repository access ，保存。然而问题并没有解决，说明不是授权过期问题。
3.2. 缺少 .travis.yml ？Travis CI论坛上有人是因为缺少 .travis.yml 文件导致。

In your case, it’s due to a missing .travis.yml.

怀疑自己也是误删了 .travis.yml 文件，但是检查了一遍，并没有误删。
3.3. 需要换免费版？Travis CI论坛上还有人没有找到原因，最终换了Github Actions。

I kept struggling on this, and finally left travis.com for Github Actions :persevere: Now I can build &amp; deploy again 

如果非换不可的话，我打算换成Travis CI免费版，毕竟更加熟悉。但是工作量很大，很多地方都需要重新配置。我不信需要这么麻烦！
3.4. 需要加入组织？stackoverflow上有人说需要为组织选择一个plan，莫非现在必须要加入组织才能使用Travis CI？

It’s also worth mentioning that this happens if you do not have a plan selected for your organization. First, you need to select a plan (Travis does not automatically select the free plan for you). Then, you can trigger a build and see if things work.


If that does not work, you’re likely out of builds and will need to upgrade the account.

于是，创建了一个Organizations：begoodcoder。然而创建组织后并没有解决问题。PS：个人感觉组织和个人账号的区别，主要是组织可以创建团队。
3.5. Plan! Plan! Plan!stackoverflow上有多个优质回答提到了Plan，这个Plan是个啥？

‘Could not authorize build request …’ usually occurs due to

Has not activated either a free plan or a paid plan.
Expiration of the subscription.
Ran out of free trial build credits available for private repositories.Travis CI is always-free only for public repositories. The free plan also provides 10,000 build credits try out building private repositories. Read updated info on Billing FAQ.


1、Travis CI右上角点击账号，选择Settings
2、点击页面上的Plan
3、选择Free plan
4、再次手动触发配送，成功！
以上，Travis CI失效的问题就定位并解决了，需要选择一个Plan！
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>cicd</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
        <tag>travis-ci</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：使用Python操作Excel表格</title>
    <url>/dev-python-excel/</url>
    <content><![CDATA[1. 前言最近疫情又变得严重，多地都推迟了开学，改为上网课。这样一来，老师们就需要每次上完课后统计学生的出勤情况，包括未上课的同学和上课时间不足40分钟的同学。
为了方便我们家谭老师的统计工作，决定趁周末写一个脚本，把人工统计变成自动统计。


2. 思路1、选择excel读写模块，经过简单比较决定选择 xlrd 和 xlwt 。2、已知学生没有重名，因此全体学生姓名作为集合A，上课学生作为集合B，上课满40分钟学生作为集合C。未上课学生为A-B，上课时间不足40分钟的学生为A-C-(A-B)。3、因为两个班级需要分开统计，因此使用学生姓名作为dict的key，班级作为dict的value。
3. 实现1、安装依赖
pip3 install xlrd==1.2.0pip3 install xlwt
之所以指定xlrd的版本，是因为更高的版本不支持读取xlsx格式的表格。
2、代码实现统计缺勤学生
3、代码执行./main.py xxx.xlsx
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python代码风格指南</title>
    <url>/dev-python-style-guide/</url>
    <content><![CDATA[1. 为什么需要代码风格指南？本文中大部分内容来自ChatGPT。
代码风格指南是一种旨在促进代码可读性、可维护性和可重用性的约定。虽然它们并不强制执行特定的编码标准，但它们建议一些最佳实践和常规约定，以帮助开发者编写更具一致性、可读性和可维护性的代码。
以下是一些代码风格指南的重要原因：

可读性：代码风格指南可以帮助开发者编写更加可读的代码。这有助于降低代码维护成本，因为代码更容易理解和修改。

一致性：代码风格指南可以确保代码在整个项目中保持一致的外观和感觉。这有助于提高代码可维护性和可重用性，因为开发者可以更轻松地了解代码如何工作。

错误：代码风格指南可以减少编码错误的可能性。例如，强制实施缩进约定可以防止不正确的缩进导致的语法错误。

沟通：代码风格指南可以提高代码的可交流性，因为开发者可以轻松地了解代码中使用的惯用语和惯例。

最佳实践：代码风格指南可以提供最佳实践和规则，以确保开发人员遵循最佳实践和规则，以提高代码质量和可重用性。


在现代软件开发中，代码风格指南已成为一种非常重要的约定，以帮助团队开发一致、可读性强且易于维护的代码。
本文中，我们来学习一下Google Python代码风格指南。
参考文档：

google/styleguide
Google Python Style Guide
PEP 257 – Docstring Conventions
PEP 3107 – Function Annotations



2. 关键十条
缩进使用四个空格。不要使用制表符，也不要混合使用空格和制表符。
变量和函数名应该是小写字母，用下划线分隔单词。模块名应该是小写字母，用下划线分隔单词。
类名应该是驼峰式命名，即首字母大写，后续单词首字母也大写。
模块级别的常量应该使用全大写字母和下划线分隔单词。
函数和方法参数应该使用self、cls作为实例方法和类方法的第一个参数。
行的长度不应该超过80个字符，注释的行长度不应该超过72个字符。
使用文档字符串（Docstrings）对模块、函数、类和方法进行文档化。
在类定义中使用无参的super()调用父类构造函数。
使用list comprehension来替代for循环进行列表操作。
每个模块应该具有如下的结构：文件注释、模块导入、常量定义、函数定义、类定义等。

除了这十条规则之外，还有很多其他的编码规范需要开发者遵循，以确保代码的质量和可维护性。
3. Docstring代码风格指南中的内容非常多，如果说哪个最重要，我认为是Docstring（文档字符串）。
Docstring指南：

模块级别的文档应该在模块的顶部使用文档字符串进行描述。
函数、类和方法应该在其定义之后的第一行使用文档字符串进行描述。
使用三重引号来创建文档字符串，并确保文档字符串格式正确。
文档字符串的第一行应该是一个简要的摘要，概括函数、类或方法的功能。
在文档字符串的第一行之后，应该使用一个空白行分隔摘要和详细描述。
在文档字符串的详细描述中，可以提供更多有关函数、类或方法的信息，包括参数、返回值、异常、实现细节等。
参数和返回值应该按照参数名称和类型进行描述，并在它们之前添加“Args”和“Returns”等关键词。
异常应该按照异常类型进行描述，并在它们之前添加“Raises”关键词。
在类的文档字符串中，应该描述类的构造函数和所有公共方法。
在函数和方法的文档字符串中，应该描述函数或方法的参数、返回值、异常、实现细节等。

函数和方法的Docstring示例：
def fetch_smalltable_rows(    table_handle: smalltable.Table,    keys: Sequence[bytes | str],    require_all_keys: bool = False,) -&gt; Mapping[bytes, tuple[str, ...]]:    &quot;&quot;&quot;Fetches rows from a Smalltable.    Retrieves rows pertaining to the given keys from the Table instance    represented by table_handle.  String keys will be UTF-8 encoded.    Args:        table_handle: An open smalltable.Table instance.        keys: A sequence of strings representing the key of each table          row to fetch.  String keys will be UTF-8 encoded.        require_all_keys: If True only rows with values set for all keys will be          returned.    Returns:        A dict mapping keys to the corresponding table row data        fetched. Each row is represented as a tuple of strings. For        example:        &#123;b&#x27;Serak&#x27;: (&#x27;Rigel VII&#x27;, &#x27;Preparer&#x27;),         b&#x27;Zim&#x27;: (&#x27;Irk&#x27;, &#x27;Invader&#x27;),         b&#x27;Lrrr&#x27;: (&#x27;Omicron Persei 8&#x27;, &#x27;Emperor&#x27;)&#125;        Returned keys are always bytes.  If a key from the keys argument is        missing from the dictionary, then that row was not found in the        table (and require_all_keys must have been False).    Raises:        IOError: An error occurred accessing the smalltable.    &quot;&quot;&quot;

类的Docstring示例：
class SampleClass:    &quot;&quot;&quot;Summary of class here.    Longer class information...    Longer class information...    Attributes:        likes_spam: A boolean indicating if we like SPAM or not.        eggs: An integer count of the eggs we have laid.    &quot;&quot;&quot;    def __init__(self, likes_spam: bool = False):        &quot;&quot;&quot;Initializes the instance based on spam preference.        Args:          likes_spam: Defines if instance exhibits this preference.        &quot;&quot;&quot;        self.likes_spam = likes_spam        self.eggs = 0    def public_method(self):        &quot;&quot;&quot;Performs operation blah.&quot;&quot;&quot;

块和行的Docstring示例：
# We use a weighted dictionary search to find out where i is in# the array.  We extrapolate position based on the largest num# in the array and the array size and then do binary search to# get the exact number.if i &amp; (i-1) == 0:  # True if i is 0 or a power of 2.

VSCode中的自动生成Docstring的方法，参考文档《VSCode常用快捷键和配置》
4. 后记更多详细内容，请移步阅读Google Python Style Guide
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>chatgpt</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>修改 MP3 的 ID3Tags</title>
    <url>/dev-modify-mp3-id3tags/</url>
    <content><![CDATA[1. ID3Tags是啥？ID3Tags，简称 ID3 ，表示MP3文件曲目标签。ID3位于一个mp3文件的开头或末尾的若干字节内，附加了关于该mp3的歌手，标题，专辑名称，年代，风格等信息，该信息就被称为ID3信息，ID3信息分为两个版本，v1和v2版。其中v1版的ID3在mp3文件的末尾128字节，以TAG三个字符开头，后面跟上歌曲信息。v2版一般位于mp3的开头，可以存储歌词，该专辑的图片等大容量的信息。
ID3全称是啥？没有找到，懂得的小伙伴感谢留言告知。


2. 思路使用python第三方库，查看mp3当前id3tags，修改id3tags并保存。找到了两个不错的python第三方库：

eyeD3 document
Mutagen document

这里选择eyeD3，因为使用更简单。使用到的参数和方法，参考eyed3.id3。
3. 安装使用eyeD33.1. 安装eyeD3python3 -m pip install eyed3 -i https://pypi.tuna.tsinghua.edu.cn/simple/

3.2. 查看mp3的id3eyeD3 -heyeD3 song.mp3

可能输出：No audio files found.这是因为，有些音频文件虽然以.mp3结尾，也可以使用音频播放器播放，但是不一定是mp3文件。查看真实文件类型的方法：
file song.mp3file *

如果发现了非mp3文件，请先转成mp3文件，再进行id3修改，因为只有mp3文件才具有mp3文件曲目标签。
4. 编码实现修改ID3Tags4.1. 查看id3查看所有mp3的id3信息
#!/usr/bin/env python3# coding: utf-8import osimport eyed3from pydub.utils import mediainfofrom pydub import AudioSegmentdef get_audio_paths(dir_path):    audio_paths = []    for dirs, dirnames, files in os.walk(dir_path):        for file in files:            if file.endswith(&#x27;.mp3&#x27;):                audio_paths.append(dirs+&#x27;/&#x27;+file)    return audio_pathsdef show_id3(audio_path):    # load 和 save 时会打印出 Invalid date: ???????:36903863 ，可忽略    audio = eyed3.load(audio_path)    audio_name = audio_path.split(&#x27;/&#x27;)[-1]    if audio is None:        info = mediainfo(audio_path)        print(&#x27;------ &#x27; + audio_name + &#x27; ------&#x27;)        print(audio_name + &#x27;不是mp3格式&#x27;)        print(&#x27;音频编码: &#x27; + info[&#x27;codec_name&#x27;])        print(&#x27;文件类型: &#x27; + info[&#x27;codec_tag_string&#x27;])        print(&#x27;------ &#x27; + &#x27;------&#x27; + &#x27; ------&#x27;)    elif audio.tag is None:        print(&#x27;------ &#x27; + audio_name + &#x27; ------&#x27;)        print(&#x27;不存在id3&#x27;)        print(&#x27;------ &#x27; + &#x27;------&#x27; + &#x27; ------&#x27;)    else:        print(&#x27;------ &#x27; + audio_name + &#x27; ------&#x27;)        if audio.tag.title:            print(&#x27;title: &#x27; + audio.tag.title)        if audio.tag.artist:            print(&#x27;artist: &#x27; + audio.tag.artist)        print(&#x27;------ &#x27; + &#x27;------&#x27; + &#x27; ------&#x27;)if __name__ == &#x27;__main__&#x27;:    audio_paths = get_audio_paths(&#x27;./mp3&#x27;)    # 显示id3信息    for audio_path in audio_paths:        show_id3(audio_path)

4.2. m4a转换为mp3m4a（mp4a）格式的文件，转换为mp3格式。
def m4a_to_mp3(audio_path):    audio = eyed3.load(audio_path)    audio_name = audio_path.split(&#x27;/&#x27;)[-1]    if audio is None:        info = mediainfo(audio_path)        print(&#x27;------ &#x27; + audio_name + &#x27; ------&#x27;)        print(&#x27;codec_name: &#x27; + info[&#x27;codec_name&#x27;])        print(&#x27;codec_tag_string: &#x27; + info[&#x27;codec_tag_string&#x27;])        print(&#x27;------ &#x27; + &#x27;------&#x27; + &#x27; ------&#x27;)        if info[&#x27;codec_tag_string&#x27;] == &#x27;mp4a&#x27;:            # 加载m4a文件            audio = AudioSegment.from_file(audio_path, format=&quot;m4a&quot;)            # 导出为mp3文件            audio.export(audio_path + &#x27;.mp3&#x27;, format=&quot;mp3&quot;)            print(audio_name + &#x27; converted to &#x27; + audio_path + &#x27;.mp3&#x27;)

4.3. 脚本修改id3需求：已有200个mp3文件，id3信息都是广告。现在想要批量清除mp3文件现有的id3，然后根据文件名修改title，根据真实作者修改artist，根据真实封面修改封面。
def modify_id3(audio_path, title, artist ,img_path):    audio = eyed3.load(audio_path)    if not audio.tag:        audio.initTag()    else:        audio.tag.clear()    audio.tag.title = title    audio.tag.artist = artist    audio.tag.images.set(3, open(img_path,&#x27;rb&#x27;).read(), &#x27;image/jpeg&#x27;)    audio.tag.save()    if __name__ == &#x27;__main__&#x27;:    audio_paths = get_audio_paths(&#x27;./mp3&#x27;)    # 修改id3信息    for audio_path in audio_paths:        title = audio_path.split(&#x27;/&#x27;)[-1]        artist = &#x27;钱文忠&#x27;        img_path = &#x27;./img.png&#x27; # 200x200        modify_id3(audio_path, title, artist ,img_path)


]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo Next主题显示文章更新时间</title>
    <url>/dev-hexo-next-update-time/</url>
    <content><![CDATA[1. 问题《Hexo更换主题为Next》一文中，关闭了文章的更新时间。因为next主题默认是使用markdown文件的修改时间作为更新时间，这个时间是不符合预期的。比如我换了电脑，clone下来markdown文件，写完后发布，那么所有文章的更新时间都会变成当前时间，这个就不符合预期，就不对。
但是，文章更新时间确实是有意义的，读者能够通过更新时间推断出一篇文章的有效性。比如很多工具和技术昨天可以用，文章写的没毛病，但是也许今天就不能用了。
本文就来研究一下，怎样让next主题显示符合预期的、准确的更新时间。


2. 思路首先，我们知道next主题是支持显示更新时间的，只不过显示的更新时间不对。那么，能不能显示对的更新时间呢？看代码。
1、_config.yml，其中关于更新时间的部分：
# Post meta display settingspost_meta:  item_text: true  created_at: true  updated_at:    enable: true    another_day: true  categories: true

2、layout/_macro/post.swig，其中关于更新时间的部分：
&#123;%- if theme.post_meta.updated_at.enable and datetime_diff %&#125;  &#123;%- set display_updated = not theme.post_meta.updated_at.another_day or theme.post_meta.updated_at.another_day and date_diff %&#125;  &#123;%- if display_updated or not theme.post_meta.created_at %&#125;    &lt;span class=&quot;post-meta-item&quot;&gt;      &lt;span class=&quot;post-meta-item-icon&quot;&gt;        &lt;i class=&quot;fa fa-calendar-check-o&quot;&gt;&lt;/i&gt;      &lt;/span&gt;      &lt;span class=&quot;post-meta-item-text&quot;&gt;&#123;&#123; __(&#x27;post.edited&#x27;) &#125;&#125;&lt;/span&gt;      &lt;time title=&quot;&#123;&#123; __(&#x27;post.modified&#x27;) + __(&#x27;symbol.colon&#x27;) + full_date(post.updated) &#125;&#125;&quot; itemprop=&quot;dateModified&quot; datetime=&quot;&#123;&#123; moment(post.updated).format() &#125;&#125;&quot;&gt;&#123;&#123; date(post.updated) &#125;&#125;&lt;/time&gt;    &lt;/span&gt;  &#123;%- endif %&#125;&#123;%- endif %&#125;

由上面的代码我们可以得知：

只要在文章头部yaml定义中添加 updated 字段，就可以显示我们自己定义的更新时间，而不是文件的修改时间。
如果开启了 post_meta.updated_at.another_day ，当 date 和 updated 日期相同时，只会显示发布时间。

因此，我们现在有两个思路来实现next主题显示符合预期的更新时间。思路一：开启 post_meta.updated_at.enable，然后给所有的md文件添加 updated 字段。思路二：开启 post_meta.updated_at.enable，修改 post.swig ，令没有 updated 字段的md文件只显示发布时间，有 updated 字段的md文件显示发布时间和更新时间。
这里选择思路一，因为实现的逻辑最简单，而且郝同学的shell脚本用的还不错。
3. 实现3.1. 开启更新时间显示_config.yml，开启更新时间显示：
# Post meta display settingspost_meta:  item_text: true  created_at: true  updated_at:    enable: true    another_day: true  categories: true

3.2. 添加updated字段1、准备脚本
#!/bin/bashdir=&quot;_posts&quot;for file in `ls $&#123;dir&#125; | grep &#x27;.md&#x27;`;do    content=$(cat $&#123;dir&#125;/$&#123;file&#125;| head -n 10 | grep &#x27;date: &#x27;)    datestr=$(echo &quot;$content&quot; | awk &#x27;&#123;print $2&quot; &quot;$3&#125;&#x27;)    newcontent=&quot;updated: &quot;$datestr    sed -i &quot;/$content/a\\$newcontent&quot; $&#123;dir&#125;/$&#123;file&#125;done

2、执行脚本把脚本放到 source 目录下，然后执行脚本 bash modify.sh备注：需要linux环境，mac环境的sed命令和linux环境的sed命令有差异。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>shell</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>使用Travis CI部署项目到服务器</title>
    <url>/dev-travis-ci-deploy-to-server/</url>
    <content><![CDATA[1. 前言《Hexo配置Travis CI自动发布》一文中，我们学习了使用travis-ci构建发布hexo项目的方法。
因为项目的特殊性，所以发布时只是把代码push到了git仓库，并没有把代码发布到我们自己的服务。本文，我们就来学习一下怎样通过travis-ci，把项目发布到我们自己的服务器。
需求：已知hexo项目部署在github，百度抓取不到github的内容，因此我们想要在自己的服务器也部署一份代码，国内的流量打到自己的服务器。实现国内流量 -&gt; 阿里云服务器，国外流量 -&gt; github pages。配置 hexo-deploy ，在发布时会同时发布静态页面到 voidking.github.io 和 voidking。阿里云服务器上有项目 /opt/nginx/work/voidking ，项目仓库为 voidking。现在想要实现的是，当使用 hexo-deploy 之后，阿里云服务器上的代码能够自动更新。


2. 思路给 voidking.github.io 项目配置travis-ci，当静态页面更新完成后，在阿里云服务器上执行 git pull。中间需要解决的主要问题是： travis-ci 访问阿里云服务器的权限问题。
另外，因为 voidking.github.io 项目是线上项目，所以我们创建一个在github上和travis-ci上分别创建一个 travis-test 项目，用来测试travis-ci的配置。
3. 配置travis-ci3.1. 服务器密钥配置1、创建一个用户专门用来更新代码
useradd -m voidking -s /bin/bashpasswd voidkingchown voidking -R /opt/nginx/work/

2、生成密钥
su voidkingcd ~ssh-keygen
执行完之后，.ssh目录下生成了 id_rsa 和 id_rsa.pub。
3、使密钥生效
ALI_IP=&quot;8.136.13.58&quot;ssh-copy-id -i .ssh/id_rsa.pub voidking@$&#123;ALI_IP&#125;ssh voidking@$&#123;ALI_IP&#125;

3.2. 安装travis client直接安装travis破坏服务器环境，因此最好使用Docker安装travis。1、运行travis容器
docker pull ruby:slimdocker run --name travis -it -d ruby:slim /bin/bashdocker exec -it travis /bin/bash

2、容器内安装travis client
# gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/gem install travistravis -h

3.3. travis-ci密钥配置1、拷贝密钥到容器
docker cp /home/voidking/.ssh/id_rsa travis:/tmpdocker exec -it travis /bin/bash

2、登录travis-ci
cd /tmptravis login --com --github-token xxxxxx

3、对id_rsa加密，生成id_rsa.enc；同时生成服务端解密命令
touch .travis.ymltravis encrypt-file id_rsa --add --com --repo=voidking/travis-test
详情参考Encrypting Files。
执行完命令，会生成加密文件 id_rsa.enc，.travis.yml 中会被写入解密命令：
before_install:- openssl aes-256-cbc -K $encrypted_f217180e22ee_key -iv $encrypted_f217180e22ee_iv -in id_rsa.enc -out id_rsa -d
并且，变量 encrypted_f217180e22ee_key 和 encrypted_f217180e22ee_iv 会填入travis-ci上travis-test项目中的环境变量配置中。
3.4. travis.yml配置1、访问Travis CI Pro，找到 travis-test 项目，Settings，在Environment Variables一栏填入Name为GITEE_TOKEN，VALUE为gitee的access token。
2、在github上 travis-test 项目中放入 id_rsa.enc
3、在github上 travis-test 项目中创建 .travis.yml，内容为
sudo: falselanguage: pythonpython:- 3.6branches:  only:  - main # build main branch onlyenv:  global:  - ALI_IP: 8.136.13.58  - GIT_USER: voidking  - GITEE_PAGES_REPO: gitee.com/voidking/voidking.gitaddons:  ssh_known_hosts:   - 8.136.13.58before_install:- openssl aes-256-cbc -K $encrypted_f217180e22ee_key -iv $encrypted_f217180e22ee_iv -in id_rsa.enc -out ~/.ssh/id_rsa -d- chmod 600 ~/.ssh/id_rsascript:- ssh voidking@$&#123;ALI_IP&#125; &quot;cd /opt/nginx/work/voidking/ &amp;&amp; git pull --force --quiet \&quot;https://$&#123;GIT_USER&#125;:$&#123;GITEE_TOKEN&#125;@$&#123;GITEE_PAGES_REPO&#125;\&quot; master:master&quot;after_success:- echo &quot;deploy success&quot;

4、提交代码
git add .git commmit -m &quot;添加travis.yml&quot;git push

5、验证打开travis voidking/travis-test ，可以看到脚本已经成功执行。
登录阿里云服务器，git log查看代码版本，确实也已经更新到了最新版。nice！
最后，同样的步骤配置到 voidking.github.io 项目上即可。
4. 配置travis-ci简化版上面的流程，安全性较高，但是整个流程很麻烦，有没有更简单的办法？必须是有的。方法一：安装sshpass，直接使用密码登录服务器执行命令。方法二：把私钥作为参数配置到travis-ci，使用时写入到文件中。
本节中我们研究一下方法二的配置方法。
4.1. 服务器密钥配置1、创建一个用户专门用来更新代码
useradd -m voidking -s /bin/bashpasswd voidkingchown voidking -R /opt/nginx/work/

2、生成密钥
su voidkingcd ~ssh-keygen
执行完之后，.ssh目录下生成了 id_rsa 和 id_rsa.pub。
3、使密钥生效
ALI_IP=&quot;8.136.13.58&quot;ssh-copy-id -i .ssh/id_rsa.pub voidking@$&#123;ALI_IP&#125;ssh voidking@$&#123;ALI_IP&#125;

4.2. travis-ci密钥配置1、加密私钥cat .ssh/id_rsa | base64 | tr -d &#39;\n&#39;
2、访问Travis CI Pro，找到 travis-test 项目，Settings，Environment Variables。创建两组环境变量：

Name为GITEE_TOKEN，VALUE为gitee的access token。
Name为ID_RSA，VALUE为加密后的私钥。

4.3. travis.yml配置1、在github上 travis-test 项目中创建 .travis.yml，内容为
sudo: falselanguage: pythonpython:- 3.6branches:  only:  - main # build main branch onlyenv:  global:  - ALI_IP: 8.136.13.58  - GIT_USER: voidking  - GITEE_PAGES_REPO: gitee.com/voidking/voidking.gitaddons:  ssh_known_hosts:   - 8.136.13.58before_install:- echo $&#123;ID_RSA&#125; | base64 -d &gt; ~/.ssh/id_rsa- chmod 600 ~/.ssh/id_rsascript:- ssh voidking@$&#123;ALI_IP&#125; &quot;cd /opt/nginx/work/voidking/ &amp;&amp; git pull --force --quiet \&quot;https://$&#123;GIT_USER&#125;:$&#123;GITEE_TOKEN&#125;@$&#123;GITEE_PAGES_REPO&#125;\&quot; master:master&quot;after_success:- echo &quot;deploy success&quot;

2、提交代码
git add .git commmit -m &quot;简化travis.yml&quot;git push

3、验证打开travis voidking/travis-test ，查看脚本执行日志。登录阿里云服务器，git log查看代码版本。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>cicd</tag>
        <tag>hexo</tag>
        <tag>travis-ci</tag>
      </tags>
  </entry>
  <entry>
    <title>怎样给文件、变量和函数命名？</title>
    <url>/dev-how-to-name-better/</url>
    <content><![CDATA[1. 前言使用user_tool.py还是user_utils.py？使用name还是username？使用user_add还是add_user？使用get_user_by_name还是get_users_by_name？等等等等，在编程活动中，我们经常会产生各种关于命名的纠结。给文件、函数、变量命名是一件很难的事，但是也是有方法的。本文中，我们就来学习一下文件、变量和函数命名的方法。
参考文档：

工程实践：给函数取一个”好”的名字
Google 开源项目风格指南 (中文版)
命名规范

2. 命名方法要领：一看就懂，保持一致。
2.1. 文件由于Windows, OSX下文件名不区分大小写(linux是区分的)，所以命名建议还是以全部小写为主。连字符可以使用中划线、下划线或者省略，关键是要统一。
目录建议连字符使用中划线，比如: my-project-name。有复数的情况使用复数命名法，比如: scripts, styles, images和data-modules。文件建议连字符使用下划线，比如：user_test.py。
2.2. 变量变量命名常用的有两种方式:下划线命名法，比如: my_variable驼峰式命名法，比如: myVariale
python语言建议使用下划线命名法。
2.3. 函数函数命名常用的有两种方式:下划线命名法，比如: get_user_by_name驼峰式命名法，比如: getUserByName
python语言建议使用下划线命名法。不同于变量命名的是，函数名称要使用动词开头，并且尽可能准确。
3. 常用动词表动词选取要精准。通常来说，动词决定了一个函数要采取什么”动作”。动词取的好，一个函数名字已经成功了80%。
常用动词表：



类别
单词



添加/插入/创建/初始化/加载
add、append、insert、create、initialize、load


删除/销毁
delete、remove、destroy、drop


打开/开始/启动
open、start


关闭/停止
close、stop


获取/读取/查找/查询
get、fetch、acquire、read、search、find、query


设置/重置/放入/写入/释放/刷新
set、reset、put、write、release、refresh


发送/推送
send、push


接收/拉取
receive、pull


提交/撤销/取消
submit、cancel


收集/采集/选取/选择
collect、pick、select


提取/解析
sub、extract、parse


编码/解码
encode、decode


填充/打包/压缩
fill、pack、compress


清空/拆包/解压
flush、clear、unpack、decompress


增加/减少
increase、decrease、reduce


分隔/拼接
split、join、concat


过滤/校验/检测
filter、valid、check


4. 常用领域词名词使用领域词汇。举个例子：集合的容量通常用capacity、集合实际元素个数用size、字符串长度用length，这种就遵循大家的使用习惯，不要用size去形如字符串的长度。
再比如，假如使用到建造者模式，那么通常会用build作为函数名字，这个时候就不要另辟蹊径，用create来作为函数名字，使用大家约定俗成的命名习惯更容易让你的代码被别人读懂。
常用名词表：



类别
单词



容量/大小/长度
capacity、size、length


实例/上下文
instance、context


配置
config、settings


头部/前面/前一个/第一个
header、front、previous、first


尾部/后面/后一个/最后一个
tail、back、next、last


区间/区域/某一部分/范围/规模
range、interval、region、area、section、scope、scale


缓存/缓冲/会话
cache、buffer、session


本地/局部/全局
local、global


成员/元素
member、element


菜单/列表
menu、list


源/目标
source、destination、target


5. 常用缩写表1、本缩写表是《编码命名规范》的附录。
2、本缩写表中列出的都是通用性缩写，不提供标准缩写，如：Win9x、COM 等。
3、使用本缩写表里的缩写时，请对其进行必要的注释说明。
4、除少数情况以外，大部分缩写与大小写无关。





缩写
全称



addr
Address


adm
Administrator


app
Application


arg
Argument


asm
assemble


asyn
asynchronization


avg
average


DB
Database


bk
back


bmp
Bitmap


btn
Button


buf
Buffer


calc
Calculate


char
Character


chg
Change


clk
Click


clr
color


cmd
Command


cmp
Compare


col
Column


coord
coordinates


cpy
copy


ctl/ctrl
Control


cur
Current


cyl
Cylinder


dbg
Debug


dbl
Double


dec
Decrease


def
default


del
Delete


dest/dst
Destination


dev
Device


dict
dictionary


diff
different


dir
directory


disp
Display


div
Divide


dlg
Dialog


doc
Document


drv
Driver


dyna
Dynamic


env
Environment


err
error


ex/ext
Extend


exec
execute


flg
flag


frm
Frame


func/fn
Function


grp
group


horz
Horizontal


idx/ndx
Index


img
Image


impl
Implement


inc
Increase


info
Information


init
Initial/Initialize/Initialization


ins
Insert


inst
Instance


INT/intr
Interrupt


len
Length


lib
Library


lnk
Link


log
logical


lst
List


max
maximum


mem
Memory


mgr/man
Manage/Manager


mid
middle


min
minimum


msg
Message


mul
Multiply


num
Number


obj
Object


ofs
Offset


org
Origin


param
Parameter


pic
picture


pkg
package


pnt/pt
Point


pos
Position


pre/prev
previous


prg
program


prn
Print


proc
Process


prop
Properties


psw
Password


ptr
Pointer


pub
Public


rc
rect


ref
Reference


reg
Register


req
request


res
Resource


ret
return


rgn
region


scr
screen


sec
Second


seg
Segment


sel
Select


src
Source


std
Standard


stg
Storage


stm
Stream


str
String


sub
Subtract


sum
summation


svr
Server


sync
Synchronization


sys
System


tbl
Table


temp/tmp
Temporary


tran/trans
translate/transation/transparent


tst
Test


txt
text


unk
Unknown


upd
Update


upg
Upgrade


util
Utility


var
Variable


ver
Version


vert
Vertical


vir
Virus


wnd
Window


]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>转载</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo本地搜索加速</title>
    <url>/dev-hexo-local-search-accelerate/</url>
    <content><![CDATA[1. 前言随着文章数量的增长，search.xml文件越来越大。因此在加载这个文件时，需要的时间很长，而且偶尔会出现加载不出来的情况。
有没有什么办法能够加快加载速度呢？必须是有的，本文就来研究一下。
《Hexo使用Gulp压缩静态资源》一文中，提到search.xml大小为3.5M。今天又看了一下，文件大小变成了4.3M（加载时会压缩传输，大小约为1.3M）。从文中的实验可以得出结论，在文件大小方面，没有什么优化的空间了。
再想优化，就得从网络方面入手：CDN加速。主要参考jsDelivr+Github 使用方法。
更多Hexo相关内容，参考Hexo系列文章。


2. 思路修改 search.xml 的文件链接，改成cdn的地址。
3. 实现1、测试jsdelivr链接，search.xml的cdn地址为https://cdn.jsdelivr.net/gh/voidking/voidking.github.io/search.xml
2、编辑 next/source/js/local-search.js ，如下修改：
// const path = CONFIG.root + searchPath;const jsdelivr = &#x27;https://cdn.jsdelivr.net/gh/&#x27;const userRepo = &#x27;voidking/voidking.github.io/&#x27;const path = jsdelivr + userRepo + searchPath;

3、本地测试，提交代码，使用修改后的主题进行部署
实测加载速度飞快，4.3M的search.xml能够稳定在2秒内加载完毕，完美。
4. 提高通用性1、cdn的配置放在 _config.yml 文件中
# Local Search# Dependencies: https://github.com/theme-next/hexo-generator-searchdblocal_search:  enable: true  # If auto, trigger search by changing input.  # If manual, trigger search by pressing enter key or search button.  trigger: auto  # Show top n results per article, show all results by setting to -1  top_n_per_article: 1  # Unescape html strings to the readable one.  unescape: false  # Preload the search data when the page loads.  preload: false  # Use CDN to accelerate the speed of loading search.xml  cdn:    enable: false    # url: //cdn.jsdelivr.net/gh/&lt;username&gt;/&lt;username&gt;.github.io/search.xml    url: 

2、编辑 next/source/js/local-search.js ，如下修改：
let path = CONFIG.root + searchPath;// Use CDN to accelerate the speed of loading search.xmlif (&#x27;cdn&#x27; in CONFIG.localsearch &amp;&amp; CONFIG.localsearch.cdn.enable === true &amp;&amp; CONFIG.localsearch.cdn.url !== null)&#123;  path = CONFIG.localsearch.cdn.url;&#125;


]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>wiki使用技巧</title>
    <url>/dev-wiki-skill/</url>
    <content><![CDATA[1. 前言wiki是在公司里最常用的工具之一，本文总结记录一些wiki使用技巧，希望对大家也有一些帮助。


2. 标题自动编号2.1. 背景给文档标题手动添加编号很麻烦，而且容易写错。如果插入或删除章节，后面的编号都要重新修改一遍，无法接受。
2.2. 目的实现wiki标题自动编号。
2.3. 原理通过wiki的markdown插件，可以插入一些自定义的css/js，来自定义页面内容。参考文档：MarkDown标题自动添加编号。
2.4. 方法1、插入，其他宏，markdown。
2、markdown中填入如下css代码
&lt;style type=&quot;text/css&quot;&gt;    #main-content &#123; counter-reset: h1counter; &#125;    #main-content h1 &#123; counter-reset: h2counter; &#125;    #main-content h2 &#123; counter-reset: h3counter; &#125;    #main-content h3 &#123; counter-reset: h4counter; &#125;    #main-content h4 &#123; counter-reset: h5counter; &#125;    #main-content h5 &#123; counter-reset: h6counter; &#125;    #main-content h6 &#123; &#125;    #main-content h1:before &#123;      counter-increment: h1counter;      content: counter(h1counter) &quot;.\0000a0\0000a0&quot;;    &#125;    #main-content h2:before &#123;      counter-increment: h2counter;      content: counter(h1counter) &quot;.&quot;                counter(h2counter) &quot;.\0000a0\0000a0&quot;;    &#125;    #main-content h3:before &#123;      counter-increment: h3counter;      content: counter(h1counter) &quot;.&quot;                counter(h2counter) &quot;.&quot;                counter(h3counter) &quot;.\0000a0\0000a0&quot;;    &#125;    #main-content h4:before &#123;      counter-increment: h4counter;      content: counter(h1counter) &quot;.&quot;                counter(h2counter) &quot;.&quot;                counter(h3counter) &quot;.&quot;                counter(h4counter) &quot;.\0000a0\0000a0&quot;;    &#125;    #main-content h5:before &#123;      counter-increment: h5counter;      content: counter(h1counter) &quot;.&quot;                counter(h2counter) &quot;.&quot;                counter(h3counter) &quot;.&quot;                counter(h4counter) &quot;.&quot;                counter(h5counter) &quot;.\0000a0\0000a0&quot;;    &#125;    #main-content h6:before &#123;      counter-increment: h6counter;      content: counter(h1counter) &quot;.&quot;                counter(h2counter) &quot;.&quot;                counter(h3counter) &quot;.&quot;                counter(h4counter) &quot;.&quot;                counter(h5counter) &quot;.&quot;                counter(h6counter) &quot;.\0000a0\0000a0&quot;;    &#125;&lt;/style&gt;

保存后，即可看到标题编号后的效果。
3、目录在插入目录宏时，通过css加的编号效果在目录中不会显示，勾选 “显示节数” 即可自动编号。
3. 浮动目录3.1. 背景wiki 的标题无浮动功能，对于大文档阅读体验不佳。
3.2. 目的通过自定义css/js，达到浮动目录的效果，提高长文档的阅读体验。
3.3. 原理通过wiki的markdown插件，可以插入一些自定义的css/js，来自定义页面内容。  
3.4. 方法1、页面布局，修改为两列一列放内容，另外一列放目录。
2、插入目录宏，CSS类名填入 floating_catalog
3、插入，其他宏，markdown，填入如下内容：
&lt;style type=&quot;text/css&quot;&gt;.floating_catalog &#123;  position: fixed;  background-color: #FFF;  background: #FFF;  margin-top: -80px;  font-size: 12px;  z-index:100;&#125;.current_catalog &#123;  font-size: 14px;  font-weight: bolder;  background-color: yellow;&#125;&lt;/style&gt;&lt;script type=&quot;text/javascript&quot;&gt;function updateCatalog()&#123;    var catalog = $(&#x27;.floating_catalog a&#x27;);    var isFirst = true;    for (var i = catalog.length - 1; i &gt;= 0; --i) &#123;        var header = $(decodeURIComponent(catalog[i].hash).replace(/([ ;?%&amp;,.+*~\&#x27;:&quot;!^$[\]()=&gt;|\/@])/g,&#x27;\\$1&#x27;));        if (isFirst &amp;&amp; (i == 0 || (!(header === null) &amp;&amp; header[0].getBoundingClientRect().top &lt; 20))) &#123;            catalog[i].parentElement.classList.add(&#x27;current_catalog&#x27;);            isFirst = false;        &#125;        else &#123;            catalog[i].parentElement.classList.remove(&#x27;current_catalog&#x27;);        &#125;    &#125;&#125;$(document).ready(updateCatalog);$(document).scroll(updateCatalog);&lt;/script&gt;

保存后，即可看到浮动标题的效果。
4. 引用页面4.1. 背景工作内容通常会放在公共wiki，个人工作内容无法很好汇总。wiki的收藏功能不能分层级，不是很好用。
4.2. 目的创建引用页面，即：创建一个页面，本身没有内容，但是会将其他页面内容引用过来，进行展示。可以方便个人信息汇总，创建多层级页面收藏等。
4.3. 原理通过wiki的markdown插件，可以插入一些自定义的css/js，来自定义页面内容。  
4.4. 方法1、新建wiki，标题随意
2、插入，其他宏，markdown，填入如下内容：
&lt;script type=&quot;text/javascript&quot;&gt;function setupPage(sourcePage) &#123;    $(&#x27;.page-metadata-modification-info&#x27;).append(&#x27; | 当前页面是其他页面的引用，无法导出pdf/work，如需导出请点击&lt;a class=&quot;like-button&quot; href=&quot;&#x27; + sourcePage + &#x27;&quot;&gt;【阅读原文】&lt;/a&gt;&#x27;);    $(&#x27;#main-content&#x27;).append($(&#x27;&lt;iframe id=&quot;frame_load_other_page&quot; style=&quot;display:none&quot; width=&quot;100%&quot; height=&quot;100%&quot; src=&quot;&#x27; + sourcePage + &#x27;&quot;&gt;&lt;/iframe&gt;&#x27;));    var frame_load_other_page = $(&#x27;#frame_load_other_page&#x27;);    frame_load_other_page.on(&quot;load&quot;, function() &#123;        setTimeout(function()&#123;            main_content = $(&#x27;#main-content&#x27;, frame_load_other_page.contents()).html();            $(&#x27;#main-content&#x27;).append(main_content);            frame_load_other_page.remove();            // 修复折叠页面无法访问的问题            $(&#x27;div[id^=expander-control-]&#x27;).click(function(event)&#123;                let item = $(&quot;#&quot; + event.currentTarget.id.replace(&#x27;control&#x27;, &#x27;content&#x27;));                item.css(&#x27;display&#x27;, item.css(&#x27;display&#x27;) == &#x27;none&#x27; ? &#x27;inline&#x27; : &#x27;none&#x27;);            &#125;);            // 隐藏绘图部分的按钮（加载不出来，影响美观，所以就暂时显示了）            $(&#x27;.gliffy-chrome-container&#x27;).css(&quot;display&quot;, &quot;none&quot;);        &#125;, 100);    &#125;);&#125;$(document).ready(function()&#123;    setupPage(&quot;http://wiki.baidu.com/pages/viewpage.action?pageId=&#123;PAGE_ID&#125;&quot;);&#125;);&lt;/script&gt;

其中，{PAGE_ID} 需要替换成引用的wiki页面的url。
保存后，即可看到引用效果。
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>wiki</tag>
      </tags>
  </entry>
  <entry>
    <title>Markdown编辑器</title>
    <url>/dev-markdown-editor/</url>
    <content><![CDATA[1. 前言假设现在要写一篇markdown格式文章，使用什么编辑器比较好？答：sublime或者typora。
假设现在要把一篇markdown格式文章导出成pdf文档，使用什么编辑器比较好？答：typora。
假设现在要把一篇markdown格式文章发布成微信公众号文章，使用什么编辑器比较好？答：wechat-format。
本文主要介绍三款 markdown 编辑器，分别是 sublime、typora和wechat-format。此外，还会介绍几个在线markdown编辑器，测试和预览都很方便。


2. sublime.md 结尾的文件，在使用 sublime 进行编辑时会自动语法高亮。之所以推荐sublime，是因为这个编辑器是个万能的编辑器，而且非常轻量。使用了六年了，依然情有独钟。
3. typora3.1. 简介typora是近两年崛起的markdown编辑器神器。传统的markdown编辑器，左边是编辑器，右边是预览。typora另辟蹊径，没有左右分开，而是把编辑和预览结合在一起，随时切换（command+/），更可以在预览时进行编辑。
3.2. 自定义主题typora提供各种主题，更是支持用户自定义主题，简单好用。下面我们按照自己的需求来定制主题，熟悉一下自定义主题的流程。
已知需求为：

github主题自己很喜欢，但是标题字号太大，想要改小一点。
标题在显示的时候自动编号。

1、Typora，偏好设置，外观，打开主题文件夹
2、备份github.css为github.css.bak
3、编辑github.css，修改字号
h1 &#123;    font-size: 1.5em;    line-height: 1.2;    border-bottom: 1px solid #eee;&#125;h2 &#123;    font-size: 1.25em;    line-height: 1.225;    border-bottom: 1px solid #eee;&#125;h3 &#123;    font-size: 1.15em;    line-height: 1.43;&#125;h4 &#123;    font-size: 1.05em;&#125;h5 &#123;    font-size: 1em;&#125;h6 &#123;   font-size: 1em;    color: #777;&#125;

4、编辑github.css，给标题前添加自动编号
/* 标题自动编号 */body &#123; counter-reset: h1counter; &#125;h1 &#123; counter-reset: h2counter; &#125;h2 &#123; counter-reset: h3counter; &#125;h3 &#123; counter-reset: h4counter; &#125;h4 &#123; counter-reset: h5counter; &#125;h5 &#123; counter-reset: h6counter; &#125;h6 &#123; &#125;h1:before &#123;  counter-increment: h1counter;  content: counter(h1counter) &quot;.\0000a0\0000a0&quot;;&#125;h2:before &#123;  counter-increment: h2counter;  content: counter(h1counter) &quot;.&quot;            counter(h2counter) &quot;.\0000a0\0000a0&quot;;&#125;h3:before &#123;  counter-increment: h3counter;  content: counter(h1counter) &quot;.&quot;            counter(h2counter) &quot;.&quot;            counter(h3counter) &quot;.\0000a0\0000a0&quot;;&#125;h4:before &#123;  counter-increment: h4counter;  content: counter(h1counter) &quot;.&quot;            counter(h2counter) &quot;.&quot;            counter(h3counter) &quot;.&quot;            counter(h4counter) &quot;.\0000a0\0000a0&quot;;&#125;h5:before &#123;  counter-increment: h5counter;  content: counter(h1counter) &quot;.&quot;            counter(h2counter) &quot;.&quot;            counter(h3counter) &quot;.&quot;            counter(h4counter) &quot;.&quot;            counter(h5counter) &quot;.\0000a0\0000a0&quot;;&#125;h6:before &#123;  counter-increment: h6counter;  content: counter(h1counter) &quot;.&quot;            counter(h2counter) &quot;.&quot;            counter(h3counter) &quot;.&quot;            counter(h4counter) &quot;.&quot;            counter(h5counter) &quot;.&quot;            counter(h6counter) &quot;.\0000a0\0000a0&quot;;&#125;

5、重启typora可见标题字号变小了，同时添加了编号。但是，如果直接复制粘贴内容到微信公众号，这些标题编号不会被复制。而且，换行效果也没有了。因此，从typora直接复制粘贴到微信公众号，不是一个好的方案。
3.3. 导出pdf文件，导出，PDF。如上，typora导出的pdf文档，简直完美。
4. wechat-format4.1. 简介wechat-format最大的优点是：适应微信公众号。比如，微信不支持外链，wechat-format生成的预览会包含 reference。但是，标题居中效果不是我想要的，标题也没有自动编号，所以也需要自定义一下。
4.2. 自定义主题下载wechat-format源码，主要修改：

src/assets/scripts/themes/default.js
src/assets/scripts/renderers/wx-renderer.js
src/assets/css/app.css

修改后的代码参考 wechat-format，最终效果如下
4.3. 失败的尝试但是，修改完成之后，复制粘贴到微信公众号，这些标题编号依然不会被复制。那么，怎样才能复制到这些编号？答：使这些编号变成元素，而不是样式。所以，需要使用js给页面添加编号元素。代码如下：
// https://www.cnblogs.com/qiudeqing/p/3229583.htmlfunction number()&#123;    var headings;    if (document.querySelectorAll)        headings = document.querySelectorAll(&quot;h1,h2,h3,h4,h5,h6&quot;);    else        headings = findHeadings(document.body, []);    function findHeadings(root, sects) &#123;        for(var c = root.firstChild; c != null; c = c.nextSibling) &#123;            if (c.nodeType !== 1) continue;            if (c.tagName.length == 2 &amp;&amp; c.tagName.charAt(0) == &quot;H&quot;)                sects.push(c);            else                findHeadings(c, sects);        &#125;        return sects;    &#125;    var sectionNumbers = [0,0,0,0,0,0];    for(var h = 0; h &lt; headings.length; h++) &#123;        var heading = headings[h];        var level = parseInt(heading.tagName.charAt(1));        if (isNaN(level) || level &lt; 1 || level &gt; 6) continue;        sectionNumbers[level-1]++;        for(var i = level; i &lt; 6; i++) sectionNumbers[i] = 0;        var sectionNumber = sectionNumbers.slice(0,level).join(&quot;.&quot;) + &quot;. &quot;;        var span = document.createElement(&quot;span&quot;);        span.className = &quot;TOCSectNum&quot;;        span.innerHTML = sectionNumber;        heading.insertBefore(span, heading.firstChild);    &#125;&#125;

但是，想要把这个代码应用到wechat-format，是有难度的，假期搞了一天，最终效果不理想，暂时放弃。思路：修改 src/assets/scripts/editor.js ，把number函数改写到这个文件里。中间会用到dom转str和str转dom，代码附上。
// https://developer.mozilla.org/zh-CN/docs/Web/API/DOMParser// https://developer.mozilla.org/zh-CN/docs/XMLSerializerfunction trans(str)&#123;      var parser = new DOMParser();    var doc=parser.parseFromString(str, &quot;text/xml&quot;);    doc = number(doc);    var s = new XMLSerializer();    return s.serializeToString(doc);&#125;

5. 在线markdown编辑器5.1. mdniceMarkdown Nice是一个支持自定义样式的 Markdown 编辑器，支持导出成微信公众号文章和知乎文章。
值得一提的是，mdnice导出的微信公众号文章，排版非常nice，比 wechat-format 还要美观。尝试了自定义样式，添加了标题编号，同样无法复制到微信公众号。
5.2. openwriteOpenWrite是一个博客群发平台，支持一键发文到微信公众号、知乎、头条、博客园、CSDN等平台。其中一项功能是Markdown格式文章导出微信公众号预览。
5.3. Cmd MarkdownCmd Markdown是一个用了很多年的在线markdown编辑器，稳定靠谱，支持高亮代码块、LaTeX 公式、流程图。
5.4. 马克飞象马克飞象是一款专为印象笔记（Evernote）打造的Markdown编辑器，支持高亮代码块、LaTeX 公式、流程图，本地图片以及附件上传。马克飞象同时提供桌面客户端以及离线Chrome App，支持移动端 Web。
5.5. StackEditStackEdit支持google登录，发布文章到web（github、gitlab、wordpress等），支持高亮代码块、LaTeX 公式、流程图。
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>编辑器</tag>
        <tag>markdown</tag>
        <tag>sublime</tag>
        <tag>typora</tag>
        <tag>wechat-format</tag>
      </tags>
  </entry>
  <entry>
    <title>资源分享</title>
    <url>/hobby-source-share/</url>
    <content><![CDATA[1. 前言本人不是资源的创造者，只是互联网的搬运工。如有侵权，请联系本人删除。
直接在【好好学习的郝】公众号回复资源的关键词，即可拿到资源链接。比如输入“Linux内核完全剖析”，就可以拿到《Linux内核完全剖析》的下载链接。如果链接失效，请在本文或者公众号留言。


2. 编程学习资料2.1. Linux


关键词
简介



Linux内核完全剖析
本书是最具知名度的Linux入门书，全面而详细地介绍了Linux操作系统。


Linux内核完全剖析
本书对早期Linux内核全部代码文件进行了详细、全面的注释和说明。


华为内部Linux学习资料
大厂学习资料。


Linux内核源代码情景分析
深入剖析了Linux最新版本核心源代码。


2.2. Python


关键词
简介



Head First Python
提问题，然后不断提供答案。


Python绝技：运用Python成为顶级黑客
结合具体的场景和真实的案例，详述了Python在安全领域发挥的作用。


Python面试宝典
常见Python面试题和答案。


2.3. Golang


关键词
简介



Go程序设计语言
Go语言圣经。


Go语言实战
通过一系列demo展示了go语言的各种特性。


Go Web 编程
只使用Go语言自带的标准库进行Web编程。


3. 英语学习资料


关键词
简介



新概念书籍
第一册到第四册全集。


新概念第一册音频
新概念第一册美音音频，包含字幕。


新概念第二册音频
新概念第二册美音音频，包含字幕。


新概念第三册音频
新概念第三册美音音频，包含字幕。


新概念第四册音频
新概念第四册美音音频，包含字幕。


清北托福备考联盟
清北托福备考联盟视频和讲义。


Coach Shane大叔
Coach Shane大叔英语口语教程。


赖世雄英语学习方法论
赖世雄英语学习方法论语音教程。


赖世雄英语语法
赖世雄英语语法语音教程。


赖世雄美语音标
赖世雄美语音标语音教程。


This is us
豆瓣9.7分的神剧，1-4季。


4. 精校版小说这里的小说，都是自己喜欢的一些小说，需要的同学自取。



关键词
简介



三体
中国大陆最畅销的科幻长篇小说之一。


雪中悍刀行
人物鲜明，文笔绝佳，大气磅礴。


一念永恒
哈哈大笑中看完的一本书，直呼万万没想到。


行者玄奘
精彩的佛理百科全书，作者把玄奘的佛性写透了。


5. 优质网站5.1. 编程学习类


网站
链接



优达学城
https://cn.udacity.com/


阿里云大学
https://edu.aliyun.com/developer


Udemy
https://www.udemy.com/


Education Ecosystem
https://www.education-ecosystem.com/


MIT OpenCourseWar
https://ocw-origin.odl.mit.edu/


Katacoda
https://www.katacoda.com/


Courera
https://www.coursera.org/


5.2. 在线工具


网站
链接



程序员工具箱
https://tool.lu/


代码在线运行
https://tool.lu/coderunner/


菜鸟工具
https://c.runoob.com/


MyScript
http://webdemo.myscript.com/


JSFiddle
https://jsfiddle.net/


HexEd.it
https://hexed.it/


持续更新中。。。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
  </entry>
  <entry>
    <title>好好学Shell：Shell和Python互相调用</title>
    <url>/dev-shell-python/</url>
    <content><![CDATA[1. 前言在linux上，最常用的编程语言是shell，其次是python。而这两种语言，很多时候需要配合使用。本文就研究一下这两种语言互相调用的方法。
参考文档：

《Shell脚本编程》
《Python基础》



2. shell调用python2.1. 调用python脚本shell调用python脚本
python main.py

2.2. 调用python模块举个简单的例子，我们想要对curl获取的结果进行json格式化。假设安装了jq，可以使用jq命令：
curl -s &quot;http://rap2api.taobao.org/app/mock/241888/example/1578301745121&quot; | jq

假设没有安装jq，那我们可以使用python的 json.tool 模块：
curl -s &quot;http://rap2api.taobao.org/app/mock/241888/example/1578301745121&quot; | python -m json.tool

2.3. 调用python函数1、准备python脚本 test.py 
#!/usr/bin/env python#-*- coding:utf-8 -*-#scriptname:test.pydef helloworld():	return &quot;helloworld&quot;def echo():	print helloworld()def get_user():	return &quot;haojin&quot;,100,&quot;beijing&quot;

2、shell调用python脚本
python -c &#x27;import test;print test.helloworld()&#x27;python -c &#x27;import test;test.echo()&#x27;res=$(python -c &#x27;import test;print test.get_user()&#x27;)name=$(echo $res | cut -d&#x27; &#x27; -f1 | sed &#x27;s/,$//&#x27; | sed &#x27;s/^(//&#x27; | sed &quot;s/\&#x27;//g&quot;)score=$(echo $res | cut -d&#x27; &#x27; -f2 | sed &#x27;s/,$//&#x27;)loc=$(echo $res | cut -d&#x27; &#x27; -f3 | sed &#x27;s/)$//&#x27; | sed &quot;s/\&#x27;//g&quot;)

3. python调用shell3.1. 调用shell命令python调用shell命令
import osval = os.system(&#x27;ls -al&#x27;)print val
其中，val的值是exit code。
3.2. 调用shell脚本1、准备shell脚本 main.sh
#!/bin/bashecho &quot;hello&quot;

2、python调用shell脚本
import osval = os.system(&#x27;sh main.sh&#x27;)print val

3.3. 获取shell指令的结果例1：获取echo命令结果。
import osres = os.popen(&#x27;echo &quot;hello&quot;&#x27;)print res.read()res.close()

例2：获取curl命令的返回结果，转化成dict。1、准备shell脚本 main.sh
#!/bin/bashcurl -s &quot;http://rap2api.taobao.org/app/mock/241888/example/1578301745121&quot;

2、python调用shell脚本
import osimport jsonres = os.popen(&#x27;sh main.sh&#x27;)data = json.loads(res.read())res.close()print data[&#x27;number&#x27;]print data[&#x27;string&#x27;]print data[&#x27;array&#x27;][0][&#x27;foo&#x27;]

4. python web服务调用shell1、准备shell脚本 main.sh
#!/bin/bashecho &quot;hello&quot;echo $1

2、准备python脚本 main.py
# -*- coding: utf-8 -*-import http.serverimport socketserverimport subprocessimport urllib.parse# 设置服务器端口PORT = 8000# 自定义请求处理程序class MyHandler(http.server.SimpleHTTPRequestHandler):    def do_GET(self):        # 检查请求路径是否为指定的API路径        if self.path == &quot;/api/test&quot;:            # 执行main.sh脚本            result = subprocess.run([&#x27;./main.sh&#x27;], stdout=subprocess.PIPE, stderr=subprocess.PIPE)            # 发送HTTP响应            self.send_response(200)            self.send_header(&#x27;Content-type&#x27;, &#x27;text/html&#x27;)            self.end_headers()            # 发送脚本执行结果作为响应内容            response_content = f&quot;Script Output: &#123;result.stdout&#125;\nScript Error: &#123;result.stderr&#125;&quot;            self.wfile.write(response_content.encode())        else:            # 如果路径不匹配，返回404 Not Found            self.send_response(404)            self.end_headers()            self.wfile.write(b&quot;404 Not Found&quot;)    def do_POST(self):        # 解析 POST 请求的内容        content_length = int(self.headers[&#x27;Content-Length&#x27;])        post_data = self.rfile.read(content_length).decode(&#x27;utf-8&#x27;)        parsed_data = urllib.parse.parse_qs(post_data)        # 检查请求路径是否为指定的 API 路径        if self.path == &quot;/api/test&quot;:            # 获取 POST 参数，例如，假设有一个参数名为 &#x27;param&#x27;            param_value = parsed_data.get(&#x27;param&#x27;, [&#x27;&#x27;])[0]            # 执行main.sh脚本            result = subprocess.run([&#x27;./main.sh&#x27;, param_value], stdout=subprocess.PIPE, stderr=subprocess.PIPE)            # 发送HTTP响应            self.send_response(200)            self.send_header(&#x27;Content-type&#x27;, &#x27;text/html&#x27;)            self.end_headers()            # 发送脚本执行结果作为响应内容            response_content = f&quot;Script Output: &#123;result.stdout&#125;\nScript Error: &#123;result.stderr&#125;&quot;            self.wfile.write(response_content.encode())        else:            # 如果路径不匹配，返回404 Not Found            self.send_response(404)            self.end_headers()            self.wfile.write(b&quot;404 Not Found&quot;)# 启动HTTP服务器with socketserver.TCPServer((&quot;&quot;, PORT), MyHandler) as httpd:    print(f&quot;Serving at port &#123;PORT&#125;&quot;)    httpd.serve_forever()

3、启动web服务
python3 main.py

4、调用web服务，web服务调用shell
curl localhost:8000/api/testcurl -X POST localhost:8000/api/test -d &quot;param=test&quot;



]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>python</tag>
        <tag>shell</tag>
        <tag>好好学Shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置Jupyter</title>
    <url>/dev-docker-jupyter/</url>
    <content><![CDATA[1. 前言《Jupyter notebook》一文中学习了jupyter的使用，本文学习一下怎样使用docker安装jupyter，并且配置访问密码。
前置条件是安装配置好了docker环境，安装方法参考《Docker入门》。已知docker宿主机IP为192.168.56.130。
参考文档：Jupyter Docker Stacks


2. 安装jupyter1、登录dockerhub查看需要的Jupyter - Docker Official Images。
2、下载jupyter镜像（以5.7.8为例）
docker pull jupyter/base-notebook:notebook-5.7.8

注意一：不要使用6.x.x，存在页面无法显示扩展插件的bug，详情参考Jupyter nbextensions_configurator not shown。
注意二：本文使用的是老版本的 jupyter 镜像，最新版本的 jupyter 镜像，已经从 dockerhub 迁移到了 quoy.io
3、创建配置目录
mkdir -p /opt/jupyter/jovyanmkdir -p /opt/jupyter/jovyan/.jupyterchmod 777 -R /opt/jupyter/jovyan

4、启动jupyter服务
docker run --name vk-jupyter -d \  -p 8888:8888 \  -v /opt/jupyter/jovyan:/home/jovyan \  jupyter/base-notebook:notebook-5.7.8

以上命令：

命名容器为vk-jupyter，后台运行
映射宿主机8888端口到容器的8888端口
挂载宿主机目录/opt/jupyter/jovyan到容器目录/home/jovyan

更多启动命令参数可以参考Jupyter Docker Stacks。
5、验证安装
docker psdocker logs vk-jupyter
jupyter启动正常的话就可以看到vk-jupyter容器。
6、查看token
docker exec -it vk-jupyter jupyter notebook list
可以查看到登录需要的token
7、登录jupyter浏览器访问 http://192.168.56.130:8888 ，可以看到Jupyter登录页面，使用token登录进入jupyter操作页面。
3. 配置jupyter1、设置密码
docker exec -it vk-jupyter jupyter notebook passworddocker restart vk-jupyter

2、使用密码浏览器访问 http://192.168.56.130:8888此时使用自己设置的密码就可以访问jupyter了。
3、根目录jupyter编辑器的默认根目录为 /home/jovyan ，对应宿主机目录 /opt/jupyter/jovyan ，创建的目录和文件都去这个路径下面去找。
4、安装ipywidgets 如果不安装ipywidgets，页面加载 /nbextensions/widgets/notebook/js/extension.js 文件时会报错404。
docker exec -it vk-jupyter pip install ipywidgets

4. 配置Nginxjupyter 使用了 websocket 协议，所以需要配置支持 websocket。如果不配置的话，通过域名访问时会报错无法连接内核，也就无法运行python脚本。
server &#123;    listen 80;    server_name jupyter.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        proxy_pass http://172.17.12.85:8888;        # WebSocket support        proxy_http_version 1.1;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection &quot;upgrade&quot;;    &#125;&#125;

5. jupyter小技巧5.1. 执行bash在代码框里输入叹号+bash命令，即可执行bash，例如：
!ls -l

5.2. 登录进容器docker exec -it vk-jupyter /bin/bashdocker exec --user root -it vk-jupyter /bin/bash

5.3. 安装常用命令docker exec --user root -it vk-jupyter /bin/bashapt updateapt install git curl unzip -y

5.4. 安装插件Jupyter Notebook 扩展插件（nbextensions）是一些 JavaScript 模块，我们可以使用插件强化 Notebook 的功能。扩展插件本质上修改了 Jupyter UI，以实现更强大的功能。1、界面添加 Nbextensions
docker exec -it vk-jupyter conda install -c conda-forge jupyter_nbextensions_configurator#docker exec -it vk-jupyter pip install jupyter_nbextensions_configurator -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/docker exec -it vk-jupyter jupyter nbextensions_configurator enable --userdocker exec -it vk-jupyter jupyter nbextension listdocker restart vk-jupyter

2、安装常用扩展集合
docker exec -it vk-jupyter pip install jupyter_contrib_nbextensions -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/#docker exec -it vk-jupyter conda install -c conda-forge jupyter_contrib_nbextensionsdocker exec -it vk-jupyter jupyter contrib nbextension install --userdocker exec -it vk-jupyter jupyter nbextension list

5.5. 启动jupyter时用户授权sudodocker run --name vk-jupyter -d \  -u root \  -p 8888:8888 \  -v /opt/jupyter/jovyan:/home/jovyan \  -e GRANT_SUDO=yes \  jupyter/base-notebook:notebook-5.7.8

这个版本的jupyter镜像，不支持直接以root用户启动jupyter程序。因此退而求其次，给jovyan用户授权sudo。如果非要以root用户启动jupyter程序，可以自行修改启动脚本 /usr/local/bin/start.sh ，然后重新打镜像。
参考文档：Running jupyterlab as root instead of jovyan when running in rootless docker mode
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置OpenVPN</title>
    <url>/dev-docker-openvpn/</url>
    <content><![CDATA[1. 前言
OpenVPN is a virtual private network (VPN) system that implements techniques to create secure point-to-point or site-to-site connections in routed or bridged configurations and remote access facilities. It implements both client and server applications.

参考文档：

WikiPedia - OpenVPN
OpenVPN Community



2. OpenVPN原理FROM ChatGPT:
OpenVPN是一个开源的虚拟私人网络（VPN）解决方案。它使用SSL/TLS协议进行安全通信，并能够穿越大多数防火墙和NAT路由器。
OpenVPN的原理如下：

通信双方首先建立一个加密通道，这个过程叫做握手（handshake）。
握手完成后，通信双方会互相验证对方的身份。
验证完成后，OpenVPN将使用TLS协议进行数据通信，所有传输的数据将通过OpenVPN的通道进行加密和解密，保证通信的安全性。
OpenVPN支持多种加密算法，例如AES和Blowfish等，同时还支持TLSv1.2协议，确保传输的数据不会被窃取或篡改。在OpenVPN中，通常会有一个服务器和多个客户端。服务器会为每个客户端分配一个唯一的IP地址，并且使用路由器将其所有客户端的流量路由到公网上。

OpenVPN可以通过在不同的操作系统上运行来实现跨平台性。此外，OpenVPN还可以使用多种网络协议，例如TCP、UDP和ICMP等。
总的来说，OpenVPN的原理是通过加密和身份验证等安全措施来实现对数据传输的保护，从而实现在不安全的公共网络中建立安全的私人网络。
3. 准备VPN域名1、登录dnspod或者其他域名解析平台2、解析vpn.voidking.com域名，指向准备安装OpenVPN Server的主机IP。
4. 安装OpenVPN Server4.1. 安装思路1、准备volume2、初始化数据，准备配置和CA证书3、启动openvpn server4、生成客户端证书和配置

参考文档：
OpenVPN for Docker
十分鐘 OpenVPN server 架設 – docker 手把手教學
DockerHub - OpenVPN

4.2. 准备volumeOVPN_DATA=&quot;ovpn-data&quot;docker volume create --name $OVPN_DATAdocker inspect volume $OVPN_DATA

4.3. 初始化volume中的数据1、生成配置
docker pull kylemanna/openvpn:2.4docker run --rm \  -v $OVPN_DATA:/etc/openvpn \  kylemanna/openvpn:2.4 \  ovpn_genconfig -u udp://vpn.voidking.com
如果没有域名，使用IP也是可以的。
执行命令后，会在 /var/lib/docker/volumes/ovpn-data/_data/ 目录中生成一些配置文件。其中最核心的是 openvpn.conf ，内容为：
server 192.168.255.0 255.255.255.0verb 3key /etc/openvpn/pki/private/vpn.voidking.com.keyca /etc/openvpn/pki/ca.crtcert /etc/openvpn/pki/issued/vpn.voidking.com.crtdh /etc/openvpn/pki/dh.pemtls-auth /etc/openvpn/pki/ta.keykey-direction 0keepalive 10 60persist-keypersist-tunproto udp# Rely on Docker to do port mapping, internally always 1194port 1194dev tun0status /tmp/openvpn-status.loguser nobodygroup nogroupcomp-lzo no### Route Configurations Belowroute 192.168.254.0 255.255.255.0### Push Configurations Belowpush &quot;block-outside-dns&quot;push &quot;dhcp-option DNS 8.8.8.8&quot;push &quot;dhcp-option DNS 8.8.4.4&quot;push &quot;comp-lzo no&quot;

2、生成CA证书
docker run --rm -it \  -v $OVPN_DATA:/etc/openvpn \  --privileged --network host \  kylemanna/openvpn:2.4 ovpn_initpki

Enter New CA Key Passphrase: # 输入密码 voidking
Re-Enter New CA Key Passphrase: # 再次输入密码 voidking
Common Name (eg: your user, host, or server name) [Easy-RSA CA]: # 输入 openvpn-server
Enter pass phrase for /etc/openvpn/pki/private/ca.key: # 输入密码 voidking
Enter pass phrase for /etc/openvpn/pki/private/ca.key: # 再次输入密码 voidking

3、查看数据
cd /var/lib/docker/volumes/ovpn-data/_data/pki/privatell

4.4. 启动openvpn serverdocker run --name openvpn -d \  -v $OVPN_DATA:/etc/openvpn \  -p 1194:1194/udp \  --cap-add=NET_ADMIN \  kylemanna/openvpn:2.4docker psdocker logs openvpn

The UDP server uses 192.168.255.0/24 for dynamic clients by default.
4.5. 生成客户端证书docker run --rm -it \  -v $OVPN_DATA:/etc/openvpn \  kylemanna/openvpn:2.4 \  easyrsa build-client-full client0 nopass

这里的client0可以替换成其他名称。
4.6. 生成客户端配置docker run --rm -it \  -v $OVPN_DATA:/etc/openvpn \  kylemanna/openvpn:2.4 \  ovpn_getclient client0 &gt; client0.ovpn

4.7. 开启防火墙在公有云防火墙或者安全组管理页面，放开1194端口的UDP协议。
4.8. OpenVPN Server日志docker logs -f openvpn
监听日志变化，测试客户端接入时。
5. OpenVPN Server自定义配置5.1. 自定义配置说明自定义配置，需要修改 openvpn.conf 配置，然后重启openvpn server。
参考文档：

GettingStartedwithOVPN
OpenVPN HOWTO

5.2. 添加路由规则让客户端自动添加路由规则：
client-to-clientpush &quot;route 192.168.56.0 255.255.255.0&quot;

5.3. 修改默认网关让客户端修改默认网关，所有流量都走vpn：
push &quot;redirect-gateway def1&quot;

5.4. 修改DNS当使用 redirect-gateway 时，客户端将通过VPN路由DNS查询，VPN服务器将需要处理这些查询。这时可以通过将DNS服务器地址推送到连接的客户端来实现，这些客户端将在VPN激活期间替换其正常的DNS服务器设置。
push &quot;dhcp-option DNS 10.8.0.1&quot;

6. MacOS安装使用OpenVPN Client6.1. 安装OpenVPN Client有很多，这里选择使用OpenVPN官方Client - OpenVPN Connect。版本选择OpenVPN Connect v3，下载安装即可。
6.2. 使用打开OpenVPN Connect v3 -&gt; File -&gt; BROWSE-&gt; 选择client0.ovpn -&gt; 选择OpenVPN Server点击启动
当按钮变绿，显示CONNECTED，表明连接成功，右上角日志可以查看到更详细信息。同时OpenVPN Server日志中也会出现新的客户端连接信息。
6.3. 查看网络配置ifconfig | grep 192netstat -r

➜  ~ ifconfig | grep 192    inet 192.168.199.141 netmask 0xffffff00 broadcast 192.168.199.255    inet 192.168.56.1 netmask 0xffffff00 broadcast 192.168.56.255    inet 192.168.255.6 --&gt; 192.168.255.5 netmask 0xfffffffc➜  ~ netstat -rRouting tablesInternet:Destination        Gateway            Flags        Netif Expire0/1                192.168.255.5      UGSc         utun2default            192.168.199.1      UGSc           en08.136.13.58/32     192.168.199.1      UGSc           en0128.0/1            192.168.255.5      UGSc         utun2192.168.255.1/32   192.168.255.5      UGSc         utun2192.168.255.4/30   192.168.255.6      UGSc         utun2192.168.255.5      192.168.255.6      UHr          utun2192.168.56.0/24    192.168.255.5      UGSc         utun2

可以看到，本机新增了一个 192.168.255.6 的IP，网关是 192.168.255.5 ，使用的网卡设备是隧道utun2。
在计算机网络中，0/1 和 128.0/1 是用CIDR（无类别域间路由）表示法来描述IP地址范围的。
0/1 表示：起始IP地址是 0.0.0.0，子网掩码的网络部分有1位，范围是从 0.0.0.0 到 127.255.255.255128.0/1 表示：起始IP地址是 128.0.0.0，子网掩码的网络部分有1位，范围是从 128.0.0.0 到 255.255.255.255
6.4. 测试连通性ping 172.17.12.85
ping OpenVPN Server 的内网IP，网络畅通。
7. Linux安装使用OpenVPN Client7.1. 安装参考文档：

OpenVPN 3 for Linux
OpenVPN 3 Linux
OpenVPN 3 Client for Linux

Debian/Ubuntu：
apt install apt-transport-httpsDISTRO=$(lsb_release -c | awk &#x27;&#123;print $2&#125;&#x27;)curl -fsSL https://swupdate.openvpn.net/repos/openvpn-repo-pkg-key.pub | gpg --dearmor &gt; /etc/apt/trusted.gpg.d/openvpn-repo-pkg-keyring.gpgcurl -fsSL https://swupdate.openvpn.net/community/openvpn3/repos/openvpn3-$DISTRO.list &gt;/etc/apt/sources.list.d/openvpn3.listapt updateapt install openvpn3

CentOS：
yum install yum-plugin-copryum copr enable dsommers/openvpn3yum install openvpn3-client

7.2. 使用1、启动OpenVPN Client
openvpn3 session-start --config client0.ovpn
输入用户名密码，即可连接到OpenVPN Server。
2、重启OpenVPN Client
ps -ef | grep openvpnopenvpn3 sessions-listopenvpn3 session-manage --config client0.ovpn --restart# oropenvpn3 session-manage --config --session-path /net/openvpn/v3/sessions/45aea145s8fb7s4143sa23as933981fb09d3 --restart

3、关闭OpenVPN Client
openvpn3 session-manage --config client0.ovpn --disconnect# oropenvpn3 session-manage --config --session-path /net/openvpn/v3/sessions/45aea145s8fb7s4143sa23as933981fb09d3 --disconnect

7.3. 查看网络配置route -n

8. OpenVPN Client自定义配置8.1. 自定义配置说明默认情况下，client会获取server配置中push的所有配置。但是某些情况下，我们可能需要自定义route、gateway、dns，此时就需要修改client配置。
自定义client配置的方法是修改 client0.ovpn 配置文件，然后重新加载。
参考文档：

GettingStartedwithOVPN
OpenVPN HOWTO
OpenVPN 路由设置

8.2. 自定义路由规则route-nopullroute 192.168.56.0 255.255.255.0 vpn_gatewayroute 172.16.0.0 255.255.0.0 vpn_gateway

8.3. 自定义网关配置启动openvpn client后，会在设备上添加很多路由规则。其中可能包含网关配置，会覆盖原有的网关。
客户端配置不修改默认网关。
pull-filter ignore redirect-gateway

8.4. 自定义DNSdhcp-option DNS 114.114.114.114dhcp-option DNS 8.8.8.8

9. OpenVPN vs IPsecOpenVPN和IPsec都是VPN协议，但是安全性和用途不同。
OpenVPN是一种高度可配置的开源协议，可以使用各种端口和加密方法。它是最安全的VPN协议之一，并通过了许多第三方安全审计。一般需要自行安装。适合用于 client-to-site 的连接，例如远程办公。
IPSec（Internet Security Protocol）是一套安全的协议，可确保数据包的身份验证和加密，以通过互联网协议（IP）网络在两个端点之间提供受保护的通信。常内置在网关设备中，也可以自行安装。适合用于 site-to-site 的连接，例如打通多个独立的网络（内网和内网、办公网和内网等）。安全性不如OpenVPN，但是速度通常比OpenVPN要更快。
参考文档：

IPSec vs. OpenVPN—what are the differences?
IPSec‌ ‌vs.‌ ‌OpenVPN:‌ ‌Understanding‌ ‌the‌ ‌Differences‌
什么是IPsec？
什么是 IPsec？| IPsec VPN 如何运作

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>chatgpt</tag>
        <tag>macos</tag>
      </tags>
  </entry>
  <entry>
    <title>使用IDEA合并代码的个人实践</title>
    <url>/dev-idea-git-merge-code/</url>
    <content><![CDATA[1. 前言团队合作开发，肯定会涉及到代码合并的问题。以前在代码合并的时候，喜欢使用beyond compare。同一个项目拷贝两份，一个保持最新代码，一个用于开发，开发完成后使用beyond compare把开发代码合并到最新代码。然后push到远程分支，提一个MR。后来发现，使用IDEA配合Git命令来合并代码，简直完美。本文就来记录一下这种方法，供大家参考。
已知项目名为 voidking，有很多分支，其中 pre 分支用来发布服务到预发环境，master分支用来发布服务到生产环境。代码合并后，创建pre或者prod开头的tag，触发CICD。


2. 开发1、在github或者gitlab新建一个issue，描述清楚问题或者功能。2、针对这个issue，创建一个分支。这个分支会以issue的标题命名，前面加上issue的编号。假设生成的branch为 3-add-new-feat 。3、拉取最新代码后，本地创建自己的分支git checkout -b haojin
4、一顿复制粘贴，功能完成，commit自己的代码。
3. pre分支代码合并到了代码合并的步骤，表演开始。
1、拉取最新代码
git checkout mastergit pull

2、创建mergepre分支
git checkout -b mergepre origin/pre

3、使用IDEA合并代码点击界面右下角Git Branches，选择 haojin 分支，Show Diff with Working Tree。根据自己的修改，合并代码到当前的mergepre分支，修改完成后commit代码。
4、push代码
git push origin HEAD:haojin --force
需要注意的是，这里我们把mergepre的分支push到了远程的haojin分支，并没有push到 3-add-new-feat 。这是因为，3-add-new-feat这个分支我们留着，代码合并到master分支时使用。
点击出现的链接，或者在gitlab上的haojin分支点击创建MR，进入创建MR的页面。
5、选择要合并到的分支为 pre ，可以看到代码的变更，再次检查。
6、检查没问题的话，提交MR即可。
4. master分支代码合并以上，假设我们已经合并了自己的代码到 pre 分支。并且在预发环境发版验证通过，没有问题。接下来就可以合并代码到 master 分支了。
1、拉取最新代码
git checkout mastergit pull

2、创建mergeprod分支
git checkout -b mergeprod

3、使用IDEA合并代码点击界面右下角Git Branches，选择 haojin 分支，Show Diff with Working Tree。根据自己的修改，合并代码到当前的mergeprod分支，修改完成后commit代码。
4、push代码
git push origin HEAD:3-add-new-feat
点击出现的链接，或者在gitlab上的haojin分支点击创建MR，进入创建MR的页面。
5、选择要合并到的分支为 master ，可以看到代码的变更，再次检查。
6、检查没问题的话，提交MR即可。
5. 代码冲突解决办法问题描述：git pull 之后，出现代码冲突。解决办法：菜单栏Git，Merge，Resolve Conflicts，选中冲突代码，Merge。
]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>编辑器</tag>
        <tag>git</tag>
        <tag>idea</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Go语言开发的小技巧</title>
    <url>/dev-golang-skills/</url>
    <content><![CDATA[1. 前言最近学到了一些Go语言开发时的小技巧，做下记录。
2. 提高下载速度方法一：科学上网
方法二：配置代理，详情参考《Golang包管理工具》
# export GOPROXY=https://goproxy.cnexport GOPROXY=https://goproxy.ioexport GO111MODULE=ongo get -v golang.org/x/tools/cmd/goimports

方法三：使用gopm
go get -v github.com/gpmgo/gopmgopm get -v golang.org/x/tools/cmd/goimports



3. 编译安装软件go build golang.org/x/tools/cmd/goimportsgo install golang.org/x/tools/cmd/goimports

4. 自动格式化Golang希望统一代码风格，因此推出了gofmt工具。gofmt可以格式化单个文件，也可以格式化整个目录下的所有go文件。除了gofmt工具，go语言中还有一个go fmt命令，该命令是gofmt的简单封装。
在IDEA中，怎样使用gofmt呢？1、菜单栏，IntelliJ IDEA，Preferences…。2、Plugins，搜索File，找到File Watchers插件并安装。3、Tools，File Watchers，添加或修改gofmt模版。
File type：GoScope：Project filesProgram：/usr/local/go/bin/gofmtArguments：-l -w -s $FilePath$Output paths to refresh：$FilePath$
4、勾选Enabled。
保存源码时，就会执行代码格式化了。
5. 自动引入依赖包参考上一节自动格式化，配置 goimports ，自动引入依赖包。
6. 查看文档如果对一个包或者函数不熟悉，可以使用go doc命令查看文档。比如：
go doc http.ListenAndServe

如果想要看一个项目的帮助文档，可以使用godoc工具启动一个文档服务器。
go get -v golang.org/x/tools/cmd/godocgodoc -http :6060

7. 测试Debugging Sucks! Testing Rocks!Go语言支持三种测试：单元测试、性能测试和http测试，下面分别来看一下。
7.1. 单元测试Go语言单元测试函数以Test为前缀，详情参考测试函数。
实际开发中推荐使用表格驱动测试，就是把测试数据和测试逻辑分开。比如：
func TestShortFilename(t *testing.T) &#123;    tests := []struct &#123;        in       string        expected string    &#125;&#123;        &#123;&quot;???&quot;, &quot;???&quot;&#125;,        &#123;&quot;filename.go&quot;, &quot;filename.go&quot;&#125;,        &#123;&quot;hello/filename.go&quot;, &quot;filename.go&quot;&#125;,        &#123;&quot;main/hello/filename.go&quot;, &quot;filename.go&quot;&#125;,    &#125;    for _, tt := range tests &#123;        actual := getShortFilename(tt.in)        if strings.Compare(actual, tt.expected) != 0 &#123;            t.Fail()        &#125;    &#125;&#125;

在IDEA中查看代码覆盖率：在单元测试函数左边，会出现一个三角箭头，点击它，选择 Run ‘Testxxx’ with Coverage，即可看到代码覆盖率。绿线代表覆盖到了，红线代表没有覆盖到。
也可以使用命令查看代码覆盖率：
go test -coverprofile=c.outgo tool covergo tool cover html=c.out

7.2. 性能测试1、编写性能测试函数，以Benchmark为前缀，详情参考基准测试。
2、运行基准测试
go test -bench .

3、详细分析（文本）
go test bench . -cpuprofile cpu.outgo tool pprof -text -nodecount=10 ./cpu.test cpu.out

4、详细分析（图表）
go tool pprof cpu.outweb
PS：需要安装graphviz，以便生成图表。
7.3. http测试Go语言的http测试使用 net/http/httptest 包，测试方法可以分为两种：一种是使用假的Request/Response，速度快，测试粒度小，适用于测试函数和方法；另外一种是启动http服务器，速度慢，代码覆盖量更大，适用于测试和模拟后端接口。
先说第一种，使用假的Request/Response：1、已知函数
func HelloHandler(w http.ResponseWriter, r *http.Request) &#123;	w.Write([]byte(&quot;hello world&quot;))&#125;

2、测试代码
import (	&quot;io/ioutil&quot;	&quot;net/http&quot;	&quot;net/http/httptest&quot;	&quot;testing&quot;)func TestHelloHandler(t *testing.T) &#123;	req := httptest.NewRequest(&quot;GET&quot;, &quot;http://www.voidking.com/&quot;, nil)	w := httptest.NewRecorder()	HelloHandler(w, req)	bytes, _ := ioutil.ReadAll(w.Result().Body)	if string(bytes) != &quot;hello world&quot; &#123;		t.Fatal(&quot;expected hello world, but got&quot;, string(bytes))	&#125;&#125;

3、运行测试go test -v .
再说第二种，启动http服务器。1、已知函数
func HelloHandler(w http.ResponseWriter, r *http.Request) &#123;	w.Write([]byte(&quot;hello world&quot;))&#125;

2、测试代码
import (	&quot;io/ioutil&quot;	&quot;net/http&quot;	&quot;net/http/httptest&quot;	&quot;testing&quot;)func TestHelloHandler(t *testing.T) &#123;	ts := httptest.NewServer(http.HandlerFunc(HelloHandler))	defer ts.Close() 	res, err := http.Get(ts.URL)	if err != nil &#123;		t.Fatal(err)	&#125;	bytes, err := ioutil.ReadAll(res.Body)	res.Body.Close()	if err != nil &#123;		t.Fatal(err)	&#125; 	if string(bytes) != &quot;hello world&quot; &#123;		t.Fatal(&quot;expected hello world, but got&quot;, string(bytes))	&#125;&#125;

3、运行测试go test -v .
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>idea</tag>
      </tags>
  </entry>
  <entry>
    <title>使用IDEA开发Golang和Python</title>
    <url>/dev-idea-golang-python/</url>
    <content><![CDATA[1. 前言突然发现，自己安装了三款 JetBrains 的产品：IDEA、GoLand和PyCharm。这三款软件大小差不多，长得差不多，用起来也差不多。唯一的差别，就是自己在使用不同的编程语言写代码时，需要在它们之间来回切换。能不能，只保留其中一款呢？一个IDE，所有编程语言通用，或者至少能够保证Java、Golang、Python通用。这个需求是合理的，而且是很简单的配置就可以实现的，本文就来总结一下IDEA配置开发Golang和Python的方法。


2. JavaIDEA本身就是适用于Java开发的，具体配置方法可以参考《IDEA的常用配置》和《使用IDEA新建Maven JavaWeb项目》。
此外，IDEA还支持Android开发，可以参考《Android开发——Android Studio》进行配置。
3. Golang1、菜单栏，IntelliJ IDEA，Preferences…。2、Plugins，搜索Go，找到Go插件并安装。This plugin extends IntelliJ platform with Go-specific coding assistance and tool integrations, and has everything you could find in GoLand.3、重启IDEA。菜单栏，IntelliJ IDEA，Preferences…。4、Languages &amp; Frameworks，Go。5、配置好GOROOT和GOPATH。
上面的IDEA配置，和Goland几乎相同，然后就可以像Goland一样开发Go语言程序了。
Go项目Debug的配置方法，参考《beego入门篇——下》中的调试一节。
4. Python配置方法参考Configure a Python SDK。
1、菜单栏，IntelliJ IDEA，Preferences…。2、Plugins，搜索Python，找到Python插件并安装。The Python plug-in provides smart editing for Python scripts. The feature set of the plugin corresponds to PyCharm IDE Professional Edition.3、重启IDEA。菜单栏，File，Project Structure…。4、Platform Settings，加号，Add Python SDK…。5、Project Settings，Project SDK，选择新添加的Python SDK。
上面的IDEA配置，和PyCharm有所不同。PyCharm中的Python SDK配置，位置在Preferences，Project: project_name。
Python项目Debug的配置方法，参考《PyCharm调试》。不同的是，如果Debug Configurations窗口点击加号，第一眼看不到Python，那么Python在Other层级下。
5. 后记以上，IDEA就成了一个全能IDE，可以开发Java、Golang和Python。如果需要支持其他语言，同样的方法，查找并安装插件即可。Goland和PyCharm可以卸载了，nice。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>golang</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>golang</tag>
        <tag>编辑器</tag>
        <tag>java</tag>
        <tag>idea</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：更好的Git提交信息</title>
    <url>/dev-git-commit-message/</url>
    <content><![CDATA[1. 还可以更好使用git很多年，也提交了很多代码，自以为使用习惯良好。因为每次提交代码，我都会在Git提交信息（git commit message）中说明清楚修改的内容。但是，偶然间读到一些关于commit message的文章，才发现还有很多进步的空间。
本文就来学习一下编写更高质量的提交信息，主要参考：

Commit message 和 Change log 编写指南
优雅的提交你的 Git Commit Message 
Conventional Commits
你可能已经忽略的 git commit 规范



2. 约定式提交2.1. 约定式提交简介约定式提交规范是一种基于提交消息的轻量级约定。它提供了一组用于创建清晰的提交历史的简单规则；这使得编写基于规范的自动化工具变得更容易。 这个约定与 SemVer 相吻合， 在提交信息中描述新特性、bug 修复和破坏性变更。
约定式提交优点：

自动化生成 CHANGELOG。
基于提交的类型，自动决定语义化的版本变更。
向同事、公众与其他利益关系者传达变化的性质。
触发构建和部署流程。
让人们探索一个更加结构化的提交历史，以便降低对你的项目做出贡献的难度。

2.2. 约定式提交结构约定式提交说明的结构如下所示：
&lt;类型&gt;[可选的作用域]: &lt;描述&gt;[可选的正文][可选的脚注]

提交方法：
git commit -m &quot;dquote&gt; &lt;类型&gt;[可选的作用域]: &lt;描述&gt;dquote&gt;dquote&gt; [可选的正文]dquote&gt;dquote&gt; [可选的脚注]dquote&gt; &quot;

2.3. 约定式提交常用类型
fix: 在代码库中修复了一个 bug（这和语义化版本中的 PATCH 相对应）。 
feat: 在代码库中新增了一个功能（这和语义化版本中的 MINOR 相对应）。 
docs: 文档相关的改动。 
refactor: 重构 
improvement: 性能提升 
test: 测试用例修改 
style: 代码格式修改，注意不是 css 修改
revert: 回退 
chore: 日常的小任务，比如构建流程，依赖管理。 
misc: 对于那些难以分类的提交，可以使用misc（Miscellaneous的缩写，意为“杂项”）标签。 
Close: 在可选的正文或脚注的起始位置，关闭issue 
BREAKING CHANGE: 在可选的正文或脚注的起始位置，表示引入了破坏性 API 变更（这和语义化版本中的 MAJOR 相对应）。破坏性变更可以是任意类型提交的一部分。 

以上类型都是可选的，其他类型也被允许，根据需要定义项目的提交规范就好。并且在语义化版本中没有隐式的影响（除非他们包含 BREAKING CHANGE）。可以为提交类型添加一个围在圆括号内的作用域，以为其提供额外的上下文信息。例如：
feat(parser): adds ability to parse arrays.

可以在类型(作用域)前缀之后，: 之前，附加 ! 字符，以进一步提醒注意破坏性变更。当有 ! 前缀时，正文或脚注内必须包含 BREAKING CHANGE: description
约定式提交和 SemVer 的关联：fix 类型提交应当对应到 PATCH 版本。feat 类型提交应该对应到 MINOR 版本。带有 BREAKING CHANGE 的提交不管类型如何，都应该对应到 MAJOR 版本。
2.4. 约定式提交参考项目
yargs：广受欢迎的命令行参数解析器。
istanbuljs：一套为 JavaScript 测试生成测试覆盖率的开源工具和类库。
massive.js：一个用于 Node 和 PostgreSQL 的数据访问类库。
electron：用 JavaScript、HTML 和 CSS 构建跨平台应用。
scroll-utility：一个居中元素和平滑动画的滚屏工具包实例。
Blaze UI：无框架开源 UI 套件。
Monica：一个开源的人际关系管理系统。
mhy：一个零配置、开箱即用的、多用途工具箱与开发环境。
sharec：一个用于模板和配置文件版本化的极简工具。

3. 提交帮助工具3.1. git commit提示1、修改 ~/.gitconfig ，添加
[commit]template = ~/.gitmessage

2、新建 ~/.gitmessage ，内容为
# head: &lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;# - type: feat, fix, docs, style, refactor, test, chore, misc# - scope: can be empty (eg. if the change is a global or difficult to assign to a single component)# - subject: start with verb (such as &#x27;change&#x27;), 50-character line## body: 72-character wrapped. This should answer:# * Why was this change necessary?# * How does it address the problem?# * Are there any side effects?## footer: # - Include a link to the ticket, if any.# - BREAKING CHANGE#

3、使用提示
git commit

3.2. commitizen参考 commitizen/cz-cli 。
]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中的网络策略</title>
    <url>/dev-k8s-network-policy/</url>
    <content><![CDATA[1. 怎样限制Pod中的网络流量？已知Pod名为 webapp ，label为 name=webapp 。现在想要模拟无法访问数据库，限制它的流量，禁止它访问 10.0.0.0/24 网段的所有3306端口，该怎么实现？
方法一：使用NetworkPolicy限流方法二：开启Pod特权模式，在Pod内部使用iptables限流方法三：登录Pod所在宿主机，在Pod所在ns中使用iptables限流


2. NetworkPolicy网络策略（NetworkPolicy）是一种关于 Pod 间及与其他网络端点间所允许的通信规则的规范。NetworkPolicy 资源使用 标签 选择 Pod，并定义选定 Pod 所允许的通信规则。
网络策略通过网络插件来实现。要使用网络策略，用户必须使用支持 NetworkPolicy 的网络解决方案。创建一个资源对象，而没有控制器来使它生效的话，是没有任何作用的。
默认情况下，Pod 是非隔离的，它们接受任何来源的流量。Pod 可以通过相关的网络策略进行隔离。一旦命名空间中有网络策略选择了特定的 Pod，该 Pod 会拒绝网络策略所不允许的连接。 (命名空间下其他未被网络策略所选择的 Pod 会继续接收所有的流量)网络策略不会冲突，它们是附加的。如果任何一个或多个策略选择了一个 Pod, 则该 Pod 受限于这些策略的 ingress/egress 规则的并集。因此评估的顺序并不会影响策略的结果。
更多内容参考网络策略和NetworkPolicy最佳实践。
基础规则：

如果 Pod 没有被 NetworkPolicy 匹配到，那么它的流量是被允许的 
如果 Pod 被 NetworkPolicy 匹配到，但是没有出口/入口规则被匹配到，那么它的出口/入口流量是被禁止的
只能指定规则来允许流量通行，而不能直接禁止流量通行
NetworkPolicy 中的 Rule 之间的匹配逻辑是 OR
NetworkPolicy 默认的作用域是 Pod 所在的 Namespace

1、创建np.yaml
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata:  name: webapp-network-policy  namespace: defaultspec:  podSelector:    matchLabels:      name: webapp  policyTypes:  - Ingress  - Egress  ingress:  - &#123;&#125;  egress:  - to:    - ipBlock:        cidr: 0.0.0.0/0        except:         - 10.0.0.0/24

这种实现方式，其实有一个问题，就是限流时不止限制了3306端口，还限制了 10.0.0.0/24 网段的所有端口。能不能只限制 10.0.0.0/24 网段的 3306 端口呢？理论上可以实现，然而我不会。。。
2、执行限流
kubectl apply -f np.yaml

3、解除限流
kubectl delete -f np.yaml

3. Pod特权模式限流1、在pod定义中添加：
securityContext:  privileged: true  #runAsUser: 0

2、在pod内部执行限流
iptables -A OUTPUT -p tcp --dport 3306 -j DROP

3、解除限流
iptables -t nat -nL --line-numberiptables -D OUTPUT 1

4. 宿主机限流1、登录宿主机
2、查找容器pid
docker ps | grep webappdocker inspect &lt;container_id&gt; | grep pid -i

3、执行限流
nsenter -t &lt;pid&gt; -n iptables -A OUTPUT -p tcp --dport 3306 -j DROP

4、解除限流
nsenter -t &lt;pid&gt; -n iptables -t nat -nL --line-numbernsenter -t 8993 -n iptables -D OUTPUT 1



]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：jq命令</title>
    <url>/dev-linux-jq/</url>
    <content><![CDATA[1. jq命令简介jq 是一款命令行下处理 JSON 数据的工具。其可以接受标准输入，命令管道或者文件中的 JSON 数据，经过一系列的过滤器(filters)和表达式的转后形成我们需要的数据结构并将结果输出到标准输出中。jq 的这种特性使我们可以很容易地在 Shell 脚本中调用它。
参考文档：

命令行 JSON 处理工具 jq 的使用介绍
jq Manual (development version)



2. 安装jq2.1. macosbrew install jq

2.2. linux通用安装wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64chmod a+x jq-linux64 &amp;&amp; mv jq-linux64 /usr/bin/jq

2.3. centosyum install epel-releaseyum install jq

2.4. ubuntuapt updateapt install -y jq

3. jq表达式3.1. 举个栗子echo &#x27;&#123;&quot;name&quot;:&quot;voidking&quot;&#125;&#x27; | jq .echo &#x27;&#123;&quot;name&quot;:&quot;voidking&quot;&#125;&#x27; | jq .nameecho &#x27;&#123;&quot;name&quot;:&quot;voidking&quot;&#125;&#x27; | jq -r .nameecho &#x27;&#123;&quot;name&quot;:&quot;voidking&quot;&#125;&#x27; | jq -c

用户在使用jq时，需要使用jq支持的语法来构建表达式(filters)并将其传给jq。jq根据语法规则解析表达式，并应用在输入的JSON数据上从而得到需要的结果。
上面例子中的 . 和 .name 就是表达式。. 符号表示对表达式输入的整个JSON对象的引用，.name和.name?表示获取JSON对象的属性。当输入不是JSON对象或数组时，带着问号的方式不会抛出异常。
选项的含义查看jq帮助即可，比如-r表示输出raw格式内容，-c表示去空格和换行压缩输出。
3.2. 串行操作echo &#x27;&#123;&quot;name&quot;:&#123;&quot;firstname&quot;:&quot;Void&quot;,&quot;lastname&quot;:&quot;King&quot;&#125;&#125;&#x27; | jq .name.firstnameecho &#x27;&#123;&quot;name&quot;:&#123;&quot;firstname&quot;:&quot;Void&quot;,&quot;lastname&quot;:&quot;King&quot;&#125;&#125;&#x27; | jq &#x27;.name | .firstname&#x27;echo &#x27;&#123;&quot;name&quot;:&#123;&quot;firstname&quot;:&quot;Void&quot;,&quot;lastname&quot;:&quot;King&quot;&#125;&#125;&#x27; | jq &#x27;.name | .firstname,.lastname&#x27;echo &#x27;[&#123;&quot;firstname&quot;:&quot;Void&quot;,&quot;lastname&quot;:&quot;King&quot;&#125;,&#123;&quot;firstname&quot;:&quot;Hao&quot;,&quot;lastname&quot;:&quot;Jin&quot;&#125;]&#x27; | jq &#x27;.[] | .firstname,.lastname&#x27; | sed -n &quot;N;s/\n/ /p&quot;

jq表达式支持串行化操作。一个复杂的表达式可以由多个简单的表达式组成，以管道符号 | 分割，串行化执行。管道前面表达式的输出，是管道后面表达式的输入。
逗号 , 表示对同一个输入应用多个表达式。
3.3. 数组操作echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq .echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq &#x27;.[]&#x27;echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq &#x27;.[0:2]&#x27;echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq &#x27;.[0,1]&#x27;echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq &#x27;.[].name&#x27;echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq &#x27;.[] | .name&#x27;echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq &#x27;.[] | .[&quot;name&quot;]&#x27;

jq 提供三种基础表达式来操作数组：迭代器操作.[]，该表达式的输入可以是数组或者JSON对象，输出的是基于数组元素或者JSON对象属性值的迭代器（iterator）。访问特定元素的操作.[index]或.[&quot;attributename&quot;]。用来访问数组元素或者JSON对象的属性值，输出是单个值。数组切片操作.[startindex:endindex]&#39;，其行为类似于 python 语言中数组切片操作。
一个表达式产生的结果是迭代器时，迭代器的每一个值会分别作为的输入，传给后面的表达式。
3.4. 字符串转json常规字符串转json：
echo &#x27;&#123;&quot;recordID&quot;:123, &quot;title&quot;:&quot;Hello World&quot;, &quot;content&quot;:&quot;Lorem ipsum...&quot;&#125;&#x27; | jq -r &#x27;.&#x27;

带反斜杠的字符串转json：
echo &quot;&#123;\&quot;recordID\&quot;:123, \&quot;title\&quot;:\&quot;Hello World\&quot;, \&quot;content\&quot;:\&quot;Lorem ipsum...\&quot;&#125;&quot; | jq -r &#x27;.&#x27;

嵌套字符串转json：
echo &#x27;&#123;&quot;recordID&quot;:123, &quot;title&quot;:&quot;Hello World&quot;, &quot;content&quot;:&quot;&#123;\&quot;recordID\&quot;:123, \&quot;title\&quot;:\&quot;Hello World\&quot;, \&quot;content\&quot;:\&quot;Lorem ipsum...\&quot;&#125;&quot;&#125;&#x27; | jq -r &#x27;.content | fromjson&#x27;

3.5. json转字符串json转字符串：
echo &#x27;&#123;&quot;recordID&quot;:123, &quot;title&quot;:&quot;Hello World&quot;, &quot;content&quot;:&quot;Lorem ipsum...&quot;&#125;&#x27; | jq -r &#x27;tojson&#x27;

json转带反斜杠的字符串：
echo &#x27;&#123;&quot;recordID&quot;:123, &quot;title&quot;:&quot;Hello World&quot;, &quot;content&quot;:&quot;Lorem ipsum...&quot;&#125;&#x27; | jq &#x27;tojson&#x27;

4. jq运算echo &#x27;&#123;&quot;num&quot;:3,&quot;str&quot;:&quot;343&quot;&#125;&#x27; | jq &#x27;.num*3&#x27;echo &#x27;&#123;&quot;num&quot;:3,&quot;str&quot;:&quot;343&quot;&#125;&#x27; | jq &#x27;.num/3&#x27;echo &#x27;&#123;&quot;num&quot;:3,&quot;str&quot;:&quot;343&quot;&#125;&#x27; | jq &#x27;.str+&quot;3&quot;&#x27;echo &#x27;&#123;&quot;num&quot;:3,&quot;str&quot;:&quot;343&quot;&#125;&#x27; | jq &#x27;.str*3&#x27;echo &#x27;&#123;&quot;num&quot;:3,&quot;str&quot;:&quot;343&quot;&#125;&#x27; | jq &#x27;.str/&quot;4&quot;&#x27;echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq &#x27;.+[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;]&#x27;echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;&#125;]&#x27; | jq &#x27;.-[&#123;&quot;name&quot;:&quot;voidking&quot;&#125;]&#x27;echo &#x27;&#123;&quot;name&quot;:&#123;&quot;firstname&quot;:&quot;Void&quot;,&quot;lastname&quot;:&quot;King&quot;&#125;&#125;&#x27; | jq &#x27;.+&#123;&quot;name&quot;:&#123;&quot;nickname&quot;:&quot;Hankin&quot;&#125;&#125;&#x27;echo &#x27;&#123;&quot;name&quot;:&#123;&quot;firstname&quot;:&quot;Void&quot;,&quot;lastname&quot;:&quot;King&quot;&#125;&#125;&#x27; | jq &#x27;.*&#123;&quot;name&quot;:&#123;&quot;nickname&quot;:&quot;Hankin&quot;&#125;&#125;&#x27;echo &#x27;&#123;&quot;name&quot;:&#123;&quot;firstname&quot;:&quot;Void&quot;,&quot;lastname&quot;:&quot;King&quot;&#125;&#125;&#x27; | jq &#x27;.name.nickname//&quot;Hankin&quot;&#x27;jq -n &#x27;([1,2]|.[])+([4,6]|.[])&#x27;

jq 内部支持的数据类型有：数字，字符串，数组和对象(object)。
数字运算：jq支持加减乘除(/)和求余(%)运算。字符串运算：jq 提供字符串的连接、复制、分割运算。数组运算：并集、差集运算。对象运算：合并。比较运算：jq 内部支持的比较运算，规则与js基本相同。逻辑运算: and/or/not。在 jq 逻辑运算中，除了 false 和 null 外，其余的任何值都等同于 true。默认值运算符：双斜杠。迭代器运算：每一个元素拿出来分别运算。
5. jq函数jq 支持函数。在使用 jq 函数时，我们应该注意区分两个概念：输入和参数。输入可能是整个表达式的输入数据也可能是表达式别的部分的输出。而参数和函数一起构成新的filter来处理输入。和其他编程语言不同的是，在调用函数时，多个参数之间以分号分隔。jq通过内置函数提供了数据处理时常用的操作，例如：删除、映射，过滤、路径操作等。
5.1. 删除echo &#x27;&#123;&quot;name&quot;:&#123;&quot;firstname&quot;:&quot;Void&quot;,&quot;lastname&quot;:&quot;King&quot;&#125;&#125;&#x27; | jq &#x27;del(.name.firstname)&#x27;
删除不需要的元素。
5.2. 映射echo &#x27;[1,2,3,4]&#x27;| jq -r &#x27;map(.+1)&#x27;

在数据处理过程中，我们经常需要将数据从一种形式转换成另外一种形式，或者改变数据的值。jq提供了两个内置映射函数来实现这种转换：map 和 map_values。其中，map处理的对象是数组，而map_values则处理对象属性的值。map 函数的参数为 filter 表达式。
5.3. 过滤echo &#x27;[1,2,3,4]&#x27;| jq -r &#x27;map(select(.&gt;2))&#x27;echo &#x27;[1,2,3,4]&#x27;| jq -r &#x27;.[]|select(.&gt;2)&#x27;echo &#x27;[&#123;&quot;name&quot;:&quot;voidking&quot;,&quot;age&quot;: 18&#125;,&#123;&quot;name&quot;:&quot;haojin&quot;,&quot;age&quot;: 28&#125;]&#x27; | jq &#x27;.[]|select(.name==&quot;haojin&quot;)&#x27;

jq中有两种类型的选择过滤操作。第一种是基于数据类型的过滤，如表达式.[]|arrays的结果只包含数组。可以用来过滤的类型过滤器有：arrays, objects, iterables, booleans, numbers, normals, finites, strings, nulls, values, scalars。第二种是select函数。select接受一个条件表达式作为参数。其输入可以是迭代器，或者和map函数配合使用来处理数组。当输入中的某个元素使select参数中的条件表达式结果为真时，则在结果中保留该元素，否则不保留该元素。
5.4. 路径jq中的path是指从根到某个叶子属性的访问路径。在jq中有两种表示路径的方式：数组表示法和属性表示法。属性表示法类似于我们在filter中访问某个属性值的方式，如.a.b。数组表示法是将路径中的每一部分表示为数组的一个元素。jq提供了一个内置函数path用来实现路径从属性表示法到数组表示法的转换。
jq还提供了函数用来读取路径的值（getpath）, 设置路径的值（setpath）和删除路径（del）。不过这三个函数对路径的处理并不一致。其中getpath和setpath只接受数组表示法的路径，而del函数只能正确处理属性表示法的路径。
jq还提供了一个函数paths用来枚举可能存在的路径。在没有参数的情况下，paths函数将输出JSON数据中所有可能的路径。paths函数可以接受一个过滤器，来只输出满足条件的路径。
jq中提供了一系列的函数用来判断某个元素或者属性是否存在于输入数据中。其中函数has和in用来判断JSON对象或数组是否包含特定的属性或索引。函数contains和inside用来判断参数是否完全包含在输入数据中。对于不同的数据类型，判断是否完全包含的规则不同。对于字符串，如果A是B的子字符串，则认为A完全包含于B。对于对象类型，如果对象A的所有属性在对象B中都能找到且值相同，则认为A完全包含于B。
5.5. 数组函数jq -nr &#x27;[1,[2,3],4]|flatten&#x27;jq -nr &#x27;[1,2,3]|reverse&#x27;jq -nr &#x27;[3,1,2]|sort&#x27;jq -nr &#x27;[&#123;&quot;a&quot;:1&#125;,&#123;&quot;a&quot;:2&#125;]|sort_by(.a)&#x27;jq -nr &#x27;&quot;abcb&quot;|indices(&quot;b&quot;)&#x27;jq -nr &#x27;[1,3,2,3]|indices(3)&#x27;
jq 提供内置函数用于完成数组的扁平化（flatten），反序（reverse），排序（sort、sort_by），比较（min、min_by、max、max_by）和查找（indices、index、rindex）。其中indices函数的输入数据可以是数组，也可以是字符串。和 index函数不同的是，其结果是一个包含所有参数在输入数据中位置的数组。
6. jq高级特性js高级特性包括：变量、Reduce、自定义函数和模块等。
已知file1.json内容为：
&#123;  &quot;auths&quot;: &#123;    &quot;harbor.voidking.com&quot;: &#123;      &quot;auth&quot;: &quot;YWRtaW46SGFyYm9yMTIzNDU=&quot;    &#125;  &#125;&#125;

file2.json内容为：
&#123;  &quot;auths&quot;: &#123;    &quot;harbor1.voidking.com&quot;: &#123;      &quot;username&quot;: &quot;haojin&quot;,      &quot;password&quot;: &quot;haojin123&quot;,      &quot;auth&quot;: &quot;aGFvamluOmhhb2ppbjEyMwo=&quot;    &#125;  &#125;&#125;

6.1. json相加json相加命令：
jq -s &#x27;add&#x27; file1.json file2.json# orjq -n &#x27;reduce inputs as $i (&#123;&#125;; . + $i)&#x27; file1.json file2.json

file1.json和file2.json相加的结果为：
&#123;  &quot;auths&quot;: &#123;    &quot;harbor1.voidking.com&quot;: &#123;      &quot;username&quot;: &quot;haojin&quot;,      &quot;password&quot;: &quot;haojin123&quot;,      &quot;auth&quot;: &quot;aGFvamluOmhhb2ppbjEyMwo=&quot;    &#125;  &#125;&#125;

file2.json会覆盖file1.json的一部分。
6.2. json组合json组合命令：
jq -s &#x27;.&#x27; file1.json file2.json

file1.json和file2.json组合的结果为：
[  &#123;    &quot;auths&quot;: &#123;      &quot;harbor.voidking.com&quot;: &#123;        &quot;auth&quot;: &quot;YWRtaW46SGFyYm9yMTIzNDU=&quot;      &#125;    &#125;  &#125;,  &#123;    &quot;auths&quot;: &#123;      &quot;harbor1.voidking.com&quot;: &#123;        &quot;username&quot;: &quot;haojin&quot;,        &quot;password&quot;: &quot;haojin123&quot;,        &quot;auth&quot;: &quot;aGFvamluOmhhb2ppbjEyMwo=&quot;      &#125;    &#125;  &#125;]

6.3. json合并json合并命令：
jq -n &#x27;reduce inputs as $i (&#123;&#125;; . * $i)&#x27; file1.json file2.json

file1.json和file2.json合并的结果为：
&#123;  &quot;auths&quot;: &#123;    &quot;harbor.voidking.com&quot;: &#123;      &quot;auth&quot;: &quot;YWRtaW46SGFyYm9yMTIzNDU=&quot;    &#125;,    &quot;harbor1.voidking.com&quot;: &#123;      &quot;username&quot;: &quot;haojin&quot;,      &quot;password&quot;: &quot;haojin123&quot;,      &quot;auth&quot;: &quot;aGFvamluOmhhb2ppbjEyMwo=&quot;    &#125;  &#125;&#125;



]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：nc命令</title>
    <url>/dev-linux-nc/</url>
    <content><![CDATA[1. nc命令简介netcat（通常简称为nc）是一个功能强大的网络工具，被誉为”网络瑞士军刀”。它可以通过TCP或UDP协议读写网络连接，提供了丰富的网络调试和探测功能。netcat最初由Hobbit在1996年发布，现已成为Linux/Unix系统管理员和网络安全专家的必备工具。
参考文档：

wikipedia - netcat
oracle - netcat
nmap - ncat



2. 不同版本的ncnetcat有多个实现，功能略有不同：

原始netcat（Hobbit版本）
GNU netcat
OpenBSD netcat（功能最丰富）
Ncat（来自nmap项目，支持SSL等高级功能）

3. 安装nc命令3.1. macos安装 GNU 版本 netcat
brew install netcat

3.2. centos安装 OpenBSD 版本 netcat
yum install -y netcat

3.3. ubuntu安装 OpenBSD 版本 netcat
apt install -y netcat

4. nc基本用法4.1. 端口扫描nc -zv example.com 20-80


-z：扫描模式，不发送任何数据
-v：详细输出
20-80：扫描20到80端口

4.2. 简单的TCP客户端/服务器服务器端（监听端口1234）：
nc -l 1234

客户端（连接到服务器）：
nc localhost 1234

4.3. 简单的UDP客户端/服务器服务器端：
nc -ul 1234

客户端：
nc -u localhost 1234

4.4. 监听端口并接收HTTP参数
启动一个TCP服务器
nc -k -l 4321
模拟发送http请求
curl &quot;http://localhost:1234&quot; \    -H &quot;Content-Type: application/json&quot; \    -H &quot;Authorization: Bearer xxx&quot; \    -d &#x27;&#123;        &quot;model&quot;: &quot;claude-3-5-sonnet-v2&quot;,        &quot;messages&quot;: [            &#123;                &quot;role&quot;: &quot;user&quot;,                &quot;content&quot;: &quot;hello&quot;            &#125;        ]    &#125;&#x27;
观察TCP服务器的输出可以看到输出内容为：
POST / HTTP/1.1Host: localhost:1234User-Agent: curl/8.7.1Accept: */*Content-Type: application/jsonAuthorization: Bearer xxxContent-Length: 175&#123;        &quot;model&quot;: &quot;claude-3-5-sonnet-v2&quot;,        &quot;messages&quot;: [            &#123;                &quot;role&quot;: &quot;user&quot;,                &quot;content&quot;: &quot;hello&quot;            &#125;        ]    &#125;

5. 高级用法5.1. 文件传输从server1发送文件到server2：
在接收方（server2）：
nc -l 1234 &gt; received_file

在发送方（server1）：
nc server2 1234 &lt; file_to_send

5.2. 发送HTTP请求nc example.com 80 &lt;&lt; EOFGET / HTTP/1.1Host: example.comEOF

5.3. 端口转发把所有发送到本地1234端口的数据转发到example.com的80端口。
nc -l 1234 | nc example.com 80

5.4. 代理服务器把所有发送到本地1234端口的数据转发到example.com的80端口，并将返回转发给客户端。
mkfifo proxy_pipenc -l 1234 0&lt;proxy_pipe | nc example.com 80 1&gt;proxy_pipe

5.5. 远程shellnc命令可以实现远程shell，但是因为版本问题会出现各种报错，因此建议使用ncat。

安装ncat
yum install -y nmap-ncat # centosapt install -y ncat # ubuntu
服务器端（谨慎使用，有安全风险）
ncat -l -p 1234 -e /bin/bash --allow 192.168.1.100
客户端
ncat server_ip 1234ls

6. nc命令的替代虽然netcat很强大，但有些场景下这些工具可能更适合：

socat：更强大的netcat替代品
ncat：支持SSL和更多协议，是 netcat 的增强版
telnet：简单的交互式连接
curl/wget：HTTP/HTTPS专用

7. 小结netcat是一个极其灵活的网络工具，可以用于：

端口扫描和测试
简单的网络服务搭建
文件和数据的网络传输
网络调试和故障排除
作为其他工具的组件构建更复杂的解决方案

掌握netcat可以提高在Linux环境下的网络管理和故障排除效率。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker Swarm</title>
    <url>/dev-docker-swarm/</url>
    <content><![CDATA[1. Docker Swarm简介Docker Swarm是一个Docker集群调度管理工具。相比于Kubernetes，Swarm内置于Docker，更加轻量，更加简单方便；相应的，就没有Kubernetes那么多强大的特性。
本文搭建使用Swarm，主要参考Swarm mode overview 和 docker swarm（一） 入门 – 搭建一个简单的swarm集群。
已有两个安装好Docker的机器（swarm-manager和swarm-worker），IP分别为 192.168.56.104 和 192.168.56.105 ，两台主机的hosts都添加：
192.168.56.104 swarm-manager192.168.56.105 swarm-worker



2. Swarm集群搭建2.1. manager node在swarm-manager节点：
1、创建swarm manager
docker swarm init --advertise-addr 192.168.56.104

这一步会生成join命令，复制记录一下。
2、验证安装
docker infodocker node ls

3、关闭防火墙，方便其他主机加入成为worker
systemctl stop firewalldsystemctl disable firewalld

2.2. worker node在swarm-worker节点：1、关闭防火墙，方便manager调用
systemctl stop firewalldsystemctl disable firewalld

2、加入swarm成为worker
docker swarm join --token SWMTKN-1-21v9lvwrzapfzdzhq4js6xbtyasak4hdcdhz9p13fch23zdbn9-bneduvi9fi4ba05eywhqbe94z 192.168.56.104:2377

PS：脱离swarmdocker swarm leave
2.3. 验证docker node ls，查看节点状态。
3. 使用Swarm1、部署一个服务
docker service create \--name=vk-nginx \--publish published=80,target=80 \--replicas 1  \voidking/nginx:v1.0

2、查看服务
docker service lsdocker inspect vk-nginx

3、访问服务
curl 192.168.56.104curl 192.168.56.105
如果此时有一个ip是curl不通的，那么说明有问题，因为所有节点都应该添加端口映射关系。
4、服务扩容
docker service scale vk-nginx=4

5、查看服务在节点的分布
docker service ps vk-nginx

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>swarm</tag>
      </tags>
  </entry>
  <entry>
    <title>youtube、youku、爱奇艺等视频平台下载方法</title>
    <url>/hobby-video-download/</url>
    <content><![CDATA[1. 视频下载的痛点很多时候，我们在youtube、youku、爱奇艺、腾讯视频、bilibili等视频平台看到了喜欢的视频，想要下载到本地，但是无法下载，要么必须要客户端才能下载。好不容易下载到本地，对于不同平台下载的视频，还必须使用不同的客户端进行播放。如果想要作为教学视频放到PPT中，还必须进行转码，不同平台还得寻找不同的转码方法。
太难了，好在一些前辈帮我们解决了这些问题，并且开发出了很多好用的工具。本文中，重点推荐两款个人在用的视频下载工具：youtube-dl和you-get。


2. youtube-dl1、访问youtube-dl，下载需要的版本，这里以macos版本为例
sudo curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dlsudo chmod a+rx /usr/local/bin/youtube-dl# or brew install youtube-dl

2、查看视频支持的下载格式，下载视频
youtube-dl -F http://v.youku.com/v_show/id_XODQzMTQ4NDQ=.htmlyoutube-dl -f mp4 http://v.youku.com/v_show/id_XODQzMTQ4NDQ=.html

3. you-get1、访问you-get，下载需要的版本，这里以macos版本为例
pip3 install you-get# orbrew install you-get

2、查看视频支持的下载格式，下载视频
you-get -i http://v.youku.com/v_show/id_XODQzMTQ4NDQ=.htmlyou-get http://v.youku.com/v_show/id_XODQzMTQ4NDQ=.html#you-get --itag=18 &#x27;https://www.youtube.com/watch?v=jNQXAC9IVRw&#x27;

4. 对比这两个软件功能上没有太大差别，下载速度也没有太大差别。youtube-dl更加轻量，安装也要比you-get快的多（you-get依赖一堆软件）。对于同一个视频，这两个软件支持的下载格式有所不同，比如上面的例子中，youtube-dl支持mp4格式，you-get支持flv格式。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>下载</tag>
      </tags>
  </entry>
  <entry>
    <title>Prometheus Operator + Blackbox exporter</title>
    <url>/dev-prometheus-operator-blackbox-exporter/</url>
    <content><![CDATA[1. 前言《Kubernetes Operator》一文中学习了Operator的基础，《Prometheus Blackbox exporter》一文中学习了blackbox exporter的安装配置。
而Prometheus Operator，顾名思义，是负责K8S中自动化管理Prometheus的Custom Controller。更多内容，参考coreos/prometheus-operator
本文中，我们研究的问题是：怎样利用Prometheus Operator，在Kubernetes集群中安装部署Prometheus，并且添加Blackbox exporter组件？


2. 安装Prom Operator参考Prometheus Operator 初体验和coreos/kube-prometheus，安装Prometheus Operator。
1、kubelet配置添加参数vim /etc/systemd/system/kubelet.service.d/10-kubeadm.conf添加：
--authentication-token-webhook=true--authorization-mode=Webhook 

2、获取源码，并切换版本（与k8s版本的对应关系可以在github仓库找到）
git clone https://github.com/coreos/kube-prometheus.gitcd kube-prometheuskubectl versiongit branch -agit checkout origin/release-0.4

3、安装Prom Operator
# Create the namespace and CRDs, and then wait for them to be availble before creating the remaining resourceskubectl create -f manifests/setupuntil kubectl get servicemonitors --all-namespaces ; do date; sleep 1; echo &quot;&quot;; donekubectl create -f manifests/

4、查看安装
kubectl get crd | grep coreoskubectl get pod -n monitoringkubectl get svc -n monitoring
以上，Prometheus Operator安装完成，Prometheus也安装完成。
PS：卸载Prom Operator
kubectl delete --ignore-not-found=true -f manifests/ -f manifests/setup

3. 安装Blackbox exporter1、创建yaml文件 blackbox-exporter.yaml
apiVersion: v1data:  config.yml: |    modules:      http_2xx:        prober: http        http:          method: GET          preferred_ip_protocol: &quot;ip4&quot;      http_post_2xx:        prober: http        http:          method: POST          preferred_ip_protocol: &quot;ip4&quot;      tcp:        prober: tcp      ping:        prober: icmp        timeout: 3s        icmp:          preferred_ip_protocol: &quot;ip4&quot;      dns_k8s:        prober: dns        timeout: 5s        dns:          transport_protocol: &quot;tcp&quot;          preferred_ip_protocol: &quot;ip4&quot;          query_name: &quot;kubernetes.default.svc.cluster.local&quot;          query_type: &quot;A&quot;kind: ConfigMapmetadata:  name: blackbox-exporter  namespace: monitoring---apiVersion: apps/v1kind: Deploymentmetadata:  creationTimestamp: null  labels:    name: blackbox-exporter    cluster: ali-huabei2-dev  name: blackbox-exporter  namespace: monitoringspec:  replicas: 1  selector:    matchLabels:      name: blackbox-exporter  strategy: &#123;&#125;  template:    metadata:      creationTimestamp: null      labels:        name: blackbox-exporter        cluster: ali-huabei2-dev    spec:      containers:      - image: prom/blackbox-exporter:v0.16.0        name: blackbox-exporter        ports:        - containerPort: 9115        volumeMounts:        - name: config          mountPath: /etc/blackbox_exporter        args:        - --config.file=/etc/blackbox_exporter/config.yml        - --log.level=info      volumes:      - name: config        configMap:          name: blackbox-exporter---apiVersion: v1kind: Servicemetadata:  #annotations:  #  service.beta.kubernetes.io/alicloud-loadbalancer-address-type: intranet  labels:    name: blackbox-exporter    cluster: ali-huabei2-dev  name: blackbox-exporter  namespace: monitoringspec:  #externalTrafficPolicy: Local  selector:    name: blackbox-exporter  ports:  - name: http-metrics    port: 9115    targetPort: 9115  type: LoadBalancer

2、应用yaml文件
kubectl apply -f blackbox-exporter.yamlkubectl get svc -n monitoringkubectl get deploy -n monitoring

4. 配置使用Blackbox exporter（错误方法）在Prometheus中配置使用Blackbox exporter是很简单的，scrape_configs里配置相应字段即可。但是，k8s中的Prometheus配置，会有一些不同。
1、获取prometheus.yml配置
kubectl get secrets -n monitoring prometheus-k8s -oyaml | grep prometheus.yaml.gz | awk &#x27;&#123;print $2&#125;&#x27; | base64 --decode | gzip -d &gt; prometheus.yml

2、查看prometheus.yml配置，下面截取一段：
global:  evaluation_interval: 30s  scrape_interval: 30s  external_labels:    prometheus: monitoring/k8s    prometheus_replica: $(POD_NAME)rule_files:- /etc/prometheus/rules/prometheus-k8s-rulefiles-0/*.yamlscrape_configs:- job_name: monitoring/node-exporter/0  honor_labels: false  kubernetes_sd_configs:  - role: endpoints    namespaces:      names:      - monitoring  scrape_interval: 15s  scheme: https  tls_config:    insecure_skip_verify: true  bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token  relabel_configs:  - action: keep    source_labels:    - __meta_kubernetes_service_label_k8s_app    regex: node-exporter  - action: keep    source_labels:    - __meta_kubernetes_endpoint_port_name    regex: https  - source_labels:    - __meta_kubernetes_endpoint_address_target_kind    - __meta_kubernetes_endpoint_address_target_name    separator: ;    regex: Node;(.*)    replacement: $&#123;1&#125;    target_label: node  - source_labels:    - __meta_kubernetes_endpoint_address_target_kind    - __meta_kubernetes_endpoint_address_target_name    separator: ;    regex: Pod;(.*)    replacement: $&#123;1&#125;    target_label: pod  - source_labels:    - __meta_kubernetes_namespace    target_label: namespace  - source_labels:    - __meta_kubernetes_service_name    target_label: service  - source_labels:    - __meta_kubernetes_pod_name    target_label: pod  - source_labels:    - __meta_kubernetes_service_name    target_label: job    replacement: $&#123;1&#125;  - source_labels:    - __meta_kubernetes_service_label_k8s_app    target_label: job    regex: (.+)    replacement: $&#123;1&#125;  - target_label: endpoint    replacement: https  - source_labels:    - __meta_kubernetes_pod_node_name    target_label: instance    regex: (.*)    replacement: $1    action: replace  - source_labels:    - __meta_kubernetes_service_label_cluster    target_label: cluster    regex: (.*)    replacement: $1    action: replace
其中，job_name配置target名称，kubernetes_sd_configs配置k8s的服务发现，relabel_configs配置标签最终的显示。source_labels是样本的原标签，target_label是显示的标签；regex使用正则匹配value，replacement代表最终显示的value。$1代表regex正则匹配到的第一个字符串。
3、添加blackbox exporter的配置
- job_name: monitoring/blackbox-exporter/0  honor_labels: false  kubernetes_sd_configs:  - role: endpoints    namespaces:      names:      - monitoring  scrape_interval: 15s  scheme: http  tls_config:    insecure_skip_verify: true  bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token  relabel_configs:  - action: keep    source_labels:    - __meta_kubernetes_service_label_name    regex: blackbox-exporter  - source_labels:    - __meta_kubernetes_service_label_name    target_label: job    regex: (.+)    replacement: $&#123;1&#125;  - source_labels:    - __meta_kubernetes_service_label_cluster    target_label: cluster    regex: (.*)    replacement: $1    action: replace

4、应用新的配置
# 1. compress prometheus.yamlcat prometheus.yaml | gzip -f | base64 | tr -d &quot;\n&quot;# 2. copy string# 3. edit secretkubectl edit secrets -n monitoring prometheus-k8s# 4. replace prometheus.yaml.gz# 5. get the latest configkubectl get secrets -n monitoring prometheus-k8s -oyaml | grep prometheus.yaml.gz | awk &#x27;&#123;print $2&#125;&#x27; | base64 --decode | gzip -d | grep blackbox
然而，配置中并没有blackbox，配置没有发生改变！证明了prometheus的配置是自动生成的，手动修改无效。
5. 配置使用Blackbox exporter（正确方法）Prometheus Operator中配置Target，是利用ServiceMonitor进行动态发现的方式。
1、创建servicemonitor的yaml文件，blackbox-exporter-sm.yaml
apiVersion: monitoring.coreos.com/v1kind: ServiceMonitormetadata:  labels:    name: blackbox-exporter    release: p  name: blackbox-exporter  namespace: monitoringspec:  namespaceSelector:    matchNames:    - monitoring  selector:    matchLabels:      name: blackbox-exporter  endpoints:  - interval: 15s    port: http-metrics    path: /probe    relabelings:    - action: replace      regex: (.*)      replacement: $1      sourceLabels:      - __meta_kubernetes_service_label_cluster      targetLabel: cluster    - action: replace      regex: (.*)      replacement: $1      sourceLabels:      - __param_module      targetLabel: module    - action: replace      regex: (.*)      replacement: $1      sourceLabels:      - __param_target      targetLabel: target    params:      module:      - http_2xx      target:      - http://prometheus.io    # Target to probe with http.      - https://prometheus.io   # Target to probe with https.      - http://example.com:8080 # Target to probe with http on port 8080.  - interval: 15s    port: http-metrics    path: /probe    relabelings:    - action: replace      regex: (.*)      replacement: $1      sourceLabels:      - __meta_kubernetes_service_label_cluster      targetLabel: cluster    - action: replace      regex: (.*)      replacement: $1      sourceLabels:      - __param_module      targetLabel: module    - action: replace      regex: (.*)      replacement: $1      sourceLabels:      - __param_target      targetLabel: target    params:      module:      - dns_k8s      target:      - 172.31.16.10 # dns ip address

2、应用到k8s集群kubectl apply -f blackbox-exporter-sm.yaml
3、等待一分钟后，进行验证访问prometheus的graph页面，可以查看blackbox-exporter指标。
&#123;job=~&quot;blackbox-exporter&quot;,__name__!~&quot;^go.*&quot;&#125;
查看结果表明，params的配置中，http_2xx 探测只有第一个target生效了，另外两个target根本没有探测记录。本实验证明了，target里只能填写一个域名，多了无效。要想配置多个站点的探测，最简单的办法就是配置多个endpoint。至于N个站点配置M种探测方式，如果你知道怎么配置，欢迎留言告知，感谢~
6. 配置告警《使用Docker安装配置Prometheus》一文中，我们知道配置告警需要在prometheus配置文件中指定alertmanager实例和报警的rules文件。而通过operator部署的prometheus，怎样配置告警呢？这里需要定义PrometheusRule资源，并且具备标签 prometheus=k8s 和 role=alert-rules。这里以配置dns服务告警为例，dns服务出问题，不能正常解析 kubernetes.default.svc.cluster.local 。
1、查看alertmanager配置
kubectl get secrets -n monitoring alertmanager-main -oyaml | grep &quot;alertmanager.yaml&quot; | awk &#x27;&#123;print $2&#125;&#x27; | base64 -d

2、创建prometheus-rule-dns.yaml
apiVersion: monitoring.coreos.com/v1kind: PrometheusRulemetadata:  labels:    prometheus: k8s    role: alert-rules  name: dns-alert-rules  namespace: monitoringspec:  groups:  - name: DNS    rules:    - alert: DNSServerError      annotations:        summary: No summary        description: No description        webhookToken: xxxxxxxxx      expr: |        probe_success&#123;module=&quot;dns_k8s&quot;&#125; == 0      for: 1m      labels:        severity: critical        alertTag: k8s

3、应用rulekubectl apply -f prometheus-rule-dns.yaml
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
        <tag>k8s</tag>
      </tags>
  </entry>
  <entry>
    <title>Prometheus Blackbox exporter</title>
    <url>/dev-prometheus-blackbox-exporter/</url>
    <content><![CDATA[1. 白盒监控和黑盒监控我们监控主机的资源用量、容器的运行状态、数据库中间件的运行数据。这些都是支持业务和服务的基础设施，通过白盒能够了解其内部的实际运行状态，通过对监控指标的观察能够预判可能出现的问题，从而对潜在的不确定因素进行优化。而从完整的监控逻辑的角度，除了大量的应用白盒监控以外，还应该添加适当的黑盒监控。黑盒监控即以用户的身份测试服务的外部可见性，常见的黑盒监控包括HTTP探针、TCP探针等用于检测站点或者服务的可访问性，以及访问效率等。
黑盒监控相较于白盒监控最大的不同在于黑盒监控是以故障为导向当故障发生时，黑盒监控能快速发现故障，而白盒监控则侧重于主动发现或者预测潜在的问题。一个完善的监控目标是要能够从白盒的角度发现潜在问题，能够在黑盒的角度快速发现已经发生的问题。
Prometheus Blackbox exporter允许通过HTTP，HTTPS，DNS，TCP和ICMP对端点进行黑盒探测。
本文中，我们会安装blackbox exporter，并且使用http探针对一些接口进行探活。
参考文档：

github - prometheus/blackbox_exporter
网络探测：Blackbox Exporter
如何使用 Blackbox Exporter 监控 URL?
Prometheus 黑盒监控
How to probe an ADDRESS + URL PATH?



2. 安装配置Blackbox exporterBlackbox exporter的安装方法有两种：bin安装和docker安装。这里我们选择使用docker安装。
1、创建配置文件 /opt/exporter/blackbox/blackbox.yml
modules:  http_2xx:    prober: http    http:      method: GET      preferred_ip_protocol: &quot;ip4&quot;      valid_status_codes: [200,201]  http_post_2xx:    prober: http    http:      method: POST      preferred_ip_protocol: &quot;ip4&quot;      valid_status_codes: [200,201]  tcp:    prober: tcp  ping:    prober: icmp    timeout: 3s    icmp:      preferred_ip_protocol: &quot;ip4&quot;  dns_baidu:    prober: dns    timeout: 5s    dns:      query_name: &quot;baidu.com&quot;      query_type: &quot;A&quot;      preferred_ip_protocol: &quot;ip4&quot;
探测时默认使用ipv6，探测不支持ipv6的站点都会显示失败，因此在配置时改成了ipv4，更多内容参考UNDERSTANDING AND USING THE MULTI-TARGET EXPORTER PATTERN。注意，最后一个配置项 dns_baidu 的作用是探测DNS解析 baidu.com 是否可以正常解析。
2、启动blackbox exporter
docker run --name blackbox-exporter -d \  -p 9115:9115  \  -v /opt/exporter/blackbox/blackbox.yml:/config/blackbox.yml \  prom/blackbox-exporter:v0.25.0 --config.file=/config/blackbox.yml

3、测试
curl &quot;http://127.0.0.1:9115/probe?module=http_2xx&amp;target=baidu.com&quot;curl &quot;http://127.0.0.1:9115/probe?module=http_post_2xx&amp;target=baidu.com&quot;curl &quot;http://127.0.0.1:9115/probe?module=tcp&amp;target=baidu.com:80&quot;curl &quot;http://127.0.0.1:9115/probe?module=ping&amp;target=baidu.com&quot;curl &quot;http://127.0.0.1:9115/probe?module=dns_baidu&amp;target=180.76.76.76&quot;
从返回的样本中，可以获取站点的DNS解析耗时、站点响应时间、HTTP响应状态码等等和站点访问质量相关的监控指标，从而帮助管理员主动的发现故障和问题。
3. 与Prometheus集成3.1. 简单配置修改 prometheus.yml，添加blackbox exporter相关字段。
scrape_configs:   - job_name: baidu_http2xx_probe    params:      module:      - http_2xx      target:      - baidu.com    metrics_path: /probe    static_configs:    - targets:      - 127.0.0.1:9115  - job_name: prometheus_http2xx_probe    params:      module:      - http_2xx      target:      - prometheus.io    metrics_path: /probe    static_configs:    - targets:      - 127.0.0.1:9115
这里分别配置了名为 baidu_http2x_probe 和 prometheus_http2xx_probe 的采集任务，并且通过params指定使用的探针（module）以及探测目标（target）。
3.2. 高级配置以上配置会有一个问题，假如我们有N个目标站点且都需要M种探测方式，那么Prometheus中将包含N * M 个采集任务，从配置管理的角度来说显然是不可接受的。这里我们利用Prometheus的Relabeling方式对这些配置进行简化，配置方式如下：
scrape_configs:  - job_name: &#x27;blackbox&#x27;    metrics_path: /probe    params:      module: [http_2xx]    static_configs:      - targets:        - http://prometheus.io    # Target to probe with http.        - https://prometheus.io   # Target to probe with https.        - http://example.com:8080 # Target to probe with http on port 8080.        - http://192.168.56.102:8080/healthz # Target to probe with http on path /healthz    relabel_configs:      - source_labels: [__address__]        target_label: __param_target      - source_labels: [__param_target]        target_label: instance      - target_label: __address__        replacement: 192.168.56.102:9115 # The blackbox exporter&#x27;s real hostname:port.

这里针对每一个探针服务（如http_2xx）定义一个采集任务，并且直接将任务的采集目标定义为我们需要探测的站点。在采集样本数据之前通过relabel_configs对采集任务进行动态设置。
上面的配置，实际上相当于：
curl &quot;http://192.168.56.102:9115/probe?module=http_2xx&amp;target=http://prometheus.io&quot;curl &quot;http://192.168.56.102:9115/probe?module=http_2xx&amp;target=https://prometheus.io&quot;curl &quot;http://192.168.56.102:9115/probe?module=http_2xx&amp;target=http://example.com:8080&quot;

这个配置实际上是比较奇怪的：static_configs.targets 一般代表的是用来exporter对外暴露的接口，但是blackbox这里的 static_configs.targets 代表的是blackbox要探测的站点，relabel_configs.replacement 才是blackbox exporter对外暴露的接口。blackbox-exporter的这种配置方式，与mysql-exporter、redis-exporter是一致的。
重启prometheus，然后进行验证：（1）访问blackbox-exporter页面，可以看到探测记录的日志。（2）访问prometheus的target页面，即可看到blackbox-exporter。（3）访问prometheus的graph页面，可以查看blackbox-exporter指标。 &#123;job=~&quot;blackbox&quot;,__name__!~&quot;^go.*&quot;&#125;
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：Kubernetes Operator</title>
    <url>/dev-k8s-operator/</url>
    <content><![CDATA[1. Kubernetes Operator是啥？
Operators are software extensions to Kubernetes that make use of custom resources to manage applications and their components. Operators follow Kubernetes principles, notably the control loop.

Operator 是 Kubernetes 的扩展软件，通过自定义资源，管理应用及其组件。 Operator 遵循 Kubernetes 的理念，特别是在控制回路方面。
Kubernetes自定义资源包括Custom resources 和 Custom controllers。
Operator 模式旨在捕获（正在管理一个或一组服务的）运维人员的关键目标。负责特定应用和service的运维人员，在系统应该如何运行、如何部署以及出现问题时如何处理等方面有深入的了解。在 Kubernetes 上运行工作负载的人们都喜欢通过自动化来处理重复的任务。Operator 模式会封装我们编写的（Kubernetes 本身提供功能以外的）任务自动化代码。
Operator可以自动化的事情包括：

按需部署应用
获取/还原应用状态的备份
处理应用代码的升级以及相关改动。例如，数据库 schema 或额外的配置设置
发布一个 service，要求不支持 Kubernetes API 的应用也能发现它
模拟整个或部分集群中的故障以测试其稳定性
在没有内部成员选举程序的情况下，为分布式应用选择首领角色

简单来说，Kubernetes实际是期望状态管理器，如果Kubernetes本身提供的功能无法达到期望状态，特别是对于有状态应用，那么就需要手动处理，而Operator可以使这种处理自动化。
更多内容，参考Operator 模式。
本文中，将会学习编写一个Operator，并应用到k8s集群。


2. 工具Operator的yaml文件和go文件，可以纯手写，没毛病。但是使用工具能够提高我们的编写效率。编写Operator的工具有：

KUDO，Kubernetes 通用声明式 Operator)
kubebuilder
Metacontroller，可与 Webhook 结合使用，以实现自己的功能。
Operator Framework

查找和分享Operator：OperatorHub
3. Operator FrameworkOperator Framework是一个开源工具包，用来管理被称为operators的k8s应用程序，高效、自动化、可扩展。它的两个核心部分是Operator SDK 和 Operator Lifecycle Manager 。
Operator SDK：允许开发人员根据专业知识来构建Operator，而无需了解Kubernetes API的复杂性。Operator Lifecycle Manager：帮助用户安装、更新和总体管理跨集群运行的所有Operators（及其相关服务）的生命周期。
在开发机上安装Operator SDK，在集群中安装Operator Operator Lifecycle Manager。
对于这两个组件，可以在katacoda上试玩，熟悉它们的安装和使用，Building Operators on OpenShift。
4. 第一个Operator参考Kubernetes Operator 快速入门教程，使用Operator Framework开发一个Operator应用，代码仓库cnych/opdemo。
5. 书签Introducing Operators: Putting Operational Knowledge into SoftwareBest practices for building Kubernetes Operators and stateful apps揭秘Kubernetes Operator（一）第一次玩 operator-sdk 就上手
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>k8s</tag>
        <tag>源码</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>kube-controller-manager指标收集</title>
    <url>/dev-kube-controller-manager-metrics/</url>
    <content><![CDATA[1. 前言大家都知道需要监控k8s指标，但是具体需要监控k8s的哪些组件的哪些指标？本文，就来学习梳理kube-controller-mananger组件的监控指标。

2. 有哪些指标？要收集指标，首先要知道有哪些指标可以收集。方法一：使用curl命令
k get pods -n kube-system -o wide | grep kube-controller-managerk get svc -n kube-system -o wide | grep kube-controller-managerssh &lt;clusterhost&gt;curl localhost:&lt;nodeport&gt;/metrics

方法二：在prometheus使用promql
count(&#123;job=&quot;kube-controller-manager&quot;&#125;) by (__name__)count(&#123;job=&quot;kube-controller-manager&quot;,__name__!~&quot;^go_.*&quot;&#125;) by (__name__)

3. 指标含义？找到了指标，接下来需要知道指标的含义。访问 Splunk doc - kube-controller-manager，使用指标名称搜索即可。
比如搜索：rest_client_request_duration_seconds_bucket得到结果：rest_client_request_duration_seconds_bucket (cumulative)Request latency in seconds. Broken down by verb and URL. (bucket)
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
        <tag>k8s</tag>
      </tags>
  </entry>
  <entry>
    <title>Prometheus查询语言：PromQL</title>
    <url>/dev-prometheus-query-language-promql/</url>
    <content><![CDATA[1. PromQL简介
Prometheus提供了一种称为PromQL（Prometheus查询语言）的功能查询语言，使用户可以实时选择和汇总时间序列数据。表达式的结果既可以显示为图形，也可以在Prometheus的表达式浏览器中显示为表格数据，也可以由外部系统通过HTTP API使用。

参考文档：

QUERYING PROMETHEUS 
QUERYING FUNCTIONS
表达式语言运算符
QUERY EXAMPLE
探索PromQL
PromQL 简明教程



2. 基本概念2.1. 指标（Metric）指标是一种测量数据，通常由名称和一组标签组成。
指标（样本）格式：
metric_name [ &#123;label_name=&quot;label_value&quot;&#125; ] value [ timestamp ]

指标说明：

metric_name：指标名称（本质是__name__的标签值），只能使用字母、数字、下划线和冒号，区分大小写，不能以下划线和数字开头
value：指标值，是一个float格式的数据
label_name：标签名称，只能使用字母、数字和下划线，区分大小写，不能以下划线和数字开头
label_value：标签值，字符串
timestamp：时间戳，类型为int64（从1970-01-01 00:00:00以来的毫秒数），timestamp可选，默认为当前时间
注释：#号开头表示注释
指标组：具有相同 metric_name 的样本必须按照一个组的形式排列，并且每一行必须是唯一的指标名称和标签键值对组合
两种指标类型：Gauge是当前状态的数值，Counter是一个永远递增的数值。

指标示例：
http_requests_total&#123;host=&quot;www.voidking.com&quot;, method=&quot;GET&quot;, status=&quot;200&quot;&#125; 34http_requests_total&#123;host=&quot;blog.voidking.com&quot;, method=&quot;GET&quot;, status=&quot;200&quot;&#125; 21

该指标组表示 HTTP GET 请求中状态码为 200 的计数，host 、method 和 status 是标签。
2.2. 时间序列（Time Series）时间序列是一系列具有相同指标和标签组合的数据点，按时间排序。
Prometheus会将所有采集到的指标数据以时间序列（time-series）的方式保存在内存数据库中，并且定时保存到硬盘上。
时间序列示例：
http_request_total&#123;host=&quot;www.voidking.com&quot;, method=&quot;GET&quot;, status=&quot;200&quot;&#125;@1434417560000 =&gt; 30http_request_total&#123;host=&quot;www.voidking.com&quot;, method=&quot;GET&quot;, status=&quot;200&quot;&#125;@1434417561000 =&gt; 34http_request_total&#123;host=&quot;www.voidking.com&quot;, method=&quot;GET&quot;, status=&quot;200&quot;&#125;@1434417562000 =&gt; 45

我们可以将time-series理解为一个数字矩阵，X轴是时间戳，Y轴是指标。
3. PromQL表达式3.1. 基本选择器基本选择器用于从 Prometheus 中选择特定的时间序列数据。它通过指标名称和标签条件来过滤数据。例如：
http_requests_total&#123;method=&quot;GET&quot;, status=&quot;200&quot;&#125;

这个表达式选择了当前时刻所有 HTTP GET 请求中状态码为 200 的时间序列，是瞬时数据（瞬时向量）。
Prometheus绘图时，只接受瞬时向量。
3.2. 操作符PromQL 支持多种操作符，用于在表达式中进行逻辑运算、比较和匹配。常见操作符有：

==：等于
!=：不等于
=~：正则表达式匹配
!~：不匹配正则表达式
and、or：逻辑与、逻辑或

PromQL 中的所有正则表达式都使用RE2语法。
例如：
http_requests_total&#123;method=~&quot;POST|PUT&quot;&#125; and status=&quot;500&quot;

这个表达式选择了所有方法为 POST 或 PUT 且状态码为 500 的时间序列。
3.3. 时间范围选择PromQL 允许我们选择特定时间范围内的数据，用于计算速率、增量等。例如：
http_requests_total[5m]

这个表达式选择了过去 5 分钟内的 HTTP 请求计数，是范围数据（范围向量）。
持续时间以数字指定，后面跟着单位。单位标识：s - seconds，m - minutes，h - hours，d - days，w - weeks，y - years
3.4. 偏移量偏移量允许更改查询中各个瞬时向量和范围向量的时间偏移。
http_requests_total offset 5msum(http_requests_total&#123;method=&quot;GET&quot;&#125; offset 5m) // GOOD.sum(http_requests_total&#123;method=&quot;GET&quot;&#125;) offset 5m // INVALID.rate(http_requests_total[5m] offset 1w)

3.5. 聚合函数聚合函数用于对时间序列数据进行聚合计算，从而获得更高级别的洞察力。常见的聚合函数有：

sum：求和，所有指标名相同的指标求和
avg：平均值，所有指标名相同的指标求平均值
min：最小值，所有指标名相同的指标求最小值
max：最大值，所有指标名相同的指标求最大指
count：计数，计算指标名相同的指标的数量

例如：
sum(http_requests_total) by (method)

这个表达式按照 HTTP 请求方法对请求计数进行了求和。
使用 by 关键字，我们可以按照指定的标签对时间序列数据进行分组，从而进行聚合操作。
3.6. 向量操作和标量操作PromQL 支持对向量（time series vectors）和标量（scalar）之间进行操作。例如：
http_requests_total + 100

这个表达式将每个时间序列的值都增加了 100。
3.7. 函数调用PromQL 提供了多种内置函数，用于执行各种操作，如计算速率、计算增量、应用分位数等。常见函数有：

rate：计算速率，根据指定的时间范围内的第一个数据点和最后一个数据点的值，计算它们之间的增加量，并将结果转化为每秒的速率，结果单位是数据单位 per second
increase：计算增量，根据指定的时间范围内的第一个数据点和最后一个数据点的值，计算它们之间的增加量
histogram_quantile：计算分位数，计算数据在不同区间的频率
irate：计算速率，根据最后两个点的差值来计算增加量，并将结果转化为每秒的速率，结果单位是数据单位 per second。此时的数据范围不是用来计算的，而是用来限制向前找点的时间范围的，时间范围内没有点，这个点的计算就放弃了。

例如：
rate(http_requests_total[1h])

这个表达式计算了过去 1 小时内 HTTP 请求计数的速率。函数通常需要接受一个或多个参数，参数可以是指标、标量、向量等。上例中的http_requests_total[1h]就是参数。
increase(http_requests_total[5m])

这个表达式计算了过去 5 分钟内 HTTP 请求的增量。
histogram_quantile(0.95, http_request_duration_seconds_bucket)
这个表达式计算了 HTTP 请求持续时间的分布中的 95 分位数值，即在所有持续时间中，95% 的请求的持续时间小于等于这个值。
3.8. 注释在 PromQL 中，我们可以使用 # 来添加注释，以提高查询的可读性。例如：
# Calculate the error rate of HTTP requestsrate(http_requests_total&#123;status=~&quot;5..&quot;&#125;[1h])

这个表达式计算了过去 1 小时内 HTTP 请求状态码以 “5” 开头（表示服务器错误）的时间序列的速率。
3.9. 子查询子查询对给定的范围进行即时查询。子查询的结果是范围向量。语法：
&lt;instant_query&gt; &#x27;[&#x27; &lt;range&gt; &#x27;:&#x27; [&lt;resolution&gt;] &#x27;]&#x27; [ offset &lt;duration&gt; ]

resolution是可选的。默认值为全局评估间隔。
4. 例子4.1. 简单时间序列查询http_requests_total&#123;__name__=&quot;http_requests_total&quot;&#125;http_requests_total&#123;job=&quot;apiserver&quot;, handler=&quot;/api/comments&quot;&#125;http_requests_total&#123;job=&quot;apiserver&quot;, handler=&quot;/api/comments&quot;&#125;[5m]http_requests_total&#123;job=~&quot;.*server&quot;&#125;http_requests_total&#123;status!~&quot;4..&quot;&#125;

4.2. 子查询rate(http_requests_total[5m])[30m:1m]max_over_time(deriv(rate(distance_covered_total[5s])[30s:5s])[10m:])

4.3. 函数运算等rate(http_requests_total[5m])sum by (job) (  rate(http_requests_total[5m]))(instance_memory_limit_bytes - instance_memory_usage_bytes) / 1024 / 1024sum by (app, proc) (  instance_memory_limit_bytes - instance_memory_usage_bytes) / 1024 / 1024topk(3, sum by (app, proc) (rate(instance_cpu_time_ns[5m])))count by (app) (instance_cpu_time_ns)

]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>promql</tag>
        <tag>监控</tag>
      </tags>
  </entry>
  <entry>
    <title>httpload使用说明</title>
    <url>/dev-httpload/</url>
    <content><![CDATA[1. httpload简介http_load可以并行启动多个http请求，以便测试Web服务器的吞吐量。但是，与大多数测试客户端不同的是，它是单线程运行的，因此不会使客户端计算机产生大量负载。此外，http_load还可以配置支持https请求。更多内容参考http_load - multiprocessing http test client。


2. 安装1、安装openssl，以便支持https
yum install opensslyum install openssl-devel

2、下载httpload并解压
# wget http://acme.com/software/http_load/http_load-12mar2006.tar.gzwget http://www.acme.com/software/http_load/http_load-09Mar2016.tar.gztar -xzvf http_load-09Mar2016.tar.gzcd http_load-09Mar2016/

3、编辑Makefile，取消SSL相关注释
SSL_TREE =     /usr/local/sslSSL_DEFS =     -DUSE_SSLSSL_INC =      -I$(SSL_TREE)/includeSSL_LIBS =     -L$(SSL_TREE)/lib -lssl -lcrypto

4、编译不安装make执行命令后，当前目录下生成可执行文件 http_load
3. 使用1、查看帮助./http_load --help
2、创建文件 url.list
https://www.baidu.com

3、启动测试
./http_load -s 10 -p 1 -r 10 url.list./http_load -s 180 -p 1 -r 10 -timeout 1 url.list./http_load -seconds 180 -parallel 1 -rate 10 -timeout 1 url.list]]></content>
      <categories>
        <category>engineering</category>
        <category>testing</category>
      </categories>
      <tags>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git提交后变更</title>
    <url>/dev-git-commit/</url>
    <content><![CDATA[1. 前言执行 git commit 后，我们不能保证这些commit信息是完美的，有时就需要优化变更，比如：

提交的 Git 用户信息配置错误
提交的 message 错误
多次提交的 message 相同

此时就需要用到一些变更 commit 的小技巧了。


2. 怎样修改 commit user ？2.1. 问题描述git的 user.name 和 user.email 配置错了，然后还提交了很多commit，怎么办？不要慌，git支持修改历史commit记录的用户名和邮箱。
2.2. 解决办法参考Changing author info，使用脚本进行修改。
1、clone项目
git clone --bare https://github.com/user/repo.gitcd repo

2、创建修改用户名的脚本 change-author.sh
#!/bin/shgit filter-branch -f --env-filter &#x27;OLD_EMAIL=&quot;your-old-email@example.com&quot;CORRECT_NAME=&quot;Your Correct Name&quot;CORRECT_EMAIL=&quot;your-correct-email@example.com&quot;if [ &quot;$GIT_COMMITTER_EMAIL&quot; = &quot;$OLD_EMAIL&quot; ]thenexport GIT_COMMITTER_NAME=&quot;$CORRECT_NAME&quot;export GIT_COMMITTER_EMAIL=&quot;$CORRECT_EMAIL&quot;fiif [ &quot;$GIT_AUTHOR_EMAIL&quot; = &quot;$OLD_EMAIL&quot; ]thenexport GIT_AUTHOR_NAME=&quot;$CORRECT_NAME&quot;export GIT_AUTHOR_EMAIL=&quot;$CORRECT_EMAIL&quot;fi&#x27; --tag-name-filter cat -- --branches --tags

其中 OLD_EMAIL、CORRECT_NAME、CORRECT_EMAIL 三个变量需要替换。
3、执行脚本
sh change-author.sh

4、检查结果，强制同步到远程仓库
git loggit push --force --tags origin &#x27;refs/heads/*&#x27;

3. 怎样修改上次 commit message ？3.1. 问题描述git commit -m &quot;something&quot;，之后，想要修改“something”为“something_new”，该怎样修改？
3.2. 解决办法使用git commit --amend，可以修改最后一次commit的附加信息。
4. 怎样修改最近几次的commit message？4.1. 问题描述已知最近几次提交记录为（最新的commit为efaec33）：
commit efaec33c2a081e427c5201898b3fc61179475f69commit 5fdf644e089dcb245b1f3dde5e8942fc935ffccccommit 550ae58fc222c5c4cd99027a580423d2567969e6commit f3e9a940df8371d5282f151a5c6e6e74a1e3dead

最近三次的commit message描述都有问题，该怎样修改？
4.2. 解决办法1、rebase到想要修改记录的前一条commit
git rebase -i HEAD~3# orgit loggit rebase -i f3e9a94

2、按照提示的命令编辑commit信息，改成e e e，保存。
e 550ae58 add: xxxe 5fdf644 modify: xxxe efaec33 modify: xxx

3、修改commit信息
git commit --amend # 修改 550ae58# GIT_COMMITTER_DATE=&quot;2023-09-02T08:00:00&quot; git commit --amend --date=&quot;2023-09-02T08:00:00&quot; # 修改 550ae58 ，同时修改时间戳git rebase --continue git commit --amend # 修改 5fdf644git rebase --continue git commit --amend # 修改 efaec33git rebase --continue # Successfully rebased and updated refs/heads/main.

和修改上次commit信息相比，修改多次commit信息也是使用 git commit --amend ，只不过变成了挨个处理。
PS：取消rebase
git rebase --abort

参考文档：Git修改旧提交的时间
5. 怎样合并多次 commit ？5.1. 问题描述已知最近几次提交记录为（最新的commit为efaec33）：
commit efaec33c2a081e427c5201898b3fc61179475f69commit 5fdf644e089dcb245b1f3dde5e8942fc935ffccccommit 550ae58fc222c5c4cd99027a580423d2567969e6commit f3e9a940df8371d5282f151a5c6e6e74a1e3dead

最近三条的提交记录，commit message相同或者意思相近，想要合并成一条commit，该怎样操作？
5.2. 解决办法1、rebase到想要合并记录的前一条commit
git rebase -i HEAD~3# orgit loggit rebase -i f3e9a94
自动进入vi编辑模式，此时看到的commit信息是按照时间顺序排列的，最新的commit在最下面。
2、按照提示的命令编辑commit信息，改成p s s，保存。
pick 550ae58 add: xxxs 5fdf644 modify: xxxs efaec33 modify: xxx

如果修改完报错：error: cannot ‘squash’ without a previous commit那么可以继续修改：
git rebase --edit-todo

3、解决冲突后提交（不使用git commit）
git add .git rebase --continue

6. 怎样删除某次 commit ？6.1. 问题描述已知最近几次提交记录为（最新的commit为efaec33）：
commit efaec33c2a081e427c5201898b3fc61179475f69commit 5fdf644e089dcb245b1f3dde5e8942fc935ffccccommit 550ae58fc222c5c4cd99027a580423d2567969e6commit f3e9a940df8371d5282f151a5c6e6e74a1e3dead
现在想要删除 550ae58fc222c5c4cd99027a580423d2567969e6 这次commit，该怎么操作？
6.2. 解决办法1、rebase到要删除的commit的前一个commit
git rebase -i f3e9a940df8371d5282f151a5c6e6e74a1e3dead

自动进入vi编辑模式，此时看到的commit信息是按照时间顺序排列的，最新的commit在最下面。
2、找到删除的commit（第一个），标记为 drop，保存
drop 550ae58 add: xxxpick 5fdf644 modify: xxxpick efaec33 modify: xxx

3、解决冲突后提交（不使用git commit）
git statusgit rm &lt;conflict-file&gt;git add .git rebase --continue


]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Dockerfile中的CMD和ENTRYPOINT</title>
    <url>/dev-dockerfile-cmd-entrypoint/</url>
    <content><![CDATA[1. 前言Dockerfile中的CMD和ENTRYPOINT有什么区别？docker run时默认执行什么命令，怎样覆盖默认命令？pod定义中的args和command字段有什么作用？本文，我们来回答一下这些问题。主要参考Docker RUN vs CMD vs ENTRYPOINT和Mumshad Mannambeth的课程。


2. 指令执行方式RUN、CMD和ENTRYPOINT指令都可以有两种执行方式：shell方式和exec方式。
2.1. shell方式shell方式格式：&lt;instruction&gt; &lt;command&gt;
例子：
RUN apt-get install python3CMD echo &quot;Hello world&quot;ENTRYPOINT echo &quot;Hello world&quot;

当指令以shell方式执行时，它会在后台调用 /bin/sh -c &lt;command&gt;，并且会进行常规的shell处理。例如，Dockerfile中的以下定义：
ENV name voidkingENTRYPOINT echo &quot;Hello, $name&quot;
docker run 会输出 Hello, voidking ，变量会被替换。
2.2. exec方式exec方式格式：&lt;instruction&gt; [&quot;executable&quot;, &quot;param1&quot;, &quot;param2&quot;, ...]
数组中每个元素都是一个整体。如果写成 [&quot;executable param1 param2&quot;]，executable param1 param2会被当成一个可执行程序，是找不到路径的。如果写成 [&quot;executable&quot;, &quot;param1 param2&quot;]，param1 param2会被当成一个参数，是不符合预期的。
例子：
RUN [&quot;apt-get&quot;, &quot;install&quot;, &quot;python3&quot;]CMD [&quot;/bin/echo&quot;, &quot;Hello world&quot;]ENTRYPOINT [&quot;/bin/echo&quot;, &quot;Hello world&quot;]

当指令以exec方式执行时，它将直接调用可执行文件，并且不会进行shell处理。例如，Dockerfile中的以下定义：
ENV name voidkingENTRYPOINT [&quot;/bin/echo&quot;, &quot;Hello, $name&quot;]
docker run 会输出 Hello, $name ，变量不会被替换。
如果需要运行bash而不是sh，需要使用exec方式。在这种情况下，将进行常规的shell处理。例如，Dockerfile中的以下定义：
ENV name voidkingENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;echo Hello, $name&quot;]
docker run 会输出 Hello, voidking ，变量会被替换。
3. CMD和ENTRYPOINT3.1. CMD定义访问dockerhub ubuntu，Supported tags and respective Dockerfile links，随便选择一个系统版本，这里选择 16.04 。点击链接，可以看到Dockerfile的定义。
FROM scratchADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /# overlook all the definitionCMD [&quot;/bin/bash&quot;]

可以看到，Dockerfile中定义了CMD为 /bin/bash ，也就是定义了默认命令为 /bin/bash。
docker run ubuntu:16.04 会执行默认命令 /bin/bash 。
3.2. 执行特定命令我们想要执行命令，那么需要在docker run时指定命令，覆盖默认命令。执行sleep 3600
docker run ubuntu:16.04 sleep 3600

如果想要使这个特定命令永久生效，那么需要使用Dockerfile定义一个新的镜像。
FROM ubuntu:16.04CMD [&quot;sleep&quot;,&quot;3600&quot;]

生成新的镜像，执行默认命令 sleep 3600
docker build -t ubuntu-sleeper .docker run ubuntu-sleeper

3.3. 特定参数如果我们想要修改sleep的时间，该怎么做？
docker run ubuntu:16.04 sleep 3600docker run ubuntu:16.04 sleep 1200

sleep命令没有变，变化的只有参数，sleep是否可以省略？可以的，定义一个新的镜像。
FROM ubuntu:16.04ENTRYPOINT [&quot;sleep&quot;]CMD [&quot;3600&quot;]

# 生成新镜像docker build -t ubuntu-sleeper .# 执行默认命令 sleep 3600docker run ubuntu-sleeper # 执行命令 sleep 1200docker run ubuntu-sleeper 1200

ENTRYPOINT里的命令是否可以被替换的呢？也是可以的，以执行 sleep2.0 1200 为例
docker run --entrypoint sleep2.0 ubuntu-sleeper 1200

综上，docker run会默认执行 ENTRYPOINT + CMD。通常情况下，我们会在Dockerfile中定义ENTRYPOINT作为固定命令，定义CMD作为默认参数。
4. k8s中的args和command在k8s中定义pod时，有args和command两个字段。这两个字段，分别覆盖CMD和ENTRYPOINT。k8s中的args和command，只支持exec的执行方式，因此参数格式为：[&quot;executable&quot;, &quot;param1&quot;, &quot;param2&quot;, ...]
例如：
apiVersion: v1kind: Podmetadata:  creationTimestamp: null  labels:    run: ubuntu  name: ubuntuspec:  containers:  - image: ubuntu:16.04    name: ubuntu    resources: &#123;&#125;    command: [&quot;sleep&quot;]    args: [&quot;1200&quot;]  dnsPolicy: ClusterFirst  restartPolicy: Alwaysstatus: &#123;&#125;
该pod启动后的执行命令为 sleep 1200
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>k8s</tag>
      </tags>
  </entry>
  <entry>
    <title>怎样做出可口的食物？</title>
    <url>/essay-how-to-cook-good-food/</url>
    <content><![CDATA[1. 前言记不得新冠疫情开始的具体时间，只知道今年的春节（2020年1月25日），没有出去拜年，春节期间也没有出去聚餐。然后，疫情日益严重，继湖北武汉之后，各市也开始封城。复工日期一推再推，2月15日经历了两次信息登记，三次体温检测，顺利返回北京。先去社区办理观察证，小区内隔离两周，拿到出入证，还要录入人脸识别，以便进出小区。3月9日年后第一次上班，需要查询近期行程，测量体温，才能进入达美中心。复工后三周休两周，直到最近，才每周上班。但是，仍然不敢在外面吃饭，因为多人用餐是最容易传播新冠的途径。
因此，厨道大兴！而郝同学不才也加入了自己做饭的行列，毕竟自己做才最放心。做的多了，自然不能只满足于能吃，而要追求好吃。
参考资料：

你是明白了哪几个基本原理之后而厨艺大增的？
来自专业厨师的36个无价的烹饪技巧
20个你不知道的聪明的烹饪技巧



2. 秘诀用最少的调料，最简单的烹饪方法，最大程度发挥出食物本来的味道。
1、糖，是万能的，别怕，不管是任何菜式，出锅前放一丢丢，总会遇到你想要的味道。糖，不是为了让菜变得甜腻，而是作为味引，激发出足以挑拨你味蕾的口感。
2、豆瓣酱在肉类处理上，是不会犯错的存在，最简单的，放油，放豆瓣酱和肉沫，加一点点料酒去腥，就可以得到满满一碗辣肉面的肉酱，放在冰箱里吧，什么时候饿了，下碗面条浇一点这个肉酱上去，端在面前，是一碗面条吗？不，是一碗准备时间只需要5分钟的幸福。
3、蚝油在好多素菜的处理上，完全可以替代盐，而且有着盐没有的那股“鲜”味，最直接的就是花菜，尤其是干锅花菜。洗干净沥干，然后下油，放一点点花椒或者辣椒，吧花菜放进去，一滴水也不要加，感觉差不多了，放耗油+生抽，别放盐，出锅，有追求的就再放些五花肉片进去，尝尝，是不是觉得外面的干锅简直是垃圾？
4、初学者，不要想着一气呵成，很多东西是可以在烹饪过程中不断调整的，容错性巨高，特别是烹饪时间较长的菜式，例如蒸菜。大个比方：梅干菜扣肉，前面不罗嗦，就说上蒸锅开蒸之后，每隔半小时用筷子舔一下，觉得咸了赶紧吧里面的汁水逼出来一点，觉得淡了就加一勺老抽进去，觉得不够甜就放一小粒冰糖在边边，容错性巨高。
5、洋葱没有什么味道，它的全部价值在于增香。举个例子：最最最最简单的，炒鸡蛋，加一两片洋葱进去，感觉是两个菜。
6、刀工没有那么重要，现在的厨房懒人工具太多了，不要把时间浪费在练习刀工上，至少我是这么认为的，还容易受伤。
7、外面的菜口感总觉得和家里不一样，是因为外面的火，远远比家里的要旺，这个是煤气灶的硬伤，解决不了的，但也并不是没有一点办法，我个人的解决方案是，尤其在所谓的“爆炒”菜式中，会将火开至最大，然后将锅倾斜至很夸张的角度，目的是让火舌舔到锅里的油，这样就会有锅里起火的效果，温度非常之高，然后上盖子压灭，出锅。这招是我自己捣鼓出来的，有的时候实在是少油，火舌舔不到，我就在锅边边刷一点油。
8、汤类中的“鲜”，我指的是的寻常汤类，不外乎来自这四种东西——笋、肉、味精、白胡椒粉（评论提醒，还有一种，但我不经常做，就是菇类）。这也是我经常做的几道，极其方便。比如，上海菜中有一道“腌笃鲜”，冬笋、精肉、咸肉，一比一比一入锅，一大锅，熬着吧，啥也不用放，真的其他啥也不用放（姜片和料酒去腥还是必需的），1个小时后出锅，撒点葱花，冬天里简直是。。。王菲有首歌叫什么来着？天上人间是吧，诶。
再比如，我这边有个卖臭豆腐的，炸完后浸入一种汤里，巨好喝，问他怎么弄的，他摆摆手说是秘方，不外传。回家自己细细一品，转身进厨房，不说一模一样，但可以乱真——牛肉汤，味精、五香粉，酸豆角，白胡椒粉，香菜。就这么简单，还秘方不外传，啊我呸。
9、用猪油，比其他任何的大豆油或色拉油炒出来的菜，都要香，我用过一次就上瘾，唯一的缺点就是凝固点太高了（很多人评论纠正我是低？我也搞不明白了，如果普通油凝固需要零下1度，而猪油凝固零上5度，到底是谁高谁低呢？），很容易就变成膏体，冬天是需要用勺子挖的。
10、最后给个心得，其实厨艺这东西没有什么玄妙的，无非是经验而已，我一个人搁那捣鼓五六十年，出来绝对也是一等一的大师，总有个阴差阳错能让我悟到一些原本没人点拨到的TIPS，那为什么不去寻求身边的高手呢？比方，我吃番茄炒蛋，永远是淡而无味，加再多调味品都没办法拯救的哪种，结果我一朋友对我说，你说说你怎么弄的呢？说完，所有的步骤和路数都是OK的，唯一一点，出错了，炒番茄的时候没有用勺子用力压番茄，这样番茄里的酱汁就不会被压出来，也就没有办法裹在蛋上形成独特的口感了。我一听，大惊，对啊，就是这个道理。
再比方，那个薯条，我随便怎么都弄不出那种麦当劳的风味，口感一模一样，就是没有那种风味，最后问题出在，土豆切条之后需要用牛奶浸泡一晚上，没有牛奶用奶粉也可以，这样会有若有若无的奶香味道，味蕾再发达的人，别人不说，这种味道你要能领悟原材料估计得靠缘分。
你看，是不是一个很小的点拨？小到你压根不会去注意。
多问多讨教就对了。
11、最后的最后，大家都是一双手，厨艺这东西练练总会上去的，但巧妇难为无米之炊，家里的调料一定要齐全，不然就会变成我，经常戴着口罩围着围裙坐电梯到小区门口超市买缺的调料（那回头率老高了）。郫县豆瓣酱、料酒、老姜、蒜瓣、老抽（酱油）、生抽（鲜酱油）、香醋、白醋、白砂糖、老冰糖、白芝麻、小磨麻油、甜面酱、蚝油、辣椒面、五香粉、八角、桂皮、香叶、咖哩粉、白胡椒粉、鲜辣粉、花椒粉、椒盐、孜然粉、番茄酱、浓汤宝（牛肉、猪肉和老母鸡）、蜂蜜、老干妈豆豉酱。
这些调味品有共同的特征——1、保质期较长，常备着，不容易坏。2、使用频率极其频繁。3、要么不用，一旦那道菜式需要用到，而你恰恰缺少，那这道菜基本就属于做不了了，非要不可的那种。
3. 调味品
油：大豆油即可，菜籽油更好。耗油、香油可选。
盐：海盐、湖盐、井盐、岩盐，任选即可。
酱：生抽，老抽。生抽一般用来蘸料，拌凉菜，或者是炒菜时提个鲜；老抽一般在红烧时做上色用。简单来说，拌菜炒菜用生抽，红烧用老抽。
醋：陈醋，白醋，米醋。陈醋用途最广，可以炒菜，可以凉拌，可以蘸酱，还能上色；白醋最大的用处体现在它的功能性，比如除臭除味、美肤养颜、洗涤除垢等等；米醋因为口味柔和，有着非常浓厚的香气，所以多用来凉拌。
糖：绵白糖，冰糖。
椒：干辣椒，花椒。
粉：辣椒粉，花椒粉，十三香，孜然粉
料：烧烤料，芝麻，大茴香（孜然），小茴香，白芷、八角、桂皮、香叶、丁香、香茅草、草果、肉蔻、陈皮

4. 小技巧
学厨APP：下厨房
保存土豆：纸箱+不透光
存肉：买来肉之后按一顿的份量切块，每块分别放入保鲜膜，冷冻。
化肉：盐+白醋
磨刀：碗底，同一个方向
炖羊肉：葱姜萝卜盐，不要放八角

5. 炒着吃万能流程：1、锅热放油，锅热放姜、花椒、干辣椒，出香2、放肉，翻炒。3、放菜，翻炒。4、放入调味料（最后放盐），翻炒。5、根据经验，观察色泽，尝一尝，出锅。
6. 炖着吃6.1. 茄子烧肉片1、茄子切成斜块放入盆中，稍微泡一下，过一下水，残留少量水2、烧热锅，放入少量油，油热下入茄子，迅速翻炒沾上油，盖上盖改成中火焖一分钟，翻面再焖一分钟3、猪肉片放入一勺酱油，一勺料酒，鸡精和盐少量，少量干辣椒和花椒，搅拌后腌着4、兑汁，姜丝，酱油一勺半，胡椒粉少量，盐少量，糖少量，味精少量，一勺淀粉，水8勺5、开大火，倒入肉片，少量水，加盖焖20秒6、翻炒肉片和茄子，下汁，稍等锅热翻炒，收汁，出锅之前撒一圈香油
7. 炸着吃7.1. 油炸虾1、处理虾，去头，去虾脚，去虾线2、腌制虾，料酒、食盐、姜、椒盐粉3、第一遍炸虾，油温至筷子蘸水起泡，根据虾的大小，炸1至3分钟至定型4、第二遍炸虾，油温至泡沫接近消失，炸20到30秒
8. 烤着吃8.1. 蒸烤箱说明烤箱最主要的配件是烤管和风机。烤管+风机+温度设置+时间设置，就可以搭配出N种模式了。
顶部内：顶部内烤管（顶部有内外两个烤管）顶部内外：顶部内烤管+顶部外烤管外顶下部：顶部外烤管+下部烤管顶内风机：顶部内烤管+风机顶下风机：顶部烤管+下部烤管+风机后部风机：后部烤管+风机后下风机：后部烤管+下部烤管+风机
8.2. 烤红薯技巧1、红薯品种选择蜜薯，家附近超市购买就行，个头适中不要太大2、红薯皮用清洁球洗干净，方便带皮吃3、红薯用水果刀钻孔，方便烤透4、蒸烤箱烤红薯：顶下风机220度烤半小时，翻面，再烤25分钟（如果有湿度选项，要选择低湿）
8.3. 烤栗子技巧1、栗子首选迁西栗子，其他品种也可2、栗子洗净，每颗栗子用刀开个小口3、栗子凉水下锅，放入少量食盐，煮20分钟，出锅擦干或者晾干水分4、用糖水搅拌（可选）5、蒸烤箱烤栗子：顶下风机200度烤20分钟
9. 煮着吃9.1. 煮羊排1、处理羊排，羊排切好，凉水泡一个小时左右2、准备配菜，白萝卜、白胡椒颗粒、大葱、白胡椒粉、干辣椒段、洋葱碎、青辣椒碎、辣椒面、孜然粉、白芷3、煮羊排，凉水放入白萝卜、白胡椒颗粒、白芷、大葱或洋葱，放入羊排，煮45分钟，加少量盐4、汤肉分开，汤中加入盐，白胡椒粉5、煎羊排，少量油，大火煎至金黄色，放入干辣椒段、洋葱碎、青辣椒碎，最后放入辣椒面、孜然粉、盐，翻炒出锅
9.2. 煮龙虾1、购买小龙虾，龙虾料（比如海底捞龙虾料）2、龙虾洗净，焖死或冻死3、放入油，姜片，蒜片，干辣椒，花椒，炒香，放入小龙虾翻炒至全身变红，加入龙虾料继续翻炒出香4、加入啤酒500ml，大火煮沸焖煮10-15分钟，汤汁收浓起锅
]]></content>
      <categories>
        <category>essay</category>
      </categories>
  </entry>
  <entry>
    <title>好好学K8S：使用curl访问k8s的apiserver</title>
    <url>/dev-curl-k8s-api-server/</url>
    <content><![CDATA[1. k8s管理工具管理k8s集群，除了kubectl和go-client，其实还可以使用curl命令。本文，我们就学习一下怎样使用curl访问k8s的apiserver，实现k8s集群的管理。主要参考如何使用curl访问k8s的apiserver。
需求：使用curl命令，实现 kubectl get pod 同样的效果。使用curl命令访问k8s集群时，分别使用证书和token两种认证方式。


2. 使用证书假设我们拥有集群的kubeconfig文件，我们需要从中拿出ca cert、client cert和client key，保存为文件。并且拿出apiserver，保存为变量。
cat config | grep certificate-authority-data | awk &#x27;&#123;print $2&#125;&#x27; | base64 -d &gt; ca.crtcat config | grep client-certificate-data | awk &#x27;&#123;print $2&#125;&#x27; | base64 -d &gt; client.crtcat config | grep client-key-data | awk &#x27;&#123;print $2&#125;&#x27; | base64 -d &gt; client.keyAPISERVER=$(cat config | grep server | awk &#x27;&#123;print $2&#125;&#x27;)

查看API的url，使用curl命令调用API
curl --cert ./client.crt --cacert ./ca.crt --key ./client.key $APISERVER/apikubectl get pods -v8curl --cert ./client.crt --cacert ./ca.crt --key ./client.key $APISERVER/api/v1/namespaces/default/pods/

以上，拿到了default空间下的pod信息，和 kubectl get pod 等同。
3. 使用token3.1. 获取token想要使用curl命令访问apiserver，首先要获得一个具有权限的token。
kubectl get secrets --all-namespaces | grep adminkubectl describe secrets admin-token-vmv2c -n kube-system

输出结果为：
Name:         admin-token-vmv2cNamespace:    kube-systemLabels:       &lt;none&gt;Annotations:  kubernetes.io/service-account.name: admin              kubernetes.io/service-account.uid: a75b4cdc-e120-11e9-8695-00163e300424Type:  kubernetes.io/service-account-tokenData====ca.crt:     1419 bytesnamespace:  11 bytestoken:      xxxthisisatokenxxx
最后一个字段就是token，那么这个token有哪些权限呢？
3.2. 查看token权限根据annotations中的key value，可以看到这个secrets绑定了一个service-account(sa)，name为admin。等同于这个token绑定了一个sa，name为admin。
查看admin这个service-account的信息。
kubectl get sa --all-namespaces | grep adminkubectl describe sa admin -n kube-system

输出结果为：
Name:                adminNamespace:           kube-systemLabels:              &lt;none&gt;Annotations:         kubectl.kubernetes.io/last-applied-configuration:                       &#123;&quot;apiVersion&quot;:&quot;v1&quot;,&quot;kind&quot;:&quot;ServiceAccount&quot;,&quot;metadata&quot;:&#123;&quot;annotations&quot;:&#123;&#125;,&quot;name&quot;:&quot;admin&quot;,&quot;namespace&quot;:&quot;kube-system&quot;&#125;&#125;Image pull secrets:  &lt;none&gt;Mountable secrets:   admin-token-vmv2cTokens:              admin-token-vmv2cEvents:              &lt;none&gt;
没有关于admin的权限信息，那么我们再看一下admin绑定了哪些role和clusterrole。
kubectl get rolebindings --all-namespaces -oyaml | grep &quot;name: admin&quot; -A10 -B10kubectl get clusterrolebindings --all-namespaces -oyaml | grep &quot;name: admin&quot; -A10 -B10

找到有用信息为：
- apiVersion: rbac.authorization.k8s.io/v1  kind: ClusterRoleBinding  metadata:    annotations:      kubectl.kubernetes.io/last-applied-configuration: |        &#123;&quot;apiVersion&quot;:&quot;rbac.authorization.k8s.io/v1beta1&quot;,&quot;kind&quot;:&quot;ClusterRoleBinding&quot;,&quot;metadata&quot;:&#123;&quot;annotations&quot;:&#123;&#125;,&quot;name&quot;:&quot;admin&quot;&#125;,&quot;roleRef&quot;:&#123;&quot;apiGroup&quot;:&quot;rbac.authorization.k8s.io&quot;,&quot;kind&quot;:&quot;ClusterRole&quot;,&quot;name&quot;:&quot;cluster-admin&quot;&#125;,&quot;subjects&quot;:[&#123;&quot;kind&quot;:&quot;ServiceAccount&quot;,&quot;name&quot;:&quot;admin&quot;,&quot;namespace&quot;:&quot;kube-system&quot;&#125;]&#125;    creationTimestamp: &quot;2019-09-27T12:16:37Z&quot;    name: admin    resourceVersion: &quot;1317&quot;    selfLink: /apis/rbac.authorization.k8s.io/v1/clusterrolebindings/admin    uid: a75e1ef9-e120-11e9-8695-00163e300424  roleRef:    apiGroup: rbac.authorization.k8s.io    kind: ClusterRole    name: cluster-admin  subjects:  - kind: ServiceAccount    name: admin    namespace: kube-system

可知admin绑定了一个名为cluster-admin的clusterrole，接着查看cluster-admin的权限。
kubectl describe clusterrole cluster-admin -n kube-system

结果为：
Name:         cluster-adminLabels:       kubernetes.io/bootstrapping=rbac-defaultsAnnotations:  rbac.authorization.kubernetes.io/autoupdate: truePolicyRule:  Resources  Non-Resource URLs  Resource Names  Verbs  ---------  -----------------  --------------  -----  *.*        []                 []              [*]             [*]                []              [*]

cluster-admin这个角色拥有集群的所有权限，因此admin这个sa拥有集群的所有权限。
3.3. 使用token1、获取admin sa的token
TOKEN=$(kubectl describe secrets $(kubectl get secrets -n kube-system |grep admin |cut -f1 -d &#x27; &#x27;) -n kube-system |grep -E &#x27;^token&#x27; |cut -f2 -d&#x27;:&#x27;|tr -d &#x27;\t&#x27;|tr -d &#x27; &#x27;)APISERVER=$(kubectl config view |grep server|cut -f 2- -d &quot;:&quot; | tr -d &quot; &quot;)

或者获取default sa的token
APISERVER=$(kubectl config view --minify | grep server | cut -f 2- -d &quot;:&quot; | tr -d &quot; &quot;)SECRET_NAME=$(kubectl get secrets | grep ^default | cut -f1 -d &#x27; &#x27;)TOKEN=$(kubectl describe secret $SECRET_NAME | grep -E &#x27;^token&#x27; | cut -f2 -d&#x27;:&#x27; | tr -d &quot; &quot;)


2、查看API的url，使用curl命令调用API
curl --header &quot;Authorization: Bearer $TOKEN&quot; --insecure $APISERVER/apikubectl get pods -v8curl -H &quot;Authorization: Bearer $TOKEN&quot; --insecure $APISERVER/api/v1/namespaces/default/pods/

以上，拿到了default空间下的pod信息，和 kubectl get pod 等同。
4. kubectl proxy上文中，我们使用curl命令访问k8s集群时，分别使用证书和token两种认证方式。
而使用kubectl proxy命令，可以启动一个apiserver的本地代理，默认监听在本地的8001端口上。使用apiserver本地代理有什么好处呢？

我们使用curl访问apiserver的时候，不用自己获取token，更加简单。
因为不需要使用token，所以还可以浏览器访问apiserver。

1、启动apiserver本地代理
kubectl proxykubectl proxy --port 8080

2、另外打开一个终端
curl http://localhost:8001curl http://localhost:8080/api/v1/proxy/namespaces/YOURNAMESPACE/services/YOURSERVICE:PORT






]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>PS绘制抛物线</title>
    <url>/hobby-ps-parabola/</url>
    <content><![CDATA[1. 需求已有一张 draw.io 绘制出的图像如下。
现在需要在小球之间绘制平抛运动的抛物线。


2. 绘制方法1、使用PS打开原始图像，使用 ctrl+加号 放大到合适的大小。
2、菜单栏图层，新建，图层。
3、工具栏选择钢笔工具。单击上面的小球，然后单击下面的小球并按住鼠标，向下拉伸，移动调整曲线的弧度。
4、右键曲线，建立选区，确定。或者直接 ctrl+Enter ，路径变成选区。
5、菜单栏编辑，描边，确定。ctrl+D 取消选区，选中图层 Enter回车 去掉钢笔笔迹。
6、图层，只显示抛物线，工具栏选择橡皮擦工具，擦除不需要的部分。
7、图层，显示所有图层，即可看到需要的抛物线。
重复上述步骤，完成另外两条抛物线，最终结果如下图。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>ps</tag>
      </tags>
  </entry>
  <entry>
    <title>MacOS快捷键</title>
    <url>/hobby-macos-shortcut-key/</url>
    <content><![CDATA[1. 前言为了提高工作效率，同时给macos省电（macos触摸板是最耗电的设备），最好能够掌握macos的快捷键。本文中，对常用的macos快捷键进行整理，备忘。
参考文档：

Mac 键盘快捷键
《MacOS系统配置》
《MacOS上软件配置》
《MacOS小技巧》
《Vim进阶》
《VSCode常用快捷键和配置》
《IDEA常用快捷键和配置》
《Sublime Text3》
mac中chrome常用快捷键
iTerm2 快捷键大全



2. 文件/文本操作
Command-X：剪切所选项并拷贝到剪贴板。
Command-C：将所选项拷贝到剪贴板。这同样适用于“访达”中的文件。
Command-V：将剪贴板的内容粘贴到当前文稿或 App 中。这同样适用于“访达”中的文件。
Command-Z：撤销上一个命令。
Shift-Command-Z：重做上一个命令。
Command-A：全选各项。
Command-F：查找文稿中的项目或打开“查找”窗口。
Command-G：再次查找，查找之前所找到项目出现的下一个位置。
Shift-Command-G：查找出现的上一个位置。
Command-O：打开所选项，或打开一个对话框以选择要打开的文件。
Command-P：打印当前文稿。
Command-S：存储当前文稿。
Shift-Command-N：在“访达”中创建一个新文件夹。
Control-Command-空格键：显示字符检视器，你可以从中选择表情符号和其他符号。

3. App/窗口操作
Command-N：新建一个App窗口。
Command-T：打开新标签页。
Command-H：隐藏最前面的 App 的窗口。
Option-Command-H：查看最前面的 App ，隐藏所有其他 App。
Command-M：将最前面的窗口最小化至“程序坞”。需要注意的是，最前面的窗口不能是最大化状态。
Option-Command-M：最小化最前面的 App 的所有窗口。
Command-W：关闭最前面的窗口。
Option-Command-W：关闭 App 的所有窗口。
Command-Q：关闭最前面的窗口并退出程序。
Option-Command-Esc：强制退出 App。
Command-Tab：在打开的 App 中切换到下一个最近使用的 App。
Command-空格键：显示或隐藏“聚焦”搜索栏。
Command–Option–空格键：从“访达”窗口执行“聚焦”搜索。
Control-Command-F：全屏使用 App（如果 App 支持），取消全屏使用App。
Command-逗号(,)：打开最前面的 App 的偏好设置。
Control-Tab：切换标签页
Shift-Control-Tab：反向切换标签页

Command-Tab额外说明：

Command-Tab，调出App选择框
按住Command，点击Tab可以切换App窗口到最前面
Command-Tab，按住Command，使用左右按键可以切换App窗口到最前面
已经最小化的App，想要恢复，Command-Tab，选中App后松开Tab，按住Option，松开Command，最后松开Option

4. 截屏录屏
Shift-Command-5：拍摄截屏或录制屏幕。
Shift-Command-3：截屏屏幕。
Shift-Command-4：截屏选定区域。

5. 文本编辑
Command-左键：跳转到本行最前面
Command-右键：跳转到本行最后面
Command-上键：跳转到文档最前面
Command-下键：跳转到文档最后面
Shift-Command-左键：选中从光标到本行最前面
Shift-Command-右键：选中从光标到本行最后面
Shift-Command-上键：选中从光标到文档最前面
Shift-Command-下键：选中从光标到文档最后面
Command-Delete：删除光标前的文字
Option-左键：跳转到上一个关键字
Option-右键：跳转到下一个关键字
Shift-Option-左键：选中上一个关键字
Shift-Option-右键：选中下一个关键字
Command-Enter：跳转到下一行，光标后文字不受影响

6. Chrome
Command-T：新建标签页
Command-W：关闭当前标签页
Shift-Command-T：恢复上次关闭的标签页
Shift-Command-W：关闭当前窗口
Command-Q：按住三秒退出App
Control-Tab：切换标签页
Shift-Control-Tab：反向切换标签页
Command-Y：打开历史记录
Shift-Command-J：打开下载记录
Shift-Command-Delete：打开清理浏览数据
Command-L：聚焦到地址栏
Option-Command-I：打开开发人员工具

7. shell通用快捷键
Control-A：到行首
Control-E：到行尾
Control-R：搜索历史命令；再次点击Control-R，继续搜索上一条命令；右键选中退出搜索
Control-L：清屏
Shift-ZZ：vim退出或者保存退出
上下键：查看历史命令
Control-D：删除当前光标字符，没有字符时退出当前登录
Control-H：删除光标前的一个字符
Control-W：删除光标前的一个单词
Control-K：删除光标到末尾

8. iterm2
Command-T：新建标签页
Command-W：关闭当前标签页或者分屏
Command-数字：切换标签页
Command-左右键：切换标签页
Command-Enter：切换全屏
Shift-Command-T：全屏时显示Tab
Command-D：垂直分屏
Shift-Command-D：水平分屏
Command-Option-左右键：切换分屏
Command-方括号([])：切换分屏
Command-分号(;)：查看历史命令
输入命令开头，Command-分号(;)：查看以某些命令开头的历史命令
Shift-Command-H：查看粘贴板历史
Command-F：搜索屏幕中的内容

9. 其他
Control-Shift-A：打开启动台
Command-光标拖动菜单栏图标：移动菜单栏图标

]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>macos</tag>
      </tags>
  </entry>
  <entry>
    <title>MacOS小技巧</title>
    <url>/hobby-macos-tricks/</url>
    <content><![CDATA[1. 使用命令打开目录和文件1、在finder中打开当前目录open .
2、使用默认程序打开指定文件open README.md
2. 切换窗口1、command + tab2、三指左右滑动3、三指上滑


3. 快速预览选中文件或者文件夹，点击空格即可预览文件，点击上下左右可以预览其他文件。
4. 隔空投送接收方：打开隔空投送功能，同时接收人选择仅限联系人或所有人。
发送方：1、打开要发送的文件，然后点按 App 窗口中的“共享”按钮 。或者，在“访达”中按住 Control 键点按相应文件，然后从快捷键菜单中选取“共享”。2、从列出的共享选项中，选取“隔空投送”。3、从“隔空投送”表单中选取一位接收者。
或者打开“隔空投送”窗口，然后将文件拖移到接收者：1、在“访达”窗口的边栏中选择“隔空投送”。或者从菜单栏中选取“前往”&gt;“隔空投送”。2、“隔空投送”窗口随即会显示附近的“隔空投送”用户。将一个或多个文稿、照片或其他文件拖移到窗口中显示的接收者。
详情参考 在 Mac 上使用“隔空投送”。
传送速度非常快，网络条件不错的情况下，可以达到几百MB每秒。
mac和ipad之间，mac和iphone之间也可以使用隔空投送，非常方便实用。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>macos</tag>
      </tags>
  </entry>
  <entry>
    <title>MacOS安装配置Homebrew</title>
    <url>/dev-macos-homebrew/</url>
    <content><![CDATA[1. Homebrew简介
Homebrew: The Missing Package Manager for macOS (or Linux).Homebrew installs the stuff you need that Apple (or your Linux system) didn’t.

Homebrew是Mac OSX上的软件包管理工具，简称brew。


2. 安装Homebrew/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)&quot;# 国内安装失败的话，建议换下面的脚本/bin/zsh -c &quot;$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)&quot;

3. 更换Homebrew软件源cd &quot;$(brew --repo)&quot; &amp;&amp; git remote set-url origin git://mirrors.ustc.edu.cn/brew.gitcd &quot;$(brew --repo homebrew/core)&quot; &amp;&amp; git remote set-url origin git://mirrors.ustc.edu.cn/homebrew-core.gitcd &quot;$(brew --repo homebrew/cask)&quot; &amp;&amp; git remote set-url origin git://mirrors.ustc.edu.cn/homebrew-cask.gitbrew update# 还原cd &quot;$(brew --repo)&quot; &amp;&amp; git remote set-url origin https://github.com/Homebrew/brew.gitcd &quot;$(brew --repo homebrew/core)&quot; &amp;&amp; git remote set-url origin https://github.com/Homebrew/homebrew-core.gitcd &quot;$(brew --repo homebrew/cask)&quot; &amp;&amp; git remote set-url origin https://github.com/Homebrew/homebrew-cask.gitbrew update

更多内容参考Homebrew/Linuxbrew 镜像使用帮助。
4. 使用Homebrew4.1. 查找软件brew search mysql

4.2. 安装最新版软件brew install mysql

4.3. 安装指定版本软件brew install mysql@5.7

4.4. 安装软件时禁用软件源更新HOMEBREW_NO_AUTO_UPDATE=1 brew install mysql

4.5. 升级软件brew upgrade mysql

4.6. 卸载软件brew uninstall mysql

5. 更新Homebrew软件源brew update-reset

6. Error opening archive6.1. 问题描述使用国内brew源，安装软件出现报错：
tar: Error opening archive: Failed to open &#x27;xxx.catalina.bottle.tar.gz&#x27;

6.2. 解决办法暂时改回默认的brew源：
unset HOMEBREW_BREW_GIT_REMOTEunset HOMEBREW_CORE_GIT_REMOTEunset HOMEBREW_BOTTLE_DOMAIN

7. No available formula with the name7.1. 问题描述brew upgrade sqlite3

使用上面的命令升级sqlite3，出现报错：
Error: An exception occurred within a child process:  FormulaUnavailableError: No available formula with the name &quot;/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Aliases/sqlite3&quot;.

7.2. 解决办法sqlite3对应的软件名是sqlite，因此修改升级命令为：
brew upgrade sqlite

使用升级后的版本：
sqlite3 --versionwhich sqlite3 # /usr/bin/sqlite3cd /usr/local/opt/sqlite/bin &amp;&amp; ./sqlite3 --versionecho &#x27;export PATH=&quot;/usr/local/opt/sqlite/bin:$PATH&quot;&#x27; &gt;&gt; ~/.bash_profilesource ~/.bash_profilesqlite3 --version

虽然系统中的sqlite3升级了，但是如果在python中调用sqlite3，还是会调用老版本。解决办法是重新安装python：
pyenv uninstall &lt;your_python_version&gt;PYTHON_CONFIGURE_OPTS=&quot;LD_RUN_PATH=/usr/local/opt/sqlite/lib&quot; \LDFLAGS=&quot;-L/usr/local/opt/sqlite/lib -I/usr/local/opt/sqlite/include&quot; \pyenv install &lt;your_python_version&gt;python -c &quot;import sqlite3; print(sqlite3.sqlite_version)&quot;




]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>troubleshooting</category>
        <category>shell</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>macos</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>MacOS安装配置iTerm2</title>
    <url>/dev-macos-iterm2/</url>
    <content><![CDATA[1. iTerm2简介
iTerm2 is a replacement for Terminal and the successor to iTerm. It works on Macs with macOS 10.14 or newer. iTerm2 brings the terminal into the modern age with features you never knew you always wanted.

iTerm2是MacOS系统的一个终端工具。
参考文档：iterm2


2. 安装iTerm2下载iTerm2，双击安装即可。
3. 配置iTerm23.1. 配置lrzsz1、安装iterm2-zmodem
brew install wgetbrew install lrzszgit clone https://github.com/aikuyun/iterm2-zmodem.gitcd iterm2-zmodemcp iterm2-* /usr/local/bincd /usr/local/binchmod +x iterm2-*

2、iterm2配置项 
Profiles-&gt;OpenProfiles-&gt;EditProfiles-&gt;Advanced-&gt;Tirgger，添加
Regular expression:  /*/*B0100Action: Run Silent CoprocessParameters: /usr/local/bin/iterm2-send-zmodem.shInstant: trueRegular expression:  /*/*B00000000000000Action: Run Silent CoprocessParameters: /usr/local/bin/iterm2-recv-zmodem.shInstant: true

3、主机上测试上传下载文件
#yum install lrzszapt install lrzszrzsz test.txt

3.2. 配置trzszMacOS 升级到15.4（Sequoia），iterm2 升级到 3.5.13 之后，rz和sz命令失效了，执行会卡住。没有找到好的解决办法，最终决定把lrzsz换成trzsz。
1、安装trzsz
#brew install trzszpython3 -m pip install --upgrade trzsz-iterm2python3 -m pip install --upgrade trzszwhich trzsz-iterm2

2、iterm2配置项（1）Profiles-&gt;OpenProfiles-&gt;EditProfiles-&gt;Advanced-&gt;Tirgger，删除原有lrzsz配置，添加
Regular expression:  :(:TRZSZ:TRANSFER:[SRD]:\d+\.\d+\.\d+:\d+)Action: Run Silent CoprocessParameters: /usr/local/bin/trzsz-iterm2 \1Instant: trueEnabled: true

（2）Profiles-&gt;OpenProfiles-&gt;EditProfiles-&gt;General-&gt;Magic，选中 Enable Python API
（3）Profiles-&gt;OpenProfiles-&gt;EditProfiles-&gt;Advanced，筛选 COOKIE，选择 Yes
3、主机上测试上传下载文件
pip install trzsztrztsz test.txt

4、兼容lrzsz（可选）trzsz如果想要兼容lrzsz，那么需要在登录时使用tssh命令替换ssh命令。
brew install trzsz-sshbrew install lrzszecho &#x27;alias ssh=&quot;tssh --zmodem&quot;&#x27; &gt;&gt; ~/.bash_profilesource ~/.bash_profilessh root@192.168.56.101rzsz test.txt

注意：使用tssh替换ssh后，clone session会失效。
参考文档：

Trzsz-iTerm2 安装文档
Trzsz-ssh ( tssh ) Document
让你的Mac iTerm优雅的上传下载文件

3.3. 配置clone session1、Iterm2-&gt;Preferences-&gt;Profiles-&gt;Working Directory勾选 Reuse previous session’s directory
2、编辑 ~/.ssh/config 文件，添加
host *ControlMaster autoControlPath ~/.ssh/master-%r@%h:%pServerAliveInterval 60

3、测试clone session重新打开终端。第一个窗口登录主机A需要密码，第二个窗口登录主机A就不需要密码了，nice。
3.4. 更换主题和背景色更换主题：Iterm2 -&gt; Preferences -&gt; Appearance -&gt; General -&gt; Theme更换背景色：Iterm2 -&gt; Preferences -&gt; Profiles -&gt; Colors -&gt; Color Presets
3.5. 设置scrollback linesIterm2 -&gt; Preferences -&gt; Profiles -&gt; Terminal -&gt; 修改为更多的行数，或者直接勾选Unlimited scrollback
3.6. 全屏时显示TabView -&gt; Show Tabs in Fullscreen快捷键：Shift-Command-T
]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>macos</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>MacOS配置发送邮件</title>
    <url>/dev-macos-mail/</url>
    <content><![CDATA[1. 邮件系统简介邮件系统有三个组成部分：

邮件用户代理（MUA，Mail User Agent）；
邮件传送代理（MTA，Mail Transport Agent）；
邮件分发代理（MDA，Mail Deliver Agent）。

本文中，我们学习一下在MacOS系统中使用命令行发送邮件。


2. 安装配置msmtpmacos自带mail（mailx）命令，这是一个MUA，与之配合的是sendmail，一个MTA。但是，sendmail不支持使用外部MDA发邮件，因此需要把sendmail替换成msmtp。
1、安装msmtp
brew install msmtp

2、配置使用msmtp
sudo vim /etc/mail.rc

mail.rc中添加：
set sendmail=/usr/local/bin/msmtp

3、配置msmtp
vim ~/.msmtprc

.msmtprc 内容为：
defaultsaccount defaulthost smtp.163.comport 25from quizthink@163.comauth logintls offuser quizthink@163.compassword xxxxxx

4、修改 .msmtprc 权限
chmod 600 ~/.msmtprc

3. 发送邮件echo &quot;Hello world&quot; | mail -s &quot;test&quot; voidking@qq.com


]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>macos</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>Chrome浏览器中好用的插件</title>
    <url>/hobby-chrome-plugins/</url>
    <content><![CDATA[1. 前言记录一下Chrome浏览器中好用插件，备忘。


2. 插件列表2.1. 密码插件
LastPass: Free Password Manager，密码保存工具，缺陷：免费版只能单设备登录
Bitwarden - 免费密码管理器，密码保存工具，小缺陷：新增密码需要手动
Bitwarden管理页面
LastPass最佳替代工具BitWarden
command+shift+L 自动填充密码



2.2. 书签插件
floccus bookmarks sync，书签同步工具，源码floccusaddon/floccus。如果同步卡住，可以尝试删除坚果云中的 xxx.lock 文件以解决问题。

2.3. 翻译插件
划词翻译，翻译工具
沙拉查词-聚合词典划词翻译，翻译工具
沉浸式翻译，翻译工具
Language Reactor，YouTube/Netfix自动添加双语字幕

2.4. 广告拦截插件
AdBlock — 最佳广告拦截工具，浏览器去广告神器
AdGuard 广告拦截器，浏览器去广告神器

2.5. 下载插件
Chrono下载管理器，下载管理器
Fatkun图片批量下载，批量下载图片的工具
ODM 的视频下载器，在线视频下载器，下载音乐、视频、mp3、图像、文件
视频下载器 - CoCoCut，在线视频下载器
FetchV，在线视频下载器

2.6. 截图插件
捕捉网页截图 - FireShot的，网页截图工具

2.7. 二维码插件
二维码（生成及识别），生成和识别二维码的工具

2.8. 身份验证插件
身份验证器，在浏览器中生成二步认证码

2.9. JSON插件
JSONVue，json格式化查看

2.10. GitHub插件
Octotree - GitHub code tree，github代码树查看工具

2.11. Tab页插件
OneTab，Tab页合并，节省浏览器内存

2.12. AI对话插件
ChatHub - All-in-one chatbot client，AI对话客户端，支持ChatGPT、Bing Chat、Google Bard、Claude等等大模型
New Bing Anywhere，支持在chrome浏览器中使用new bing，注意科学上网节点最好选择美国
Monica - ChatGPT4 驱动的 AI Copilot，AI全能助手，支持回答复杂问题、撰写邮件、阅读文章、智能搜索、翻译、绘画等多种任务

2.13. 脚本注入插件
Tampermonkey（油猴），在网站上运行用户脚本

]]></content>
      <categories>
        <category>computer</category>
      </categories>
  </entry>
  <entry>
    <title>MacOS上软件配置</title>
    <url>/hobby-macos-settings/</url>
    <content><![CDATA[1. 前言本文中，整理自己使用过的一些MacOS系统中的优秀软件，包括软件简介、下载链接、基本配置等。


2. MacOS软件推荐参考文档：

Mac 生产力工具分享

2.1. 系统工具
Chrome，浏览器
Mounty，磁盘挂载工具
EasyRes，调节屏幕分辨率工具
Stretchly，提醒休息工具
幕享，屏幕共享工具
splashtop，屏幕共享工具
Turbo Boost Switcher，CPU温度监测工具
AppCleaner，macos软件卸载工具
openconnect，anyconnect替代工具
KeepingYouAwake，macos屏幕常亮工具
Amphetamine，macos屏幕常亮工具
hiddenbar，折叠菜单栏的工具，让人更容易专注
balenaEtcher，制作USB启动盘的工具，使用方法参考文档Create a bootable USB stick on macOS
Ventoy，制作USB启动盘的工具，可以让U盘同时支持多个系统的安装，需要借助虚拟机使用（因为不支持MacOS）
Bob，翻译工具，支持划词翻译、截图翻译、翻译多开等等，使用方法参考文档ripperhe/Bob
LocalSend，局域网文件共享工具，Windows/macOS/Linux/Android/iOS都可以使用
爱思助手，软件安装工具，在IOS上安装第三方软件
QuickFTP Server，macos启动ftp服务器
FileZilla，开源FTP软件，包含服务端和客户端

2.2. 远程工具
iterm2，shell终端
tabby，shell终端，支持macos、windows和linux
RealVNC Viewer，vnc client
TigerVNC Viewer，vnc client
RustDesk，远程桌面工具，是 TeamViewer、向日葵的替代软件
Royal TS，跨平台远程管理工具，支持rdp、ssh、ftp等协议

2.3. 网盘/云盘
阿里云盘，云盘
百度云盘，云盘
夸克网盘，云盘

2.4. 下载工具
youtube-dl，视频下载工具
you-get，视频下载工具
FDM，下载工具
Motrix，下载工具
imFile，下载工具
BitComet，下载工具

2.5. 阅读器
Skim，pdf阅读器
CHM Viewer Star，chm阅读器
simple-comic，看cbz格式漫画工具，备用下载地址
Calibre，电子书阅读器，支持mobi、azw3、epub、pdf等格式

2.6. 编辑器
sublime，编辑器
typora，markdown编辑器
marktext，markdown编辑器
wps，文档表格编辑器

2.7. 图床工具
PicGo，图床上传图片工具
PicList，图床上传图片工具，基于PicGo二次开发，比PicGo更好用

2.8. 虚拟机
Multipass，虚拟机管理工具
网易MuMu模拟器，安卓模拟器，使用简单方便

2.9. 绘图工具
draw.io/diagrams，软件工程绘图软件。小技巧：飞书云文档绘图和本地绘图可以相互复制粘贴。

2.10. 影音娱乐
QuickTime Player，系统自带的视频播放工具和录屏工具
OBS Studio，录屏工具
IINA，视频播放器
VLC，支持IPTV的播放器，但是不支持IPv6
Fig Player（原 PotPlayer X），支持IPTV的播放器，但是不支持IPv6
TinyPNG4Mac，开源图片压缩工具
HandBrake，开源视频压缩工具
CompressO，开源视频压缩工具
iMovie，系统自带的视频剪辑工具
Audacity，音频剪辑工具

2.11. 数据库客户端
DBeaver，通用数据库客户端，能够连接mysql、postgresql、sqlite、oracle等等数据库
sequelpro，mysql客户端。sequelpro测试版解决了退出报错问题
Apache Directory Studio，ldap client
PrettyZoo，zookeeper可视化客户端工具
RedisInsight，redis可视化客户端工具

2.12. 编程工具
He3，开发者必备的万能工具箱，内置超过200种实用工具
Meld，代码对比工具
postman，接口测试工具
JSON Viewer，json格式化工具
Proxyman，抓包工具
Wireshark，抓包工具

2.13. AI工具
Cursor，AI辅助编程工具
Dive，开源的 MCP 主机桌面应用程序，可以与支持函数调用功能的各种大型语言模型无缝集成

3. 终端配置3.1. MacOS Terminal1、打开terminalcommand＋space，输入terminal。或者 Launchpad，其他，终端。为了方便使用，可以把终端放到Dock。
2、个性设置在home目录下创建 .bash_profile 文件，内容为：
# llalias ll=&quot;ls -l&quot;# for colorexport CLICOLOR=1# \h:\W \u\$export PS1=&#x27;\[\033[01;33m\]\u@\h\[\033[01;31m\] \W\$\[\033[00m\] &#x27;# grepalias grep=&#x27;grep --color=always&#x27;

更好的方式是，使用ohmyzsh。
3.2. ohmyzshOh My Zsh is a delightful, open source, community-driven framework for managing your Zsh configuration. It comes bundled with thousands of helpful functions, helpers, plugins, themes, and a few things that make you shout…”Oh My ZSH!”
sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;# orsh -c &quot;$(wget https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)&quot;

安装完ohmyzsh后，默认使用的shell配置文件是 .zshrc ，在最后添加：
# bash_profilesource ~/.bash_profile
平时添加环境配置，依然在 .bash_profile 中添加，这样就保持了和linux配置的一致性。
在 .zshrc 或者 .bash_profile 中添加一些设置后，也许会提示：
zsh compinit: insecure directories, run compaudit for list.Ignore insecure directories and continue [y] or abort compinit [n]? 

解决办法：
compaudit | xargs chmod g-w,o-w

禁用ohmyzsh更新：1、.zshrc 中添加配置
DISABLE_AUTO_UPDATE=&quot;true&quot;

2、~/.oh-my-zsh/tools/check_for_upgrade.sh 中修改配置
zstyle -s &#x27;:omz:update&#x27; mode update_mode || &#123;  update_mode=disabled&#125;

之后可以手动更新ohmyzsh，omz update
3.3. iterm2参考文档：《MacOS安装配置iTerm2》
3.4. homebrew参考文档：《MacOS安装配置Homebrew》
3.5. git参考文档：《Git实用命令》
3.6. ssh参考文档：《shell命令之ssh》
3.7. tldrtldr项目是社区维护的命令行工具帮助页面的集合，旨在成为对传统帮助的更简单的补充。
1、安装tldr
pip3 install tldr

2、使用
tldr tartldr iconv

3.8. gnu-sedmac（类unix系统）使用bsd sed，而各种linux发行版用的是linux sed。因此，mac sed的用法和linux sed的用法会有一些差异，这就带来很多不便，解决办法是给mac安装linux sed。
1、安装gnu-sed
brew install gnu-sed

2、.bash_profile添加配置
export PATH=&quot;/usr/local/opt/gnu-sed/libexec/gnubin:$PATH&quot;

3、使配置生效
source .bash_profile

3.9. 快捷cd目录参考文档：github - wting/autojump
1、macos中安装autojump
brew update &amp;&amp; brew install autojumpbrew info autojump

2、.bash_profile中添加配置
# autojump[ -f /opt/homebrew/etc/profile.d/autojump.sh ] &amp;&amp; . /opt/homebrew/etc/profile.d/autojump.sh

3、使配置生效
source .bash_profile

4、使用
j dirname
然后配合tab补全，回车跳转目录。
PS：linux中可以通过配置CDPATH变量实现快捷cd目录.bash_profile添加：
# workdirCDPATH=&quot;.:~:~/git&quot;

3.10. ftp client1、解除telnet链接
brew unlink telnet

如果之前已经安装过telnet，需要解除链接。因为ftp命令依赖inetutils，inetutils包含telnet，安装inetutils时会和单独安装的telnet发生冲突。
2、安装inetutils
brew install libidnbrew install inetutilstelnet --helpftp --help

4. 编辑器4.1. sublimesublime破解方法：
1、vim /etc/hosts，添加
127.0.0.1 www.sublimetext.com127.0.0.1 license.sublimehq.com

2、sublime中输入license
—– BEGIN LICENSE —–Mifeng UserSingle User LicenseEA7E-1184812C0DAA9CD 6BE825B5 FF935692 1750523AEDF59D3F A3BD6C96 F8D33866 3F1CCCEA1C25BE4D 25B1C4CC 5110C20E 5246CC42D232C83B C99CCC42 0E32890C B6CBF018B1D4C178 2F9DDB16 ABAA74E5 95304BEF9D0CCFA9 8AF8F8E2 1E0A955E 4771A57650737C65 325B6C32 817DCB83 A7394DFA27B7E747 736A1198 B3865734 0B434AA5—— END LICENSE ——

5. 编程环境5.1. java参考文档：《全平台安装JDK（Java开发环境）》
5.2. pyenv参考文档：《好好学Python：Python版本管理器pyenv》
5.3. conda参考文档：《好好学Python：Python包管理工具Conda》相比于pyenv，更推荐conda，因为conda支持同一个python版本虚拟出多个隔离的环境。
5.4. golang参考文档：《好好学Golang：Golang入门篇》
5.5. nvmnvm是node版本管理管理工具（Node Version Manager），允许我们通过命令行快速安装和使用不同版本的node。
1、安装nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.36.0/install.sh | bash
如果下载脚本报错，就本地创建install.sh，从网页拷贝内容到install.sh后执行。
2、编辑 .bash_profile ，添加内容：
export NVM_DIR=&quot;$HOME/.nvm&quot;[ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;&amp; \. &quot;$NVM_DIR/nvm.sh&quot;  # This loads nvm[ -s &quot;$NVM_DIR/bash_completion&quot; ] &amp;&amp; \. &quot;$NVM_DIR/bash_completion&quot;  # This loads nvm bash_completion

3、使配置生效
source .bash_profilenvm

4、安装使用指定版本
export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/nodenvm -hnvm ls-remotenvm install v10.16.3nvm listnvm use v10.16.3nvm alias default v10.16.3

5.6. puerpuer是一个前端工具，能够在任意目录启动web静态服务，并且实时刷新。
1、全局安装puer
npm install puer -g

2、启动服务
cd /path/to/workspacepuer

svrx是puer的进化版，同样好用。1、全局安装svrx
npm install @svrx/cli -g

2、启动服务
cd /path/to/workspacesvrx

5.7. nginx5.7.1. 安装nginxbrew install nginxnginx -V

默认配置路径：/usr/local/etc/nginx默认页面路径：/usr/local/var/www
5.7.2. 启动nginxnginxlsof -i :8080

5.7.3. 关闭nginxnginx -s stop

5.7.4. 重载配置nginx -tnginx -s reload

6. 数据库6.1. mysql1、安装启动mysql
brew install mysqlmysql.server start

2、mysqlclient连接mysql
mysql -uroot

3、使用sequelpro连接mysql报错： MySQL said: Authentication plugin ‘caching_sha2_password’ cannot be loaded: dlopen(/usr/local/mysql/lib/plugin/caching_sha2_password.so, 2): image not found
解决：参考MySQL重置密码，设置密码。
6.2. mysqlclient1、安装mysqlclient
brew install mysql-client

2、.bash_profile 中添加路径
# mysql-clientexport PATH=&quot;/usr/local/opt/mysql-client/bin:$PATH&quot;export LDFLAGS=&quot;-L/usr/local/opt/mysql-client/lib&quot;export CPPFLAGS=&quot;-I/usr/local/opt/mysql-client/include&quot;export PKG_CONFIG_PATH=&quot;/usr/local/opt/mysql-client/lib/pkgconfig&quot;

3、测试使用
source ~/.zshrcmysql --help

6.3. dbeaver6.3.1. dbeaver连接hive问题dbeaver连接hive，需要从github下载jar包驱动，可能会下载失败。解决办法：手动下载jar包1、编辑驱动设置，点击库，选中库的URL，点击信息2、从信息中复制库或者复制路径，粘贴到浏览器中进行下载3、从信息中复制文件路径，用下载的jar包覆盖这个文件路径
6.3.2. dbeaver报错java heap space问题dbeaver迁移数据，过程中报错：
unhandled event loop exception java heap space

解决办法：调大-Xmx参数参考文档Java heap space
vim /Applications/DBeaver.app/Contents/Eclipse/dbeaver.ini

修改 -Xmx1024m 为 -Xmx2048m，然后重启dbeaver。
7. Uncategories7.1. wscatwscat是用来测试websocket接口的工具，参考文档websockets/wscat
npm install -g wscat wscat -c ws://websocket-echo.com

7.2. 压缩与解压zip压缩和解压zip文件：
zip filename.zip filenameunzip filename.zip

加密压缩和解压zip文件：
zip -er filename.zip filenamezip -er -P xxxxxx filename.zip filenamefor i in `ls`;do zip -er -P xxxxxx $i.zip $i;doneunzip -P xxxxxx filename.zipfor i in *.zip;do unzip -P xxxxxx $i;done

解压中文名zip文件：
brew updatebrew install unarunar -e GBK 中文名.zipunar -e GBK -p xxxxxx 中文名.zipfor i in *.zip;do unar -e GBK -p xxxxxx $i;done

7.3. 解压rarbrew install unrarunrar x package-name.rar

7.4. 命令行发邮件参考文档：《MacOS配置发送邮件》
7.5. frp前提：参考《使用frp进行内网穿透》配置好frp服务端。本节中，配置macos的内网穿透，
1、下载macos用的frp，这里下载frp_0.31.1_darwin_amd64.tar.gz。
2、解压并配置 frpc.ini
[common]server_addr = 120.77.36.182server_port = 7000token = 12345678[ssh]type = tcplocal_ip = 127.0.0.1local_port = 22remote_port = 3422

3、启动frpc客户端
./frpc -c frpc.ini# control+Cbrew install screenscreen ./frpc -c frpc.ini# control+A+D

4、启用远程登录系统偏好设置，共享，勾选远程登录。
5、测试
ssh localhostssh vk@120.77.36.182 -p 3422

6、临时禁用mac休眠关闭屏幕后，mac很快会进入休眠模式，无法再远程登录mac，因此需要临时禁用mac休眠。
pmset noidle

7.6. docker参考Docker on Mac with Homebrew，安装配置docker。
1、安装docker
brew cask install docker

2、初始化配置启动台，点击docker图标，按照提示进行初始化配置。
3、查看安装结果  
docker --version

7.7. stretchlyStretchly is a cross-platform Electron app that reminds you to take breaks when working on your computer.这款软件界面友好，设置简单，提醒也很nice。唯一缺点是软件比较大，70M。
brew update &amp;&amp; brew install --cask stretchly

或者下载dmg软件包进行安装。如果下载不下来，建议使用FDM进行下载。
7.8. virtualboxvirtualbox配置win10文件夹共享：1、启动win10虚拟机2、virtualbox菜单栏，Devices，Insert Guest Additions CD images3、win10内，点击CD驱动器里的VBoxWindowAddions.exe进行插件安装4、virtualbox win10虚拟机设置，共享文件夹5、添加共享文件，共享文件夹路径选择宿主机(Mac)上的一个路径，共享文件夹名称随意，挂载点填入X:6、win10虚拟机内出现了X盘，用于共享
7.9. ffmpeg《m3u8视频下载方法》一文中提到ffmpeg是一个很好用的视频格式转换工具，在macos上安装和使用也很方便。
1、安装ffmpeg
brew install ffmpeg

安装报错 tar: Error opening archive: Failed to open ‘…–bdw-gc-8.0.4_2.catalina.bottle.tar.gz’解决办法：
export HOMEBREW_BOTTLE_DOMAIN=&#x27;&#x27;

2、flv转mp4
ffmpeg -i filename.flv filename.mp4

7.10. openconnectanyconnect下载费劲，需要注册并填写完整信息，而且配置也很麻烦，因此选择使用openconnect替代。
1、安装openconnect
brew install openconnect

2、启动openconnect
sudo openconnect vpn.voidking.com
第一次成功连接后，第二次连接时想要修改用户名密码，但是没有找到入口，于是换成图形界面的openconnect。
3、安装图形界面的openconnect
brew install openconnect-gui --cask

参考文档：

RV34x: Install Cisco AnyConnect Secure Mobility Client on a Mac Computer
MacOS上使用Openconnect代替Cisco Anyconnect

7.11. iperf1、安装iperf
brew install iperf

2、服务器端安装iperf，启动iperf服务端
yum install -y iperfiperf -s

3、使用iperf客户端测试网速
iperf -c 192.168.56.112 -i 3

7.12. 图片压缩参考文档：TinyPNG4Mac
1、注册一个TinyPNG账号
2、安装TinyPNG4Mac（TinyPNG客户端）
brew install --cask tinypng4mac

3、使用TinyPNG4Mac打开TinyPNG4Mac，拖拽要压缩的图片到软件中即可。
PS：如果使用Python进行图片压缩，推荐使用项目GGTinypng，它是TinyPNG的一个Python客户端。
]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>macos</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>MacOS系统配置</title>
    <url>/hobby-macos-system-settings/</url>
    <content><![CDATA[1. 前言本文记录MacOS系统的一些常见配置方法和问题，备忘。
硬件配置：
MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)处理器 2 GHz 四核Intel Core i5内存 16 GB 3733 MHz LPDDR4X图形卡 Intel Iris Plus Graphics 1536 MB控制器型号名称 Apple T2安全芯片

系统版本：macos Catalina 版本10.15.7


2. 基础配置2.1. 用户名配置系统偏好设置，用户与群组，点按锁按钮以进行修改。右键用户，高级选项。修改账户名称为vk，这个名称是shell中显示的用户名。修改全名为voidking，这个名称是mac系统显示的名称。个人目录，修改为 /Users/vk ，这个目录是新的home目录。
修改完成后点击“好”，系统重启，然后整个系统恢复到出厂状态。
2.2. 密码系统偏好设置，用户与群组，更改密码。
2.3. 电脑名称系统偏好设置，共享，电脑名称，编辑。
2.4. 触控板系统偏好设置，触控板，光标与点按。勾选 轻点来点按，勾选 辅助点按，跟踪速度调整为喜欢的速度。
2.5. 菜单快捷键右上角苹果按钮，点击会出现“睡眠”、“重新启动…”、“关机…”等菜单选项，这些选项可以设置对应的快捷键。以设置“睡眠”快捷键为例：右上角苹果按钮，系统偏好设置，键盘，快捷键，App快捷键，加号，菜单标题输入“睡眠”，键盘快捷键按下“control+command+S”。以上，就设置好了睡眠的快捷键。 
2.6. 开机声音1、关闭开机声音
sudo nvram BootAudio=%00sudo nvram SystemAudioVolume=%80

2、打开开机声音
sudo nvram BootAudio=%01sudo nvram -d SystemAudioVolume

2.7. 禁止系统更新系统偏好设置，系统更新，取消勾选 自动保持我的Mac运行最新版本。高级，取消勾选 所有选项。
点击 有其他更新可用 下面的 更多信息 ，取消更新计划。
2.8. 删除系统更新文件系统更新文件放在/Library/Updates目录中，但是普通模式下是没有权限删除该目录下的文件的。
1、重启电脑，同时按下cmd+R，看到白苹果的进度条时松开手，然后会进入恢复模式。
2、关闭系统完整性保护（SIP）
csrutil disablereboot

3、重启后，删除升级文件
cd /Library/Updatessudo rm -rf *

4、重新进入恢复模式，打开系统完整性保护
csrutil enablereboot

3. 小技巧3.1. 不关闭屏幕设置临时不关闭屏蔽。
sudo supmset -g | grep sleeppmset -a displaysleep 30pmset -g | grep sleeppmset -a displaysleep 10pmset -g | grep sleep

或者使用Amphetamine，macos屏幕常亮工具
3.2. 查看所有文件打开Finder，command＋逗号，出现Finder偏好设置，边栏，设备中勾选硬盘。然后在侧边栏中，就可以打开硬盘，看到所有文件了。
当然， /etc 这种文件夹是看不到的。
3.3. 设置默认文件排序打开Finder，点击家目录，command＋J，选择排序方式（种类），用做默认。家目录下的所有目录，都会继承这种默认排序方式。
3.4. 禁止生成 .DS_Store.DS_Store(Desktop Services Store)文件目的在于存贮目录的自定义属性，例如显示方式为图标还是列表。
禁止：
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool TRUE

开启：
defaults delete com.apple.desktopservices DSDontWriteNetworkStores

注意：禁止生成 .DS_Store 后，已经存在的 .DS_Store 不受影响。
3.5. 清除最近项目点击左上角苹果图标，最近使用的项目，清除菜单。
但是，上面的方法并不会清除“最近项目”这个目录下的文件记录，这是因为“最近项目”是spotlight管理的。解决办法：1、点击左上角苹果图标，系统偏好设置，聚焦，隐私，点击+号。2、选择目录A，点击选取。3、再看“最近项目”，已经不再显示目录A里的文件。需要注意的是，如果从隐私里去掉目录A，那么目录A里的文件还会慢慢出现在“最近项目”。
如果想要改变“最近项目”目录筛选文件的规则，按照如下方法处理：1、给“最近项目”添加一个搜索条件，让其只显示最近三天内打开或修改过的项目2、在“最近项目”窗口中点击工具栏的齿轮按钮然后选择“显示搜索条件”3、在搜索项目里选择“上次打开日期”4、然后设定好相应的搜索条件，点击“存储”5、最后为其命名，切记勾选“添加到边栏”再点击“存储”6、边栏就会新增这个只显示最近 3 天项目的选项，然后把原来的“最近项目”从边栏移除掉。
3.6. 预览清除最近项目打开预览，菜单栏，文件，打开最近使用，清除菜单。
3.7. 安装字体1、从windows系统 C:\Windows\Fonts 目录中拷贝需要的字体。2、command+shift+G，输入 /Library/Fonts，前往。3、字体放入 Fonts 目录。
3.8. 启动sftp1、系统偏好设置，共享，勾选远程登录
2、测试
sftp localhost

3.9. 启动ftp使用QuickFTP Server
3.10. 目录规划$&#123;HOME&#125;目录下，创建以下目录：

tmp：在该目录中执行命令、测试脚本等，随时可删。
scripts：成熟的脚本放在该目录下。
git：在该目录中存放git仓库。

3.11. 读写ntfsmac不借助第三方软件，也是可以读写ntfs文件系统的移动硬盘的。1、查看移动硬盘名称在桌面上查看硬盘名称，或者通过diskutil list命令查看硬盘名称。
2、修改fstab配置比如硬盘名称为WD，那么在/etc/fstab中添加配置：
LABEL=WD none ntfs rw,auto,nobrowse

3、重新插入硬盘推出硬盘，再重新插入，发现桌面不再显示该硬盘，没关系。
4、读写移动硬盘
df -hcd /Volumes/WDopen .

5、推出移动硬盘方法一：
cd ..open .# 界面点击推出移动硬盘

方法二：
cd ..diskutil unmount /Volumes/WDdiskutil unmount force /Volumes/WD

3.12. 检查wifi信号强度3.12.1. 图形界面打开wireless Diagnostics（无线诊断），忽略打开的窗口。菜单栏点击窗口，扫描，看到网络信息中，RSSI表示信号强度，这个值越接近0值，信号强度越好。
3.12.2. 命令行显示附近可用wifi网络列表，包括信号强度、信道和加密类型等。
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s
其中RSSI表示信号强度，这个值越接近0值，信号强度越好。
查看当前连接的wifi网络信息，包括信号强度、信道和加密类型等。
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I
其中agrCtlRSSI表示信号强度，这个值越接近0值，信号强度越好。
3.13. 访问samba1、打开Finder，菜单，前往，连接服务器2、填写服务器地址，注意格式为smb://samba服务器的IP地址，这里也可以点一下“+”号把它添加到收藏里面3、填写用户名和密码，连接samba服务器
4. 问题记录4.1. WindowServer占用资源过多系统偏好设置，辅助功能，显示，勾选减弱动态效果和减少透明度
4.2. 安装软件提示身份不明开发者问题从网络下载的软件，安装时提示：打不开“xxx”，因为它来自身份不明的开发者。
解决办法：系统偏好设置，安全性和隐私，点锁按钮以进行更改，任何来源，允许来自任何来源。
4.3. 打开软件提示将对电脑造成伤害打开V2rayU时，报错：“V2rayUTool”将对您的电脑造成伤害。 您应该将它移到废纸篓。
问题原因：证书过期问题导致的。
解决办法：1、执行应用程序代码签名的命令
sudo codesign --force --deep --sign - /Applications/V2rayU.app

2、在应用程序中找到V2rayU，右键，显示简介，勾选覆盖恶意软件保护
3、打开V2rayU软件
4、执行应用程序代码签名的命令
sudo codesign --force --deep --sign - ~/.V2rayU/V2rayUToolsudo codesign --force --deep --sign - ~/.V2rayU/v2ray-core/v2ray

参考文档：频繁提示 “V2rayUTool”将对你的电脑造成伤害。 你应该将它移到废纸篓
4.4. 解锁屏幕后卡住提示连接键盘问题解锁屏幕后卡住：蓝牙键盘设置 没有连接好的键盘
解决办法：系统偏好设置，蓝牙，高级，取消勾选 “如果未检测到键盘，则在启动时打开蓝牙设置助理”
4.5. 解锁屏幕后卡住两分钟问题解锁屏幕后卡住：鼠标、键盘都没反应，过一两分钟才能恢复正常
解决办法：重置系统管理控制器（SMC）。具体操作：1、将 Mac 关机。2、在内建键盘上，按住左Control + 左Option (Alt)  + 右Shift ，Mac 可能会开机。3、按住这三个按键 7 秒钟，然后在不松开这些按键的情况下按住电源按钮。如果 Mac 处于开机状态，它将在你按住这些按键时关机。4、继续按住这四个按键 7 秒钟，然后松开这些按键。5、等待几秒钟，然后按下电源按钮以将 Mac 开机。
参考文档：

重置 Mac 的 SMC
重置 Mac 上的 NVRAM

4.6. 触摸板拖动失效问题描述：触摸板拖动窗口和选择文本功能突然失效，触摸板设置正常，关机重启问题依然存在。
解决办法：重置系统管理控制器（SMC）。
4.7. 雷电接口突然不可用问题问题描述：MacBook四个雷电3接口，其中一个接口突然不能用了，充电没反应，接显示器也没反应，另外三个接口正常。
解决办法：将 Mac 关机并合上显示屏等待 30 秒，然后翻开显示屏以启动 Mac。
4.8. command+space失效问题问题描述：command+space是打开聚焦搜索的快捷键，但是，有时候发现点击了command+space也不会出现聚焦搜索的窗口。查看系统偏好配置，快捷键也没有被更改。重启机器后，快捷键能够能够恢复正常。
解决办法：看看另外一块屏幕！！！有时候聚焦搜索会跑到另外一块屏幕上。
4.9. 腾讯云iOA无法退出问题问题描述：腾讯云iOA客户端退出之后，后台依然存在QQPCMgr进程。哪怕手动kill掉这些进程，也会很快拉起。
尝试解决办法（实际不可行）：
# 查找自启动项ls ~/Library/LaunchAgents | grep -i &quot;tencent\|qq&quot;ls /Library/LaunchAgents | grep -i &quot;tencent\|qq&quot;ls /Library/LaunchDaemons | grep -i &quot;tencent\|qq&quot;ls /System/Library/LaunchAgents | grep -i &quot;tencent\|qq&quot;ls /System/Library/LaunchDaemons | grep -i &quot;tencent\|qq&quot;# 禁用自启动项#sudo launchctl bootout system /Library/LaunchAgents/com.tencent.PreLoginQQPCMgr.Daemon.plist#sudo launchctl bootout system /Library/LaunchAgents/com.tencent.QQPCMgr.plist#sudo launchctl unload /Library/LaunchDaemons/com.tencent.runmgr.plist# 查看是否禁用成功#sudo launchctl list | grep -i &quot;tencent\|qq&quot;#launchctl print system/com.tencent.runmgr# 删除自启动项sudo mv /Library/LaunchAgents/com.tencent.PreLoginQQPCMgr.Daemon.plist&#123;,.bak&#125;sudo mv /Library/LaunchAgents/com.tencent.QQPCMgr.plist&#123;,.bak&#125;sudo mv /Library/LaunchDaemons/com.tencent.runmgr.plist&#123;,.bak&#125;# 再次kill进程ps -ef | grep QQPCMgr | awk &#x27;&#123;print $2&#125;&#x27; | xargs sudo kill -9

可行解决办法一：终端管理，终端信息，处置，卸载客户端，下发卸载任务；处置，回收授权。
可行解决办法二：给IOA客户端开启特权模式，特权模式下执行IOA客户端卸载
1、客户端管理，终端自保护策略，允许退出客户端，允许客户端进入特权模式
2、登录IOA，IOA客户端右上角目录栏，选择特权模式
3、终端信息生成特权码，并在IOA客户端输入
4、退出IOA，执行卸载操作
5、kill残留进程 ps -ef | grep QQPCMgr | awk &#39;&#123;print $2&#125;&#39; | xargs sudo kill -9
6、删除残留文件 sudo rm -rf /Applications/QQPCMgr.localized
]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>macos</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：安装部署K8S集群的艰难之路</title>
    <url>/dev-install-k8s-the-hard-way/</url>
    <content><![CDATA[1. 前言《使用kubeadm安装部署K8S集群》一文中，使用kubeadm安装部署了k8s集群。但是，kubeadm的安装方式太简单了，而cka的要求不止这么简单。因此，我们还需要学习从零开始，一个一个组件安装配置k8s集群的方法，所谓k8s the hard way。
本文的目标是在virtualbox中，搭建一个k8s集群，一个master节点，一个node01节点。
主要参考kelseyhightower/kubernetes-the-hard-way和mmumshad/kubernetes-the-hard-way。


2. 准备
创建两台centos7虚拟机，master节点1C2G，node01节点1C1G
配置网络，master节点IP为192.168.56.150，node01节点的IP为192.168.56.151
配置hostname，并且把两个节点的hostname添加到/etc/hosts
安装Docker，参考《Docker入门》

3. 安装流程1、安装kubectl
2、创建CA，给每个组件生成TLS证书TLS证书包括：ETCD Server CertificateKubernetes API Server CertificateController Manager Client CertificateScheduler Client Certificate
Service Account Key PairKube Proxy Client CertificateKubelet Client CertificatesAdmin Client Certificate
3、给每个组件生成k8s配置文件，用于访问apiserver
4、生成数据加密配置和密钥，使集群支持静态加密
5、指定CA和TLS，在master节点启动etcd
6、指定CA和TLS，在master节点启动kube-apiserver、kube-controller-manager、kube-scheduler
7、指定CA和TLS，在node01节点启动kubelet和kube-proxy
8、指定CA和TLS，生成admin用户的配置文件，使用kubectl可以访问集群
9、部署weave，使pod可以获取到IP
10、部署coredns，使svc服务名可以使用
11、Smoke Test和End-to-End Tests
4. 实践篇操作过程太长，具体还是参考前言中的两个 kubernetes-the-hard-way 文档吧。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：MySQL中的游标</title>
    <url>/dev-mysql-cursor/</url>
    <content><![CDATA[1. 需求已知app表有两个字段：id和name，build_history表有四个字段：id、app_id、image_path和create_time。app构建后会生成镜像，镜像地址存在于build_history表中的image_path字段。
现在想要查找出每个app的最新的五个镜像，该怎样实现？


2. 思路思路一：使用select拿到所有的app id，然后使用shell脚本生成很多条sql语句，每条sql语句的查询结果插入到一个结果表中。具体参考《Shell实用脚本》循环读取多列文本部分。
思路二：使用mysql cursor，拿到所有的app id，然后循环读取build_image中的内容，插入到结果表中。本文中采用mysql cursor的方法，实现我们的需求。
3. 代码实现-- create tabledrop table if exists latest_image;CREATE TABLE test.latest_image (    id INT auto_increment NOT null primary key,	app_id INT NULL,	app_name varchar(128) NULL,	app_image varchar(512) NULL)ENGINE=InnoDBDEFAULT CHARSET=utf8COLLATE=utf8_general_ci;-- create proceduredrop procedure if exists get_latest;CREATE PROCEDURE get_latest (num int(2))BEGINdeclare appid int default 0;declare appname varchar(128) default &#x27;&#x27;;declare appimage varchar(512) default &#x27;&#x27;;declare finished int default 0;	-- declare cursor for appdeclare appcursor CURSOR FOR SELECT id,name FROM app;	-- declare NOT FOUND handlerdeclare CONTINUE HANDLER FOR NOT FOUND SET finished = 1;OPEN appcursor;get_item: LOOP	FETCH appcursor INTO appid,appname;	IF finished = 1 THEN 		LEAVE get_item;	END IF;	-- select appid,appname;	-- select id,app_id,image_path from build_history where app_id=appid order by create_time desc limit 5;	-- insert into latest_image(`app_id`,&#x27;app_name&#x27;,&#x27;app_image&#x27;) values(appid,appname,appimage);	insert into latest_image(`app_id`,`app_image`)	select app_id as app_id,image_path as app_image 	from build_history 	where app_id=appid order by create_time desc limit num;END LOOP get_item;CLOSE appcursor;END-- call procedurecall get_latest(5);
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>mysql</tag>
        <tag>数据库</tag>
        <tag>好好学MySQL</tag>
      </tags>
  </entry>
  <entry>
    <title>PostgreSQL入门篇</title>
    <url>/dev-postgresql-start/</url>
    <content><![CDATA[1. PostgreSQL简介
PostgreSQL is a powerful, open source object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.

本文中，我们会学习PostgreSQL的安装和使用方法。
参考文档：

PostgreSQL官网
PostgreSQL Documentation
维基百科 - PostgreSQL
PostgreSQL 菜鸟教程。



2. 安装pg2.1. Docker安装Postgres1、登录dockerhub查看需要的postgres版本。
2、下载postgres镜像（以postgres12.2为例）
docker pull postgres:12.2

3、启动postgres服务
docker run --name vk-pg -d \    -p 5432:5432 \    -e POSTGRES_PASSWORD=voidking \    -e PGDATA=/var/lib/postgresql/data/pgdata \    -v /opt/postgresql/data:/var/lib/postgresql/data \    postgres:12.2

以上命令：

命名容器为vk-pg，后台运行
映射宿主机5432端口到容器5432端口
设置pg数据库密码和数据存放目录
映射宿主机/opt/postgresql/data目录（不需要提前创建目录）到容器/var/lib/postgresql/data目录

更高级的启动命令参考How to use this image。
4、验证安装
docker ps
postgres启动正常的话就可以看到vk-pg容器。如果启动失败，可以使用docker logs vk-pg查看失败原因并进行解决。
使用nc命令验证：
yum install ncnc -v localhost 5432

5、登录postgres
docker exec -it vk-pg /bin/bash$ psql -U postgres$ psql -U postgres -h localhost -p 5432

直接能登录进去，不需要密码。这是因为，pg镜像在本地设置了信任身份验证，因此从容器内连接时不需要密码。
但是，如果从其他主机/容器进行连接，则需要输入密码。比如在宿主机（系统为centos7）上登录postgres：
yum list | grep postgresqlyum install postgresql.x86_64psql -U postgres -h localhost -p 5432
这次必须输入正确的密码才能登录。
以上，postgres安装配置完成。
2.2. K8S安装Postgres参考文档：PostgreSQL packaged by Bitnami
1、添加helm chart仓库
helm repo add bitnami https://charts.bitnami.com/bitnami

2、获取postgres chart这里选择chart版本11.6.6，对应app版本14.3.0
helm search repo bitnami/postgresql -lhelm pull bitnami/postgresql --version 11.6.6tar -xzvf postgresql-11.6.6.tgzvim postgresql/values.yaml

3、values.yaml 修改配置

global.storageClass 修改为期望的 storageclass
global.postgresql.auth.postgresPassword 修改admin用户的密码（覆盖auth.postgresPassword）
global.postgresql.auth.username 自定义用户名（覆盖auth.username）
global.postgresql.auth.password 自定义用户密码（覆盖auth.password）
global.postgresql.auth.database 自定义数据库（覆盖auth.database）

4、安装postgres
helm install postgres ./postgresql \    -n postgres --create-namespace

5、查看安装
kubectl get all -n postgres

3. Postgres启动报错问题3.1. 问题描述使用bitnami chart安装的postgres，pod重建后无法启动，一直重启，看日志报错：
LOG: database system was interrupted; last known up at xxxx-xx-xx xx:xx:xx GMTFATAL: the database system is starting up

3.2. 解决办法修改探活，增加 initialDelaySeconds 或者增加 failureThreshold 。
问题原因：如果服务器突然停止，或者突然失去对文件系统的访问权限，那么内存中的所有内容都会丢失，下次启动它时，它需要重新播放日志，以使表恢复到正确的状态（这可能需要相当长的时间，具体取决于自上次检查点以来发生了多少）。在完成之前，任何使用服务器的尝试都将导致 FATAL: the database system is starting up
参考文档：Postgresql fatal the database system is starting up - windows 10
4. 基本操作4.1. 连接postgrespsql -U postgres -h localhost -p 5432

4.2. 创建数据库create database mydb;

4.3. 创建新用户create user testuser with password &#x27;test123&#x27;;

或者：
create user testuser;alter role testuser with password &#x27;test123&#x27;;

PS：删除用户
drop user testuser;

4.4. 授权访问数据库grant all privileges on database mydb to testuser;

4.5. 指定用户访问数据库psql -U testuser -d mydb

4.6. 切换数据库\l\c mydb;

4.7. 创建修改表创建表：
create table mytable(    id serial primary key,    name varchar(100));

修改表：
alter table mytable add column password varchar(50);

4.8. 查看表\dt\d \d mytable;

4.9. 增删查改PostgreSQL数据的增删查改语法，与MySQL相同。
插入数据：
insert into mytable(name,password) values(&#x27;voidking&#x27;,&#x27;test123&#x27;);

查询数据：
select * from mytable;

修改数据：
update mytable set name=&#x27;test&#x27; where name=&#x27;voidking&#x27;;

删除数据：
delete from mytable where name=&#x27;test&#x27;;

5. Harbor中的Postgres1、进入postgres容器
docker exec -it harbor-db /bin/bash

2、登录postgres
psql -U postgres -h postgresql -p 5432
默认密码为 root123 ，实际密码查看harbor.yml中的配置。
3、查看帮助
\help\help select

4、查看数据库，切换数据库，查看表
\l\c registry;\d\dt

5、查看表结构
\d registry;\d replication_policy;

6、查看表数据
select * from registry;select * from replication_policy;
可以看到，查看表数据的方法和mysql完全相同。实际上，postgres支持标准的sql语法，因此增删查改的语法可以参考《MySQL常用命令》即可。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>database</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>数据库</tag>
        <tag>harbor</tag>
        <tag>bitnami</tag>
        <tag>postgresql</tag>
      </tags>
  </entry>
  <entry>
    <title>Harbor入门篇</title>
    <url>/dev-harbor-start/</url>
    <content><![CDATA[1. Harbor简介
Harbor is an open source container image registry that secures images with role-based access control, scans images for vulnerabilities, and signs images as trusted. As a CNCF Incubating project, Harbor delivers compliance, performance, and interoperability to help you consistently and securely manage images across cloud native compute platforms like Kubernetes and Docker.

更多内容参考Harbor官网。


2. Harbor架构Harbor的目标是帮助用户迅速搭建一个企业级的私有 Docker registry 服务。它以Docker公司开源的registry  为基础，提供了管理UI，基于角色的访问控制(Role Based Access Control)，AD/LDAP集成、以及审计日志(Audit logging) 等企业用户需求的功能。
Harbor在架构上主要由五个组件构成：Proxy、Registry、Core services、Database、Log collector

Proxy：Harbor的registry、UI、token等服务，通过一个前置的反向代理统一接收浏览器、Docker客户端的请求，并将请求转发给后端不同的服务。
Registry：负责储存Docker镜像，并处理docker push/pull 命令。
Core services：Harbor的核心功能，主要包括UI、Webhook和Token。UI提供图形化界面；Webhook及时获取registry上image的变化，同步给UI；Token服务根据用户权限给每个docker push/pull命令签发token。
Database：为core services提供数据库服务，负责储存用户权限、审计日志、Docker image分组信息等数据。
Log collector：负责收集其他组件的log。

参考文档：

企业级Registry开源项目Harbor架构简介
Architecture Overview of Harbor

3. 安装Harbor3.1. 前置条件参考Harbor Installation Prerequisites
1、硬件需求硬件最小需求：2C4G40G。硬件推荐需求：4C8G160G。
2、软件需求Docker engine，Version 17.06.0-ce+ or higherDocker Compose，Version 1.18.0 or higherOpenssl，Latest is preferred
3、网络端口444、4443和80
3.2. 下载安装Harborharbor计划安装目录为/opt/harbor，数据目录为/opt/harbor_data
1、访问Harbor releases page，下载需要的harbor版本，这里选择下载 harbor-offline-installer-v1.10.1.tgz
cd ~wget https://github.com/goharbor/harbor/releases/download/v1.10.1/harbor-offline-installer-v1.10.1.tgz# orcurl -C - -O -L https://github.com/goharbor/harbor/releases/download/v1.10.1/harbor-offline-installer-v1.10.1.tgz

2、解压
tar -xzvf harbor-offline-installer-v1.10.1.tgzmv harbor /optcd /opt/harbor

3、配置harbor.yml
# The IP address or hostname to access admin UI and registry service.# DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.hostname: harbor.voidking.com# http related confighttp:  # port for http, default is 80. If https enabled, this port will redirect to https port  port: 80# https related config# https:  # https port for harbor, default is 443  #port: 443  # The path of cert and key files for nginx  #certificate: /your/certificate/path  #private_key: /your/private/key/path# The initial password of Harbor admin# It only works in first time to install harbor# Remember Change the admin password from UI after launching Harbor.harbor_admin_password: Harbor12345# The default data volumedata_volume: /opt/harbor_data# Harbor DB configurationdatabase:  # The password for the root user of Harbor DB. Change this before any production use.  password: root123  # The maximum number of connections in the idle connection pool. If it &lt;=0, no idle connections are retained.  max_idle_conns: 50  # The maximum number of open connections to the database. If it &lt;= 0, then there is no limit on the number of open connections.  # Note: the default number of connections is 100 for postgres.  max_open_conns: 100# Log configurationslog:  # options are debug, info, warning, error, fatal  level: info  # configs for logs in local storage  local:    # Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated.    rotate_count: 50    # Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes.    # If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G    # are all valid.    rotate_size: 200M    # The directory on your host that store log    location: /var/log/harbor

配置说明：

hostname: 指定harbor域名，这个域名的作用有两个：1）这是用来鉴权的域名，我们可以给harbor配置多个域名，但是在鉴权时只会请求这个域名；2）从页面拷贝docker pull命令时会使用这个域名作为前缀。
https：建议注释掉，不要在这里配置证书，而是配置到nginx层。
harbor_admin_password：harbor的admin用户密码。
data_volume：存储镜像数据的路径。

更多配置内容，参考Configure the Harbor YML File
4、执行安装
./install.sh
如果安装完成发现配置错误，可以修改配置后再次执行脚本。如果报错 ERROR: Failed to Setup IP tables: Unable to enable SKIP DNAT rule ，那么重启docker后再次执行脚本。
至此，harbor安装完成，没有配置https。
4. 修改Harbor配置1、停止
docker-compose down -v

2、修改配置修改harbor.yml后，执行
./prepare

3、启动
docker-compose up -d

注意：如果是修改harbor的hostname，harbor重新启动后，数据库内容和镜像内容是依然存在的，可以放心修改。
5. 验证Harbor安装5.1. 浏览器验证浏览器访问 http://192.168.56.200 ，可以看到harbor登录页面。输入用户名密码，admin和Harbor12345，登录harbor控制台。
5.2. 登录验证docker psdocker login 192.168.56.200docker login harbor.voidking.com
输入用户名密码，admin和Harbor12345，登录报错。
5.3. 登录验证报错处理5.3.1. 报错connection refusedError response from daemon: Get https://192.168.56.200/v2/: dial tcp 192.168.56.200:443: connect: connection refused
这是因为，docker1.3.x之后与registry交互，默认使用https协议。
1、修改/etc/docker/daemon.json，添加insecure-registries参数
&#123;  &quot;registry-mirrors&quot;: [    &quot;https://mirror.ccs.tencentyun.com&quot;  ],  &quot;insecure-registries&quot;: [    &quot;http://192.168.56.200&quot;,    &quot;http://harbor.voidking.com&quot;  ]&#125;

2、重启docker和harbor
systemctl daemon-reloadsystemctl restart dockerdocker infocd /opt/harbordocker-compose up -ddocker-compose ps -a

5.3.2. 报错Service UnavailableError response from daemon: Get “https://192.168.56.200/v2/&quot;: Service Unavailable或者Error response from daemon: Get “http://192.168.56.200/v2/&quot;: received unexpected HTTP status: 503 Service Unavailable
这个错误，大概率是因为docker配置了代理，docker配置代理的方法参考《Linux配置网络代理》。
1、去掉docker代理，或者在代理配置的NO_PROXY变量中加入harbor的IP和域名
vim /etc/systemd/system/docker.service.d/http-proxy.conf

2、重启docker和harbor
systemctl daemon-reloadsystemctl restart dockerdocker infocd harbordocker-compose up -d

5.3.3. 报错no such hostError response from daemon: Get “http://192.168.56.200/v2/&quot;: Get “http://harbor.voidking.com/service/token?account=admin&amp;client_id=docker&amp;offline_token=true&amp;service=harbor-registry&quot;: dial tcp: lookup harbor.voidking.com: no such host
在/etc/hosts中添加解析：
harbor.voidking.com 192.168.56.200

重新登录，成功。
6. 使用Harbor6.1. 启动和停止Harbor参考文档Reconfigure Harbor and Manage the Harbor Lifecycle
cd /opt/harbor# 查看容器状态docker-compose ps -a# 停止harbordocker-compose stop# 启动harbordocker-compose start

6.2. 上传镜像docker pull busybox:1.31docker tag busybox:1.31 harbor.voidking.com/voidking/busybox:1.31docker tag busybox:1.31 harbor.voidking.com/voidking/subpath/busybox:1.31docker push harbor.voidking.com/voidking/busybox:1.31

docker push如果报错：The push refers to repository [harbor.voidking.com/voidking/busybox]a6d503001157: Preparingdenied: requested access to the resource is denied这是因为，需要先创建项目。在web控制台创建项目 voidking，再次上传，成功。带有subpath的镜像，同样可以上传成功。
6.3. 下载镜像docker pull harbor.voidking.com/voidking/busybox:1.31

6.4. 查找docker-compose.yml如果不知道harbor docker-compose.yml的目录，就没有办法对harbor服务进行启停。查找docker-compose.yml最好使用locate命令，速度快。
locate docker-compose.yml

6.5. 修改harbor数据库参考文档：《PostgreSQL入门篇》
7. 错误排查cd /var/log/harbor/ls -ltail -n 100 core.log

8. 配置Harbor自动启动当docker发生了重启之后，需要手动执行docker-compose start启动harbor，比较麻烦，因此最好配置上harbor的自动启动。详情参考Docker 笔记 - 将 Harbor 设置为系统服务
1、准备systemd配置文件
vim /etc/systemd/system/harbor.service

harbor.service内容如下：
[Unit]Description=harborAfter=docker.service systemd-networkd.service systemd-resolved.serviceRequires=docker.serviceDocumentation=http://github.com/vmware/harbor[Service]Type=simpleRestart=on-failureRestartSec=5ExecStart=/usr/local/bin/docker-compose -f  /opt/harbor/docker-compose.yml startExecStop=/usr/local/bin/docker-compose -f  /opt/harbor/docker-compose.yml stopRemainAfterExit=yes[Install]WantedBy=multi-user.target

2、设置开机自启动
systemctl daemon-reloadsystemctl enable harbor.servicesystemctl status harbor.servicedocker-compose ps -a

3、测试重启docker后harbor自启动
systemctl restart dockersystemctl status harbor.servicedocker-compose ps -a

9. 高可用如果搭建高可用harbor，比如搭建两个实例的harbor，那么需要XSRFKey保持一致，在 common/config/core/app.conf 中配置。
10. Harbor同步配置参考文档：

Creating Replication Endpoints
Creating a Replication Rule

可能的问题：harbor配置了复制管理，当出现多个大镜像需要同步时，harbor复制可能会卡死，出现大量pending状态的任务。在页面上手动停止卡死的任务，新的复制同步任务依然会处于pending状态。解决办法：手动停止任务后，还需要重启一下 harbor jobservice 组件。
docker-compose -f /path/to/harbor/docker-compose.yml restart jobservice


]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>docker-compose</tag>
        <tag>harbor</tag>
        <tag>错误排查</tag>
      </tags>
  </entry>
  <entry>
    <title>JSONPath入门篇</title>
    <url>/dev-jsonpath-start/</url>
    <content><![CDATA[1. JSONPath简介
JSON (JavaScript Object Notation) allows for easy interchange of data, often between a program and a database.


JSONPath is a query language for JSON, similar to XPath for XML. 

如上，json是一种常用的数据格式，jsonpath是json的查询语言，类似于XPath和SQL。
jsonpath在线测试：JSONPath Online Evaluator


2. YAML和JSONYAML和JSON可以相互转化，详情参考《YAML语言》。
3. JSONPath语法摘自 json-path/JsonPath
3.1. 操作符


Operator
Description



$
The root element to query. This starts all path expressions.


@
The current node being processed by a filter predicate.


*
Wildcard. Available anywhere a name or numeric are required.


..
Deep scan. Available anywhere a name is required.


.&lt;name&gt;
Dot-notated child


[&#39;&lt;name&gt;&#39; (, &#39;&lt;name&gt;&#39;)]
Bracket-notated child or children


[&lt;number&gt; (, &lt;number&gt;)]
Array index or indexes


[start:end]
Array slice operator


[?(&lt;expression&gt;)]
Filter expression. Expression must evaluate to a boolean value.


3.2. 函数Functions can be invoked at the tail end of a path - the input to a function is the output of the path expression.The function output is dictated by the function itself.



Function
Description
Output



min()
Provides the min value of an array of numbers
Double


max()
Provides the max value of an array of numbers
Double


avg()
Provides the average value of an array of numbers
Double


stddev()
Provides the standard deviation value of an array of numbers
Double


length()
Provides the length of an array
Integer


sum()
Provides the sum value of an array of numbers
Double


3.3. 过滤器Filters are logical expressions used to filter arrays. A typical filter would be [?(@.age &gt; 18)] where @ represents the current item being processed. More complex filters can be created with logical operators &amp;&amp; and ||. String literals must be enclosed by single or double quotes ([?(@.color == &#39;blue&#39;)] or [?(@.color == &quot;blue&quot;)]).   



Operator
Description



==
left is equal to right (note that 1 is not equal to ‘1’)


!=
left is not equal to right


&lt;
left is less than right


&lt;=
left is less or equal to right


&gt;
left is greater than right


&gt;=
left is greater than or equal to right


=~
left matches regular expression  [?(@.name =~ /foo.*?/i)]


in
left exists in right [?(@.size in [‘S’, ‘M’])]


nin
left does not exists in right


subsetof
left is a subset of right [?(@.sizes subsetof [‘S’, ‘M’, ‘L’])]


anyof
left has an intersection with right [?(@.sizes anyof [‘M’, ‘L’])]


noneof
left has no intersection with right [?(@.sizes noneof [‘M’, ‘L’])]


size
size of left (array or string) should match right


empty
left (array or string) should be empty


3.4. DEMOGiven the json
&#123;    &quot;store&quot;: &#123;        &quot;book&quot;: [            &#123;                &quot;category&quot;: &quot;reference&quot;,                &quot;author&quot;: &quot;Nigel Rees&quot;,                &quot;title&quot;: &quot;Sayings of the Century&quot;,                &quot;price&quot;: 8.95            &#125;,            &#123;                &quot;category&quot;: &quot;fiction&quot;,                &quot;author&quot;: &quot;Evelyn Waugh&quot;,                &quot;title&quot;: &quot;Sword of Honour&quot;,                &quot;price&quot;: 12.99            &#125;,            &#123;                &quot;category&quot;: &quot;fiction&quot;,                &quot;author&quot;: &quot;Herman Melville&quot;,                &quot;title&quot;: &quot;Moby Dick&quot;,                &quot;isbn&quot;: &quot;0-553-21311-3&quot;,                &quot;price&quot;: 8.99            &#125;,            &#123;                &quot;category&quot;: &quot;fiction&quot;,                &quot;author&quot;: &quot;J. R. R. Tolkien&quot;,                &quot;title&quot;: &quot;The Lord of the Rings&quot;,                &quot;isbn&quot;: &quot;0-395-19395-8&quot;,                &quot;price&quot;: 22.99            &#125;        ],        &quot;bicycle&quot;: &#123;            &quot;color&quot;: &quot;red&quot;,            &quot;price&quot;: 19.95        &#125;    &#125;,    &quot;expensive&quot;: 10&#125;




JsonPath (click link to try)
Result



$.store.book[*].author
The authors of all books


$..author
All authors


$.store.*
All things, both books and bicycles


$.store..price
The price of everything


$..book[2]
The third book


$..book[-2]
The second to last book


$..book[0,1]
The first two books


$..book[:2]
All books from index 0 (inclusive) until index 2 (exclusive)


$..book[1:2]
All books from index 1 (inclusive) until index 2 (exclusive)


$..book[-2:]
Last two books


$..book[2:]
Book number two from tail


$..book[?(@.isbn)]
All books with an ISBN number


$.store.book[?(@.price &lt; 10)]
All books in store cheaper than 10


$..book[?(@.price &lt;= $[‘expensive’])]
All books in store that are not “expensive”


$..book[?(@.author =~ /.*REES/i)]
All books matching regex (ignore case)


$..*
Give me every thing


$..book.length()
The number of books


4. kubectl + JSONPathkubelet支持JSONPath，具体参考JSONPath 支持。除了标准jsonpath语法外，kubernetes jsonpath模板还额外支持以下语法:

用&quot;&quot;双引号来引用JSONPath表达式中的文本
使用range和end来遍历集合
使用负数来从尾部索引集合

例如查看node的cpu信息：
kubectl get nodes -o=jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#x27;kubectl get nodes -o=jsonpath=&#x27;&#123;.items[*].status.capacity.cpu&#125;&#x27;kubectl get nodes -o=jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#123;.items[*].status.capacity.cpu&#125;&#x27;kubectl get nodes -o=jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#123;&quot;\n&quot;&#125;&#123;.items[*].status.capacity.cpu&#125;&#x27;kubectl get nodes -o=jsonpath=&#x27;&#123;range .items[*]&#125;&#123;.metadata.name&#125;&#123;&quot;\t&quot;&#125;&#123;.status.capacity.cpu&#125;&#123;end&#125;&#x27;kubectl get nodes -o=custom-columns=NODE:.metadata.name,CPU:.status.capacity.cpukubectl get nodes --sort-by=.metadata.namekubectl get nodes --sort-by=.status.capacity.cpu

5. 特殊说明如果key中包含点.，例如key为config.yaml，那么在使用jsonpath的时候需要对key中的点.进行转义。
kubectl get cm test-config -n test -o jsonpath=&#x27;&#123;.data.config\.yaml&#125;&#x27;



]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>json</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：使用kubeadm跨云搭建K8S集群</title>
    <url>/dev-kubeadm-install-k8s-in-multi-cloud/</url>
    <content><![CDATA[1. 前言搭建K8S集群时，我们往往使用同一个云厂商的机器，或者自建机房，能够保证宿主机的二层网络是通的。基于这种网络环境搭建K8S集群，简单而稳定。但是某些情况下，我们需要跨云（使用不同云厂商的机器）搭建K8S集群，这时候就需要充分考虑网络问题。本文，我们就来学习一下跨云搭建K8S集群的方法。


2. 需求说明当前我们在腾讯云使用kubeadm已经搭建好了K8S集群，网络插件使用的是Flannel VXLAN模式，现在我们想要添加一台阿里云的机器到这个腾讯云的K8S集群。
3. 网络配置思路3.1. 基于NAT配置思路：腾讯云node和阿里云node间通信配置双向dnat转发
缺陷：1、node和node间通信配置dnat，是多对多关系，太复杂，而且增加节点时所有节点都要添加配置2、node无法跨云和pod通信，因此不可行
3.2. 基于K8S配置配置思路：1、controlPlaneEndpoint地址和apiServer地址改为公网IP2、Flannel修改每个节点Annotations的public-ip参数3、master和node间通信配置双向dnat转发4、修改metrics server配置，增加hostAliases来自定义hosts
缺陷：需要重新配置原有的K8S集群，影响太大。
参考文档：

跨VPC或者跨云供应商搭建K8S集群正确姿势
跨VPC或者跨云供应商搭建K8S集群正确姿势-番外篇

3.3. 基于OpenVPN通信配置思路：1、阿里云机器连接到腾讯云OpenVPN，实现阿里云到腾讯云的网络畅通。2、腾讯云配置dnat，实现通过阿里云内网IP访问阿里云机器。
缺陷：node无法跨云和pod通信，因此不可行。
参考文档：《使用Docker安装配置OpenVPN》
3.4. 基于IPsec VPN通信配置思路：1、腾讯云配置IPsec，对端连接阿里云IPsec；阿里云配置IPsec，对端连接腾讯云IPsec2、腾讯云配置路由表，阿里云网段转发到VPN；阿里云配置路由表，腾讯云网段转发到VPN
缺陷：1、集群稳定性依赖VPN的稳定性2、VPN Client会自动修改路由规则，影响云主机的网络，需要提前做好网络规划，VPN、腾讯云和阿里云的网络范围不要重合。
参考文档：

《使用Docker安装配置OpenVPN》
什么是IPsec？

3.5. 小结综上，基于IPsec VPN通信的方式可行，并且相对简单，选择它。
4. IPsec网络配置4.1. IPsec网络配置概述在云厂商控制台配置IPsec时，大致都是三个步骤：1、创建VPN网关2、创建对端网关（用户网关）3、创建和配置VPN通道（VPN隧道、IPsec连接）
其中，最关键的一步是创建和配置VPN通道，配置时需要注意：

两端IKE版本一致，加密算法、认证算法、DH组一致
IKE版本建议选择V2
加密算法建议选择aes128
认证算法建议选择sha1
DH组建议选择14


两端IKE的端口ID类型一致，选择IP地址类型时IP值要对应
两端IKE的预共享密钥一致，SA超时时间一致
两端IPsec的安全协议、加密算法、认证算法、PFS DH组一致
安全协议建议选择ESP
加密算法建议选择aes128
认证算法建议选择sha1
PFS DH组建议选择disable


两端IPsec的报文封装模式一致，Tunnel即可
两端IPsec的SA超时保持一致
本端网段和对端网段配置正确
VPN通道创建完成后，依然可以调整部分配置
当VPN通道发生变更时，要重置连接才能生效

4.2. 腾讯云配置参考文档：

创建 VPN 网关
创建对端网关
创建 VPN 通道
路由表 - 概述

1、创建VPN网关，绑定VPC
2、创建对端网关，填入阿里云VPN网关
3、创建VPN通道，与刚刚创建的VPN网关和对端网关关联特别注意：通信模式建议选择SPD策略，可以自定义本端网络和对端网络，清晰明了。
4、配置路由表（VPN网关路由表和VPC路由表）
4.3. 阿里云配置参考文档：

什么是VPN网关
IPsec-VPN入门概述

1、创建VPN网关
2、创建用户网关
3、创建IPsec连接
4、配置路由表
5. 添加节点经过VPN网络打通，此时阿里云和腾讯云的机器已经可以通过内网IP互相访问了。此时，直接使用kubeadm join命令添加节点即可。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>网络</tag>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：使用kubeadm安装部署K8S集群——CentOS篇</title>
    <url>/dev-kubeadm-install-k8s-in-centos/</url>
    <content><![CDATA[1. kubeadm简介《使用kubeadm升级K8S集群》一文中，了解了k8s集群中常见组件，并且使用kubeadm对k8s集群进行了升级。本文中，会学习使用kubeadm安装部署k8s集群。

Kubeadm is a tool built to provide kubeadm init and kubeadm join as best-practice “fast paths” for creating Kubernetes clusters.

参考文档：

Overview of kubeadm
Installing kubeadm



2. 安装流程目标：搭建一个k8s集群，包括master和node01两个节点，节点系统为centos7.6.1810。master节点ip为192.168.56.200，node01节点ip为192.168.56.201。
1、环境准备。
2、在master节点和node01节点安装kubeadm。
3、初始化master节点，创建k8s集群（记得安装网络插件）。
4、node01节点加入到k8s集群。
5、验证安装。
3. 环境准备1、配置主机名
2、配置IP地址
4. 安装docker参考Docker入门，安装Docker
5. 允许iptables检查桥接流量允许 iptables 检查桥接流量，允许流量转发
cat &lt;&lt;EOF | sudo tee /etc/modules-load.d/k8s.confbr_netfilterEOFcat &lt;&lt;EOF | sudo tee /etc/sysctl.d/k8s.confnet.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1EOFsudo sysctl --system

所有节点都要执行。
6. 安装kubeadm+kubelet+kubectl参考安装kubeadm。
6.1. 准备kubernetes.repo配置cat &lt;&lt;EOF | sudo tee /etc/yum.repos.d/kubernetes.repo[kubernetes]name=Kubernetesbaseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-$basearchenabled=1gpgcheck=1repo_gpgcheck=0gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpgexclude=kubelet kubeadm kubectlEOF

如果不能连通google，kubernetes.repo中的baseurl就替换成aliyun
cat &lt;&lt;EOF | sudo tee /etc/yum.repos.d/kubernetes.repo[kubernetes]name=Kubernetesbaseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64enabled=1gpgcheck=0EOF

所有节点都要执行。
6.2. SELinux设置为permissive模式将 SELinux 设置为 permissive 模式（相当于将其禁用）
sudo setenforce 0sudo sed -i &#x27;s/^SELINUX=enforcing$/SELINUX=permissive/&#x27; /etc/selinux/config

6.3. 安装kubeadm+kubelet+kubectl1、安装默认版本的kubeadm+kubelet+kubectl
sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetessudo systemctl enable --now kubelet

centos安装这些组件时如果报错：[Errno -1] repomd.xml signature could not be verified for kubernetes不考虑安全性的话，最简单的解决办法是修改 /etc/yum.repos.d/kubernetes.repo 文件，设置 repo_gpgcheck=0
2、查看kubelet版本kubelet --version
如果想要安装指定版本的k8s，比如1.22.15，那么需要安装指定版本的kubeadm、kubelet和kubectl。
sudo yum remove -y kubelet kubeadm kubectlsudo yum list kubeadm kubelet kubectl --showduplicatessudo yum install -y kubeadm-1.22.15-0 kubelet-1.22.15-0 kubectl-1.22.15-0 --disableexcludes=kubernetes
下文中kubeadm.conf中的版本也需要同步修改kubernetesVersion: 1.22.15
7. 关闭swap
Swap disabled. You MUST disable swap in order for the kubelet to work properly.

如官方文档所说，kubelet正常工作的前提是关闭swap，否则kubeadm执行初始化时会报错：“–cgroups-per-qos enabled, but –cgroup-root was not specified.  defaulting to /““Failed to run kubelet” err=”failed to run Kubelet: running with swap on is not supported, please disable swap!报错查看方法：
systemctl status kubeletjournalctl -xeu kubelet

1、临时关闭swapswapoff -a
2、永久关闭swapsed -i &#39;/ swap / s/^/#/&#39; /etc/fstab或者编辑 /etc/fstab，手动注释掉swap配置。
#/dev/mapper/centos-swap swap swap defaults 0 0

无论是master节点，还是worker节点，记得都要关闭swap。
8. 配置cgroup驱动
Both the container runtime and the kubelet have a property called “cgroup driver”, which is important for the management of cgroups on Linux machines.

如官方文档所说，cgroup驱动对于在linux中管理cgroups中非常重要。因此需要正确配置 kubelet 的 cgroup 驱动以匹配 kubeadm 集群中的容器运行时的 cgroup 驱动，详情参考 配置 cgroup 驱动
否则kubeadm执行初始化时会报错：[kubelet-check] It seems like the kubelet isn’t running or healthy.The kubelet is not runningThe kubelet is unhealthy due to a misconfiguration of the node in some way (required cgroups disabled)这是因为， docker 和 kubelet 服务中的cgroup不一致，docker默认使用cgroupfs，kubelet默认使用systemdcgroup驱动查看方法：
docker system info | grep -i drivercat /var/lib/kubelet/config.yaml | grep cgroup

方法一：kubelet匹配docker编辑kubeadm.conf，添加kubelet的cgroup配置，指定驱动为cgroupfs
---kind: KubeletConfigurationapiVersion: kubelet.config.k8s.io/v1beta1#cgroupDriver: systemdcgroupDriver: cgroupfs

方法二：docker匹配kubelet编辑 /etc/docker/daemon.json ，添加：
&#123;    &quot;exec-opts&quot;: [        &quot;native.cgroupdriver=systemd&quot;    ]&#125;
重启docker
systemctl daemon-reloadsystemctl restart docker

这里选择方法二，因为官方更加推荐，可以避免后续kubeadm升级可能引发的cgroup驱动问题。
9. 初始化master节点参考Installing kubeadm on your hosts。
9.1. 下载镜像文件kubeadm默认从gcr.io下载k8s组件镜像，国内需要科学上网，或者更改为国内源。本文选择更改为国内源。
1、导出一份默认配置文件kubeadm config print init-defaults &gt; kubeadm.conf
默认配置内容为：
apiVersion: kubeadm.k8s.io/v1beta3bootstrapTokens:- groups:  - system:bootstrappers:kubeadm:default-node-token  token: abcdef.0123456789abcdef  ttl: 24h0m0s  usages:  - signing  - authenticationkind: InitConfigurationlocalAPIEndpoint:  advertiseAddress: 1.2.3.4  bindPort: 6443nodeRegistration:  criSocket: /var/run/dockershim.sock  imagePullPolicy: IfNotPresent  name: node  taints: null---apiServer:  timeoutForControlPlane: 4m0sapiVersion: kubeadm.k8s.io/v1beta3certificatesDir: /etc/kubernetes/pkiclusterName: kubernetescontrollerManager: &#123;&#125;dns: &#123;&#125;etcd:  local:    dataDir: /var/lib/etcdimageRepository: k8s.gcr.iokind: ClusterConfigurationkubernetesVersion: 1.23.0networking:  dnsDomain: cluster.local  serviceSubnet: 10.96.0.0/12scheduler: &#123;&#125;

2、下载地址改为国内镜像源编辑kubeadm.conf，imageRepository改为国内源
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers

3、指定配置文件，执行下载kubeadm config images pull --config kubeadm.conf
[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver:v1.23.0[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/kube-controller-manager:v1.23.0[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/kube-scheduler:v1.23.0[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/kube-proxy:v1.23.0[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/etcd:3.5.1-0[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:v1.8.6
9.2. k8s集群配置编辑kubeadm.conf，指定k8s集群配置，详情参考kubeadm 配置 (v1beta2)
9.2.1. advertiseAddress修改 localAPIEndpoint.advertiseAddress 字段，也就是apiserver对外开放的地址，这里改为master节点地址（因为没有配置高可用）
localAPIEndpoint:  advertiseAddress: 192.168.56.200  bindPort: 6443

如果在执行初始前不指定可用的advertiseAddress，那么kubeadm init初始化时会报错：Error getting node” err=”node “node” not found
9.2.2. 指定节点名称修改 nodeRegistration.name 字段，这个字段会作为执行初始化的这个master节点的名称。建议改为master节点的主机名，否则默认就会叫node。
nodeRegistration:  criSocket: /var/run/dockershim.sock  imagePullPolicy: IfNotPresent  name: master  taints: null

如果在执行初始化前没有指定节点名字，想要修改的话，需要修改kubeadm.conf后重新部署k8s集群。
不重新部署k8s集群，是否可以直接编辑修改节点的名称？kubectl edit nodes/node，修改如下：
metadata:  labels:    kubernetes.io/hostname: master  name: master

很遗憾，不可以，会报错：error: At least one of apiVersion, kind and name was changed
9.2.3. 指定pod ip范围添加 networking.podSubnet 字段，指定pod ip范围（pod-network-cidr）
networking:  dnsDomain: cluster.local  serviceSubnet: 10.96.0.0/12  podSubnet: &quot;10.244.0.0/16&quot;

关于pod ip范围和servcie ip范围的设置，可以参考子网划分详解与子网划分实例精析和详解网络分类ABC
如果在执行初始化前没有指定pod ip范围，也是可以正常初始化的，但是后面会导致CNI插件无法正常启动。比如kube-flannel会报错：Error registering network: failed to acquire lease: node “node” pod cidr not assigned后期想要修改的话，需要修改kubeadm.conf后重新部署k8s集群。
不重新部署k8s集群，能否补救？这个是可以的编辑 /etc/kubernetes/manifests/kube-controller-manager.yaml ，添加参数：
- command:  - --allocate-node-cidrs=true  - --cluster-cidr=10.244.0.0/16
然后重启kubelet：systemctl restart kubelet
9.3. 执行初始化#rm -rf /etc/kubernetes/manifests/ #kubeadm resetkubeadm init --config kubeadm.conf
完成后，屏幕输出会提示创建配置文件，以及添加worker node的join命令，记录下来。例如：
kubeadm join 192.168.56.200:6443 --token abcdef.0123456789abcdef \    --discovery-token-ca-cert-hash sha256:6743eaae41ba8fd99e495e3df94ff9a996878e7c9cf8ac590a3c52b97ea8012e

PS：如果忘记了添加worker node的join命令，可以重新生成。
kubeadm token create --helpkubeadm token create --print-join-command
生成新的join命令后，之前的join命令同样可以使用。
9.4. 创建配置文件mkdir -p $HOME/.kubecp -i /etc/kubernetes/admin.conf $HOME/.kube/configchown $(id -u):$(id -g) $HOME/.kube/config

9.5. 验证安装docker ps -akubectl get nodeskubectl get pods --namespace kube-system

9.6. 验证发现问题问题一：coredns处于pending状态查看coredns报错信息：
kubectl get deployment/coredns -n kube-system -o yamlkubectl get pods -n kube-systemkubectl describe pods coredns-65c54cc984-srzl4 -n kube-systemkubectl logs pods coredns-65c54cc984-srzl4 -n kube-system
Warning  FailedScheduling  2m57s (x36 over 38m)  default-scheduler  0/1 nodes are available: 1 node(s) had taint {node.kubernetes.io/not-ready: }, that the pod didn’t tolerate.原来是coredns默认无法部署在master节点，因为没有配置容忍。参考污点和容忍度进行配置即可。kubectl edit deployment/coredns -n kube-system -o yamltolerations部分添加：
tolerations:- effect: NoSchedule  key: node.kubernetes.io/not-ready

问题二：coredns无法启动问题一解决后，coredns可以调度了，但是无法启动。kubectl describe pods/coredns-55b7b56cf8-jv2dc -n kube-systemnetwork is not ready: container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
仔细研读使用 kubeadm 创建集群，原来问题一是符合预期的，master节点还没有ready，因为缺少cni插件。
9.7. 安装CNI插件参考集群网络系统选择一个cni插件，这里我们选择flannel。
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

10. node01加入k8s集群1、使用join命令，添加node01节点到集群
kubeadm join 192.168.56.200:6443 --token abcdef.0123456789abcdef \    --discovery-token-ca-cert-hash sha256:6743eaae41ba8fd99e495e3df94ff9a996878e7c9cf8ac590a3c52b97ea8012e

2、验证结果在master节点执行：kubectl get nodes看到master节点和node01节点都是Ready的状态，说明安装成功。
11. 开发模式出于安全原因，k8s集群默认不会在控制节点(master节点)上调度Pod。如果我们希望可以在master节点上调度Pod，比如在开发测试时，可以去掉master节点的污点。kubectl taint nodes --all node-role.kubernetes.io/master-
12. 验证安装12.1. 手动验证kubectl get nodeskubectl get pods --all-namespacesservice kube-apiserver statusservice kube-controller-manager statusservice kube-scheduler statusservice kubelet statusservice kube-proxy status

kubectl run nginxkubectl get podskubectl scale --replicas=3 deploy/nginxkubectl get podskubectl expose deployment nginx --port=80 --type=NodePortkubectl get servicecurl http://node01:31850

12.2. test-infra源码地址：kubernetes/test-infra
1、拉取源码go get -u k8s.io/test-infra/kubetest
2、执行kubetest
kubetest --extract=v1.11.3cd kubernetesexport KUBE_MASTER_IP=&quot;192.168.56.200:6443&quot;export KUBE_MASTER=kube-masterkubetest --test --provider=skeleton &gt; testout.txtkubetest --test --provider=skeleton --test_args=&quot;ginkgo.focus=Secrets&quot; &gt; testout.txtcat testout.txt

12.3. Smoke Test按照Smoke Test文档操作一遍。
12.4. sonobuoy官网地址：sonobuoy源码地址：vmware-tanzu/sonobuoy
13. 节点重启后coredns启动问题master节点重启后，发现coredns无法正常启动。Warning  Unhealthy  2m1s (x22 over 5m)  kubelet            Readiness probe failed: HTTP probe failed with statuscode: 503[ERROR] plugin/errors: HINFO: read udp 10.244.0.7:48010-&gt;202.106.195.68:53: i/o timeout[INFO] plugin/ready: Still waiting on: “kubernetes”
参考How to solve CoreDNS always stuck at “waiting for kubernetes”?，发现需要关闭防火墙，低级错误！
systemctl status firewalldsystemctl stop firewalldsystemctl disable firewalld

14. 集群配置更改执行kubeadm init初始化后，kubeadm.conf中的内容就写入到了k8s集群了configmap，可以通过kubectl命令查看。kubectl get cm kubeadm-config -n kube-system -oyaml
如果想要修改配置，可以直接修改kubeadm-config这个configmap，详情参考重新配置 kubeadm 集群。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>网络</tag>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：使用kubeadm安装部署K8S集群——Ubuntu篇</title>
    <url>/dev-kubeadm-install-k8s-in-ubuntu/</url>
    <content><![CDATA[1. 前言《使用kubeadm安装部署K8S集群——CentOS篇》一文中，在CentOS系统中部署了K8S集群。本文中，我们学习使用kubeadm在Ubuntu系统中安装部署k8s集群。
参考文档：

Overview of kubeadm
Installing kubeadm



2. 安装流程目标：搭建一个k8s集群，包括master和node01两个节点，节点系统为ubuntu16.04.2。
1、环境准备。
2、在两个节点上安装kubeadm。
3、使用kubeadm初始化节点。
4、安装网络插件。
5、验证安装。
3. 环境准备1、配置主机名
2、配置IP地址
3、参考Letting iptables see bridged traffic，配置iptables
# ensure legacy binaries are installedsudo apt-get install -y iptables arptables ebtables# switch to legacy versionssudo update-alternatives --set iptables /usr/sbin/iptables-legacysudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacysudo update-alternatives --set arptables /usr/sbin/arptables-legacysudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy# Letting iptables see bridged trafficcat &lt;&lt;EOF &gt; /etc/sysctl.d/k8s.confnet.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1EOFsysctl --system

4、参考Docker入门，安装Docker
4. 安装kubeadm参考Installing kubeadm。
1、确认系统版本
cat /etc/os-release

2、执行安装kubeadm、kubelet和kubectl（两个节点都要执行）
sudo apt-get update &amp;&amp; sudo apt-get install -y apt-transport-https curlcurl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -cat &lt;&lt;EOF | sudo tee /etc/apt/sources.list.d/kubernetes.listdeb https://apt.kubernetes.io/ kubernetes-xenial mainEOFsudo apt-get updatesudo apt-get install -y kubelet kubeadm kubectlsudo apt-mark hold kubelet kubeadm kubectl

3、查看kubelet版本
kubelet --version

5. CONTINUE后续步骤，和《使用kubeadm安装部署K8S集群——CentOS篇》中的步骤相同。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>ubuntu</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>AutoHotkey配置鼠标光标在屏幕之间瞬移</title>
    <url>/hobby-autohotkey-mouse-cursor-switch-between-screens/</url>
    <content><![CDATA[1. 双屏幕带来的问题很多同事都会在笔记本电脑之外，再配备一个或两个屏幕，我也另外配备了一块屏幕。但是，使用双屏幕会带来两个问题：一个是窗口的切换移动不方便，另一个是鼠标的切换移动不方便。
《Windows快捷键》一文中，提到了两个可以用于多个屏幕窗口操作的快捷键：1、alt+tab，切换窗口。2、win+shift+左右键，可以移动窗口到另一个屏幕。
窗口的问题基本解决了，但是鼠标的问题还没有解决。怎么办？很多文章说，通过“显示设置”，调整两个屏幕的相对位置，就能更加容易地在两个屏幕间移动鼠标光标。但是，这个方法只是缓解了问题，并没有解决问题，鼠标光标移动依然很麻烦。
此时，就该本文的主角 AutoHotkey 出马了！


2. AutoHotkey2.1. 简介
AutoHotkey is a free, open-source scripting language for Windows that allows users to easily create small to complex scripts for all kinds of tasks such as: form fillers, auto-clicking, macros, etc.

正如官方的介绍，autohotkey是一个免费开源的脚本语言，可以帮助用户在Windows上轻松创建脚本，例如：表格填充，自动单击，宏等。
2.2. 安装1、访问AutoHotkey官网或者AutoHotkey-github，下载安装包，本文中选择下载 AutoHotkey_1.1.32.00_setup.exe 
2、双击安装即可
3. 基本语法3.1. helloworld1、新建文件helloworld.ahk，内容为：
!H::Send, helloworldReturn
第一行定义快捷键，其中!代表alt，H就是H。第二行Send命令，,后的是参数。第三行Return停止后面的脚本。
2、启动脚本双击helloworld.ahk，或者右键helloworld.ahk，Run Script。查看系统托盘，可以看到多了一个H的图标，这就是正在运行的脚本。
3、测试脚本打开任意一个文档，按下 alt+H ，发现会自动填入 “helloworld”，nice。
3.2. 语法说明1、符号说明
#	Win!	Alt^	Control+	Shift&amp;	连接两个按键(含鼠标按键)，合并成一个自定义热键

2、常用命令
Send 发送一段字符SendInput 发送一段字符MsgBox 弹出对话框Run 运行一个软件或者打开网页WinActivate 窗口激活WinWaitActive 等待窗口激活

更多内容参考AutoHotkey官方文档。
4. 定义快捷键利用autohotkey，可以实现自定义快捷键。
4.1. 双脚本1、新建文件 mousemove1.ahk，内容为
!1::CoordMode, Mouse, ScreenMouseMove, (A_ScreenWidth // 2), (A_ScreenHeight // 2)return

这段脚本运行后，按下 alt+1 ，鼠标光标会跳转到屏幕1的中心。
2、新建文件 mousemove2.ahk，内容为：
!2::CoordMode, Mouse, ScreenMouseMove, (A_ScreenWidth + 100), 100return

这段脚本运行后，按下 alt+2 ，鼠标光标会跳转到屏幕2的左上角的(100,100)位置。
这两个脚本配合，已经可以鼠标光标在两个屏幕间切换，nice。但是，mousemove2.ahk 还有改进的空间。既然屏幕1是跳转到屏幕中间，那么屏幕2自然也是跳转到屏幕中间更好。
屏幕2的分辨率为3840x2160，因此脚本修改为：
!2::CoordMode, Mouse, ScreenMouseMove, (A_ScreenWidth + 1920), 1080return

4.2. 单脚本以上，已经实现了鼠标光标在两个屏幕间的完美切换。但是，因为切换时有两个快捷键，所以切换鼠标所在屏幕时，还要想一下是切到1还是切到2，这也是一个可以改进的点。
创建文件 mousemove.ahk，内容为：
!J::CoordMode, Mouse, ScreenMouseGetPos, xpos, yposif (xpos &lt;= A_ScreenWidth) and (ypos &lt;= A_ScreenHeight)&#123;    MouseMove, (A_ScreenWidth // 2), (A_ScreenHeight // 2)&#125;else &#123;	MouseMove, (A_ScreenWidth + 1920), 1080&#125;return

这段脚本运行后，按下 alt+J ，可以实现鼠标光标切换到另一个屏幕的中心。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中用户账户的RBAC鉴权</title>
    <url>/dev-k8s-rbac-auth/</url>
    <content><![CDATA[1. RBAC Authorization
Role-based access control (RBAC) is a method of regulating access to computer or network resources based on the roles of individual users within your organization.

更多内容，参考Using RBAC Authorization。
《为K8S创建用户账户和服务账户》一文中，我们创建了新用户jane，并且使用CA证书给该用户签发了证书。现在我们有了jane.crt和jane.key，本文中会给jane配置一些k8s集群的管理权限。


2. 角色和绑定首先，给用户jane配置权限，使其能够创建和查看default空间下的pods。
2.1. 命令实现授权1、创建角色
kubectl create role --helpkubectl create role developer --resource=pods --verb=list,create

2、角色绑定
kubectl create rolebinding dev-user-binding --role=developer --user=jane

2.2. manifest实现授权直接通过配置清单，也可以实现角色创建和角色绑定。
1、创建授权文件auth.yaml
---kind: RoleapiVersion: rbac.authorization.k8s.io/v1metadata:  namespace: default  name: developerrules:- apiGroups: [&quot;&quot;]  resources: [&quot;pods&quot;]  verbs: [&quot;list&quot;, &quot;create&quot;]---kind: RoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata:  name: dev-user-bindingsubjects:- kind: User  name: jane  apiGroup: rbac.authorization.k8s.ioroleRef:  kind: Role  name: developer  apiGroup: rbac.authorization.k8s.io

2、执行授权
kubectl apply -f auth.yaml

2.3. 验证权限kubectl auth can-i list pods --as janekubectl get pods --as jane
至此，用户jane的权限配置完成。
3. 集群角色和绑定以上，给jane授权，是在namespace范围内的。当我们想给jane授权cluster范围的权限时，就需要clusterroles。
比如，我们想给jane授权node相关的权限，可以如下实现。
3.1. 命令实现1、创建集群角色
kubectl create clusterrole node-reader --verb=get,list,watch --resource=nodes

2、绑定集群角色
kubectl create clusterrolebinding node-reader-binding --user=jane --clusterrole=node-reader

3.2. manifest实现1、创建授权文件auth.yaml
---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:  creationTimestamp: null  name: node-readerrules:- apiGroups:  - &quot;&quot;  resources:  - nodes  verbs:  - get  - list  - watch---apiVersion: rbac.authorization.k8s.io/v1beta1kind: ClusterRoleBindingmetadata:  creationTimestamp: null  name: node-reader-bindingroleRef:  apiGroup: rbac.authorization.k8s.io  kind: ClusterRole  name: node-readersubjects:- apiGroup: rbac.authorization.k8s.io  kind: User  name: jane

2、执行授权
kubectl apply -f auth.yaml

4. 授权范围如果使用clusterrole指定的资源是pods这种namespace级别的资源，该集群角色绑定给jane后会有什么效果？答：jane对所有namespace中的pods资源拥有clusterrole中定义的操作权限。

ClusterRole + ClusterRoleBinding = All Namespaces
ClusterRole + RoleBindings = Particular Namespaces
Role + RoleBinding = Same Namespace

更多内容参考How to Use ClusterRoleBinding With A ServiceAccount in All Namespaces (or a few)
5. logs&amp;exec命令权限授权apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:  namespace: default  name: pod-and-pod-logs-readerrules:- apiGroups:   - &quot;&quot;  resources:   - pods  - pods/log  verbs:   - get  - list  - watch- apiGroups:   - &quot;&quot;  resources:   - pods/exec  verbs:   - create

详情参考kubernetes RBAC role verbs to exec to pod
6. CRD资源授权如果是CRD资源需要授权，可以先查看CRD资源，然后写入到yaml文件。例如，想要授权sparkapplition资源的操作权限：
1、首先查看spark crd资源
kubectl get crd | grep spark

看到内容为：
scheduledsparkapplications.sparkoperator.k8s.io       sparkapplications.sparkoperator.k8s.io               

2、编写clusterrole yaml
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:  creationTimestamp: null  name: sparkdev-rolerules:- apiGroups:  - &quot;sparkoperator.k8s.io&quot;  resources:  - sparkapplications  - scheduledsparkapplications  verbs:  - get  - list  - watch  - create  - update  - patch  - delete

7. 用户账户访问K8S集群用户jane已经拥有了需要的权限，该怎样访问k8s集群呢？答案是通过kubeconfig文件。
7.1. 获取集群基本信息假设当前用户是admin，那么可以通过命令拿到apiserver地址和ca.crt文件。
1、查看apiserver地址
kubectl config view | grep server

2、保存ca.crt
cat ~/.kube/config | grep certificate-authority-data | awk &#x27;&#123;print $2&#125;&#x27; | base64 --decode &gt; ca.crt

7.2. 生成jane.kubeconfig1、生成（设置）kubeconfig集群参数
kubectl config set-cluster kubernetes \--server=&quot;https://172.17.0.69:6443&quot; \--certificate-authority=ca.crt \--embed-certs=true \--kubeconfig=jane.kubeconfig
当前目录生成jane.kubeconfig文件。
2、设置kubeconfig客户端认证参数
kubectl config set-credentials jane \--client-certificate=jane.crt \--client-key=jane.key \--embed-certs=true \--kubeconfig=jane.kubeconfig

3、设置kubeconfig上下文参数
kubectl config set-context jane@kubernetes \--cluster=kubernetes \--user=jane \--namespace=default \--kubeconfig=jane.kubeconfig

4、查看配置
cat jane.kubeconfigkubectl config view --kubeconfig jane.kubeconfig

7.3. 验证访问1、设置默认上下文
export KUBECONFIG=jane.kubeconfigkubectl config get-contextskubectl config use-context jane@kubernetes

2、测试执行命令
kubectl get pods
如果没有配置权限，会输出：
Error from server (Forbidden): pods is forbidden: User &quot;jane&quot; cannot list resource &quot;pods&quot; in API group &quot;&quot; in the namespace &quot;default&quot;
如果配置好了权限，会输出pod相关信息。
但是，以上权限测试只是在minikube或者katacoda平台生效。如果使用kodekloud或者阿里云k8s集群，会报错：
error: You must be logged in to the server (Unauthorized)
研究了四个多小时，才发现是平台的问题，服气了。。。
8. 后记以上，就实现了用户账户的授权方法和访问k8s的方法。服务账户的授权方法和访问k8s的方法类似，但是略有不同，详情请参阅《好好学K8S：K8S中服务账户的RBAC鉴权》
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中服务账户的RBAC鉴权</title>
    <url>/dev-k8s-sa-rbac-auth/</url>
    <content><![CDATA[1. 前言《好好学K8S：K8S中用户账户的RBAC鉴权》一文中，实现了用户账户（UserAccount）的授权方法和访问K8S的方法。本文中，我们主要学习服务账户（ServiceAccount）的授权方法和访问K8S的方法。


2. 服务账户授权2.1. 账户授权概述UserAccount授权的方法，是把UserAccount通过 RoleBinding/ClusterRoleBinding 和 Role/ClusterRole 绑定。
ServiceAccount授权的方法，与UserAccount授权的方法相同：把ServiceAccount通过 RoleBinding/ClusterRoleBinding 和 Role/ClusterRole 绑定。
因此，ServiceAccount与UserAccount的授权命令基本相同，只有几个参数的差异。授权方法完全可以参考《好好学K8S：K8S中用户账户的RBAC鉴权》。
创建账户的方法，参考文档《为K8S创建用户账户和服务账户》假设当前我们有一个用户账户jane，和一个服务账户default:harkin。下面我们对比一下用户账户和服务账户授权的差异。
2.2. 命令实现授权2.2.1. 用户账户授权例如：用户账户jane，授权default namespace下的pod查看和创建权限。
kubectl create role --helpkubectl create role developer --resource=pods --verb=list,create -n defaultkubectl create rolebinding dev-user-binding --role=developer --user=jane -n default

2.2.2. 服务账户授权例如：default namespace下的sa账户harkin，授权default namespace下的pod查看和创建权限。
kubectl create role --helpkubectl create role developer --resource=pods --verb=list,get,create -n defaultkubectl create rolebinding dev-user-binding --role=developer --serviceaccount=default:harkin -n default

2.3. manifest实现授权2.3.1. 用户账户授权---kind: RoleapiVersion: rbac.authorization.k8s.io/v1metadata:  namespace: default  name: developerrules:- apiGroups: [&quot;&quot;]  resources: [&quot;pods&quot;]  verbs: [&quot;list&quot;, &quot;get&quot;, &quot;create&quot;]---kind: RoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata:  name: dev-user-bindingsubjects:- kind: User  name: jane  apiGroup: rbac.authorization.k8s.ioroleRef:  kind: Role  name: developer  apiGroup: rbac.authorization.k8s.iomaster

2.3.2. 服务账户授权---kind: RoleapiVersion: rbac.authorization.k8s.io/v1metadata:  namespace: default  name: developerrules:- apiGroups: [&quot;&quot;]  resources: [&quot;pods&quot;]  verbs: [&quot;list&quot;, &quot;get&quot;, &quot;create&quot;]---kind: RoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata:  name: dev-user-bindingsubjects:- kind: ServiceAccount  name: harkin  namespace: defaultroleRef:  kind: Role  name: developer  apiGroup: rbac.authorization.k8s.io

3. 用户账户访问K8S集群想要让Pod使用ServiceAccount访问K8S APIServer，基本步骤如下：
1、向Pod分配ServiceAccount
apiVersion: v1kind: Podmetadata:  name: my-podspec:  serviceAccountName: harkin  containers:  - name: my-container    image: myimage
当Pod启动时，Kubernetes会自动挂载ServiceAccount的凭据到Pod里运行的容器中。ServiceAccount的JWT令牌和CA证书会被自动挂载到Pod的/var/run/secrets/kubernetes.io/serviceaccount路径下。

Token的路径: /var/run/secrets/kubernetes.io/serviceaccount/token
CA证书的路径: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt

2、使用ServiceAccount的令牌访问APIServer当Pod使用ServiceAccount时，容器内的应用程序可以使用这些凭据（Token和CA证书）来与Kubernetes API服务器进行认证和通信。
代码示例：
package mainimport (  &quot;context&quot;  &quot;fmt&quot;  metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;  &quot;k8s.io/client-go/kubernetes&quot;  &quot;k8s.io/client-go/rest&quot;)func main() &#123;  var kubeconfig *rest.Config  var err error  // load token and ca from path /var/run/secrets/kubernetes.io/serviceaccount  if kubeconfig, err = rest.InClusterConfig(); err != nil &#123;    panic(err.Error())  &#125;  clientset, err := kubernetes.NewForConfig(kubeconfig)  if err != nil &#123;    panic(err.Error())  &#125;  pods, err := clientset.CoreV1().Pods(&quot;default&quot;).List(context.Background(), metav1.ListOptions&#123;&#125;)  if err != nil &#123;    panic(err.Error())  &#125;  fmt.Printf(&quot;There are %d pods in the default namespace\n&quot;, len(pods.Items))  for _, pod := range pods.Items &#123;    fmt.Printf(&quot;Pod name: %s in namespace: %s\n&quot;, pod.Name, pod.Namespace)  &#125;&#125;
在这个Go程序中，它使用rest.InClusterConfig()获取集群内的配置，这个过程会自动从挂载的ServiceAccount令牌和CA证书位置读取信息。然后，创建一个Clientset来查询集群内default namespace下的所有Pod。
4. 手动指定Token访问K8S集群在 Go 程序中，如果我们不是从集群内的 Pod 访问 Kubernetes API 而是想手动指定 Token 和 CA 证书文件的路径（例如在集群外部或在特殊情况下访问），我们可以使用 client-go 库的 rest 包来自定义配置。
以下是一个示例代码，它展示了如何在 Go 中手动配置访问 Kubernetes API 的客户端，包括指定 Token 和 CA 证书路径：
package mainimport (  &quot;context&quot;  &quot;fmt&quot;  &quot;os&quot;  metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;  &quot;k8s.io/client-go/kubernetes&quot;  &quot;k8s.io/client-go/rest&quot;)func main() &#123;  tokenPath := &quot;/Users/vk/tmp/serviceaccount/token&quot; // 指定Token文件的路径  caPath := &quot;/Users/vk/tmp/serviceaccount/ca.crt&quot;   // 指定CA证书文件的路径  host := &quot;https://192.168.50.133:6443&quot;             // Kubernetes API Server 地址  // 读取ServiceAccount的Token  token, err := os.ReadFile(tokenPath)  if err != nil &#123;    panic(err.Error())  &#125;  // 读取CA证书文件  caCert, err := os.ReadFile(caPath)  if err != nil &#123;    panic(err.Error())  &#125;  // 创建客户端配置  config := &amp;rest.Config&#123;    Host: host,    TLSClientConfig: rest.TLSClientConfig&#123;      CAData: caCert,    &#125;,    BearerToken: string(token),  &#125;  clientset, err := kubernetes.NewForConfig(config)  if err != nil &#123;    panic(err.Error())  &#125;  pods, err := clientset.CoreV1().Pods(&quot;default&quot;).List(context.Background(), metav1.ListOptions&#123;&#125;)  if err != nil &#123;    panic(err.Error())  &#125;  fmt.Printf(&quot;There are %d pods in the default namespace\n&quot;, len(pods.Items))  for _, pod := range pods.Items &#123;    fmt.Printf(&quot;Pod name: %s in namespace: %s\n&quot;, pod.Name, pod.Namespace)  &#125;&#125;

在上面的代码中，我们首先从指定路径读取了服务帐户的 Token 和 CA 证书文件，然后创建了一个包含这些数据的 rest.Config 结构。之后，使用这个配置创建了 kubernetes.Clientset 对象，用来执行对 Kubernetes API Server 的请求。
请确保将 tokenPath、caPath 和 host 替换为我们实际环境中的正确路径和API服务器地址，并确保提供的 ServiceAccount Token 具有足够的权限执行我们希望进行的 API 调用。
最后，使用 clientset 对象来访问 Kubernetes API，例如列出集群中的 Pods。记得处理任何可能发生的错误。这种方式的访问更多用于开发和调试目的，因为它涉及到手动管理敏感的凭据。在生产环境中建议使用标准的服务帐户令牌，以确保安全。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>golang</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>golang</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中创建用户账户和服务账户</title>
    <url>/dev-k8s-useraccount-serviceaccount/</url>
    <content><![CDATA[1. K8S中的账户简介k8s中的账户分为useraccount和serviceaccount，主要差异有四点：

useraccount是给人使用的，serviceaccount是给pod中进程调用APIServer使用的
useraccount是跨namespace的，serviceaccount是局限在它所在的namespace的
useraccount不能通过k8s api添加，但是能够通过证书签名方式进行认证；serviceaccount可以通过k8s api添加
每个namespace都有一个默认的serviceaccount，叫做default

本文中，我们主要研究怎样为K8S创建用户账户和怎样创建服务账户。
参考文档：用户认证


2. 创建用户账户需求：创建一个名为jane的用户账户。
2.1. 创建用户密钥对openssl genrsa -out jane.key 2048openssl req -new -key jane.key -subj  &quot;/CN=jane&quot; -out jane.csr
其中，key文件是私钥；csr文件是证书签名申请（Certificate Signing Request），里面包含证书的公钥和其他信息。
2.2. 使用CA签名假设可以登录运行kube-controller-manager的master节点，那么可以直接在该节点进行CA签名。
1、查看签名用的CA路径
cat /etc/kubernetes/manifests/kube-controller-manager.yaml | grep ca.crtcat /etc/kubernetes/manifests/kube-controller-manager.yaml | grep ca.key

查到的路径为/etc/kubernetes/pki/ca.crt 和 /etc/kubernetes/pki/ca.key
2、使用CA签名
openssl x509 -req -in /root/jane.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out /root/jane.crt
其中，crt文件是签名后的证书。
以上，有了crt和key，这俩加起来就是jane用户账户。一般的用户账户是用户名和密码，K8S的用户账户就是签名证书和私钥。
2.3. 使用API签名如果无法直接使用CA证书进行签名，那么可以通过APIServer的方式进行签名。
1、查看证书签名申请，base64加密
cat jane.csr | base64 | tr -d &#x27;\n&#x27;

2、创建jane-csr.yaml文件，把证书签名申请的内容粘贴到spec.request部分
apiVersion: certificates.k8s.io/v1beta1kind: CertificateSigningRequestmetadata:  name: janespec:  groups:  - system:authenticated  usages:  - digital signature  - key encipherment  - server auth  - client auth  request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ1ZEQ0NBVHdDQVFBd0R6RU5NQXNHQTFVRUF3d0VhbUZ1WlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRApnZ0VQQURDQ0FRb0NnZ0VCQUtxbWFIa3BJeE94dDN2UmxJT1FnSUFxSUFsekhQcTRRVTBDTDVhS04xbmY4NXRzCi9LU3o0eml1a1hEQ1NOSVNIT1pWbTY5NzVJa3RXcGFySmhaTXptc1B2eUFSeXFWbWY2L1h0bmwyeE0xblhaUzAKZGc0b0E1dXFuR0w2dHpaQzF3VFY4RVFIZnRlcWYzbUpTN2JtdlppaXFlak12a2UzVkk5RTNFK0xsUUttNnVXRwprS2RDZ2ZHNUszRGJFczR1VzR6M0lMdTdEa1BlamJodWFtYzlxYVZNRVpLSGZ0bnlBYlFITkZVLzhvWVYvR1VzCnRFVWZMRXBBTmlqUFc5U0pPWHJtNUg1NXhOdExXVHMwenU3YlRSZWE0ZjFVaDFCbkZuUkhWYUJqNysydHpITTgKaklJS01KakdWOS9rUVltRmo3UTJZUW1wYzdXWGpPZEFWcHBSc1kwQ0F3RUFBYUFBTUEwR0NTcUdTSWIzRFFFQgpDd1VBQTRJQkFRQUZ2ZUxrUmYxd0xDQmN6cWdMVkJIUGZBa0MzeU1CTDA3VXl0QUlCcVhkR3h1QWtyL3NQT1dkClNxTkhIRkNzQVNmU0lNVC96djBrQS9yN3Fnd25BMCtZREZJSjNzUlBKZkJmNm1Ic3FrbjlPd1htR1E3d0orNFQKWXVCc1lJSllnNWtzVWJoQVhiQkVZekk2OUY0Uk52U0d0K1ZLOHBBdUQzcXRvejJsd3liV0cvaUo4V3FESTZNegpuMURBeDBkRDZmRWhIKy9DTWdSREY5OExCL1ZqMWZOUUlqZ2k3Rmc1aTByU1NtZUdUMllOblJldERZYWN4aWlzCjNFN1B4STdYWDd2QjRjY3pITlUrTG92N3JnSkVXM3lRMXZRTXRCNTZlbWJaNGVnL01XZEhkeWliVXo2aDQ1ZW8KUGN5b3QxaW1wdFRyK3kwSkt0SmJ1YllQOGd2RG5FeFYKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==

3、签名请求并通过
kubectl apply -f jane-csr.yamlkubectl get csr kubectl certificate approve jane

4、导出签名证书
kubectl get csr jane -o yamlkubectl get csr jane -o jsonpath=&#x27;&#123;.status.certificate&#125;&#x27; | base64 --decode &gt; jane.crt

以上，有了crt和key，这俩加起来就是jane用户账户。
更多内容参考Manage TLS Certificates in a Cluster。
3. 证书格式转换X.509是一种证书标准，定义了证书中应该包含哪些内容，详情参考RFC5280，SSL使用的就是这种证书标准。同样的X.509证书，可能有不同的编码格式，目前有以下两种编码格式。PEM：Privacy Enhanced Mail，BASE64编码，以”—–BEGIN—–”开头，”—–END—–”结尾。查看PEM格式证书的信息：
openssl x509 -in cert.pem -text -noout

DER：Distinguished Encoding Rules，二进制格式，不可读。查看DER格式证书的信息：
openssl x509 -in cert.der -inform der -text -noout

问题来了，k8s中的证书，除了使用pem格式，还有就是crt格式，并没有der格式啊？这是因为，crt只是一个文件后缀，编码格式可能是pem也可能是der。
那么，pem和der怎样互相转换呢？
# pem to deropenssl x509 -in cert.crt -outform der -out cert.der# der to pemopenssl x509 -in cert.crt -inform der -outform pem -out cert.pem

4. K8S中的数字证书我们自己创建的用户账户拥有自己的数字证书，除此之外，K8S的各个组件也拥有自己的数字证书，用于组件之间的安全访问。
HTTPS(SSL/TLS)加密原理，参考文档《浅谈数据加密》。
4.1. 查看证书1、查看证书位置
ps aux | grep kubelet# find config filecat /var/lib/kubelet/config.yaml | grep staticPodPathcd /etc/kubernetes/manifestscat kube-apiserver.yaml

2、查看证书详情
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text

4.2. 签名签名，或者签名过期后重新签名
openssl x509 -req -in /etc/kubernetes/pki/apiserver-etcd-client.csr -CA /etc/kubernetes/pki/etcd/ca.crt -CAkey /etc/kubernetes/pki/etcd/ca.key -CAcreateserial -out /etc/kubernetes/pki/apiserver-etcd-client.crt

5. 创建服务账户5.1. v1.22之前的版本1、创建ServiceAccount和Secret
kubectl create serviceaccount harkin

v1.22 之前的 Kubernetes 版本，当创建ServiceAccount时，会自动创建Secret，Secret中包含Token和CA，用于访问 Kubernetes API。当Pod中指定使用这个ServiceAccount时，TOKEN和CA会挂载到Pod中，以便程序使用。

Token的路径: /var/run/secrets/kubernetes.io/serviceaccount/token
CA证书的路径: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt

2、查看ServiceAccount和Secret
kubectl get sakubectl get secretkubectl get secret harkin-token-xxx -oyaml # 获取token部分和ca.crt部分

5.2. v1.22之后的版本在包括 Kubernetes v1.27 在内最近的几个版本中，使用 TokenRequest API 直接获得 API 凭据，并使用投射卷挂载到 Pod 中。使用这种方法获得的令牌具有绑定的生命周期，当挂载的 Pod 被删除时这些令牌将自动失效。
简单来说，新版本的K8S创建sa时，已经不会自动生成admin-user-token-xxx这种secret，也就无法从这种secret中获取token。
详情参考文档手动管理 ServiceAccount 的 Secret
1、创建ServiceAccount
kubectl create serviceaccount harkin

2、创建ServiceAccount的Secret（1）创建有期限Token（令牌）
kubectl create token harkin --duration=8760h
该令牌只会显示一次，请保存好它。至于ca.crt，从/etc/kubernetes/pki/ca.crt获取即可。
（2）创建长期有效Token（令牌）我们仍然可以手动创建 Secret 来保存服务账号令牌，例如在我们需要一个永不过期的令牌的时候。一旦我们手动创建一个 Secret 并将其关联到 ServiceAccount， Kubernetes 控制平面就会自动将令牌填充到该 Secret 中。
kubectl apply -f - &lt;&lt;EOFapiVersion: v1kind: Secretmetadata:  name: harkin-secret  annotations:    kubernetes.io/service-account.name: harkintype: kubernetes.io/service-account-tokenEOF

3、查看ServiceAccount和Secret
kubectl get sakubectl get secretkubectl get secrets/harkin-secret -oyaml # 获取token部分和ca.crt部分

6. 授权创建完用户账户和服务账户，只是有了账户，并没有K8S集群的权限。账户授权方法参考文档《好好学K8S：K8S中用户账户的RBAC鉴权》和《好好学K8S：K8S中服务账户的RBAC鉴权》。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S集群中etcd备份和恢复</title>
    <url>/dev-k8s-etcd-backup-restore/</url>
    <content><![CDATA[1. 前言就像备份数据库一样，很多时候，我们也想对k8s资源配置进行备份。
kubectl get all --all-namespaces -o yaml &gt; all-deploy-services.yaml
上面的方法，可以实现对k8s资源配置的备份。但是更好的办法，是对etcd进行备份。本文就学习一下k8s中etcd的备份和恢复方法。


2. 安装etcdctl1、查看etcd版本
kubectl get pod/etcd-$nodename -n kube-system -oyaml | grep image# orcat /etc/kubernetes/manifests/etcd.yaml | grep image

2、下载对应版本的etcdctl（这里以3.4.13为例）
wget https://github.com/etcd-io/etcd/releases/download/v3.4.13/etcd-v3.4.13-linux-amd64.tar.gztar -xzvf etcd-v3.4.13-linux-amd64.tar.gzcp etcd-v3.4.13-linux-amd64/etcdctl /usr/local/bin/ln -s /usr/local/bin/etcdctl /usr/bin/etcdctl

3、测试
etcdctl version

3. 查看etcd集群状态HOST_1=10.240.0.17HOST_2=10.240.0.18HOST_3=10.240.0.19ENDPOINTS=$HOST_1:2379,$HOST_2:2379,$HOST_3:2379etcdctl --endpoints=$ENDPOINTS member listetcdctl --write-out=table --endpoints=$ENDPOINTS endpoint statusetcdctl --endpoints=$ENDPOINTS endpoint health

4. 备份etcd数据1、查看配置
kubectl describe pod etcd-master -n kube-system | grep Command -i -A 20# orcat /etc/kubernetes/manifests/etcd.yaml | grep Command -i -A 20

看到Command字段为：
Command:  etcd  --advertise-client-urls=https://172.17.0.10:2379  --cert-file=/etc/kubernetes/pki/etcd/server.crt  --client-cert-auth=true  --data-dir=/var/lib/etcd  --initial-advertise-peer-urls=https://172.17.0.10:2380  --initial-cluster=master=https://172.17.0.10:2380  --key-file=/etc/kubernetes/pki/etcd/server.key  --listen-client-urls=https://127.0.0.1:2379,https://172.17.0.10:2379  --listen-metrics-urls=http://127.0.0.1:2381  --listen-peer-urls=https://172.17.0.10:2380  --name=master  --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt  --peer-client-cert-auth=true  --peer-key-file=/etc/kubernetes/pki/etcd/peer.key  --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt  --snapshot-count=10000  --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt

2、执行备份
ETCDCTL_API=3 etcdctl \--endpoints=https://[127.0.0.1]:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key \snapshot save /tmp/snapshot-pre-boot.db

3、查看备份
ETCDCTL_API=3 etcdctl \--endpoints=https://[127.0.0.1]:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key \snapshot status /tmp/snapshot-pre-boot.db -w table

5. 恢复etcd数据1、恢复etcd数据
ETCDCTL_API=3 etcdctl \--endpoints=https://[127.0.0.1]:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key \--initial-cluster=master=https://127.0.0.1:2380 \--initial-cluster-token etcd-cluster-1 \--initial-advertise-peer-urls=https://127.0.0.1:2380 \--name=master \--data-dir /var/lib/etcd-from-backup \snapshot restore /tmp/snapshot-pre-boot.db

2、修改etcd.yaml
vim /etc/kubernetes/manifests/etcd.yaml

如下修改：
# Update --data-dir to use new target location--data-dir=/var/lib/etcd-from-backup# Update new initial-cluster-token to specify new cluster--initial-cluster-token=etcd-cluster-1# Update volumes and volume mounts to point to new path    volumeMounts:    - mountPath: /var/lib/etcd-from-backup      name: etcd-data    - mountPath: /etc/kubernetes/pki/etcd      name: etcd-certs  hostNetwork: true  priorityClassName: system-cluster-critical  volumes:  - hostPath:      path: /var/lib/etcd-from-backup      type: DirectoryOrCreate    name: etcd-data  - hostPath:      path: /etc/kubernetes/pki/etcd      type: DirectoryOrCreate    name: etcd-certs

6. sealos + etcd如果是使用sealos安装的k8s集群，那么会有一个默认备份机制，etcd会定期备份到master节点的 /var/lib/etcd/member/snap 目录中。
如果想要手动执行备份，也是可以的：
kubectl exec -it etcd-k8s-master-0 -n kube-system -- /bin/shETCDCTL_API=3 /usr/local/bin/etcdctl \  --endpoints https://127.0.0.1:2379 \  --cacert=/etc/kubernetes/pki/etcd/ca.crt \  --cert=/etc/kubernetes/pki/etcd/server.crt \  --key=/etc/kubernetes/pki/etcd/server.key \  snapshot save /var/lib/etcd/etcd_backup.db





]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>database</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>数据库</tag>
        <tag>好好学K8S</tag>
        <tag>etcd</tag>
        <tag>sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>使用kubeadm升级K8S集群</title>
    <url>/dev-kubeadm-upgrade/</url>
    <content><![CDATA[1. K8S组件版本说明k8s集群中的常见组件包括：A类：kube-apiserverB类：controller-manager、kube-schedulerC类：kubelet、kube-proxyD类：etcd cluster、CoreDNSE类：kubectl
组件的版本号一般表示为 major.minor.patch，比如v1.10.3。其中，A类组件是主要组件，以它为版本基准。比如，A类组件版本的minor号为x，那么B类组件版本必须为x或者x-1，C类组件版本必须为x、x-1或者x-2，E类组件版本必须为x、x-1或者x+1。而D类组件，和A类组件不是同一版本体系，版本兼容情况需要查看文档。整理成表格如下：



组件类别
minor版本
组件



A类
x
kube-apiserver


B类
x、x-1
controller-manager、kube-scheduler


C类
x、x-1、x-2
kubelet、kube-proxy


E类
x、x-1、x+1
kubectl


D类
查看文档
etcd cluster、CoreDNS


本文学习使用kubeadm进行k8s集群的升级，参考升级 kubeadm 集群。


2. 升级顺序推荐的升级方法，是根据minor版本号逐级进行升级。比如v1.10.0想要升级到v1.13.0，不应该直接升级到v1.13.0，而是应该v1.10.0-&gt;v1.11.0-&gt;v1.12.0-&gt;v1.13.0。
升级顺序一般为：1、升级kubeadm2、升级master node3、升级worker node4、升级kubelet
3. 升级操作以v1.11.0升级v1.12.0为例。
3.1. master节点1、查看升级帮助kubeadm upgrade plan
2、升级kubeadm
apt-get upgrade -y kubeadm=1.12.0-00# orapt install kubeadm=1.12.0-00

3、升级k8s的AB类组件
kubeadm upgrade apply v1.12.0
此时使用kubectl get nodes，看到的version依然是v1.11.0，因为这里显示的是kubelet的版本，而不是kube-apiserver的版本。
4、升级master节点的kubelet
apt install kubelet=1.12.0-00systemctl restart kubelet

3.2. worker节点1、驱逐worker节点的pods，封锁节点
kubectl drain node-1kubectl cordon node-1

2、升级kubeadm和kubectl
apt-get install kubeadm=1.12.0-00apt-get install kubelet=1.12.0-00kubeadm upgrade node config --kubelet-version v1.12.0systemctl restart kubelet

3、解除节点封锁kubectl uncordon node-1
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置GateOne</title>
    <url>/dev-docker-gateone/</url>
    <content><![CDATA[1. GateOne简介
Gate One is a web-based Terminal Emulator and SSH client that brings the power of the command line to the web. It requires no browser plugins and is built on top of a powerful plugin system that allows every aspect of its appearance and functionality to be customized.

本文使用Docker安装配置GateOne，搭建一个Web Shell环境。前置条件是安装配置好了docker环境，安装方法参考《Docker入门》。已知docker宿主机IP为192.168.56.130。


2. 安装GateOne1、登录dockerhub查看需要的liftoff/gateone版本，实际上只有一个版本。
2、下载gateone镜像docker pull liftoff/gateone
3、启动gateone服务
docker run --name=vk-gateone -d \-p 8000:8000 \liftoff/gateone gateone

以上命令：命名容器为vk-gateone，后台运行；映射宿主机8000端口到容器8000端口。
更多命令参考Using this Image。
4、验证安装docker psgateone启动正常的话就可以看到vk-gateone容器。如果启动失败，可以使用docker logs vk-gateone查看失败原因并进行解决。
使用nc命令验证：
yum install ncnc -v localhost 8000

浏览器访问： https://192.168.56.130:8000
5、使用点击 Terminal:SSH ，输入主机IP、端口号、用户名和密码，即可登录主机。
如果想要添加鉴权验证，参考archlinux-Gateone和API Authentication。
3. 开放端口如果浏览器无法访问8000端口，那么需要对防火墙进行设置。
firewall-cmd --add-port=8000/tcp --permanentsystemctl reload firewalld# 或者systemctl stop firewalld

4. 安装openssh-server1、进入vk-gateonedocker exec -it vk-gateone /bin/bash
2、安装openssh-server
apt updateapt install openssh-server

3、vim /etc/ssh/sshd_config，如下修改：
PasswordAuthentication yes

4、重启sshservice ssh restart
5、添加用户
useradd -m voidking -s /bin/bashpasswd voidkingadduser voidking sudo# 安装sudo命令（该容器内没有sudo）apt install sudo

6、登录vk-gateone容器点击 Terminal:SSH ，主机名和端口使用默认，输入用户名voidking和密码，即可登录vk-gateone。
5. 小技巧在gateone shell里，可以使用 ctrl+insert 进行复制，使用 shift+insert 进行粘贴。如果在界面无法使用鼠标光标选中一些区域，那么可以在任意位置进行双击，即可激活光标选择。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>ssh</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：Ingress常用配置</title>
    <url>/dev-ingress-config/</url>
    <content><![CDATA[1. 前言本文记录Ingress的常用配置，备忘。
关于Ingress更基础的内容，请参考《K8S中安装使用Ingress》


2. 支持websocket推荐方法：
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: minimal-ingress  annotations:    nginx.ingress.kubernetes.io/proxy-http-version: &quot;1.1&quot;    nginx.ingress.kubernetes.io/proxy-read-timeout: &quot;3600&quot;    nginx.ingress.kubernetes.io/proxy-send-timeout: &quot;3600&quot;

参考文档：

ingress-nginx Websockets
ingress-nginx Annotations
Advanced Configuration with Annotations
WebSocket support

历史方法：
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: minimal-ingress  annotations:    nginx.ingress.kubernetes.io/configuration-snippet: |      proxy_http_version 1.1;      proxy_set_header Upgrade &quot;websocket&quot;;      proxy_set_header Connection &quot;Upgrade&quot;;

3. 域名证书配置3.1. 普通域名配置证书1、创建secret
kubectl create secret tls cert \  --cert=/etc/letsencrypt/live/voidking.com/fullchain.pem \  --key=/etc/letsencrypt/live/voidking.com/privkey.pem \  --dry-run=client \  -oyaml &gt; secret.yaml

secret.yaml 结构如下：
apiVersion: v1data:  tls.crt: xxx  tls.key: yyykind: Secretmetadata:  name: certtype: Opaque

其中，tls.crt是base64加密后的证书，tls.key是base64加密后的key。
2、ingress添加tls定义
spec:  ingressClassName: nginx  rules:  - host: test.voidking.com    http:      paths:      - path: /        pathType: Prefix        backend:          service:            name: test            port:              number: 80  tls:  - hosts:    - test.voidking.com    secretName: cert

3.2. 泛域名配置证书泛域名的证书配置方法，和普通域名的证书配置方法完全一致。
spec:  ingressClassName: nginx  rules:  - host: *.voidking.com    http:      paths:      - path: /        pathType: Prefix        backend:          service:            name: test            port:              number: 80  tls:  - hosts:    - &#x27;*.voidking.com&#x27;    secretName: cert

参考文档：

Ingress 证书配置

3.3. 多个域名配置泛域名证书多个域名使用同一个泛域名证书，建议集中配置泛域名证书。
1、域名证书配置
spec:  ingressClassName: nginx  rules:  - host: test0.voidking.com  - host: test1.voidking.com  tls:  - hosts:    - &#x27;test0.voidking.com&#x27;    - &#x27;test1.voidking.com&#x27;    secretName: cert

2、路由配置
spec:  ingressClassName: nginx  rules:  - host: test0.voidking.com    http:      paths:      - path: /        pathType: Prefix        backend:          service:            name: test            port:              number: 80

3.4. 批量修改域名证书已知条件：

k8s集群中，使用ingress配置了很多四级域名，例如 test0.intl.voidking.com 和 test1.intl.voidking.com 
除了四级域名，还有五级域名，例如 web.core.intl.voidking.com 和 api.core.intl.voidking.com
每个域名都有一个自己的ingress配置文件，包含tls配置
ingress位于不同的namespace

需求：*.intl.voidking.com域名证书过期，需要批量修改k8s中对应的四级域名证书，不要影响五级域名证书。
脚本实现：patch-ingress-tls
4. 前后端分离需求描述：前端后端是两个服务，位于两个namespace，要配置ingress，使发往路由/的流量打到前端服务，发往/api的流量打到后端服务。
方法一：在一个namespace中，创建一个ingress和一个externalname类型的service。在ingress中配置两个path。
方法二：在两个namespace中，分别创建一个ingress。host相同，配置不同的path，ingress-controller会自动合并这两个配置。
5. 允许跨域配置apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: minimal-ingress  annotations:    nginx.ingress.kubernetes.io/enable-cors: &quot;true&quot;    nginx.ingress.kubernetes.io/cors-allow-origin: &quot;https://example.com&quot;    nginx.ingress.kubernetes.io/cors-allow-methods: &quot;GET, PUT, POST, DELETE, PATCH, OPTIONS&quot;

参考文档：Nginx Ingress跨域配置说明
6. 设置max_client_body_sizeingress-nginx-controller有一个全局配置文件（configmap），类似于nginx中的nginx.conf，通过它可以配置一些通用配置，例如max_client_body_size。
1、编辑配置文件
kubectl edit cm ingress-nginx-controller -n ingress-nginx

修改data部分的配置：
data:  proxy-body-size: &quot;1000m&quot;

2、重启ingress-nginx-controller
kubectl rollout restart deployment ingress-nginx-controller -n ingress-nginx

7. 修改日志格式1、编辑配置文件
kubectl edit cm ingress-nginx-controller -n ingress-nginx

修改data部分的配置：
data:  log-format-upstream: &#x27;$remote_addr - $remote_port [$time_local] &quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot; $host $http_host $scheme &quot;$http_referer&quot; &quot;$request&quot; &quot;$request_uri&quot; &quot;$request_method&quot; $request_time $request_length $status $bytes_sent $body_bytes_sent $upstream_addr $upstream_response_time&#x27;  access-log-path: &quot;/var/log/nginx/access.log&quot;  error-log-path: &quot;/var/log/nginx/error.log&quot;

这里的日志格式参考《Nginx入门篇》
2、重启ingress-nginx-controller
kubectl rollout restart deployment ingress-nginx-controller -n ingress-nginx

8. 路径重写参考文档：

How to proxy_pass with nginx-ingress?
ingress-resources/rewrites
Advanced Configuration with Annotations

需求：发往 /api/* 的流量，变成 /* 转发到 api-service ；发往 /cms/* 的流量，变成 /* 转发到 cms-service；其余流量发往 frontend-service。
配置方法：
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: ingress  annotations:    nginx.ingress.kubernetes.io/rewrite-target: /$1    nginx.ingress.kubernetes.io/use-regex: truespec:  ingressClassName: nginx  rules:  - host: test0.voidking.com    http:      paths:      - path: /(.*)        backend:          serviceName: frontend-service          servicePort: 80      - path: /api/(.*)        backend:          serviceName: api-service          servicePort: 8080      - path: /cms/(.*)        backend:          serviceName: cms-service          servicePort: 8080

其中，path中的(.*)是第一个匹配，与rewrite-target中的$1对应。
需求：发往 /api/* 的流量，变成 /api/v1/* 转发到 api-service ；发往 /api-v2/* 的流量，变成 /api/v2/* 转发到 api-service。
配置方法：
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: fe  namespace: default  annotations:    kubernetes.io/ingress.class: &quot;nginx&quot;    nginx.ingress.kubernetes.io/rewrite-target: /    nginx.ingress.kubernetes.io/configuration-snippet: |      rewrite ^/api/(.*)$ /api/v1/$1 redirect;      rewrite ^/api-v2/(.*)$ /api/v2/$1 redirect;spec:  ingressClassName: nginx  rules:  - host: test0.voidking.com    http:      paths:      - path: /api        backend:          serviceName: api-service          servicePort: 8080

需求：发往 /tea/* 的流量，变成 /* 转发到 tea-svc ；发往 /coffee/* 的流量，变成 /beans/* 转发到 api-service。
配置方法：
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: cafe-ingress  annotations:    nginx.org/rewrites: &quot;serviceName=tea-svc rewrite=/;serviceName=coffee-svc rewrite=/beans/&quot;spec:  rules:  - host: cafe.example.com    http:      paths:      - path: /tea/        pathType: Prefix        backend:          service:            name: tea-svc            port:              number: 80      - path: /coffee/        pathType: Prefix        backend:          service:            name: coffee-svc            port:              number: 80

9. 泛域名根据域名匹配服务需求：访问四级域名 *.foo.voidking.com 时，根据域名与服务名匹配的规则，将请求分发到不同的服务。例如：匹配规则为域名与服务名相同

dev1.foo.voidking.com 的流量转发到 dev1.default.svc 
test1.foo.voidking.com 的流量转发到 test1.default.svc 
prod100.foo.voidking.com 的流量转发到 prod100.default.svc

想要只使用ingress，但是没有找到配置方法。
最终配置方法：所有流量转发给一个nginx服务，然后通过nginx配置实现。
10. 域名重定向需求：访问 voidking.com 域名的请求，自动跳转到 www.voidking.com。
配置方法：
kind: IngressapiVersion: networking.k8s.io/v1metadata:  name: voidking-com-redirect  namespace: voidking  annotations:    nginx.ingress.kubernetes.io/rewrite-target: &#x27;https://www.voidking.com&#x27;spec:  ingressClassName: nginx  #tls:  #  - hosts:  #      - voidking.com  #    secretName: voidking-com  rules:    - host: voidking.com      http:        paths:          - path: /            pathType: ImplementationSpecific            backend:              service:                name: fake-service                port:                  number: 80

注意：这里的 pathType 需要配置为 ImplementationSpecific，不能配置为 Prefix。否则，哪怕配置了 nginx.ingress.kubernetes.io/force-ssl-redirect，跳转前的voidking.com也是需要配置证书的。
其实，更简单的方法是使用dnspod的显性URL记录类型解析。但是，并不是所有的DNS服务器都支持这种显性URL记录类型，所以很多情况下还是需要用到ingress的域名重定向。
参考文档：Ingress-Nginx Controller Rewrite
11. 404重定向到首页需求：访问URL test.voidking.com/x/y/z 时，如果路径不存在出现404，那么自定义404页面或者跳转到首页。
配置方法：开启custom-http-errors，创建一个自定义backend，出现404等错误时都重定向到这个backend。
1、创建自定义backend，包括deployment和svc，资源清单为custom-default-backend.yaml
kubectl create -f custom-default-backend.yaml -n ingress-nginx

2、修改ingress-nginx的配置
kubectl edit cm ingress-nginx-controller -n ingress-nginx

添加配置：
data:  custom-http-errors: 404,503

3、修改ingress-nginx启动参数
kubectl edit deployment ingress-nginx-controller -n ingress-nginx#kubectl edit daemonset ingress-nginx-controller -n ingress-nginx

启动命令的参数中添加：
--default-backend-service=ingress-nginx/nginx-errors

注意这里的 ingress-nginx/nginx-errors 对应的格式为 namespace/name。
ingress-nginx重启后，报错404时会提示The page you&#39;re looking for could not be found.
如果想要自定义404页面，那么可以添加一个configmap.yaml，然后挂载到custom-default-backend.yaml
如果想要404重定向到首页，那么可以在该域名的ingress的annotations配置中，添加配置：
metadata:  annotations:    nginx.ingress.kubernetes.io/configuration-snippet: |      error_page 404 = /404.html;      location = /404.html &#123;        return 301 https://test.voidking.com;      &#125;

参考文档：Ingress-Nginx Custom Errors
12. 404重定向到不同路径需求：访问URL test.voidking.com/x/y/z 时，如果路径不存在出现404，那么跳转到 test.voidking.com/x。其中 x、y、z 都代表一个字符串。
结论：单纯使用ingress，是无法实现这个配置的。对于静态页面，可以借助Nginx进行配置，参考文档：《好好学Nginx：Nginx入门篇》。对于动态页面，可以在后端服务代码中自行处理404的跳转。
13. 限制上传文件大小需求：上传文件到K8S中的某个服务时，限制文件大小不能超过50M。
配置方法：
metadata:  annotations:    nginx.ingress.kubernetes.io/proxy-body-size: 50m

14. 限制单位时间内的访问次数需求：访问K8S中的某个服务时，限制单位时间内某个IP可以访问该服务的次数。
配置方法：
metadata:  annotations:    nginx.ingress.kubernetes.io/limit-rps: &quot;10&quot;  # 每秒10个请求    nginx.ingress.kubernetes.io/limit-rpm: &quot;300&quot; # 每分钟300个请求

参考文档：Kubernetes Ingress-nginx高级用法
15. 查看最终的nginx配置我们编写的ingress，实际上最终会生成nginx.conf，可以在ingress-nginx-controller中查看是否符合预期。
kubectl exec -it ingress-nginx-controller-xxx -- /bin/bashcd /etc/nginxcat nginx.conf





]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>nginx</tag>
        <tag>k8s</tag>
        <tag>shell</tag>
        <tag>https</tag>
        <tag>tls</tag>
        <tag>ssl</tag>
        <tag>好好学K8S</tag>
        <tag>ingress</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中的admissionWebhooks</title>
    <url>/dev-k8s-admissionwebhooks/</url>
    <content><![CDATA[1. 什么是admissionWebhooks？admissionWebhooks是Kubernetes的一种高级特性，它允许集群管理员通过Webhook服务来验证和修改提交到Kubernetes API服务器的资源请求。这些资源请求可以包括创建、更新或删除Kubernetes对象，如Pod、Service、Deployment等。
参考文档：Kubernetes webhook原理介绍及执行演示


2. admissionWebhooks类型admissionWebhooks分为两种主要类型：

Validating Webhooks：这些Webhooks用于验证资源请求是否符合自定义规则。如果请求不符合规则，它将被拒绝。这有助于防止不正确或不安全的配置进入集群。例如，我们可以强制执行名称约定、安全策略或其他最佳实践。

Mutating Webhooks：这些Webhooks用于自动修改或补充资源请求。它们可以用于自动添加默认值、生成TLS证书、自动注入侧边车（sidecar）容器等。Mutating Webhooks允许我们以自动化的方式改善和定制资源配置。


3. 使用场景admissionWebhooks在Kubernetes中有广泛的应用场景，以下是一些常见的用例：

安全性强制执行：管理员可以使用Validating Webhooks来确保Kubernetes资源遵循安全性最佳实践。例如，我们可以拒绝未经授权的镜像或容器访问。

默认值注入：使用Mutating Webhooks，我们可以自动向Pod或其他资源添加默认配置。这有助于简化配置，降低人为错误的风险。

自动TLS证书管理：通过Mutating Webhooks，我们可以自动为Ingress或其他资源生成和管理TLS证书，从而简化了HTTPS配置。

自动扩展功能：通过自定义验证规则和变更配置，我们可以增强Kubernetes的功能。例如，我们可以强制执行自定义认证、流量控制或其他策略。


4. 部署和配置要配置和部署admissionWebhooks，我们需要执行以下一般步骤：
1、创建Webhook服务：首先，我们需要创建一个Webhook服务，它将处理资源请求的验证或变更。这个服务通常运行在Kubernetes集群中，并能够接收来自Kubernetes API服务器的HTTP请求。
2、创建Webhook资源：创建一个ValidatingWebhookConfiguration或MutatingWebhookConfiguration资源，以定义Webhook的配置。这个资源将包含Webhook的名称、URL、验证规则和选择器等信息。
3、部署Webhook服务：确保Webhook服务能够正常运行并监听来自Kubernetes API服务器的请求。
4、测试和验证：测试Webhook配置，确保它们按预期工作。创建或更新资源，并检查验证规则或配置变更是否按预期生效。
5、绑定ClusterRole和ClusterRoleBinding：为Webhook服务创建必要的ClusterRole和ClusterRoleBinding，以确保它具有适当的权限。
6、监控和日志：设置监控和日志以监视Webhook服务的性能和运行状况，以及及时发现和解决问题。
5. 小结admissionWebhooks是Kubernetes中强大的安全性和验证机制，允许管理员通过自定义验证规则和配置变更来增强集群的安全性和功能。它们可以用于强制执行安全性策略、添加默认配置、自动生成证书等多种用途。虽然配置和部署admissionWebhooks可能需要一些工作，但它们为Kubernetes提供了更大的灵活性和可定制性，以满足不同环境和应用程序的需求。因此，对于需要强大的安全性和配置管理的Kubernetes集群来说，admissionWebhooks是不可或缺的工具之一。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中安装使用Ingress</title>
    <url>/dev-k8s-ingress/</url>
    <content><![CDATA[1. Ingress简介ingress是k8s中的一个七层的负载均衡，可以把流量路由到service。如果没有ingress，我们可以通过外部的nginx、haproxy等也可以实现七层负载均衡，但是配置nginx比较麻烦，因为想要配置nginx的upstream必须先查询service的ip，那如果使用service的name，还要维护一个dns；如果想要换成haproxy，成本也比较高。而有了ingress，实际上就是把nginx等七层负载均衡集成到了k8s中，配置就更加简单也更加通用。
它的工作原理是ingress服务作为流量的入口，收到请求后，根据host、path等规则，匹配到对应的service和端口，然后把流量转发到service的端口，service再转发流量给pod。
参考文档：

Ingress
Ingress Controllers



2. 安装Ingress Controller本节中，我们使用helm在K8S集群中安装Ingress NGINX Controller。
参考文档：

Set up Ingress on Minikube with the NGINX Ingress Controller
Ingress NGINX Controller
Ingress NGINX Controller - Installation Guide
使用Helm安装Ingress-nginx

官方推荐一键安装：
helm upgrade --install ingress-nginx ingress-nginx \  --repo https://kubernetes.github.io/ingress-nginx \  --namespace ingress-nginx --create-namespace

因为郝同学喜欢安装指定版本，并且对配置做一些了解，因此本节使用稍微麻烦一点的方式。
1、添加chart仓库
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo updatehelm repo list

2、下载chart（chart4.1.4 对应 app1.2.1）
helm search repo ingress-nginx/ingress-nginx -lhelm fetch ingress-nginx/ingress-nginx --version 4.1.4tar -xzvf ingress-nginx-4.1.4.tgzvim ingress-nginx/values.yaml

3、修改 values.yaml 配置

controller.image.registry：改为k8s.dockerproxy.com
controller.image.tag：保持不变v1.2.1
controller.image.digest：注释掉
controller.image.digestChroot：注释掉
controller.admissionWebhooks.enabled：保持 true，避免创建的错误 Ingress 等资源对象影响控制器重新加载
defaultBackend.enabled：按需修改

4、安装ingress nginx controller
helm install ingress-nginx ingress-nginx -n ingress-nginx --create-namespace --dry-run --debughelm install ingress-nginx ingress-nginx -n ingress-nginx --create-namespace

5、查看安装
kubectl get all -n ingress-nginxkubectl get ingressclass

注意：新版本的ingress，会创建ingressclass，但是一些旧版本的ingress，并没有ingressclass这种资源。在 Kubernetes 1.18 之前，通常是在 Ingress 资源上通过 kubernetes.io/ingress.class 注解来指定使用的 Ingress Controller。虽然这个注解从未被正式定义过，但却是被各个 Ingress Controller 所广泛支持的，注解的值可以在Ingress Controller启动时指定。Kubernetes 1.18 起提供了 IngressClass 资源，IngressClass指定关联spec.controller，Ingress指定关联spec.ingressClassName，就实现了Ingress指定Ingress Controller。详情参考文档K8s Ingress、Ingress Controller 和 Ingress Class
3. KS中的Ingress Controller如果k8s集群中安装了kubesphere，那么可以直接启用kubesphere的网关，本质也是ingress nginx controller。开启办法：admin用户登录控制台，集群设置，网关设置，启用网关。
参考文档：

Guide to Kubernetes Ingress Controllers
Canary Release in Kubernetes with Nginx Ingress
集群网关
项目网关
应用路由

4. 配置Ingress参考文档：

Ingress
Default IngressClass

4.1. Ingress Controller配置可以认为Ingress Controller就是一个K8S内部的Nginx。查看 Ingress Controller 的 service 配置，找到80和433端口对应的NodePort，用于访问Nginx。假设一个节点IP为192.168.56.102，service的80端口对应30490，443端口对应30499。
4.2. 创建测试服务1、创建测试服务（Nginx）
kubectl create deployment test --image=nginx --dry-run=client -oyaml &gt; deployment.yamlkubectl create service clusterip test --tcp=80:80 --tcp=443:443 --dry-run=client -oyaml &gt; service.yamlkubectl apply -f deployment.yamlkubectl apply -f service.yaml

2、查看测试服务
kubectl get podskubectl get svc

4.3. 创建Ingress1、准备 minimal-ingress.yaml
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: minimal-ingress  annotations:    nginx.ingress.kubernetes.io/rewrite-target: /spec:  ingressClassName: nginx  rules:  - http:      paths:      - path: /testpath        pathType: Prefix        backend:          service:            name: test            port:              number: 80

其中，ingressClassName可以省略，省略后会使用默认的ingressClass。因为配置了rewrite-target，所以请求到达service时，/testpath会被替换为/。
2、创建ingress
kubectl apply -f minimal-ingress.yamlkubectl get ingress

3、通过ingress controller访问测试服务
curl 192.168.56.102:30490/testpath

访问链路为：ip+port（ingress） -&gt; nginx service
5. 多个Ingress Controller怎样在k8s集群中配置多个Ingress Controller？答：多个Ingress Controller的启动命令中，--controller-class和--ingress-class参数设置成不同的值。
如果k8s集群中存在多个Ingress Controller，哪个会生效呢？答：在Ingress的spec中指定ingressClassName，或者在Ingress的annotations中指定kubernetes.io/ingress.class（即将废弃）。如果不指定，那么将会使用默认的ingressClass，默认的ingressClass中会包含一个annotations：
annotations:  ingressclass.kubernetes.io/is-default-class: &quot;true&quot;

Ingress NGINX Controller的--controller-class默认值为k8s.io/ingress-nginx，--ingress-class的默认值为nginx。
参考文档：

Ingress
Default IngressClass
Multiple Ingress controllers
kubernetes部署多个ingress controller（ingress controller分组部署）实践
部署多个Ingress Controller
同一kubernetes部署多个Nginx Ingress Controller

6. Ingress常用配置参考文档《Ingress常用配置》
7. 查看最终的nginx配置我们编写的ingress，实际上最终会生成nginx.conf，可以在ingress-nginx-controller中查看是否符合预期。
kubectl exec -it ingress-nginx-controller-xxx -- /bin/bashcd /etc/nginxcat nginx.conf

8. Ingress推荐链路8.1. LB -&gt; IngressDomain -&gt; LB -&gt; Ingress -&gt; Service -&gt; Pod
这种链路，适用于云厂商提供的k8s集群。
创建方法：1）在创建ingress-controller的Service时，直接创建LoadBanlancer类型的Service。2）或者单独创建LB，后端配置Ingress的Node Port。
这种链路最大的优点是链路短，速度快。通过云厂商提供的LB，可以配置四层或者七层负载均衡到Ingress，还可以在LB层加一些安全防护产品。如果使用的是七层负载均衡，还可以统一配置泛域名证书。
8.2. Nginx -&gt; IngressDomain -&gt; Nginx -&gt; Ingress -&gt; Service -&gt; Pod
这种链路，适用于所有k8s集群。通过Nginx，可以配置四层或者七层负载均衡到Ingress。如果使用的是七层负载均衡，还可以统一配置泛域名证书。
8.3. 开发/测试/生产环境链路

外网访问K8S生产集群中的服务：域名 -&gt; 云厂商Prod CLB(80/443) -&gt; Ingress(Node Port) -&gt; Service
外网访问K8S测试集群中的服务：域名 -&gt; 云厂商Test CLB(80/443) -&gt; Ingress(Node Port) -&gt; Service
外网访问K8S开发集群中的服务：域名 -&gt; 云厂商Nginx(裸机版，80/443) -&gt; Ingress(Node Port) -&gt; Service
内网访问K8S开发集群中的服务：域名 -&gt; 自建机房Nginx1(80/443) -&gt; Ingress(Node Port)-&gt; Service
外网访问云厂商非K8S集群中的服务：域名 -&gt; 云厂商混用 CLB(80/443) -&gt; 云厂商Nginx(Docker版，1080/1443) -&gt; Service
外网访问自建机房非K8S集群中的服务：域名 -&gt; 云厂商混用 CLB(80/443) -&gt; 云厂商Nginx(Docker版，1080/1443) -&gt;Service 
内网访问自建机房非K8S集群中的服务：域名 -&gt; 自建机房Nginx2(80/443) -&gt; Service
外网访问云厂商需要四层转发的服务：云厂商混用CLB(自行选择端口) -&gt; 四层Service
外网访问自建机房需要四层转发的服务：云厂商混用CLB(自行选择端口) -&gt; 云厂商Nginx(裸机版，自行选择端口) -&gt; 四层Service

]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>架构</tag>
        <tag>网络</tag>
        <tag>nginx</tag>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>好好学K8S</tag>
        <tag>ingress</tag>
        <tag>kubesphere</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：Ingress配置使用宿主机80/443端口</title>
    <url>/dev-k8s-ingress-80-443/</url>
    <content><![CDATA[1. 前言《K8S中安装使用Ingress》一文中，我们安装配置了Ingress。安装Ingress后，默认的服务端口是NodePort，范围 30000-32767 。因为端口不是80和443，所以我们无法直接把域名解析到节点IP上，上层还需要增加一个LB或者Nginx进行转发。而增加了上层，整个链路就变长了，增加了响应耗时和出现问题的概率。
本文中，我们学习一种Ingress配置方法，能够使Ingress直接使用宿主机的80和443端口。


2. 安装配置思路K8S中有一个机制是 hostNetwork，允许Pod使用宿主机网络，也就是说可以直接将Pod的80和443直接绑定到K8S宿主机网络上。基于此，可以使用 DaemonSet+hostNetwork 部署Ingress。
参考文档：

DNS for Services and Pods
Kubernetes进阶训练营 ingress-nginx
适合ingress-nginx接入外部负载均衡的部署

3. 安装配置实践1、添加chart仓库
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo updatehelm repo list

2、下载chart（chart4.1.4 对应 app1.2.1）
helm search repo ingress-nginx/ingress-nginx -lhelm fetch ingress-nginx/ingress-nginx --version 4.1.4tar -xzvf ingress-nginx-4.1.4.tgzvim ingress-nginx/values.yaml

3、修改 values.yaml 配置

controller.image.registry：改为k8s.dockerproxy.com
controller.image.tag：保持不变v1.2.1
controller.image.digest：注释掉
controller.image.digestChroot：注释掉
controller.admissionWebhooks.enabled：保持 true，避免创建的错误 Ingress 等资源对象影响控制器重新加载
defaultBackend.enabled：按需修改
controller.hostNetwork：改为 true
controller.publishService.enabled：改为 false
controller.dnsPolicy：改为 ClusterFirstWithHostNet
controller.kind：改为 DaemonSet
controller.nodeSeletor：按需修改
controller.tolerations：按需修改
controller.service.enabled：改为 false

4、安装ingress nginx controller
helm install ingress-nginx ingress-nginx -n ingress-nginx --create-namespace --dry-run --debughelm install ingress-nginx ingress-nginx -n ingress-nginx --create-namespace

5、查看安装
kubectl get all -n ingress-nginxkubectl get ingressclass


]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>架构</tag>
        <tag>网络</tag>
        <tag>nginx</tag>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>好好学K8S</tag>
        <tag>ingress</tag>
        <tag>kubesphere</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中的Service详解</title>
    <url>/dev-k8s-service/</url>
    <content><![CDATA[1. K8S中的Service分类K8S中Service的作用是四层负载均衡，把发往Service的流量负载均衡到对应的Pod（endpoints）上。根据暴露服务方式的不同，可以分为四种类型：ClusterIP、NodePort、LoadBalancer和ExternalName。
参考文档：

服务
一文讲透K8s的Service概念



2. ClusterIPClusterIP：通过集群的内部 IP 暴露服务，选择该值时服务只能够在集群内部访问。 这也是默认的 ServiceType。
3. NodePortNodePort：通过每个节点上的 IP 和静态端口（NodePort）暴露服务。 NodePort 服务会路由到自动创建的 ClusterIP 服务。 通过请求 &lt;节点 IP&gt;:&lt;节点端口&gt;，你可以从集群的外部访问一个 NodePort 服务。NodePort的默认范围是30000-32767，可以通过–service-node-port-range修改。
4. LoadBalancerLoadBalancer：使用云提供商的负载均衡器向外部暴露服务。 外部负载均衡器可以将流量路由到自动创建的 NodePort 服务和 ClusterIP 服务上。
5. ExternalNameExternalName：通过返回 CNAME 和对应值，可以将服务映射到 externalName 字段的内容（例如，foo.bar.example.com），实现外部服务的对内映射。
这个类型的作用主要是为了方便迁移，比如现在集群中的服务依赖一个外部的服务，那就可以使用ExternalName把这个外部服务映射到集群内部，以后这个外部服务迁移到集群内部，我们可以通过修改service类型使用内部的新服务，对于依赖方是无感的。
externalName虽然可以配置IP地址，但是Ingress和CoreDNS并没有解析。如果外部服务为IP，则可以使用Headless Service来实现映射。
示例1：
apiVersion: v1kind: Servicemetadata:  name: mysqlspec:  type: ExternalName  externalName: mysql.example.com  ports:    - protocol: TCP      port: 3306      targetPort: 3306

示例2：
apiVersion: v1kind: Servicemetadata:  name: mysqlspec:  type: ClusterIP  clusterIP: None  ports:  - port: 3306    targetPort: 3306---apiVersion: v1kind: Endpointsmetadata: name: mysqlsubsets:- addresses:  - ip: 192.168.56.10  ports:  - port: 3306

注意：在示例1中，externalName尽量不要填写IP（虽然可以填写），因为填写IP可能会导致Ingress报错。报错类似于：
2025/06/01 12:10:12 [error] 27285#27285: *626616236 [lua] dns.lua:152: dns_lookup(): failed to query the DNS server for 192.168.56.10:server returned error code: 3: name error, context: ngx.timer

在遇到externalName需要填写IP的场景，建议参考示例2的写法。
参考文档：Kubernetes External Service Mapping
注意：一个service可能有多个endpoint，这些endpoint要求IP不同。比如，mysql:3306对应的endpoint是192.168.56.10:3306，如果想要再增加endpoint，那么需要增加IP，而不是增加port。
6. Headless ServiceHeadless Service，实际上属于ClusterIP类型，是一种特殊的ClusterIP。有时不需要负载均衡，也不需要单独的 ServiceIP，可以通过指定 ClusterIP（spec.clusterIP）的值为 “None” 来创建 Headless Service。Headless Service常用于StatefulSet类型负载的对外服务暴露，或者外部服务的对内映射。
Headless Service和普通Service的不同有两点：1、在DNS查询时普通Service会返回ServiceIP，而Headless Service会返回多个真实的endpoint，这样客户端就可以自己选择调用哪一个endpoint。2、Headless Service对应的每一个pod都会分配一个DNS域名，这样pod间就可以通过域名互相访问了。而有状态服务就需要这种互相不同的识别，因此，Headless Service常常和statefulset配合使用。
参考文档：

Headless Services
K8S容器编排之Headless浅谈

]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>架构</tag>
        <tag>网络</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：CKA和CKAD备考</title>
    <url>/dev-cka-ckad-prepare/</url>
    <content><![CDATA[1. Flag《K8S入门篇》一文中，定下了目标：拿到CKA和CKAD证书。转眼过去了大半年，但是一直没有付诸行动。2020年02月23日立下flag：2020年5月1前考下CKA，2020年6月1日前考下CKAD。


2. CKA和CKAD是啥？
The Certified Kubernetes Administrator (CKA) program was created by the Cloud Native Computing Foundation (CNCF), in collaboration with The Linux Foundation, to help develop the Kubernetes ecosystem. 


The Certified Kubernetes Application Developer (CKAD) program has been developed by the Cloud Native Computing Foundation (CNCF), in collaboration with The Linux Foundation, to help expand the Kubernetes ecosystem through standardized training and certification. 

简而言之，CKA/CKAD是CNCF和Linux基金会联合推出的两个K8S考试认证。委托给PSI来进行监督考试。其中，CKA侧重于K8S管理，CKAD侧重于K8S开发。
想要拿到CKA和CKAD证书，需要哪些准备工作？本文就来梳理一下。主要参考：

CKA及CKAD认证考试经验分享
CKA认证获取历程
How to prepare and pass CKA exam
How to prepare for CKA and CKAD exam ?
Mumshad Mannambeth Courses
官方 - Exam Tips
官方 - Candidate Handbook
官方 - CNCF FAQ

3. 考试报名和约考3.1. 科学上网参考CKA和CKAD考试，首先要准备好稳定的科学上网工具。
3.2. 考试报名报名网址：Certified Kubernetes Administrator (CKA) Program和Certified Kubernetes Application Developer (CKAD) Program。报名时使用折扣码DEVOPS15可以获得15%的折扣。
报名时需要支持海外支付的信用卡（VISA或者MasterCard）。
3.3. 考试预约1、支付完成后，跳转到Certification Status页面。
2、按照提示，Check System Requirements，验证成功后以后也可以再次验证。
3、点击 Schedule Exam，跳转到考试时间安排页面，选择一个考试时间。
4、点击 Get Candidate Handbook，获取考试手册。
5、点击 Verify Name，确认姓名，以后会出现在证书上。
6、点击 Important Tips，获取帮助提示。
7、点击右侧 Schedule Exam，即可看到考试信息，包括考试的时间、倒计时、确认码、考生ID等。
以上，报名和约考完成。
4. 考试当天注意事项在约定的考试时间登录考试系统。

硬件检测：使用Chrome浏览器访问CompatibilityCheck，选择 ”Linux Foundation” as the Exam Sponsor and “CKA” as the Exam。
按照考官要求打开摄像头和麦克风，并且共享桌面。
身份检测：考官查看考生证件，比如护照（身份证不行，需要有照片和Latin字母写的全名）。
环境检测：考官通过摄像头检查房间，要求房间只有考生一个人，桌子只能放电脑，没有其他东西。
软件检测：考官要求打开任务管理器，确认除浏览器之外的没有其他任务，确认无误后考试开始。
要求Chrome浏览器当前只能有一个tab页（就是考试系统），考试过程中可以再打开一个tab页用于访问Kubernetes文档，就是说考试全程最多两个tab页。
考试系统的网页一半是试题，一半是GateOne的终端界面。
考试中不能喝水、吃东西，可以申请休息。

CKA 考试 180 分钟，25 道题。CKAD 考试 120 分钟，20 道题。
5. 获取考试成绩考试结束后，36个小时之内，CNCF就会通过邮件告诉考生成绩。CKA分数大于74%则通过考试，CKAD分数大于66%则为通过考试，并且附件包含证书。如果考试不通过，考生账号上就会有一次一年内Free Retake的机会。
6. Get技能6.1. Docker
浙江大学SEL实验室编写的《Docker容器与容器云》
Docker Documentation
菜鸟教程之Docker教程
Learn Docker &amp; Containers using Interactive Browser

6.2. K8S
《Kubernetes权威指南：从Docker到Kubernetes实践全接触 第4版》
Kubernetes Documentation
kubernetes中文社区
华为云-Cloud Native Lives
阿里云-K8s 资源全汇总 | K8s 大咖带你 31 堂课从零入门 K8s
阿里云-云原生技术公开课
阿里云-云原生技术公开课（备用地址）
Certified Kubernetes Administrator (CKA) with Practice Tests
Labs - Certified Kubernetes Administrator with Practice Tests
Kubernetes Certified Application Developer (CKAD) with Tests
Labs - Certified Kubernetes Application Developer
cka&amp;cakd考纲-curriculum
Studying for the Certified Kubernetes Administrator Exam
K8S Tasks
Learn Kubernetes using Interactive Browser-Based Scenarios
Play with Kubernetes
CKA CKAD Simulator

7. k8s-tool备考的过程中，完善自己的k8s-tool，考试的时候可以使用。
8. 后记8.1. 5月19日更新三个月忙碌的早晚，无休的周末，终于在5月19日早上，收到了CKAD的证书。加上4月份拿到的CKA证书，目标达成！CKA考了90%（74%通过），CKAD考了89%（66%通过）。
8.2. 考试心得CKA和CKAD有什么差别？1、考试内容有重叠，但是也有各自独立的内容。比如CKA会考etcd和k8s部署相关的内容，CKAD不会涉及。CKAD会有探活、cronjob和hpa的内容，CKA不会涉及。2、CKAD比CKA难度更大一些。考过了CKA，本以为可以轻松拿下CKAD，结果考试时发现CKAD的题目更复杂一些，花费时间更长。后来想想也合理，毕竟CKA需要74分通过，而CKAD只需要66分就可以通过。3、其实我觉得CKA和CKAD完全可以合成一个考试，因为没有特别明显的界限，官方可能就是为了多赚点钱。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>cka</tag>
        <tag>ckad</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo使用Gulp压缩静态资源</title>
    <url>/dev-hexo-gulp/</url>
    <content><![CDATA[1. Gulp简介gulp是一个自动化构建工具，能够强化我们的前端工作流。

gulp is an open-source JavaScript toolkit created by Eric Schoffstall used as a streaming build system (similar to a more package-focussed Make) in front-end web development.


It is a task runner built on Node.js and npm, used for automation of time-consuming and repetitive tasks involved in web development like minification, concatenation, cache busting, unit testing, linting, optimization, etc.


gulp uses a code-over-configuration approach to define its tasks and relies on its small, single-purpose plugins to carry them out. The gulp ecosystem includes more than 3500 such plugins.

更多内容，参考wikipedia-gulp.js、Gulp官网和Gulp中文网。本文中，会使用gulp来压缩hexo生成的静态资源文件，加快站点的访问速度。


2. 安装配置gulp1、安装gulpnpm install --global gulp-cli
2、安装gulp模块
npm install gulp --savenpm install gulp-htmlclean gulp-htmlmin gulp-clean-css gulp-uglify gulp-imagemin --savenpm install gulp-babel babel-preset-env babel-preset-mobx --savenpm install -D @babel/core @babel/preset-react @babel/preset-env --save
最终生成的package.json为：
&#123;  &quot;name&quot;: &quot;hexo-site&quot;,  &quot;version&quot;: &quot;0.0.0&quot;,  &quot;private&quot;: true,  &quot;scripts&quot;: &#123;    &quot;build&quot;: &quot;hexo generate&quot;,    &quot;clean&quot;: &quot;hexo clean&quot;,    &quot;deploy&quot;: &quot;hexo deploy&quot;,    &quot;server&quot;: &quot;hexo server&quot;  &#125;,  &quot;hexo&quot;: &#123;    &quot;version&quot;: &quot;4.2.0&quot;  &#125;,  &quot;dependencies&quot;: &#123;    &quot;babel-preset-env&quot;: &quot;^1.7.0&quot;,    &quot;babel-preset-mobx&quot;: &quot;^2.0.0&quot;,    &quot;gulp&quot;: &quot;^4.0.2&quot;,    &quot;gulp-babel&quot;: &quot;^8.0.0&quot;,    &quot;gulp-clean-css&quot;: &quot;^4.2.0&quot;,    &quot;gulp-htmlclean&quot;: &quot;^2.7.22&quot;,    &quot;gulp-htmlmin&quot;: &quot;^5.0.1&quot;,    &quot;gulp-imagemin&quot;: &quot;^7.1.0&quot;,    &quot;gulp-uglify&quot;: &quot;^3.0.2&quot;,    &quot;hexo&quot;: &quot;^4.0.0&quot;,    &quot;hexo-generator-archive&quot;: &quot;^1.0.0&quot;,    &quot;hexo-generator-baidu-sitemap&quot;: &quot;^0.1.6&quot;,    &quot;hexo-generator-category&quot;: &quot;^1.0.0&quot;,    &quot;hexo-generator-feed&quot;: &quot;^2.2.0&quot;,    &quot;hexo-generator-index&quot;: &quot;^1.0.0&quot;,    &quot;hexo-generator-searchdb&quot;: &quot;^1.2.0&quot;,    &quot;hexo-generator-sitemap&quot;: &quot;^2.0.0&quot;,    &quot;hexo-generator-tag&quot;: &quot;^1.0.0&quot;,    &quot;hexo-neat&quot;: &quot;^1.0.4&quot;,    &quot;hexo-renderer-ejs&quot;: &quot;^1.0.0&quot;,    &quot;hexo-renderer-marked&quot;: &quot;^2.0.0&quot;,    &quot;hexo-renderer-stylus&quot;: &quot;^1.1.0&quot;,    &quot;hexo-server&quot;: &quot;^1.0.0&quot;  &#125;,  &quot;devDependencies&quot;: &#123;    &quot;@babel/core&quot;: &quot;^7.8.4&quot;,    &quot;@babel/preset-env&quot;: &quot;^7.8.4&quot;,    &quot;@babel/preset-react&quot;: &quot;^7.8.3&quot;  &#125;&#125;

4、在hexo目录创建gulpfile.js，内容为：
let gulp = require(&#x27;gulp&#x27;)let cleanCSS = require(&#x27;gulp-clean-css&#x27;)let htmlmin = require(&#x27;gulp-htmlmin&#x27;)let htmlclean = require(&#x27;gulp-htmlclean&#x27;)let babel = require(&#x27;gulp-babel&#x27;) /* 转换为es2015 */let uglify = require(&#x27;gulp-uglify&#x27;)let imagemin = require(&#x27;gulp-imagemin&#x27;)// 设置根目录const root = &#x27;./public&#x27;// 匹配模式， **/*代表匹配所有目录下的所有文件const pattern = &#x27;**/*&#x27;// 压缩htmlgulp.task(&#x27;minify-html&#x27;, function() &#123;  return gulp    // 匹配所有 .html结尾的文件    .src(`$&#123;root&#125;/$&#123;pattern&#125;.html`)    .pipe(htmlclean())    .pipe(      htmlmin(&#123;        removeComments: true,        minifyJS: true,        minifyCSS: true,        minifyURLs: true      &#125;)    )    .pipe(gulp.dest(&#x27;./public&#x27;))&#125;)// 压缩cssgulp.task(&#x27;minify-css&#x27;, function() &#123;  return gulp    // 匹配所有 .css结尾的文件    .src(`$&#123;root&#125;/$&#123;pattern&#125;.css`)    .pipe(      cleanCSS(&#123;        compatibility: &#x27;ie8&#x27;      &#125;)    )    .pipe(gulp.dest(&#x27;./public&#x27;))&#125;)// 压缩jsgulp.task(&#x27;minify-js&#x27;, function() &#123;  return gulp    // 匹配所有 .js结尾的文件    .src(`$&#123;root&#125;/$&#123;pattern&#125;.js`)    .pipe(      babel(&#123;        presets: [&#x27;env&#x27;]      &#125;)    )    .pipe(uglify())    .pipe(gulp.dest(&#x27;./public&#x27;))&#125;)// 压缩图片gulp.task(&#x27;minify-images&#x27;, function() &#123;  return gulp    // 匹配public/images目录下的所有文件    .src(`$&#123;root&#125;/images/$&#123;pattern&#125;`)    .pipe(      imagemin(        [          imagemin.gifsicle(&#123; optimizationLevel: 3 &#125;),          imagemin.jpegtran(&#123; progressive: true &#125;),          imagemin.optipng(&#123; optimizationLevel: 7 &#125;),          imagemin.svgo()        ],        &#123; verbose: true &#125;      )    )    .pipe(gulp.dest(&#x27;./public/images&#x27;))&#125;)gulp.task(&#x27;default&#x27;, gulp.series(&#x27;minify-html&#x27;, &#x27;minify-css&#x27;, &#x27;minify-js&#x27;))

4、执行压缩gulp
3. 命令精简使用了gulp时候，构建发布需要四个命令：
hexo cleanhexo ggulphexo d
这四个命令，可以都写在package.json。
&quot;scripts&quot;: &#123;  &quot;build&quot;: &quot;hexo clean &amp;&amp; hexo g &amp;&amp; gulp&quot;,  &quot;deploy&quot;: &quot;hexo clean &amp;&amp; hexo g &amp;&amp; gulp &amp;&amp; hexo d&quot;&#125;
构建只需要执行npm run build，构建发布只需要执行npm run deploy。
4. travis配置对应的，修改.travis.yml配置为：
sudo: falselanguage: node_jsnode_js:- 10.16.3cache: npmbranches:  only:  - master # build master branch onlyenv:  global:  - GIT_USER: voidking  - HEXO_BACKUP_REPO: github.com/voidking/hexo-backup.git  - HEXO_THEME_REPO: github.com/voidking/hexo-theme-next.git  - GITHUB_PAGES_REPO: github.com/voidking/voidking.github.io.git  - VOIDKING_REPO: github.com/voidking/voidking.gitbefore_install:- export TZ=&#x27;Asia/Shanghai&#x27;- npm install hexo -g- npm install gulp-cli -ginstall:- npm installscript:- git clone https://$&#123;HEXO_THEME_REPO&#125; themes/next- git clone https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;HEXO_BACKUP_REPO&#125; hexo-backup- mv hexo-backup/source .- rm -rf source/private- npm run buildafter_success:- git config --global user.name &quot;voidking&quot;- git config --global user.email &quot;voidking@qq.com&quot;- git clone https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;GITHUB_PAGES_REPO&#125; voidking- unalias cp- cp -rf public/. voidking- cd voidking- git add .- git commit -m &quot;Travis CI Auto Builder&quot;- git push --force --quiet &quot;https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;GITHUB_PAGES_REPO&#125;&quot; master:master

5. 精简search.xmlhtml、css和js都压缩了，很开心。但是，还有一个大文件没有压缩，就是本地搜索的DB文件search.xml。我的博客有接近600篇文章，这个search.xml文件的大小为7.5M，很大。打开search.xml文件，发现里面不止包含文章内容，还包含html标签。参考hexo-generator-searchdb，发现可以设置不生成标签。
5.1. 修改localsearch配置1、修改hexo/_config.yml的localsearch配置为：
# local searchsearch:  path: search.xml  field: post  format: striptags  limit: 10000

2、重新生成search.xml文件，新的文件只有3.5M。
3、修改local-search.js为了正常使用搜索功能，需要修改hexo主题的local-search.js。如果不修改的话，搜索时只搜索标题，不会搜索内容。编辑next/source/js/local-search.js，如下修改：
// line 120// let content = data.content ? data.content.trim().replace(/&lt;[^&gt;]+&gt;/g, &#x27;&#x27;) : &#x27;&#x27;;let content = data.content;

以上，实现了search.xml的精简，nice。
5.2. 自定义精简但是，3.5M依然很大，能不能再精简一下？可以。1、localsearch的format改回html。
2、修改xml_generator.js编辑hexo/node_modules/hexo-generator-searchdb/lib/xml_generator.js，定义自己想要删除的字符：
&#x27;use strict&#x27;;const path = require(&#x27;path&#x27;);const fs = require(&#x27;fs&#x27;);const nunjucks = require(&#x27;nunjucks&#x27;);var env = new nunjucks.Environment();var searchTmplSrc = path.join(__dirname, &#x27;../templates/search.xml&#x27;);var searchTmpl = nunjucks.compile(fs.readFileSync(searchTmplSrc, &#x27;utf8&#x27;), env);var stripe_code_line_num = function(str) &#123; // 去除代码    return str.replace(/&lt;figure class=&quot;highlight.*?&lt;\/figure&gt;/ig, &#x27;&#x27;);&#125;var stripe = function (str) &#123; // 去除html标签    return str.replace(/(&lt;([^&gt;]+)&gt;)/ig, &#x27;&#x27;);&#125;var minify = function (str) &#123; // 压缩成一行    return str.trim().replace(/\n/g, &#x27; &#x27;).replace(/\s+/g, &#x27; &#x27;);&#125;module.exports = function(locals) &#123;  var config = this.config;  var database = require(&#x27;./database&#x27;)(locals, config);  database.forEach( function(element, index) &#123;    element.content = minify(stripe(stripe_code_line_num(element.content)));  &#125;);  var xml = searchTmpl.render(&#123;    articles: database,    config  : config.search  &#125;);  return &#123;    path: config.search.path,    data: xml  &#125;;&#125;;

3、重新生成search.xml，这次只有2.5M，nice。
以上两种精简search.xml的方法都很好，这里我选择使用修改localsearch format的方法。因为如果在xml_generator.js不删除文章中的代码，两种方法的压缩结果基本相同，而方法一通用性更好。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>cicd</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
        <tag>gulp</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo配置Travis CI自动构建发布</title>
    <url>/dev-hexo-travis-ci/</url>
    <content><![CDATA[1. 为什么要配置自动构建发布？《Hexo配置多个git仓库》一文中已经学习了hexo配置多个git仓库的方法，发布项目也很简单。为什么还需要hexo的自动发布？因为发布流程还可以更简单。而且，现在的发布方式，如果想要多台电脑同时使用，那么每台电脑都需要配置hexo环境，很麻烦。如果本地只负责写写markdown文件，而构建发布都放到云端，是不是更加美好？本文要做的，就是这样一件事。
在gitlab中，CI/CD可以通过配置gitlab-ci.yml来实现。而github，在2019年8月8日也支持内置的CI/CD了。但是本文中，并不是使用github内置的CI/CD，而是使用Travis CI来实现hexo的自动构建发布。
更多关于CI/CD的内容，参考《GitLab CI/CD》


2. 安装配置本地hexo参考《Hexo环境搭建2018年5月版》，安装v10.16.3版本的node，创建hexo项目，然后把需要的依赖写入package.json：
npm install hexo-generator-feed --savenpm install hexo-generator-sitemap --savenpm install hexo-generator-baidu-sitemap --savenpm install hexo-deployer-git --savenpm install hexo-generator-searchdb --save
接下来改造hexo配置，用于Travis CI发布。
1、创建hexo-deploy项目在github创建public项目hexo-deploy，注意一定要是public项目，否则不会触发travis-ci。git clone到本地，然后把hexo项目里的内容剪切到hexo-deploy目录。
2、配置config.yml编辑config.yml，根据自己的需要进行配置。
3、清理文件hexo-deploy目录里只需要保留：
.git.gitignorepackage.json_config.ymlREADME.md

3. 配置travis.yml主要参考使用Travis CI持续部署Hexo博客和將 Hexo 部署到 GitHub Pages。已知 voidking.github.io 和 voidking 是hexo发布后的git仓库（public），hexo-backup是存储markdown文件的git仓库（private）。
1、安装Travis CI插件在github添加Travis CI插件，并且在Applications settings配置Travis CI的权限。
2、生成access token访问github的Personal access tokens页面，Generate new token。Note输入travis，Select scope选择repo，然后点击Generate token。生成了一个token，保存它。
3、添加access token访问Travis CI Pro，找到hexo-deploy项目，Settings，在Environment Variables一栏填入Name为GITHUB_TOKEN，VALUE为github的access token。
3、在hexo-deploy目录下添加.travis.yml文件，内容为：
sudo: falselanguage: node_jsnode_js:  - 10.16.3cache: npmbranches:  only:  - master # build master branch onlyenv:  global:  - GIT_USER: voidking  - HEXO_BACKUP_REPO: github.com/voidking/hexo-backup.git  - HEXO_THEME_REPO: github.com/voidking/hexo-theme-next.git  - GITHUB_PAGES_REPO: github.com/voidking/voidking.github.io.git  - VOIDKING_REPO: github.com/voidking/voidking.gitbefore_install:- export TZ=&#x27;Asia/Shanghai&#x27;- npm install hexo -ginstall:- npm installscript:- git clone https://$&#123;HEXO_THEME_REPO&#125; themes/next- git clone https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;HEXO_BACKUP_REPO&#125; hexo-backup- mv hexo-backup/source .- rm -rf source/private- hexo g# 此处有bug，参考下文继续修改after_success:- git config --global user.name &quot;voidking&quot;- git config --global user.email &quot;voidking@qq.com&quot;- cd ./public- git init- git add .- git commit -m &quot;Travis CI Auto Builder&quot;- git push --force --quiet &quot;https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;GITHUB_PAGES_REPO&#125;&quot; master:master- git push --force --quiet &quot;https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;VOIDKING_REPO&#125;&quot; master:master

4. hexo发布在hexo-deploy项目中，修改README.md（建议添加日期信息），然后提交代码，触发发布。
git add .git commit -m &quot;20200202deploy&quot;git push

然后打开Travis CI - hexo-deploy，即可看到构建发布的日志。如果构建发布出错，根据提示修改发布脚本或者markdown文件即可。
至此，Hexo配置Travis CI自动发布完成！
5. 域名404问题发布完成，出现了一个神奇的问题，www.voidking.com 域名404。查看github pages配置，发现自定义域名变回了 voidking.github.io 。神奇了！莫非，是因为force push刷掉了git仓库的commit？那就保留commit试试。修改.travis.yml的aftersuccess部分：
after_success:- git config --global user.name &quot;voidking&quot;- git config --global user.email &quot;voidking@qq.com&quot;- git clone https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;GITHUB_PAGES_REPO&#125; voidking- unalias cp- cp -rf public/. voidking- cd voidking- git add .- git commit -m &quot;Travis CI Auto Builder&quot;- git push --force --quiet &quot;https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;GITHUB_PAGES_REPO&#125;&quot; master:master- cd ../public- git init- git add .- git commit -m &quot;Travis CI Auto Builder&quot;- git push --force --quiet &quot;https://$&#123;GIT_USER&#125;:$&#123;GITHUB_TOKEN&#125;@$&#123;VOIDKING_REPO&#125;&quot; master:master
保留commit之后，自定义域名果然不会再变化，nice。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>hexo</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>cicd</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
        <tag>travis-ci</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo配置多个git仓库</title>
    <url>/dev-hexo-multi-git/</url>
    <content><![CDATA[1. 吐槽在经历过域名被封之后，今天又经历了搬瓦工服务器IP被封，我太难了。。。o(╥﹏╥)o服务器IP被封，带来了三个问题：第一个是不能愉快地科学上网了；第二个是内网穿透不能使用了；第三个是自己的 www.voidking.com 域名无法访问了。
对于问题一，暂时使用免费的公共飞机场。对于问题二，暂时使用百度服务器，但是因为没有备案，所以不能进行域名解析。对于问题三，就是本文要解决的问题之一。


2. 回到github那么，因为服务器IP被封，域名无法访问怎么办？
方法一：再购买一个新的搬瓦工IP，或者再购买一个新的国外服务器。但是因为不想再花钱（就是这么抠），所以这个方法被否定了。方法二：使用免费的git pages服务，比如github pages、gitlab pages、gitee pages等，并且把域名解析到git pages服务器。这里我选择最开始搭建hexo就使用的github pages，它支持免费的自定义域名和https。
参考《Hexo环境搭建》和《Hexo环境搭建2018年5月版》，配置好github pages服务，并且配置域名的CNAME解析到 voidking.github.io ，开启强制https（github会自动申请ssl证书，点个赞）。
3. 百度收录问题除了访问速度慢一点之外，现在我的域名已经正常了，nice。但是，另外的问题出现了：百度搜索不到我的域名了！
使用百度抓取诊断工具诊断，发现抓取异常信息是拒绝访问，403 Forbidden。这是因为github屏蔽了百度蜘蛛，所以百度无法收录解析到github的域名。
浏览了一些文章，找到了解决办法：在DNSPod配置两个www的域名解析，线路类型“默认”解析到 voidking.github.io ，线路类型“百度”解析到一个百度可以抓取的地址，如下图。也就是说，需要维护两个站点，两个站点分别处理来自不同线路的流量。其中一个站点建立在github，另外一个站点建立在哪儿呢？这里我选择ZEIT Now。
理想是美好的，现实是残酷的！因为无论是github还是zeit，都需要验证域名解析，而它们都是境外服务器，所以都需要线路类型“境外”的CNAME解析，否则无法通过验证！尴尬了。。。只能选择其一。
为了百度的收录，看起来只能选择zeit了，不过zeit每个月只有20G的流量。为了在zeit流量用光后方便地切到github，因此还是需要维护两个站点。
4. hexo配置多个git仓库为了维护两个站点，所以每次更新，需要发布github pages和zeit。github pages的发布很方便，上传代码后直接就发布了；而zeit的发布有多种方式，其中一种方式是通过Deploy Hooks触发。
如果github和zeit都使用 voidking.github.io 这一个git仓库，那么问题很简单。hexo d后，再访问一个url触发 Deploy Hooks就可以了。而zeit，不支持 voidking.github.io 这种项目名称，因此稍微麻烦一点。
1、新建git仓库在github上再创建一个git仓库叫做voidking，import voidking.github.io仓库。
2、配置zeit关联把voidking这个仓库和zeit做关联，并且发布项目，配置生成一个Deploy Hooks。
3、配置域名按照提示绑定 www.voidking.com 这个域名到zeit站点。
4、修改config.yml文件修改hexo的config.yml文件中的deploy部分为：
# githubdeploy:- type: git  repo: https://voidking.com/voidking/voidking.github.io.git  branch: master- type: git  repo: https://voidking.com/voidking/voidking.git  branch: master
以上配置参考Hexo一键发布。
5、发布项目正常执行hexo d，静态资源代码会上传到两个git仓库中。其中，voidking.github.io这个仓库就直接发布了。而voidking这个仓库想要发布到zeit上，还需要手动触发一下（访问Deploy Hooks的url）。
至此，hexo多个git仓库，多个站点配置完成。
5. 后记如果zeit上的流量用完了，可以在DNSPod上修改域名解析到github。虽然有延迟，但是对于个人使用来说足够了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>m3u8视频下载方法</title>
    <url>/hobby-m3u8-download/</url>
    <content><![CDATA[1. m3u8格式M3U是一种播放多媒体列表的文件格式，它的设计初衷是为了播放音频文件，比如MP3，但是越来越多的软件现在用来播放视频文件列表，M3U也可以指定在线流媒体音频源。很多播放器和软件都支持M3U文件格式。
M3U8是Unicode版本的M3U，用UTF-8编码。”M3U”和”M3U8”文件都是苹果公司使用的HTTP Live Streaming格式的基础，这种格式可以在iPhone和Macbook等设备播放。
HTTP Live Streaming（缩写是HLS）是由苹果公司提出基于HTTP的流媒体网络传输协议。是苹果公司QuickTime X和iPhone软件系统的一部分。它的工作原理是把整个流分成一个个小的基于HTTP的文件来下载，每次只下载一些。当媒体流正在播放时，客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源，允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时，客户端会下载一个包含元数据的extended M3U (m3u8) playlist文件，用于寻找可用的媒体流。
综上，m3u8是一种文件格式。下载m3u8格式的文件时，会下载一个m3u8文件（文件列表），以及一个包含了很多ts文件（视频片段）的隐藏文件夹。
更多内容参考M3U和HTTP Live Streaming。


2. 怎样下载m3u8格式视频？2.1. 获取下载地址1、安装chrome插件，Stream Video Downloader。
2、打开一个包含m3u8视频的页面，比如云原生技术的前世今生。点击播放，Stream Video Downloader就可以嗅探到m3u8文件。
3、复制下载地址并保存，此处的下载地址为：https://myun-hw-s3.myun.tv/melj80jz/5a3ydjj0/1551265736611296603.m3u8
2.2. FFmpegFFmpeg是一个非常好用的视频格式转换工具，同时可以用来下载m3u8文件。
1、访问github-FFmpeg或者FFmpeg官网，下载FFmpeg，本文中下载ffmpeg-4.2.2-win64-static.zip。
2、解压重命名文件夹为ffmpeg，进入ffmpeg/bin目录。
3、右键加+Shift，打开Powershell，输入下载合并命令：
.\ffmpeg.exe -i &quot;https://myun-hw-s3.myun.tv/melj80jz/5a3ydjj0/1551265736611296603.m3u8&quot; -c copy 01-云原生技术的前世今生.mp4

耐心等待，下载完成即可在ffmpeg/bin目录中看到下载好的mp4视频文件。
2.3. m3u8转mp4手机QQ浏览器的视频下载功能确实强大，大部分能看到的视频都可以下线，包括云原生技术的前世今生这个页面中的视频。
下载好的文件，是m3u8格式视频。正如上文所说，这个视频文件实际上是一个视频列表和一个隐藏文件夹组成。想要给它合并转换成mp4格式视频，怎么办？同样可以使用FFmpeg。
1、把视频列表和隐藏文件夹都拷贝到PC。
2、使用sublime打开m3u8文件，批量替换路径。替换前为：
#EXTM3U#EXT-X-TARGETDURATION:11#EXTINF:10.000000,file:///storage/emulated/0/QQBrowser/视频/.820a585fc4f1a85d4875007aff5ba7a5/0.ts#EXTINF:10.000000,file:///storage/emulated/0/QQBrowser/视频/.820a585fc4f1a85d4875007aff5ba7a5/1.ts#EXTINF:10.000000,file:///storage/emulated/0/QQBrowser/视频/.820a585fc4f1a85d4875007aff5ba7a5/2.ts#EXTINF:10.000000,file:///storage/emulated/0/QQBrowser/视频/.820a585fc4f1a85d4875007aff5ba7a5/3.ts#EXTINF:10.000000,file:///storage/emulated/0/QQBrowser/视频/.820a585fc4f1a85d4875007aff5ba7a5/4.ts#EXTINF:10.000000,file:///storage/emulated/0/QQBrowser/视频/.820a585fc4f1a85d4875007aff5ba7a5/5.ts
转换后为：
#EXTM3U#EXT-X-TARGETDURATION:11#EXTINF:10.000000,.820a585fc4f1a85d4875007aff5ba7a5/0.ts#EXTINF:10.000000,.820a585fc4f1a85d4875007aff5ba7a5/1.ts#EXTINF:10.000000,.820a585fc4f1a85d4875007aff5ba7a5/2.ts#EXTINF:10.000000,.820a585fc4f1a85d4875007aff5ba7a5/3.ts#EXTINF:10.000000,.820a585fc4f1a85d4875007aff5ba7a5/4.ts#EXTINF:10.000000,.820a585fc4f1a85d4875007aff5ba7a5/5.ts

3、打开Powershell，执行转换命令：
# 直接合并转换（速度快）.\ffmpeg.exe -i input-filename.m3u8 -c copy output-filename.mp4# 合并转码压缩（速度慢）.\ffmpeg.exe -i input-filename.m3u8 output-filename.mp4
执行完成，即可看到转换好的mp4文件。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>chrome</tag>
        <tag>下载</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django开发微信公众平台管理系统——第1章</title>
    <url>/dev-django-wechat-1/</url>
    <content><![CDATA[1. 前言紧接着《Django开发微信公众平台管理系统——第0章》，本文学习一下微信公众平台各种消息的接收和回复，主要参考WeRoBot文档。
消息类型包括：TextMessage、ImageMessage、LinkMessage、LocationMessage、VoiceMessage、VideoMessage、UnknownMessage；事件类型包括：SubscribeEvent、UnSubscribeEvent、ScanEvent、ScanCodePushEvent、ScanCodeWaitMsgEvent、PicSysphotoEvent、PicPhotoOrAlbumEvent、PicWeixinEvent、LocationSelectEvent、ClickEvent、ViewEvent、LocationEvent、TemplateSendJobFinishEvent、UserScanProductEvent、UserScanProductEnterSessionEvent、UserScanProductAsyncEvent、UserScanProductVerifyActionEvent、CardPassCheckEvent、CardNotPassCheckEvent、UserGetCardEvent、UserGiftingCardEvent、UserDelCardEvent、UserConsumeCardEvent、UserPayFromPayCellEvent、UserViewCardEvent、UserEnterSessionFromCardEvent、UpdateMemberCardEvent、CardSkuRemindEvent、CardPayOrderEvent、SubmitMembercardUserInfoEvent、UnknownEvent；回复类型包括：TextReply、ImageReply、VoiceReply、VideoReply、ArticlesReply、MusicReply、TransferCustomerServiceReply、SuccessReply。


2. 原理微信发给 /main/wechat/ 接口的信息（message），都传给了robot对象，robot对象根据消息类型调用自己的handler，处理后返回结果给微信。
3. 实现3.1. 根据消息类型返回信息修改 wecms/main/views_wechat.py ，内容为：
import werobotrobot = werobot.WeRoBot(token=&#x27;vkwechat&#x27;)@robot.handlerdef index(message):    return &#x27;Today is wonderful day!&#x27;@robot.textdef text(message):    return &#x27;您发送了文本消息，内容为：&#x27; + message.content@robot.imagedef image(message):    return &#x27;您发送了图片消息，图片为：&#x27; + message.img@robot.linkdef link(message):    return &#x27;您发送了链接消息，链接为：&#x27; + message.url@robot.locationdef location(message):    return &#x27;您发送了位置消息，位置为：&#x27; + message.label@robot.voicedef voice(message):    return &#x27;您发送了声音消息，media_id为：&#x27; + message.media_id@robot.videodef video(message):    return &#x27;您发送了视频消息，media_id为：&#x27; + message.media_id

此时给公众号发送不同类型的消息，返回的内容也是不同的。
3.2. 返回图片消息1、登录微信公众平台，查看“公众号开发信息”中的“开发者ID(AppID)”和“开发者密码(AppSecret)”，并记录下来。
2、修改 wecms/main/views_wechat.py ，添加AppID和AppSecret的配置，添加media函数：
import werobotfrom werobot.replies import ImageReplyrobot = werobot.WeRoBot(token=&#x27;vkwechat&#x27;)robot.config[&#x27;APP_ID&#x27;] = &#x27;app_id&#x27;robot.config[&#x27;APP_SECRET&#x27;] = &#x27;app_secret&#x27;client = robot.client@robot.handlerdef index(message):    return &#x27;Today is wonderful day!&#x27;@robot.filter(&#x27;image&#x27;)def media(message):    media_id = client.upload_permanent_media(&#x27;image&#x27;, open(r&#x27;C:\Users\haojin\Desktop\favicon.png&#x27;, &#x27;rb&#x27;))[&#x27;media_id&#x27;]    reply = ImageReply(message=message, media_id=media_id)    return reply# other code

需要注意的是，robot.filter(‘image’)需要放在robot.text的前面。因为werobot是链式匹配的，如果robot.text在前，匹配上了“image”，就会被text()函数处理。
2、查看本机的出口IP，修改“公众号开发信息”中的“IP白名单”，把出口IP填进去。
此时给公众号发送“image”，会返回一张图片。
3.3. 事件处理以订阅事件和取消事件为例，添加事件处理：
@robot.subscribedef subscribe(event):    print(&#x27;用户&#x27; + event.source + &#x27;关注了公众号&#x27;)    return &#x27;感谢关注voidking，您的ID为：&#x27; + event.source@robot.unsubscribedef unsubscribe(event):    print(&#x27;用户&#x27; + event.source + &#x27;取消了关注&#x27;)    return &#x27;&#x27;
此时关注公众号，会收到自己的OpenID。
4. 配置文件前面的开发中，用到了token、APP_ID和APP_SECRET。这些信息应该写在配置文件中，而不是代码中，因此需要调整。
1、编辑 wecms/wecms/settings.py，添加：
# Wechat ConfigTOKEN = &#x27;vkwechat&#x27;APP_ID = &#x27;app_id&#x27;APP_SECRET = &#x27;app_secret&#x27;

2、编辑 wecms/main/views_wechat.py ，修改为：
from django.conf import settingsimport werobotfrom werobot.replies import ImageReplyrobot = werobot.WeRoBot(token=settings.TOKEN)robot.config[&#x27;APP_ID&#x27;] = settings.APP_IDrobot.config[&#x27;APP_SECRET&#x27;] = settings.APP_SECRETclient = robot.client# other code

5. 源码分享https://github.com/voidking/wecms/releases/tag/v0.1.0
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>django</tag>
        <tag>微信公众平台</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django开发微信公众平台管理系统——发布篇</title>
    <url>/dev-django-wechat-deploy/</url>
    <content><![CDATA[1. 前言《Django开发微信公众平台管理系统——第0章》一文中完成了wecms项目初始框架的搭建，并且绑定了微信公众号，实现了回复用户的任意消息。但是，wecms项目是运行在本地的，关机后服务就不可用了，因此需要把项目部署到线上。本文中会使用docker搭建django环境，并且部署wecms项目。


2. 安装django1、登录dockerhub查看需要的Django - Docker Official Images。wecms依赖django2.1.7，但是没有对应版本，因此需要采用Plan B，自己安装Django。
2、登录dockerhub查看需要的Python - Docker Official Images。
3、下载python镜像（wecms依赖python3.6.3）docker pull python:3.6.3
4、导出本地依赖信息
pip freeze &gt; requirements.txt
导出后把requirements.txt上传到服务器。
5、编写Dockerfile安装django2.1.7，指定工作目录，添加启动命令
FROM python:3.6.3COPY requirements.txt /tmpRUN pip install --no-cache-dir -i https://pypi.doubanio.com/simple/ -r /tmp/requirements.txtWORKDIR /opt/wecmsEXPOSE 8000CMD [&quot;python&quot;, &quot;manage.py&quot;, &quot;runserver&quot;, &quot;0.0.0.0:8000&quot;]

6、生成wecms镜像并上传
docker build -t voidking/wecms:v1.0 .docker logindocker push voidking/wecms:v1.0

7、下载项目源码
cd /optgit clone https://github.com/voidking/wecms.gitcd wecmsgit checkout v0.0.0

8、启动wecms服务
docker run --name vk-wecms -d \-p 8000:8000 \-v /opt/wecms:/opt/wecms \voidking/wecms:v1.0

以上命令：

命名容器为vk-wecms，后台运行
映射宿主机8000端口到容器的8000端口
挂载宿主机目录/opt/wecms到容器目录/opt/wecms

更多启动命令参数可以参考python - How to use this image和django - How to use this image。
9、验证安装docker ps，nginx启动正常的话就可以看到vk-wecms容器。curl localhost:8000，可以看到welcome to wecms!
浏览器访问 http://hostip:8000/ ，提示You may need to add ‘hostip’ to ALLOWED_HOSTS.因此修改 wecms/wecms/settings.py ，添加：
ALLOWED_HOSTS = [&#x27;*&#x27;]
修改后的tag为v0.0.1。
3. 安装mysql当前项目还没有使用到数据库，而且默认使用sqlite3，不过后期会改成mysql，所以这里做个铺垫。mysql安装配置参考《使用Docker安装配置Mysql》。
4. 域名配置4.1. 域名解析dnspod添加A记录，wecms记录值解析到hostip。
4.2. 配置Nginx创建 /etc/nginx/conf.d/wecms.voidking.com.conf ，内容为：
server &#123;    server_name wecms.voidking.com;     listen 80;    location / &#123;        proxy_set_header Host $host;        proxy_set_header X-Forward-For $remote_addr;        proxy_set_header X-Real-IP $remote_addr;        proxy_pass http://127.0.0.1:8000;     &#125;&#125;
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>django</tag>
        <tag>微信公众平台</tag>
        <tag>好好学Python</tag>
        <tag>dokcer</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django开发微信公众平台管理系统——第0章</title>
    <url>/dev-django-wechat-0/</url>
    <content><![CDATA[1. 前言两年前，学习了Django的开发，整理了一个系列的文档，《Django入门》、《Django开发简单Blog系统》、《Django部署到线上》等。半年前，使用Django和图像处理方法完成了毕业设计。
最近，打算空闲时间打造一下自己的微信公众号，于是安装了微擎系统。但是发现微擎系统存在很多问题，最不能忍受的是添加自动回复，添加完成没有报错，但是数据库根本没有记录！找客服？对不起咱不是付费用户，没人给咱服务。看文档？对不起文档不涉及咱这个具体问题。找社区？对不起社区不活跃，没人遇到过咱这个问题，无人解答。学完微擎框架自己解决？有这个闲工夫，咱就自己写一个了！。。。行吧，拜拜了您哪！
找了一圈，没有找到特别满意的其他微信公众平台管理系统，那就，自己写一个吧！基于Django和WeRoBot，参考文档WeRoBot与其他 Web 框架集成。


2. 开发环境准备1、Python版本3.6.3
2、Django版本2.1.7
3、PyCharm版本2018.1.4(Community Edition)
4、安装WeRoBotpip install werobot
3. 创建项目首先给项目起个好名字吧，万一以后火了呢！就叫wecms吧，没错就是这么随意。
1、创建项目django-admin startproject wecms
2、运行wecms
cd wecmspython manage.py runserver# orpython manage.py runserver 0.0.0.0:8080

3、测试访问浏览器访问 http://localhost:8000/ ，即可看到项目首页。
4. 创建应用1、创建应用前期所有的模块都放在一起，以后再进行拆分优化，因此创建应用名为main。python manage.py startapp main
2、注册应用编辑 wecms/wecms/settings.py，添加：
# Application definitionINSTALLED_APPS = [    &#x27;django.contrib.admin&#x27;,    &#x27;django.contrib.auth&#x27;,    &#x27;django.contrib.contenttypes&#x27;,    &#x27;django.contrib.sessions&#x27;,    &#x27;django.contrib.messages&#x27;,    &#x27;django.contrib.staticfiles&#x27;,    &#x27;main&#x27;,]

3、第一个函数编辑 wecms/main/views.py，如下修改：
from django.shortcuts import renderfrom django.http import HttpResponse# Create your views here.def index(request):    return HttpResponse(&#x27;welcome to wecms!&#x27;)

4、添加路由编辑 wecms/wecms/urls.py，如下修改：
from django.contrib import adminfrom django.urls import path, includeurlpatterns = [    path(&#x27;admin/&#x27;, admin.site.urls),    path(&#x27;&#x27;, include(&#x27;main.urls&#x27;)),    path(&#x27;main/&#x27;, include(&#x27;main.urls&#x27;)),]

创建 wecms/main/urls.py，内容如下：
from django.urls import pathfrom . import viewsurlpatterns = [    path(&#x27;&#x27;,views.index, name=&#x27;index&#x27;),    path(&#x27;index/&#x27;,views.index, name=&#x27;index&#x27;),]

5、启动服务python manage.py runserver
6、测试访问浏览器访问http://127.0.0.1:8000/http://127.0.0.1:8000/main/http://127.0.0.1:8000/main/index/都可以看到welcome to wecms!
5. 引入WeRoBot1、创建 wecms/main/views_wechat.py，内容为：
import werobotrobot = werobot.WeRoBot(token=&#x27;vkwechat&#x27;)@robot.handlerdef index(message):    return &#x27;Today is wonderful day!&#x27;
收到的所有信息返回Today is wonderful day!
2、wecms/main/urls.py修改为：
from django.urls import pathfrom . import viewsfrom . import views_wechatfrom werobot.contrib.django import make_viewurlpatterns = [    path(&#x27;&#x27;,views.index),    path(&#x27;index/&#x27;,views.index),    path(&#x27;wechat/&#x27;,make_view(views_wechat.robot)),]

注意，make_views函数中的参数并不是函数，而是robot对象。
3、测试访问浏览器访问 http://127.0.0.1:8000/main/wechat/
6. 微信配置1、使用内网穿透，把本地8000端口代理到 wecms-dev.voidking.com 域名。
2、测试访问http://wecms-dev.voidking.com/main/wechat/
3、配置微信公众号登录微信公众平台，开发，基本配置，服务器配置，修改配置。填入URL和代码中定义的Token，随机生成EncodingAESKey，消息加解密方式选择明文，提交。提交成功，就完成了微信公众号和服务器的绑定。用户发送给微信公众号的消息，会转发给我们的服务器；服务器处理完成后的信息返回给微信公众号，微信公众号再把消息转发给用户。
4、测试扫码关注自己的公众号，然后发送任意信息，看看返回了啥？Today is wonderful day!
7. 源码分享https://github.com/voidking/wecms/releases/tag/v0.0.0
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>django</tag>
        <tag>微信公众平台</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置Grafana</title>
    <url>/dev-docker-grafana/</url>
    <content><![CDATA[1. Grafana简介
Grafana allows you to query, visualize, alert on and understand your metrics no matter where they are stored.

本文中，我们会使用Docker来安装配置Grafana（已知主机IP为192.168.56.102），并且展示Prometheus中的数据。
参考文档：

《Docker入门》
《使用Docker安装配置Prometheus》



2. 安装Grafana1、下载grafana镜像（以grafana6.7.4为例）
docker pull grafana/grafana:6.7.4

更多版本，可以访问dockerhub - grafana获取，比如8.5.4。
2、启动grafana服务
docker run --name=grafana -d \    -p 3000:3000 \    grafana/grafana:6.7.4

以上命令：

命名容器为grafana，后台运行
映射宿主机3000端口到容器3000端口

grafana的配置文件为 /etc/grafana/grafana.ini ，可以进入容器进行修改，或者挂出到宿主机。
更多启动命令，参考文档How to use the container。
4、验证安装
docker ps
mysql启动正常的话就可以看到grafana容器。如果启动失败，可以使用docker logs grafana查看失败原因并解决。
3. 开放端口firewall-cmd --add-port=3000/tcp --permanentsystemctl reload firewalld# 或者systemctl stop firewalld

4. 访问Grafana1、本机测试
curl localhost:3000

2、浏览器访问访问 http://192.168.56.102:3000用户名密码默认都是admin，第一次登录会提示修改。
5. 配置Prometheus数据5.1. 添加数据资源参考《使用Docker安装配置Prometheus》，安装配置好了prometheus。
访问Grafana，添加数据源。
5.2. 配置Prometheus数据Name填入 Prometheus ，URL填入 http://192.168.56.102:9090，其他不用变。Save&amp;Test。
5.3. 配置Dashboard1、添加自带Dashboard点击Dashboards，点击三个Import，引入三个Dashboard。
2、添加常用Dashboard点击Import，填入Dashboard的ID，导入Dashboard。比如Node Exporter的一个Dashboard为 https://grafana.com/grafana/dashboards/405 ，那么填入该URL，点击Load，就可以下载该Dashboard。选择Folder，选择Prometheus数据源，Import。
推荐Dashboard：

405：用于查看node-exporter数据
1860：用于查看node-exporter数据
3125：用于查看cAdvisor数据

https://grafana.com/grafana/dashboards/405https://grafana.com/grafana/dashboards/1860https://grafana.com/grafana/dashboards/3125

5.4. 查看Dashboard左上角HOME，出现下拉框，即可选择dashboard。
查看dashboard时，记得要选择一下右上角的时间范围，否则可能因为采集时间过短看不到监控曲线。
5.5. 自定义Dashboard1860 这个 dashboard，同一时间只能显示一台主机的监控曲线，因为不同主机的数据源是不同的。因此，如果想要在一个图表中同时展示多个主机的数据进行比较，就需要自行配置dashboard，创建一个整合数据的数据源变量。详情参考文档用Grafana Variables变量配置快速切换不同主机的图表数据展示
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>prometheus</tag>
        <tag>监控</tag>
        <tag>grafana</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置Prometheus</title>
    <url>/dev-docker-prometheus/</url>
    <content><![CDATA[1. 前言本文中，我们会使用Docker安装配置Prometheus核心组件，实现一个完整的监控告警系统。具体的组件包括：Prometheus Server、Exporters、PushGateway、Alertmanager、Grafana
参考文档：

《Prometheus入门篇》
Prometheus官方文档
Prometheus - GETTING STARTED
Prometheus下载
Prometheus - CONFIGURATION
Prometheus实战 - Prometheus配置
Prometheus监控系统之入门篇



2. 安装配置Prometheus Server1、下载prometheus镜像（以v2.45.6为例）
docker pull prom/prometheus:v2.45.6

更多版本镜像，访问dockerhub - prometheus获取。
2、拷贝配置文件
mkdir -p /opt/prometheus/&#123;conf,data&#125;docker run --name tmp -d prom/prometheus:v2.45.6docker cp tmp:/etc/prometheus/prometheus.yml /opt/prometheus/conf/docker rm tmp -f

3、编辑配置文件
mkdir -p /opt/prometheus/conf/targetstouch /opt/prometheus/conf/targets/targets.ymlvim /opt/prometheus/conf/prometheus.ymlvim /opt/prometheus/conf/targets/targets.yml

配置方法参考文档《Prometheus服务发现》中的【文件服务发现】一节。
4、启动prometheus
docker run --name=prometheus -d \  -u root \  -p 9090:9090 \  -v /opt/prometheus/conf/prometheus.yml:/etc/prometheus/prometheus.yml \  -v /opt/prometheus/conf/targets:/etc/prometheus/targets \  -v /opt/prometheus/conf/rules:/etc/prometheus/rules \  -v /opt/prometheus/data:/data/prometheus \  -v /etc/localtime:/etc/localtime \  prom/prometheus:v2.45.6 \  --config.file=/etc/prometheus/prometheus.yml \  --web.enable-lifecycle \  --storage.tsdb.path=/data/prometheus \  --storage.tsdb.retention.time=30d \  --web.enable-remote-write-receiver

5、查看prometheus状态
docker logs prometheus

3. 访问Prometheus Server1、访问Prometheus浏览器访问 http://192.168.56.102:9090/
2、查看targets浏览器访问 http://192.168.56.102:9090/targets/
3、查看指标浏览器访问 http://192.168.56.102:9090/metrics/
4. 安装Node Exporter4.1. Docker安装Node Exporter参考文档：github - prometheus/node_exporter
1、下载node-exporter镜像（以v1.6.0为例）
docker pull prom/node-exporter:v1.6.0

更多版本镜像，访问dockerhub - node exporter获取。
2、启动node exporter
docker run --name=node-exporter -d \  -p 9100:9100 \  --net=&quot;host&quot; \  --pid=&quot;host&quot; \  -v &quot;/:/host:ro,rslave&quot; \  prom/node-exporter:v1.6.0 \  --path.rootfs=/host

3、访问node exporter
curl http://localhost:9100/metrics

4.2. Bin安装Node Exporterbin安装node exporter的优势是适用于所有主机，不管主机上有没有安装docker。
1、安装node exporter
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.0/node_exporter-1.6.0.linux-amd64.tar.gztar -xzvf node_exporter-1.6.0.linux-amd64.tar.gzmv node_exporter-1.6.0.linux-amd64/node_exporter /usr/local/bin

2、测试启动node exporter
node_exporter

3、配置systemd管理
cat &lt;&lt;EOF &gt; /etc/systemd/system/node_exporter.service [Unit]Description=Node Exporter[Service]User=rootExecStart=/usr/local/bin/node_exporterRestart=always[Install]WantedBy=multi-user.targetEOF

4、启动node exporter
systemctl start node_exportersystemctl status node_exportersystemctl enable node_exporter

5、访问node exporter
curl http://localhost:9100/metrics

5. Promserver采集Node Exporter数据1、修改targets.yml （假设主机IP为 192.168.56.102）
- targets: [&#x27;192.168.56.102:9100&#x27;]  labels:    group: &#x27;node-exporter&#x27;

2、查看targets浏览器访问 http://192.168.56.102:9090/targets/
6. 安装PushGateway1、登录dockerhub查看需要的pushgateway
2、下载pushgateway镜像（以v1.1.0为例）
docker pull prom/pushgateway:v1.1.0

3、启动pushgateway
docker run --name=pushgateway -d \-p 9091:9091 \prom/pushgateway:v1.1.0

4、测试服务浏览器访问 http://192.168.56.102:9091/#
5、推送数据给pushgateway
echo &quot;exam_metric 100&quot; | curl --data-binary @- http://127.0.0.1:9091/metrics/job/examcat &lt;&lt;EOF | curl --data-binary @- http://127.0.0.1:9091/metrics/job/exam/instance/testchinese 120math 150english 140EOF


7. Promserver采集PushGateway数据1、修改targets.yml文件
- targets: [&#x27;192.168.56.102:9091&#x27;]  labels:    group: &#x27;pushgateway&#x27;

2、在promserver查看数据
8. 安装Alertmanager1、登录dockerhub查看需要的alertmanager
2、下载alertmanager镜像（以v0.23.0为例）
docker pull prom/alertmanager:v0.23.0

v0.15.0以下版本只支持/api/v1，不支持/api/v2
3、创建配置文件（建议和prometheus.yml放在一起）
docker run --name tmp -d prom/alertmanager:v0.23.0docker cp tmp:/etc/alertmanager/alertmanager.yml /opt/prometheus/conf/alertmanager.ymldocker rm tmp -f

alertmanager.yml 默认配置为：
global:  resolve_timeout: 5mroute:  group_by: [&#x27;alertname&#x27;]  group_wait: 10s  group_interval: 10s  repeat_interval: 1h  receiver: &#x27;web.hook&#x27;receivers:- name: &#x27;web.hook&#x27;  webhook_configs:  - url: &#x27;http://127.0.0.1:5001/&#x27;inhibit_rules:  - source_match:      severity: &#x27;critical&#x27;    target_match:      severity: &#x27;warning&#x27;    equal: [&#x27;alertname&#x27;, &#x27;dev&#x27;, &#x27;instance&#x27;]

修改 alertmanager.yml 为：
global:  resolve_timeout: 5mroute:  group_by: [&#x27;exam&#x27;]  #与prometheus告警规则配置的groupname对应  group_wait: 10s #报警等待时间  group_interval: 10s #报警间隔时间  repeat_interval: 1m #重复报警间隔时间  receiver: &#x27;web.hook&#x27; #告警处理方式，我们这里通过web.hook方式，也可以配置成邮件等方式receivers:  - name: &#x27;web.hook&#x27;    webhook_configs:      - url: &#x27;http://192.168.56.102:8080/exam/test&#x27; #告警webhook地址，告警信息会post到该地址，需要编写服务接收该告警数据inhibit_rules:  - source_match:      severity: &#x27;critical&#x27;    target_match:      severity: &#x27;warning&#x27; #目标告警状态    equal: [&#x27;alertname&#x27;, &#x27;dev&#x27;, &#x27;instance&#x27;]

4、启动alertmanager
docker run --name=alertmanager -d \  -p 9093:9093 \  -v /opt/prometheus/conf/alertmanager.yml:/etc/alertmanager/alertmanager.yml \  prom/alertmanager:v0.23.0 \  --config.file=/etc/alertmanager/alertmanager.yml \  --storage.path=/alertmanager

5、测试服务浏览器访问 http://192.168.56.102:9093/
9. 配置告警1、编辑prometheus.yml配置文件，添加alertmanager配置和报警规则配置
global:  scrape_interval: 15s #默认采集监控数据时间间隔scrape_configs:  - job_name: &#x27;file_sd_targets&#x27;    file_sd_configs:    - files:      - &#x27;/etc/prometheus/targets/*.yml&#x27;alerting:   #告警管理器设置  alertmanagers:    - static_configs:      - targets: [&#x27;192.168.56.102:9093&#x27;] #告警信息会发送给alertmanager进一步处理rule_files:  - /etc/prometheus/rules/*.rules   #告警规则文件路径，对应宿主机 /opt/prometheus/conf/rules/*.rules

2、设置报警规则
vim /opt/prometheus/conf/rules/exam.rules

exam.rules内容为：
groups:  - name: exam    rules:      - alert: exam告警测试        expr: chinese &gt; 100 #语文成绩大于100告警，注：我们在pushgateway推送数据的时候，设置的是120，因此会触发告警        for: 1m        labels:          status: warning        annotations:          summary: &quot;&#123;&#123;$labels.instance&#125;&#125;:语文成绩优秀! nb了!!!&quot;          description: &quot;&#123;&#123;$labels.instance&#125;&#125;: 语文成绩优秀! nbnbnb!!!&quot;

exam.rules 也可以先配置为空。
3、重新加载配置或者重启promserver
4、查看告警在prometheus alerts页面查看告警
或者在alertmanager alerts页面查看告警
10. 告警通知处理alertmanager.yml文件中配置的告警方式是webhook，告警发送到 http://192.168.56.102:8080/exam/test ，因此，我们需要一个服务来接收处理这个告警。
1、安装golang环境，参考《CentOS7部署beego项目》golang安装一节
2、创建main.go
package mainimport (  &quot;fmt&quot;  &quot;io/ioutil&quot;  &quot;github.com/gin-gonic/gin&quot;)func main() &#123;  r := gin.Default()  r.POST(&quot;/exam/test&quot;, func(c *gin.Context) &#123;    res, _ := ioutil.ReadAll(c.Request.Body)    fmt.Println(string(res)) //这里我们只简单打印告警信息    c.JSON(200, gin.H&#123;      &quot;message&quot;: &quot;alert message&quot;,    &#125;)  &#125;)  r.Run(&quot;:8080&quot;)&#125;

3、运行代码
go get github.com/gin-gonic/gingo run main.go
如上图，服务接收到了告警信息，并且打印了告警信息。
11. GrafanaGrafana常被用来展示Prometheus的数据，详情参考《使用Docker安装配置Grafana》。
12. 后记以上，配置完成了Prometheus Server、Exporters、PushGateway、Alertmanager、Grafana，跑通了数据的收集和显示，测试了告警信息的收集，nice。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>prometheus</tag>
        <tag>监控</tag>
      </tags>
  </entry>
  <entry>
    <title>Grafana和Prometheus问题记录</title>
    <url>/dev-grafana-prometheus-problem/</url>
    <content><![CDATA[1. 前言本文记录使用Grafana和Prometheus时遇到的一些问题，备忘。


2. Grafana Dashboard相关问题2.1. 常用的Dashboard问：Grafana常用的Dashboard有哪些？
答：

node exporter：1860
cAdvisor：3125
dcgm exporter：12239

更多的Dashboard，可以到网站Grafana Dashboards上查找。
2.2. 数据源和Dashboard问：一个prometheus数据源中，会包含不同类型exporter采集的metrics数据，grafana中的一个dashboard怎样知道哪些数据是自己应该展示的？
答：和exporter无关，grafana只关心metrics，grafana根据指标名称和标签过滤自己需要的数据。
2.3. 复制Dashboard后，怎样更换数据源？问：Dashboard的设置中，选择Save As保存到另外一个文件夹，但是数据源并不会发生变化。如果想要替换数据源，应该怎样配置？
答：编辑JSON Model，查找uid对应的value，全部替换成新的数据源uid。
数据源uid获取方法：打开配置，数据源配置，点击数据源进入编辑页面，此时浏览器地址栏 /edit/ 后面的就是数据源的uid。
2.4. 怎样拷贝Dashboard配置？问：如果已经有了一个Dashboard B，想要拷贝另一个Dashboard A的配置，该怎样操作？
答：拷贝Dashboard A 的 JSON Model 中的 panels 和 templating 部分，粘贴到Dashboard B的JSON Model，保存即可。
2.5. Node Exporter Full 显示问题问：Node Exporter Full 中，当选择的时间范围小于24小时的时候，CPU Basic和Network Traffic Basic的内容为空，该怎么解决？
答：修改Grafana数据源抓取时间间隔为30s或者1m。
Grafana中默认的数据源抓取数据时间间隔是15s，而Prometheus中抓取数据时间间隔可能是1m，这时就会出现问题。
参考文档：No data available when interval is less than 24 hours
2.6. Docker monitoring 显示问题问：Docker monitoring - alicek106 revision中，一个容器的 Network Rx 和 Network Tx 对应两条线，是怎么回事？
答：正常现象，说明该容器存在多个网卡。
查看query语句
irate(container_network_receive_bytes_total&#123;image!=&quot;&quot;, instance=~&quot;$server&quot;, name=~&quot;$container&quot;&#125;[5m])irate(container_network_transmit_bytes_total&#123;image!=&quot;&quot;, instance=~&quot;$server&quot;, name=~&quot;$container&quot;&#125;[5m])

在explore页面使用PromQL查询，可以发现指定容器查询可以查询出两个结果，分别有 eth0 和 tunl0 两张卡。
container_network_receive_bytes_total&#123;id=&quot;/system.slice/docker-cff8168986d12d8399fde55d3e65a056af56bcf6e67187d926a9d974b4a2c1c2.scope&quot;, image=&quot;gcr.dockerproxy.com/cadvisor/cadvisor:v0.47.1&quot;, instance=&quot;192.168.56.101:9101&quot;, interface=&quot;eth0&quot;, job=&quot;cadvisor&quot;, name=&quot;cadvisor&quot;&#125;container_network_receive_bytes_total&#123;id=&quot;/system.slice/docker-cff8168986d12d8399fde55d3e65a056af56bcf6e67187d926a9d974b4a2c1c2.scope&quot;, image=&quot;gcr.dockerproxy.com/cadvisor/cadvisor:v0.47.1&quot;, instance=&quot;192.168.56.101:9101&quot;, interface=&quot;tunl0&quot;, job=&quot;cadvisor&quot;, name=&quot;cadvisor&quot;&#125;

比较好的处理方式：修改Options中的Legend，修改为 &#123;&#123;name&#125;&#125;-&#123;&#123;interface&#125;&#125; ，这样图表就清晰了。
3. Grafana Variables相关问题3.1. label_values函数问：Grafana的Dashboard中，可以创建Variables用来筛选数据，其中Query经常使用 label_values ，这个函数是Grafana自带的，还是PromQL自带的？
答：Grafana自带的，PromQL中用不了。
label_values(node_uname_info, job)详解：

label_values() 函数用于从指定指标中提取指定标签的唯一值列表。
node_uname_info 是一个指标名称，代表了包含有关节点操作系统和内核的信息的指标。
job 是要提取的标签名称，这里表示作业或服务的标识。

label_values的一个典型应用：在cAdvisor的3125的dashboard中增加一个根据容器名称的二级筛选。
3125的dashboard，默认有一个变量为server，能够根据主机筛选出主机上的所有容器。但是容器时不方便观察，最好能够根据容器名称再进行一层筛选。
1、新增一个变量Settings，Variables，New
2、配置变量

Name：container
Type：Query
Data source：prometheus
Refresh：On dashboard load
Query：label_values(container_cpu_usage_seconds_total&#123;instance=&quot;$server&quot;&#125;,name)

3、调整PromQL配置
rate(container_cpu_user_seconds_total&#123;image!=&quot;&quot;, instance=~&quot;$server&quot;, name=~&quot;$container&quot;&#125;[5m]) * 100

3.2. Regex的作用是什么？问：配置Variables时，Query Options中有一个Regex，它的作用是什么？
答：使用Query中的表达式筛选出的数据，可能不符合预期，这时可以使用Regex对这些数据进一步过滤。
3.3. 怎样允许多选？问：在dashboard页面，想要选中多个主机，该怎样配置？
答：修改Variables配置，启用 Selection options 中的 Multi-value
3.4. 怎样显示所有选项？问：在dashboard页面，通过变量筛选一个主机后，想要返回查看全部的主机，但是并没有这个选项，该怎样配置？
答：修改Variables配置，启用 Selection options 中的 Include All option， Custom all value 设置一个值。Custom all value 一般设置为 blank = auto，能够根据用户的选择动态地显示或隐藏”全部”选项。Custom all value 也会经常设置 .+ ，以便dashboard中的PromQL能否匹配到所有的主机。
3.5. 怎样创建文本Panel？问：怎样创建文本Panel，以便给Dashboard添加一些说明？
答：创建Panel，然后点击右上角，选择Visualizations类型为Text（默认为Time series）。
3.6. Docker monitoring 显示问题问：Docker monitoring - alicek106 revision中，看不到数据，但是通过PromQL是可以查询到 container 开头的指标的。
答：这是因为Dashboard中配置的Variables Query和真实指标不匹配，没有获取到instance。解决办法：Query替换为 label_values(container_cpu_usage_seconds_total&#123;instance=&quot;$server&quot;&#125;,name)
4. Prometheus相关问题4.1. Dashboard变量和Panel缺失数据问题问：Docker monitoring - alicek106 revision中，变量和Panel都缺失数据，该怎么解决？
答：通过kubernetes-cadvisor获取到的指标，是经过过滤的，可能导致数据缺失。确认方法：访问Prometheus Web，查询变量和Panel中用到的指标。
如果确认是指标缺失，那么就需要修改ServiceMonitor的定义，开放获取更多指标。
kubectl edit servicemonitor prometheus-kube-prometheus-kubelet -n prometheus

参考无法显示数据的Panel的表达式，修改 metricRelabelings.regex 部分，例如针对3125 Dashboard，需要如下修改：
spec:  endpoints:  - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token    metricRelabelings:    - action: keep      regex: container_last_seen|machine_memory_bytes|container_cpu_user_seconds_total|container_cpu_usage_seconds_total|container_memory_usage_bytes|container_memory_cache|container_network_.+_bytes_total|container_memory_working_set_bytes|container_cpu_cfs_.*periods_total|container_processes.*|container_threads.*      sourceLabels:      - __name__    path: /metrics/cadvisor    port: https-metrics    ...

和默认配置相比，regex中添加了 container_last_seen|machine_memory_bytes|container_cpu_user_seconds_total|
4.2. 缺失文件句柄指标问题prometheus套装中的node-exporter，默认情况下是缺少文件句柄指标的，需要自行开启，开启方法同【Dashboard变量和Panel缺失数据问题】。
kubectl edit servicemonitor node-exporter -n -n prometheus

在regex中新增 node_filefd_(maximum|allocated) ，过几分钟，在prometheus web页面就可以查询到文件句柄指标了。
grafana dashboard中，新增panel，表达式为：
node_filefd_allocated&#123;instance=&quot;$node&quot;&#125;

4.3. 部分cAdvisor Endpoind DOWN问题问：Docker monitoring - alicek106 revision中，缺少了一些Endpiont，通过Prometheus Targets页面查看，发现缺失的Endpoint处于 DOWN 状态，该怎样解决？
答：同样的配置，部分正常，部分DOWN，大概率是超时问题。建议interval改成30s，timeout或scrapeTimeout改成5s。
kubectl edit servicemonitor prometheus-kube-prometheus-kubelet -n prometheus

4.4. 部分cAdvisor Endpoint缺label问题问：K8S集群中，使用Prometheus套装获取Kubelet cAdvisor指标。部分节点可以正常获取到指标和label，但是另外一部分节点获取指标正常，缺少label。例如指标 container_cpu_user_seconds_total 中没有label name ，该怎样解决？
答：重启有问题节点的 kubelet。
systemctl restart kubelet

4.5. node-exporter主机名和IP问题1问：K8S中的Prometheus，获取集群外部的 node-exporter 指标，instance 的 value 默认配置为 IP:PORT，怎样把 instance 的 value 改成 IP？
答：通过relabel_configs实现。
- job_name: &#x27;node-exporter&#x27;  static_configs:    - targets:        - 192.168.56.101:9100        - 192.168.50.102:9100      labels:        job: node-exporter  relabel_configs:    - action: replace      regex: (.*):.*      replacement: $1      source_labels:      - __address__      target_label: instance

具体修改方法，参考文档《主机、Docker和K8S统一配置监控告警方案》中的【kube-prometheus监控集群外部服务】部分。
4.6. node-exporter主机名和IP问题2问：K8S中的Prometheus， node-exporter servicemonitor 把 instance 的 value 配置为 主机名，怎样把 instance 的 value 改成 IP|主机名？
答：instance的value，是通过relabelings实现的赋值，需要修改配置。
kubectl edit servicemonitor node-exporter -n prometheus

原配置为：
spec:  endpoints:  - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token    interval: 1m    metricRelabelings: &#123;&#125;    relabelings:    - action: replace      regex: (.*)      replacement: $1      sourceLabels:      - __meta_kubernetes_pod_node_name      targetLabel: instance    - action: labeldrop      regex: (service|endpoint)

修改为：
spec:  endpoints:  - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token    interval: 1m    metricRelabelings: &#123;&#125;    relabelings:    - action: replace      regex: (.*):.*\|(.*)$      replacement: $1|$2      separator: &#x27;|&#x27;      sourceLabels:      - __address__      - __meta_kubernetes_endpoint_node_name      #- __meta_kubernetes_pod_node_name      targetLabel: instance    - action: labeldrop      regex: (service|endpoint)

该配置中，首先选择了两个source label（IP和主机名）作为数据来源，然后用|分隔使两个label value的字符串合并成一个字符串，接着使用正则匹配这个字符串，匹配到的$1和$2，拼接为$1|$2作为最终的label value。
该方法同样适用于cadvisor，__meta_kubernetes_endpoint_node_name 需要对应修改为 __meta_kubernetes_endpoint_address_target_name。
其他的exporter也是同理修改，主机名对应的指标名称可能不同。
主机名对应的指标名称，可以通过prometheus targets页面查看before relabeling获取到，也可以通过接口获取到。
curl http://prometheus-server:9090/api/v1/targets | jq &gt; targets.jsoncat targets.json | jq &#x27;.data.activeTargets[]|select(.labels.job==&quot;kubelet&quot;)&#x27;

参考文档：

Prometheus HTTP API Targets
《jq命令的安装使用》

5. serviceMonitor没有生效问题问：修改serviceMonitor中instance的值之后，发现没有生效，该怎么处理？
答：大概率是因为配置错误，查看prometheus日志进行修改。如果没有及时修改，还可能导致prometheus无法启动。
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
        <tag>问题排查</tag>
        <tag>grafana</tag>
      </tags>
  </entry>
  <entry>
    <title>Promtheus配置告警规则</title>
    <url>/dev-prometheus-alerting-rules/</url>
    <content><![CDATA[1. 前言本文中，我们学习配置Prometheus的告警规则。
参考文档：

《Prometheus入门篇》
Prometheus Alertmanager
《Prometheus AlertManager WebHook通知到到钉钉群》



2. Prometheus告警流程2.1. 1. 定义告警规则告警规则是通过PromQL表达式来定义的,当表达式查询结果满足一定条件时，就会触发告警。告警规则通常包括以下几个部分:

告警名称
PromQL表达式 
持续时间(duration)
标签(labels)
注解(annotations)

告警规则可以定义在Prometheus的配置文件中，也可以通过API动态添加。
2.2. 2. Prometheus Server触发告警Prometheus Server会定期(默认每分钟)评估所有的告警规则，如果某个规则的PromQL表达式在持续时间内一直满足，就会触发相应的告警。
2.3. 3. 告警发送到AlertmanagerPrometheus Server将触发的告警发送到Alertmanager组件。Alertmanager负责对告警进行去重、分组、路由等处理。
2.4. 4. Alertmanager发送告警通知Alertmanager根据配置的路由规则，将告警发送到指定的接收器，如邮件、PagerDuty、Slack、Webhook等。
2.5. 5. 消息中转服务如果Alertmanager把消息发送到了Webhook（消息中转服务），那么消息中转服务会再次进行消息转发，转发给微信、钉钉、飞书等平台。
3. 配置告警规则3.1. 配置告警规则概述从上一节【Prometheus告警流程】中我们可以得知，Prometheus的告警流程跑通后，后续增删改告警，实际上就是配置告警规则。配置告警规则的一般流程为：1、准备告警语句2、Prometheus添加告警规则3、模拟告警
3.2. 准备告警语句准备告警语句有两种方法：一种是从网上获取成熟的告警语句；另一种是自己调试告警语句。
自己调试告警语句的方法如下：
1、查看监控指标访问Grafana Dashboard，点击一个panel的Edit，可以看到panel使用的变量，以及变量的PromQL计算方法。
2、编写PromQL语句访问Grafana Explore页面，左上角数据源选择Prometheus。参考panel，选择合适的指标，编写PromQL，计算出自己想要用来报警的指标  
以CPU使用率为例：
100 - (avg by (environment,instance) (irate(node_cpu_seconds_total&#123;job=&quot;node-exporter&quot;,mode=&quot;idle&quot;&#125;[5m])))  * 100

以内存使用率为例：
100 - ((node_memory_MemAvailable_bytes * 100) / node_memory_MemTotal_bytes)

3.3. 配置告警假设规则文件路径为 /opt/prometheus/conf/rules
1、创建告警规则
vim /opt/prometheus/conf/rules/host.rules

host.rules内容为：
groups:- name: hostStatsAlert  rules:  - alert: hostCpuUsageAlert    expr: 100 - (avg by (environment,instance) (irate(node_cpu_seconds_total&#123;job=&quot;node-exporter&quot;,mode=&quot;idle&quot;&#125;[5m])))  * 100 &gt; 85    for: 1m    labels:      severity: page    annotations:      summary: &quot;Instance &#123;&#123; $labels.instance &#125;&#125; CPU usgae high&quot;      description: &quot;&#123;&#123; $labels.instance &#125;&#125; CPU usage above 85% (current value: &#123;&#123; $value &#125;&#125;)&quot;  - alert: hostMemUsageAlert    expr: 100 - ((node_memory_MemAvailable_bytes * 100) / node_memory_MemTotal_bytes) &gt; 85    for: 1m    labels:      severity: page    annotations:      summary: &quot;Instance &#123;&#123; $labels.instance &#125;&#125; MEM usgae high&quot;      description: &quot;&#123;&#123; $labels.instance &#125;&#125; MEM usage above 85% (current value: &#123;&#123; $value &#125;&#125;)&quot;

告警规则里的expr，是根据【准备告警语句】一节中获取到的PromQL语句改写的。
2、修改prometheus server配置prometheus.yml 修改为：
alerting:  alertmanagers:    - static_configs:      - targets: [&#x27;192.168.56.101:9093&#x27;] #告警信息会发送给alertmanagerrule_files:  - /etc/prometheus/rules/*.rules   #告警规则文件路径，对应宿主机 /opt/prometheus/rules/*.rules

3、重载或者重启prometheus server
curl -X POST http://localhost:9090/-/reloaddocker restart prometheus

3.4. 模拟告警1、模拟CPU使用率升高
yum install stress-ngstress-ng --cpu 7 --verbose --timeout 2m

2、查看告警触发情况http://192.168.56.101:9090/alertshttp://192.168.56.101:9093/#/alerts
3、查看群能否收到告警
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
      </tags>
  </entry>
  <entry>
    <title>Prometheus中标签（Labels）详解</title>
    <url>/dev-prometheus-labels/</url>
    <content><![CDATA[1. 前言Prometheus中，数据模型的核心是标签（Labels）。标签允许我们对指标数据进行维度化和分类，从而更好地理解系统的运行状态和性能。
本文将学习 Prometheus 中标签的基础知识，以及如何使用标签来优化指标的管理和查询。
参考文档：

服务发现与Relabel
prometheus label标签的使用
relabel_config



2. 标签是什么？标签是 Prometheus 数据模型中的重要组成部分，它是一个键值对，用于描述指标的维度和属性。每个样本数据都附带一组标签，这样我们可以将数据划分为不同的维度，从而更好地分析和查询数据。例如，对于 CPU 使用率指标，我们可以使用 instance 标签来表示不同的主机，使用 job 标签来表示不同的任务。
标签的作用包括：

数据分类和分组： 标签允许我们将数据按照不同的维度进行分类和分组，从而在查询和分析时更精确地选择数据。
查询和过滤： 标签使我们能够在查询时按照特定维度进行过滤，从而获取我们所需的数据。
聚合和计算： 标签使我们能够对数据进行聚合和计算，生成更高级别的指标，如平均值、总和等。

3. 使用标签3.1. 添加标签scrape_configs:  - job_name: &#x27;example-job&#x27;    static_configs:      - targets: [&#x27;example-host:9090&#x27;]        labels:          app: &#x27;example-app&#x27;          environment: &#x27;production&#x27;

在上述示例中，我们通过 labels 字段添加了两个标签：app 和 environment。这样，所有从 example-host:9090 采集到的指标都会带有这两个标签，从而更好地描述数据的来源和环境。
3.2. 查询和分组在 PromQL 查询中，我们可以使用标签进行数据的选择、分组和聚合。例如，我们可以使用以下查询来获取特定应用的请求计数：
http_requests_total&#123;app=&quot;example-app&quot;&#125;

4. relabel_configs是什么？relabel_configs 是Prometheus中的一个关键配置项，它允许我们在数据采集之前对指标的标签进行操作。relabel_configs 允许我们对标签进行添加、删除、重命名、合并，以及对标签值进行提取和替换等操作，以满足不同场景下的需求。
5. 使用relabel_configs5.1. relabel_configs 的配置格式在 Prometheus 的配置文件中，我们可以在 scrape_config（用于数据采集配置）或者 service_monitor（用于服务监控配置）中使用 relabel_configs。下面是一个示例配置：
scrape_configs:  - job_name: &#x27;example-job&#x27;    static_configs:      - targets: [&#x27;example-host:9090&#x27;]    relabel_configs:      - source_labels: [source_label]        target_label: target_label        action: [keep|drop|replace|labelmap]        regex: &#x27;regex_pattern&#x27;        replacement: &#x27;replacement_string&#x27;

在上面的配置中，我们可以配置以下属性：

source_labels：源标签，用于指定操作的源标签。
target_label：目标标签，用于存储操作的结果。
action：操作类型，可以是 keep（保留）、drop（删除）、replace（替换）和 labelmap（从标签名中提取）。
regex：正则表达式，用于匹配源标签的值。
replacement：在替换操作中使用的字符串。

5.2. relabel_configs 常见操作scrape_configs:  - job_name: &#x27;example-app&#x27;    metrics_path: /metrics    static_configs:      - targets: [&#x27;example-app:8080&#x27;]    relabel_configs:      # 添加标签      - source_labels: []        target_label: added_label        replacement: &#x27;new_value&#x27;            # 重命名标签      - source_labels: [old_label]        target_label: new_label            # 删除标签      - action: labeldrop        regex: label_to_remove              # 多值标签的操作 - 合并多个标签值为一个      - action: replace        source_labels: [label1, label2]        target_label: merged_labels        separator: &#x27;;&#x27;            # 使用正则表达式从标签值中提取信息      - source_labels: [instance]        target_label: extracted_info        regex: ^(.*):8080$        replacement: $1            # 对标签值进行保留      - action: keep        source_labels: [important_label]            # 替换标签值      - source_labels: [instance]        target_label: instance        regex: (.*).example-app.*$        replacement: $1

5.3. relabel_configs示例场景： 我们有一个 Kubernetes 集群中运行的微服务架构，每个服务都暴露了 /metrics 端点。我们想要为每个服务添加一个标签，以标识不同的应用。
步骤：1、在 Prometheus 的 scrape_configs 中，配置每个服务的 job_name 和 targets。2、使用 relabel_configs 来为每个服务添加一个标签，如 app_name。
以下是一个示例的配置：
scrape_configs:  - job_name: &#x27;service-metrics&#x27;    static_configs:      - targets: [&#x27;service-1:8080&#x27;, &#x27;service-2:8080&#x27;, &#x27;service-3:8080&#x27;]    relabel_configs:      - source_labels: [__meta_kubernetes_service_label_app]        target_label: app_name

在上述配置中，我们使用 relabel_configs 将 Kubernetes 服务标签 __meta_kubernetes_service_label_app 的值赋给了 app_name 标签。
6. relabelings是什么？relabelings是Kubernetes servicemonitor中专用配置项，配置方式和 relabel_configs 相同。relabelings 和 relabel_configs 有什么区别？名字不同，子项的大小写不同，其余没有区别。
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
      </tags>
  </entry>
  <entry>
    <title>Prometheus服务发现</title>
    <url>/dev-prometheus-service-discovery/</url>
    <content><![CDATA[1. 什么是Prometheus服务发现？在一个复杂的分布式系统中，监控目标可能会频繁变化。虚拟机、容器、云服务等的启动、停止或扩缩容，都会导致监控目标的变化。如果靠手动配置来添加或删除这些目标，将会非常繁琐且容易出错。服务发现机制通过自动识别和注册这些目标，大大简化了这一过程。
Prometheus服务发现是一种自动发现和注册监控目标的机制，它使得Prometheus能够动态地识别和监控系统中新增、删除或变化的目标。这种机制大大减轻了手动管理监控目标的负担，使得监控系统能够自适应地适应基础架构的变化。
参考文档：

prometheus服务发现
Prometheus - Configuration file
深入浅出prometheus之服务发现(sd)
Prometheus监控神器-服务发现篇（一）



2. Prometheus的服务发现方式Prometheus提供了多种服务发现的方式，以适应不同场景的需求。以下是其中一些常用的方式：
2.1. 静态配置静态配置（static_configs）：这是最简单的服务发现方式，将监控目标的配置直接写在Prometheus的配置文件中。
这种方式适用于目标不经常变化的情况。
prometheus.yml 配置示例：
global:  scrape_interval: 15s #默认采集监控数据时间间隔  evaluation_interval: 15s #Evaluate rules every 15 seconds.scrape_configs:  #监控对象设置  - job_name: prometheus #任务名称    scrape_interval: 5s #每隔5s获取一次监控数据    static_configs: #监控对象地址      - targets: [&#x27;localhost:9090&#x27;]  # 将自己加入到监控对象中

2.2. 文件服务发现文件服务发现（file_sd_configs）：Prometheus周期性地读取存储在指定文件中的监控目标列表。
这种方式适用于一些需要批量生成监控目标的场景。
prometheus.yml 配置示例：
global:  scrape_interval: 15s #默认采集监控数据时间间隔  evaluation_interval: 15sscrape_configs:  - job_name: &#x27;file_sd_targets&#x27;    file_sd_configs:      - files:          #- &#x27;/etc/prometheus/targets/targets.yml&#x27;          - &#x27;/etc/prometheus/targets/*.yml&#x27;

/etc/prometheus/targets/targets.yml 内容为：
- targets:    - localhost:9090  labels:    job: prometheus- targets:    - localhost:9100  labels:    job: node_exporter- targets:    - localhost:8080  labels:    job: my_app

targets.yml 说明：

targets.yml 可以拆成多个文件，以便target分组。
targets.yml 可以改成 json 格式，效果是一样的。
默认情况下，Prometheus每隔15秒会重新读取一次配置文件，因此修改了targets.yml后，不需要重新Prometheus。

2.3. Kubernetes服务发现Kubernetes服务发现（kubernetes_sd_configs）：在Kubernetes集群中，Prometheus可以通过查询Kubernetes API来自动发现和监控Pod、Service等资源。
Prometheus Operator通过ServiceMonitor资源自动发现运行在集群中的服务，并根据定义的标签选择器将它们加入监控目标列表。
这种方式适用于K8S集群，同时也可以监控K8S集群外部服务，配置非常方便。
serviceMonitor示例：参考文档《Ingress Nginx日志持久化》
2.4. DNS服务发现DNS服务发现（dns_sd_configs）：基于DNS发现监控目标。基于DNS的服务发现，有两种配置方法，第一种是使用DNA A记录来做自动发现，第二种方法是使用DNS SRV记录来做服务发现。
使用DNA A记录来做服务发现，其实和基于指定文件的服务发现并没有太大差别。使用DNS SRV记录来做服务发现，监控目标的配置就从Prometheus转移到了DNS。
2.5. Consul服务发现Consul服务发现（consul_sd_configs）：Prometheus可以与Consul等服务发现工具集成，通过查询服务注册表来获取监控目标的信息。
2.6. 云厂商集成云厂商集成：对于使用云服务的场景，Prometheus可以与云厂商的服务发现机制集成，如AWS的EC2实例标签、GCP的GCE服务发现等。
3. 实现Prometheus服务发现的步骤无论采用哪种服务发现方式，实现Prometheus的服务发现通常包括以下几个步骤：
1、配置服务发现：在Prometheus的配置文件中，指定使用哪种服务发现方式。不同的方式需要不同的配置参数。
2、配置目标标签：对于从服务发现获得的监控目标，通常会自动附加一些标签信息，如实例名称、环境等。这些标签将有助于后续查询和区分不同的目标。
3、启用自动发现：配置Prometheus定时从服务发现源获取监控目标，并将其添加到监控目标列表中。
4、数据采集与存储：Prometheus会根据配置的采样频率，定期从监控目标获取指标数据，并将其存储在时间序列数据库中。
5、数据查询与可视化：用户可以通过Prometheus提供的查询语言PromQL来查询和分析监控数据，并通过Grafana等工具进行可视化展示。
4. 小结Prometheus服务发现机制为分布式系统的监控提供了灵活、自动化的解决方案。它使得监控目标的管理变得更加简单、可靠，为工程团队提供了更好的运维体验。无论是在传统的虚拟机环境还是现代的容器化、云原生环境中，Prometheus的服务发现都能为监控系统的搭建和维护带来便利和高效。
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
      </tags>
  </entry>
  <entry>
    <title>Prometheus入门篇</title>
    <url>/dev-prometheus-start/</url>
    <content><![CDATA[1. Prometheus简介
Prometheus is an open-source systems monitoring and alerting toolkit originally built at SoundCloud. Since its inception in 2012, many companies and organizations have adopted Prometheus, and the project has a very active developer and user community. It is now a standalone open source project and maintained independently of any company. To emphasize this, and to clarify the project’s governance structure, Prometheus joined the Cloud Native Computing Foundation in 2016 as the second hosted project, after Kubernetes.


Prometheus是在SoundCloud的基础上构建的开源系统监视和警报工具。自从2012年以来，许多公司和组织都采用了Prometheus，该项目拥有非常活跃的开发人员和用户社区。现在，它是一个独立的开源项目，并且独立于任何公司进行维护。为了强调这一点并阐明项目的治理结构，Prometheus在2016年加入了Cloud Native Computing Foundation，这是继Kubernetes之后的第二个托管项目。

参考文档：

Prometheus官方文档
Prometheus - GETTING STARTED



2. Prometheus的核心概念Prometheus的核心概念包括：

数据模型：时序数据，每个时间序列数据点由四部分唯一标识：指标名称、一组键值对（标签）、样本值、时间戳。
抓取机制：Prometheus通过HTTP协议定期抓取被监控应用的指标数据。被监控的应用需要暴露一个HTTP端点，Prometheus会定期访问该端点获取数据。
查询语言（PromQL）：Prometheus提供了一种强大的查询语言PromQL，用户可以利用它对时间序列数据进行复杂的查询和计算。
警报管理：Prometheus支持通过Alertmanager进行警报管理，用户可以根据自定义的规则设置警报，并通过多种渠道（如电子邮件、Slack等）发送通知。

3. Prometheus的核心组件3.1. Prometheus ServerPrometheus Server是整个监控系统的核心组件。它负责定期从配置的目标中拉取(pull)监控数据，并将数据存储在本地的时间序列数据库中。Prometheus Server还提供了一种灵活的查询语言PromQL，用于查询和分析监控数据。
3.2. ExportersExporters是一组工具，用于将不支持直接暴露监控指标的应用程序或服务的指标数据转换为Prometheus可以抓取的格式。常见的Exporter包括Node Exporter(用于收集机器级别指标)、MySQL Exporter(用于收集MySQL数据库指标)等。
3.3. PushGatewayPrometheus采集数据是用的Pull方式，Prometheus配置文件设置的X秒就是采集数据的频率。但是有些数据并不适合这样的方式，比如短期作业或批处理作业生成的指标数据。由于这些作业是short-lived的，如果采用Pull的模式，可能在Prometheus采集之前，作业已经执行结束，最终采集不到数据。
对这样的数据可以使用PushGateway服务，PushGateway相当于一个暂存器，用于接收由短期作业或批处理作业生成的指标数据，并允许Prometheus Server从PushGateway中拉取这些数据。
如果没有短期作业或者批处理作业，那么我们并不需要PushGateway，因此PushGateway是一个可选的组件。
3.4. AlertmanagerAlertmanager负责处理来自Prometheus Server的告警。它可以对告警进行去重、分组和路由，并通过电子邮件、Slack、PagerDuty、Webhook等方式发送通知。
因为Alertmanager支持的通知方式是有限的，所以如果想要支持微信、钉钉、飞书等更多平台，我们还需要借助Webhook实现一个消息中转服务。该消息中转服务能够把Prometheus AlertManager发出的WebHook请求，转成微信、钉钉、飞书等平台可以接收的请求。
在实际开发和生产环境场景中，我们并不需要自己来实现消息中转服务，github上有很多开源的消息中转服务，推荐 PrometheusAlert 和 prometheus-webhook-dingtalk 。
消息中转服务的安装配置方法，请参考文档：

《PrometheusAlert全家桶》
《Prometheus AlertManager WebHook通知到到钉钉群》

4. Prometheus的工作原理1、Prometheus Server根据配置文件中定义的监控目标，定期通过HTTP协议拉取(pull)监控数据。
2、对于直接支持Prometheus的监控目标，Prometheus Server可以直接从目标获取数据。对于不支持的目标，需要使用Exporter将数据转换为Prometheus可以识别的格式。
3、Prometheus Server将拉取到的监控数据存储在本地的时间序列数据库中。
4、用户可以使用PromQL查询语言对存储的监控数据进行分析和可视化。Prometheus Server内置了一个Web UI，也可以与Grafana等可视化工具集成。
5、Prometheus Server会根据定义的告警规则，持续检查监控数据，一旦满足告警条件，就会向Alertmanager发送告警。Alertmanager负责对告警进行处理和通知。
5. 安装Prometheus1、下载Prometheus访问Prometheus官网下载最新版本的Prometheus。
2、解压缩文件
tar xvfz prometheus-*.tar.gz

3、启动Prometheus进入解压后的目录，使用以下命令启动Prometheus
./prometheus --config.file=prometheus.yml

4、访问Prometheus界面打开浏览器，访问http://localhost:9090，即可进入Prometheus的Web界面。
更多安装方法，请参考文档：

《好好学Docker：使用Docker安装配置Prometheus》
《好好学K8S：安装配置Prometheus官方套装》
《好好学K8S：安装配置ks-prometheus套装》

6. 配置PrometheusPrometheus的配置文件prometheus.yml用于定义抓取目标和其他设置。以下是一个基本的配置示例：
global:  scrape_interval: 15s  # 默认抓取间隔scrape_configs:  - job_name: &#x27;my_app&#x27;    static_configs:      - targets: [&#x27;localhost:8080&#x27;]  # 被监控应用的地址

在这个示例中，Prometheus将每15秒抓取一次位于localhost:8080的应用的指标数据。
Prometheus找到抓取目标的途径，被称为服务发现方法。上面的例子中，直接配置抓取目标地址，是服务发现方法中的静态配置法。静态配置法配置简单，但是灵活性差。更多服务发现方法，请参考文档《Prometheus服务发现》
7. 使用PromQL查询数据PromQL是Prometheus的查询语言，用户可以使用它对存储的时间序列数据进行查询。以下是一些基本的PromQL查询示例：
1、获取所有指标
&#123;__name__=~&quot;.+&quot;&#125;

2、计算某个指标的平均值
avg(my_metric)

3、获取某个标签的指标数据
my_metric&#123;job=&quot;my_app&quot;&#125;

更多内容参考文档：《Prometheus查询语言：PromQL》
8. 可视化数据Prometheus可以与多种可视化工具集成，最常用的可视化工具是Grafana。通过Grafana，用户可以创建丰富的仪表板来展示Prometheus收集的数据。Grafana的安装配置方法，参考文档《使用Docker安装配置Grafana》
9. Alertmanager和Grafana Alertinggrafana alerting也可以配置告警，它和alertmanager有什么区别？答：它们是互为替代的关系，grafana alerting更加简单，alertmanager更加强大。
grafana alerting优点：1）支持多个数据源。整合各种数据源发出告警，Prometheus只是数据源之一。2）使用友好。可视化和告警放在一个地方，所见即所得，非常友好。
alertmanager优点：1）不依赖dashboard。grafana alerting需要dashboard，alertmanager不需要dashboard，只要编写PromQL就可以了。2）更加强大。grafana alerting只支持单个指标的简单阈值告警，而alertmanager通过PromQL支持更复杂的告警，还支持聚合、分组、删除重复等。3）高可用。alertmanager可以非常容易实现高可用。
参考文档：

Alerting via Grafana Vs Alertmanager
Prometheus AlertManager vs Grafana v.4.0.2 Alerting
Deep dive into the Grafana, Prometheus, and Alertmanager stack for alerting and on-call management

10. 后记Prometheus 是CNCF基金会继K8S后第二个毕业项目，已经成为云原生监控领域的事实标准。但是，Prometheus也有一些缺陷，比如数据量大的时候需要拆分集群，聚合数据很难数据去重，可用性较低等。因此出现了Thanos，能够解决Prometheus的很多问题，详情参考 Thanos官网 和 分布式 Promethues 之 Thanos。
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>prometheus</tag>
        <tag>监控</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker容器中使用定时任务</title>
    <url>/dev-docker-crontab/</url>
    <content><![CDATA[1. 前言《Linux设置定时任务》一文中学习了crontab的使用方法，但是同样的配置方法，在Docker中是不会生效的。因为Docker中默认只会启动一个进程，crond并不会启动。本文研究一下在Docker中配置定时任务的方法，踩踩坑。


2. 启动crond要想在容器中使用定时任务，有三个办法：

把crond作为一号进程启动。
supervisor作为一号进程启动，使用supervisor启动crond。
进入容器手动启动crond。

本文使用第一个方法启动crond，启动命令为：
docker run --name vkcrond -d -it voidking/crond:v1.0 &quot;/bin/bash&quot; &quot;-c&quot; &quot;/usr/sbin/crond &gt; start.log &amp;&amp; tail -f start.log&quot;

3. crond日志1、查看crond日志tail /var/log/cron报错没有这个文件，这是因为没有打开rsyslog。
2、打开rsyslog
# centos/etc/init.d/rsyslog start# centos7systemctl restart rsyslog.service# centos6service rsyslog restart

再次查看，crond日志文件就存在了。
4. 使用crondcrond启动后，其他的配置参考《Linux设置定时任务》即可。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>crontab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置Tomcat</title>
    <url>/dev-docker-tomcat/</url>
    <content><![CDATA[1. 前言《CentOS7设置tomcat开机自启动》一文中，学习了常规的tomcat安装配置方法；《Docker安装tomcat服务》一文中，学习了在Docker容器中安装配置tomcat的方法；《Dockerfile构建Tomcat》一文中，学习了使用Dockerfile安装配置tomcat的方法。本文是对前三种方法的升级，更加简单。不需要自己制作tomcat镜像，而是使用dockerhub提供的tomcat镜像。
前置条件是安装配置好了docker环境，安装方法参考《Docker入门》。已知docker宿主机IP为192.168.56.130。


2. 安装Tomcat2.1. 安装Tomcat1、登录dockerhub查看需要的tomcat版本。
2、下载tomcat镜像（以tomcat8.5.51为例）docker pull tomcat:8.5.51-jdk8-openjdk
3、启动tomcat服务
docker run --name vk-tomcat -d \-p 8080:8080 \-v /opt/tomcat/webapps:/usr/local/tomcat/webapps \tomcat:8.5.51-jdk8-openjdk

以上命令：

命名容器为vk-tomcat，后台运行
映射宿主机8080端口到容器8080端口
映射宿主机/opt/tomcat/webapps目录（不需要提前创建目录）到容器/usr/local/tomcat/webapps目录

更高级的启动命令参考How to use this image。
4、验证安装docker pstomcat启动正常的话就可以看到vk-tomcat容器。如果启动失败，可以使用docker logs vk-tomcat查看失败原因并进行解决。
使用nc命令验证：
yum install ncnc -v localhost 8080

使用curl命令验证：curl localhost:8080
2.2. 开放端口firewall-cmd --add-port=8080/tcp --permanentsystemctl reload firewalld# 或者systemctl stop firewalld

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>JVM参数设置规范描述</title>
    <url>/dev-jvm-params/</url>
    <content><![CDATA[1. 配置模板-Xms2304m -Xmx2304m -XX:NewRatio=2 -XX:G1HeapRegionSize=8m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:MaxTenuringThreshold=10 -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=45 -XX:MaxGCPauseMillis=200 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC -XX:+PrintAdaptiveSizePolicy -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=6 -XX:GCLogFileSize=32m -Xloggc:logs/gc.log.$(date +%Y%m%d%H%M) -Dfile.encoding=UTF-8 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
1）无论如何参数中-XX:MaxMetaspaceSize=256m，此参数不要改动。所有类型的机型都一样。2）GC回收器，统一使用G1。3）其他参数，meteor 2.x与其他类型项目略有不同。4）Xms和Xmx的值保持相同。


2. heap大小设置1、标准机型内存1G的机器，jvm参数保持默认，不再设置xmx和xms。内存2G[±200M]，heap大小为1G。内存4G[±200M]，heap大小为2G。内存8G[±200M]，heap大小为4G。内存16G，heap大小为12G。
2、非标准机型和容器对于非标准机型或者容器内存，统一计算方式为：
heap大小 = (总内存 - 256M) * 60%

以内存为4G的docker容器为例：
heap大小 = (4096 - 256) * 60% = 2304M

因此Xms和Xmx为：
-Xms2304m -Xmx2304m

3. GC频次调优评估参考（通用WEB系统）1、YGC，5到10S一次，暂定规则：为10次/分钟。2、FGC，数小时甚至数天一次，暂定规则：2次/天。3、GC时间占比标准 小于等于 5%如果你的应用GC频次超出上述规则，需要调优或者评估规则可行性。
4. 例子4.1. spring bootset -e &amp;&amp; cd &lt;workDir&gt;/&lt;appName&gt; &amp;&amp; source /etc/bashrc &amp;&amp; java -Xms2304m -Xmx2304m -XX:NewRatio=2 -XX:G1HeapRegionSize=8m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:MaxTenuringThreshold=10 -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=45 -XX:MaxGCPauseMillis=200 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC -XX:+PrintAdaptiveSizePolicy -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=6 -XX:GCLogFileSize=32m -Xloggc:logs/gc.log.$(date +%Y%m%d%H%M) -Dfile.encoding=UTF-8 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -javaagent:/opt/vipkid/jmx_agent.jar=18080:- -jar &lt;packageName&gt; --server.port=&lt;port&gt;

4.2. meteor2set -e &amp;&amp; cd &lt;workDir&gt;/&lt;appName&gt; &amp;&amp; source /etc/bashrc &amp;&amp; JMX_PORT=9999 JAVA_OPTS=&quot;-Xms2304m -Xmx2304m -javaagent:/opt/vipkid/vkcat-agent-1.0.2/vkcat-agent-1.0.2.jar -DapplicationDomain=&lt;appName&gt; -DvkcatDomain=vkcat.vipkid.com.cn:80 -javaagent:/opt/vipkid/jmx_agent.jar=18080:-&quot; ./application.jar start --profile=prod --cloud=ali &amp;&amp; tail -f std.out

5. 参考文档jvm参数和gc普及
初步诊断你的GC
从实际案例聊聊Java应用的GC优化
官方oracle宝典细读-Tuning Tips
官方oracle宝典细读-VM Heap Size and Garbage Collection（可以得出gc服务时间占比5%最佳，heap size设置考究）
4G内存设置的过程宝典-5 Tips for Proper Java Heap Size
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置Apache和PHP环境（微擎环境）</title>
    <url>/dev-docker-apache-php/</url>
    <content><![CDATA[1. 前言《在CentOS7上配置PHP运行环境》一文中学习了安装配置LNMP环境，《CentOS安装Apache和PHP环境》一文中学习了安装配置Apache和PHP环境。
本文学习使用Docker安装配置Apache和PHP，与Mysql结合，搭建一个容器化的LAMP环境，部署微擎服务。
前置条件是安装配置好了docker环境，安装方法参考《Docker入门》。已知docker宿主机IP为192.168.56.130。


2. 安装Mysql参考《使用Docker安装配置Mysql》，安装好mysql server和mysql client。
1、登录mysql servermysql -h 127.0.0.1 -u root -p
2、创建数据库
create database `w7` default character set utf8 collate utf8_general_ci; 

3. 安装PHP1、登录dockerhub查看需要的PHP - Docker Official Images。
2、下载php镜像（以7.2-apache为例）docker pull php:7.2-apache
选择带有apache的版本，省去了安装apache的步骤。
3、启动php和apache服务
docker run --name vk-php -d \-p 8080:80 \-v /opt/php/w7:/var/www/html \php:7.2-apache

以上命令：

命名容器为vk-php，后台运行
映射宿主机8080端口到容器的80端口
挂载宿主机目录/opt/php/w7到容器目录/var/www/html

更多启动命令参数可以参考How to use this image。
4、创建测试文件vim /opt/php/w7/index.php，内容为：
&lt;?php    echo &#x27;hello php7.2&#x27;;?&gt;

5、验证安装curl localhost:8080，可以看到hello php7.2。浏览器访问 http://192.168.56.130:8080 ，可以看到hello php7.2。
以上，apache和php环境安装配置完成。
4. 安装微擎4.1. 初始尝试主要参考《微擎系统搭建》和微擎linux服务器安装。1、下载微擎安装引导文件
cd /opt/php/w7wget https://cdn.w7.cc/download/WeEngine-Laster-Online.zipunzip WeEngine-Laster-Online.zip

2、浏览器访问安装页面http://192.168.56.130:8080/install.php填入用户名密码后，点击验证后安装微擎。没有通过验证，如下图：由报错看，主要是目录权限问题和缺少扩展问题。
4.2. 重做镜像1、修改sources.list在容器中查看/etc/apt/sources.list，发现镜像版本是debian buster，因此新建一个sources.list为：
deb http://mirrors.aliyun.com/debian/ buster main non-free contribdeb-src http://mirrors.aliyun.com/debian/ buster main non-free contribdeb http://mirrors.aliyun.com/debian-security buster/updates maindeb-src http://mirrors.aliyun.com/debian-security buster/updates maindeb http://mirrors.aliyun.com/debian/ buster-updates main non-free contribdeb-src http://mirrors.aliyun.com/debian/ buster-updates main non-free contribdeb http://mirrors.aliyun.com/debian/ buster-backports main non-free contribdeb-src http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib

2、编写Dockerfile
FROM php:7.2-apacheCOPY sources.list /etc/apt/sources.listRUN apt-get update &amp;&amp; apt-get install -y libpng-dev libzip-dev \    &amp;&amp; docker-php-ext-install zip gd pdo pdo_mysqlRUN chmod 777 /var/www/html

3、生成新镜像docker build -t voidking/w7:v1.0 .
4.3. 再次尝试1、删除原有容器
docker stop vk-phpdocker rm vk-php

2、启动新的容器
docker run --name vk-php -d \-p 8080:80 \-v /opt/php/w7:/var/www/html \voidking/w7:v1.0

3、再次安装，再次验证可以看到，这次还剩一个问题：外网不可访问。这就奇怪了，在容器内明明是可以正常访问外网的，为啥报这个错？既然没错，那就忽略它好了。但是这个错存在，就无法进行下一步，因此这里我们换一种安装方式：源码安装。
4.4. 源码安装1、下载源码
cd /opt/php/w7rm -rf ./*cd ..git clone https://gitee.com/we7coreteam/pros.git w7

2、再次安装，再次验证nice，完美跳过了外网不可访问这个坑。然后，data目录权限报错。修改data目录权限，chmod 777 /opt/php/w7/data/，问题解决。
3、填入数据库连接信息，以及管理员用户名密码
4、然后，安装完成。
5、测试访问浏览器访问首页 http://192.168.56.130:8080/index.php ，即会跳转到微擎登录页。
以上，微擎系统安装配置完成，可以愉快地使用了。
5. 百度云解析5.1. 备案失败百度智能云年终盛典，全场云服务器一折起，于是151块钱入手了一台1C2G2M的百度云BCC主机。但是，没有想到的是，网站需要新增接入备案，否则无法使用域名！！！这个不怪百度，如果使用百度云备案成功，以后想要接入阿里云或者腾讯云，也需要新增接入备案。
不过百度云比较坑的有两点：第一点是备案期间域名不能解析，而阿里云备案期间可以正常解析。第二点是无法通过百度云的备案审核，提交备案后百度云给出了六条不符合审核条件的理由，而同样的审核材料，一个月前在阿里云通过了备案审核。
真的是没有对比就没有伤害，第一个念头是退货，然而退货失败。不能退货，那该怎样使用这台服务器呢？那该怎样在这台服务器上部署服务，然后通过域名访问呢？
在github找到了一些项目，awesome-selfhosted，想要部署一些不需要域名的服务，好歹给利用起来了。但是，依然不甘心，想要给这些服务加上域名！然后，真的找到了办法！前提是你还有一台可以进行域名接入的主机，无论是备案过的阿里云主机，还是不需要备案的海外主机，都可以。
5.2. 解析方案已知两台主机：可以域名接入的主机（主机A），百度云主机（主机B）。我们在主机B上部署好了微擎服务，想要给这个服务添加一个域名：w7.voidking.com 
1、安装frp参考《使用frp进行内网穿透》，在主机A上安装配置好frp server，在主机B上安装配置好frp client。主机B上的8080端口，映射为主机A上的3480端口。
2、nginx配置在主机A上，添加nginx解析 w7.voidking.com.conf ，内容为：
server &#123;    server_name w7.voidking.com;    listen 80;    location / &#123;        proxy_pass http://127.0.0.1:3480/;        proxy_set_header X-Real-IP $remote_addr;    &#125;&#125;

注意，不用多加其他参数，不然会出现502等错误。
3、域名解析在dnspod上添加A记录解析到主机A。
以上，可以通过域名访问百度云上的微擎服务了。
6. 微擎后续问题6.1. 站点URL问题确实可以通过域名访问微擎服务了，但是站点的很多请求地址为 127.0.0.1:3480 ，因为微擎服务使用proxy_pass里的URL作为了站点URL。我们想让微擎服务把域名作为站点URL，解决办法很简单，添加：
proxy_set_header Host $http_host;
但是，添加完这个参数，就会出现502错误，尴尬了吧。。。nginx层没法进行修复，看来这个问题只能通过修改微擎源码来修复了。编辑/opt/php/w7/framework/bootstrap.inc.php，如下修改：
# line 90, change// $_W[&#x27;siteroot&#x27;] = htmlspecialchars($_W[&#x27;sitescheme&#x27;] . (isset($_SERVER[&#x27;HTTP_HOST&#x27;]) ? $_SERVER[&#x27;HTTP_HOST&#x27;] : &#x27;&#x27;) . $sitepath);$_W[&#x27;siteroot&#x27;] = &#x27;w7.voidking.com&#x27;;

然后，请求地址就全部变成 w7.voidking.com ，nice。
6.2. 跨域问题但是，一些请求报错：
Access to XMLHttpRequest at &#x27;javascript:;&#x27; from origin &#x27;http://w7.voidking.com&#x27; has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

明明都是同一个url，居然还是跨域错误，没有找到解决办法。
6.3. 重装微擎既然自己安装微擎问题这么多，那就使用官网给的docker镜像好了，参考docker安装微擎。
1、删除原有环境
docker stop vk-phpdocker rm vk-php

2、启动新的容器
docker run -it --name vk-php -d \-p 8080:80 \-v /opt/w7/mysql:/var/lib/mysql \-v /opt/w7/html:/var/www/html \-e MYSQL\_ROOT\_PASSWORD=123456 -d \--restart=always \ccr.ccs.tencentyun.com/weiqing/nginxphpmysql:1.0

3、重新安装微擎http://ip:8080/install.php
4、设置用户名密码
5、同样修改微擎源码，修改站点URL。
然后，同样有跨域问题，但是站点已经可以正常使用了，就先这么滴吧。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>apache</tag>
        <tag>php</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Nginx：使用Docker安装配置Nginx</title>
    <url>/dev-docker-nginx/</url>
    <content><![CDATA[1. 前言《CentOS7设置nginx开机自启动》一文中学习了在CentOS7中安装配置Nginx的方法，本文学习一下使用Docker安装配置Nginx的方法。
前置条件是安装配置好了docker环境，安装方法参考《Docker入门》。


2. 安装Nginx1、登录dockerhub查看需要的Nginx - Docker Official Images。
2、下载nginx镜像（以 1.23.4 为例）
docker pull nginx:1.23.4

3、创建nginx挂载目录
mkdir -p /opt/nginx/&#123;conf,html,ssl,work,log&#125;

4、拷贝nginx镜像中的配置到宿主机
docker run --name tmp-nginx -d nginx:1.23.4docker cp tmp-nginx:/etc/nginx/nginx.conf /opt/nginx/conf/docker cp tmp-nginx:/etc/nginx/conf.d/ /opt/nginx/conf/docker cp tmp-nginx:/usr/share/nginx/html/. /opt/nginx/html/docker stop tmp-nginxdocker rm tmp-nginx

5、启动nginx服务
docker run --name vk-nginx -d \  --privileged=true \  -p 80:80 -p 443:443 \  -v /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \  -v /opt/nginx/conf/conf.d:/etc/nginx/conf.d \  -v /opt/nginx/ssl:/etc/nginx/ssl \  -v /opt/nginx/html:/usr/share/nginx/html \  -v /opt/nginx/work:/usr/share/nginx/work \  -v /opt/nginx/log:/var/log/nginx \  -v /etc/localtime:/etc/localtime \  nginx:1.23.4

以上命令：

命名容器为vk-nginx，后台运行
映射宿主机80、443端口到容器的80、443端口
挂载宿主机文件 /opt/nginx/conf/nginx.conf 到容器文件 /etc/nginx/nginx.conf
挂载宿主机目录 /opt/nginx/conf/conf.d 到容器目录 /etc/nginx/conf.d
挂载宿主机目录 /opt/nginx/ssl 到容器目录 /etc/nginx/ssl
挂载宿主机目录 /opt/nginx/html 到容器目录 /usr/share/nginx/html
挂载宿主机目录 /opt/nginx/work 到容器目录 /usr/share/nginx/work
挂载宿主机目录 /opt/nginx/log 到容器目录 /var/nginx/log
挂载宿主机目录 /etc/localtime 到容器目录 /etc/localtime

更多启动命令参数可以参考How to use this image。
6、验证安装（1）docker ps，nginx启动正常的话就可以看到vk-nginx容器。（2）curl localhost，可以看到html文本。（3）浏览器访问 http://8.136.13.58 ，可以看到Welcome to nginx!
3. 常用命令3.1. 测试配置docker exec -it vk-nginx nginx -t

3.2. 重新加载配置docker exec -it vk-nginx nginx -s reload

3.3. 重启nginxdocker restart vk-nginx

4. 配置Web服务参考文档：

《Nginx入门篇》
《Hexo启用https加密连接》
《Hexo加速访问》

以配置 www.voidking.com 域名的Web服务为例。
1、下载博客项目
mkdir /opt/nginx/workgit clone https://gitee.com/voidking/voidking.github.io.git voidking

2、上传证书上传 1_www.voidking.com_bundle.crt 和 2_www.voidking.com.key 到 /opt/nginx/conf.d/ssl 目录
3、创建配置文件
cd /opt/nginx/conf/conf.dvim www.voidking.com.conf 

内容为：
server &#123;    listen 80;    listen 443 ssl;    server_name www.voidking.com;    charset utf-8;    ssl_certificate /etc/nginx/ssl/1_www.voidking.com_bundle.crt;    ssl_certificate_key  /etc/nginx/ssl/2_www.voidking.com.key;    ssl_session_timeout  5m;    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;    ssl_ciphers  HIGH:!ADH:!EXPORT56:RC4+RSA:+MEDIUM;    ssl_prefer_server_ciphers on;    if ($ssl_protocol = &quot;&quot;) &#123;        return 301 https://$host$request_uri;    &#125;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        root /usr/share/nginx/work/voidking/;        index index.html;    &#125;    error_page 404 /404.html;    location = /404.html &#123;        root /usr/share/nginx/work/voidking/;        index 404.html;    &#125;    location ~ /\.git &#123;        return 404;    &#125;&#125;

4、重启nginx
docker restart vk-nginx

5、查看是否启动成功
docker psdocker logs vk-nginxdocker logs --tail 10 vk-nginx

至此， www.voidking.com 域名配置完成，在dnspod添加好解析即可访问，nice。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>network</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>nginx</tag>
        <tag>hexo</tag>
        <tag>好好学Nginx</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Docker安装配置Jenkins</title>
    <url>/dev-docker-jenkins/</url>
    <content><![CDATA[1. 前言《CentOS7安装部署Jenkins》一文中学习了Jenkins的安装方法，如果使用Docker来安装Jenkins，同样可以简单很多。
前置条件是安装配置好了docker环境，安装方法参考《Docker入门》。已知docker宿主机IP为192.168.56.130。


2. 安装Jenkins1、登录dockerhub查看需要的jenkins-Docker Official Images。需要注意，官方的jenkins最新版本是2.60.3，版本太低！如果安装该版本，安装后会出现各种插件使用不了的情况。因此，这里真正需要使用的jenkins命名空间下的jenkins。
2、下载jenkins镜像（以2.210为例）docker pull jenkins/jenkins:2.210
3、启动jenkins服务
docker run --name vk-jenkins -d \-p 8080:8080 -p 50000:50000 \-v /opt/jenkins_home:/var/jenkins_home \jenkins/jenkins:2.210
以上命令：

命名容器为vk-jenkins，后台运行
映射宿主机8080、50000端口到容器8080、50000端口
挂载宿主机目录/opt/jenkins_home到容器目录/var/jenkins_home

更多启动命令参数可以参考How to use this image。
启动报错：
docker: Error response from daemon: driver failed programming external connectivity on endpoint vk-jenkins (d95f7502bb87d6795547061a2f13b5e2ba44648977399fcde1c05640f678554a):  (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 50000 -j DNAT --to-destination 172.17.0.2:50000 ! -i docker0: iptables: No chain/target/match by that name. (exit status 1)).

解决办法：重启docker，然后重新执行启动命令。
systemctl restart dockerdocker rm vk-jenkinsdocker run --name vk-jenkins -d \-p 8080:8080 -p 50000:50000 \-v /opt/jenkins_home:/var/jenkins_home \jenkins/jenkins:2.210

4、验证安装docker psjenkins启动正常的话就可以看到vk-jenkins容器。如果启动失败，可以使用docker logs vk-jenkins查看失败原因并进行解决。本例中确实报错了，提示目录权限问题：
touch: cannot touch &#x27;/var/jenkins_home/copy_reference_file.log&#x27;: Permission deniedCan not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions?

这是因为/opt/jenkins_home目录的拥有者为root用户，而容器中jenkins user的uid为1000，因此需要修改目录权限。
chown -R 1000:1000 /opt/jenkins_homedocker start vk-jenkins

3. 配置使用Jenkins1、加速插件安装（推荐）参考Jenkins安装插件提速，修改/opt/jenkins_home/updates/default.json。
sed -i &#x27;s/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g&#x27; default.jsonsed -i &#x27;s/http:\/\/www.google.com/https:\/\/www.baidu.com/g&#x27; default.json
修改完成重启vk-jenkins，正常进入安装流程，直观感觉就是“安装推荐的插件”这一步速度能够提高一百倍。
2、查看初始密码cat /opt/jenkins_home/secrets/initialAdminPassword
3、jenkins配置浏览器访问：http://192.168.56.130:8080/初始密码填入页面，开始进行jenkins配置。
4、后续接下来的配置和使用参考《CentOS7安装部署Jenkins》即可。
至此，docker安装配置jenkins完成。因为第一次使用的是官方的jenkins2.60.3镜像，所以遇到了一些坑，下面一节中进行记录。
4. jenkins2.60.3问题解决1、安装推荐的插件，会卡在Getting Started。。这是因为安装插件前会尝试连接 www.google.com ，来判断网络是否连通。国内用户需要改成 www.baidu.com ，vim /opt/jenkins_home/updates/default.json，找到google，修改为baidu。
# 修改前&quot;connectionCheckUrl&quot;:&quot;http://www.google.com/&quot;,# 修改后&quot;connectionCheckUrl&quot;:&quot;http://www.baidu.com/&quot;,

2、重启jenkins特别慢docker restart vk-jenkins发现重启特别特别慢，大概要10分钟左右才能重启成功。而且，重启后connectionCheckUrl中的baidu又变成了google！！！
3、修改updatecenter怀疑在jenkins启动时会根据其他地方的配置更改default.json，但是没有找到。查找资料发现了一篇update jenkins Updatecenter from CLI，其中一行：
wget -O default.json http://updates.jenkins-ci.org/update-center.json
那么，如果修改updatecenter为中国的镜像站，是不是就会使用中国的update-center.json代替default.json？试一试，vim /opt/jenkins_home/hudson.model.UpdateCenter.xml，原文为：
&lt;?xml version=&#x27;1.0&#x27; encoding=&#x27;UTF-8&#x27;?&gt;&lt;sites&gt;  &lt;site&gt;    &lt;id&gt;default&lt;/id&gt;    &lt;url&gt;http://updates.jenkins-ci.org/update-center.json&lt;/url&gt;  &lt;/site&gt;&lt;/sites&gt;
中国镜像站去哪里找呢？可以在the status of Jenkins mirrors网站上选择。hudson.model.UpdateCenter.xml修改为：
&lt;?xml version=&#x27;1.0&#x27; encoding=&#x27;UTF-8&#x27;?&gt;&lt;sites&gt;  &lt;site&gt;    &lt;id&gt;default&lt;/id&gt;    &lt;url&gt;https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json&lt;/url&gt;  &lt;/site&gt;&lt;/sites&gt;
删除/opt/jenkins_home/updates目录，再次重启vk-jenkins。重启速度飞快，10秒左右，但是default.json中依然是google，因为镜像站里的update-center.json和jenkins源站里的update-center.json相同。既然jenkins启动时都会使用远端的update-center.json替代本地的default.json，那么解决该问题的方法就是给hudson.model.UpdateCenter.xml一个远端的修改后的update-center.json，或者不让jenkins获取到远端update-center.json。
4、填写假的updatecenter修改hudson.model.UpdateCenter.xml为：
&lt;?xml version=&#x27;1.0&#x27; encoding=&#x27;UTF-8&#x27;?&gt;&lt;sites&gt;  &lt;site&gt;    &lt;id&gt;default&lt;/id&gt;    &lt;url&gt;http://localhost&lt;/url&gt;  &lt;/site&gt;&lt;/sites&gt;
因为获取不到update-center.json，然后就可以跳过安装插件的步骤。
5、更换新版jenkins镜像jenkins安装成功，但是在jenkins的系统配置页面，可以看到很多插件报错，而且无法修复，系统建议进行jenkins升级。此时，郝同学才意识到，官方jenkins镜像不是最新版！！！需要改用jenkins命名空间下的新版jenkins镜像，但是“安装推荐的插件”这一步依然很慢，几个小时甚至更久才能安装成功。加速方法可以参考本文 配置使用Jenkins 一节中的 加速插件安装。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>cicd</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：使用Docker安装配置GitLab</title>
    <url>/dev-docker-gitlab/</url>
    <content><![CDATA[1. 前言《好好学GitLab：CentOS7安装配置GitLab》一文中学习了GitLab的安装方法，如果使用Docker来安装GitLab，可以简单很多。
参考文档：

Install GitLab using Docker
Install self-managed GitLab
GitLab Security Release



2. 安装DockerDocker安装方法参考文档《好好学Docker：Docker入门》。
3. 安装GitLab1、登录dockerhub查看可选的GitLab Docker images。
2、下载gitlab镜像（以16.3.6-ce.0版本为例，这是一个Security Release版本）
docker pull gitlab/gitlab-ce:16.3.6-ce.0

3、启动gitlab服务
docker run --name vk-gitlab -d \  --restart always \  --hostname gitlab.voidking.com \  --shm-size 256m \  --env GITLAB_OMNIBUS_CONFIG=&quot;external_url &#x27;http://gitlab.voidking.com&#x27;&quot; \  -p 443:443 -p 80:80 -p 3422:22 \  -v /opt/gitlab/config:/etc/gitlab \  -v /opt/gitlab/logs:/var/log/gitlab \  -v /opt/gitlab/data:/var/opt/gitlab \  gitlab/gitlab-ce:16.3.6-ce.0

参数说明：

命名容器为vk-gitlab，后台运行
设置容器意外关闭自动重启
设置容器名为 gitlab.voidking.com
容器的 /dev/shm 分区大小是256m，而不是默认的64m，用作进程之间共享内存的接口
设置gitlab的对外url为 http://gitlab.voidking.com
映射宿主机80、443、3422端口到容器80、443、22端口
挂载三个宿主机目录到容器，以便持久化数据

启动大概需要5到10分钟，请耐心等待。
4、验证安装
docker psdocker logs vk-gitlab
gitlab启动正常的话就可以看到vk-gitlab容器。如果启动失败，通过查看启动日志，排查启动失败原因并解决。
5、查看gitlab root用户密码
docker exec -it vk-gitlab grep &#x27;Password:&#x27; /etc/gitlab/initial_root_password

PS：如果是低版本的gitlab，比如 12.6.1-ce.0 ，那么不需要这个步骤，在首次访问dashboard时会提示修改密码。
4. 放通外部访问对外开放端口
firewall-cmd --add-port=80/tcp --permanentfirewall-cmd --add-port=443/tcp --permanentfirewall-cmd --add-port=3422/tcp --permanentsystemctl reload firewalld

或者关闭防火墙
systemctl stop firewalld

5. 测试GitLab5.1. 测试访问GitLab端口宿主机上访问gitlab端口
nc -zv localhost 80nc -zv localhost 443nc -zv localhost 3422

外部主机访问gitlab端口（已知Docker宿主机IP为 192.168.56.130 ）
nc -zv 192.168.56.130 80nc -zv 192.168.56.130 443nc -zv 192.168.56.130 3422

5.2. 测试访问GitLab服务宿主机上访问gitlab服务：curl localhost -L外部主机浏览器访问gitlab服务：http://192.168.56.130
6. 配置GitLab6.1. 配置对外展示的URL1、配置url和ssh协议端口vim /opt/gitlab/config/gitlab.rb，如下修改：
external_url &#x27;http://gitlab.voidking.com&#x27;gitlab_rails[&#x27;gitlab_ssh_host&#x27;] = &#x27;gitlab.voidking.com&#x27;gitlab_rails[&#x27;gitlab_shell_ssh_port&#x27;] = 3422

gitlab.rb 配置中又涉及到了 external_url ，这里的优先级高于 docker run 时配置的环境变量的优先级。
该配置的效果是，当用户在gitlab项目页面点击Clone时，Clone with HTTP和Clone with SSH的结果类似于：
http://gitlab.voidking.com/root/voidking.gitssh://git@gitlab.voidking.com:3422/root/voidking.git

2、重启vk-gitlab
docker restart vk-gitlab

6.2. 禁用更新提醒管理员登录dashboard后，无论点击什么页面，都会跳出一个更新提示框：
Important notice - Critical security release

禁用方法：Admin Area -&gt; Settings -&gt; Metrics and Profiling -&gt; Usage Statistics -&gt; turn off the option for Enable version check
参考文档：How to disable update notifications for admin which aren’t possible to update at all?
6.3. http域名访问6.3.1. 单机配置hosts修改同一局域网内某台设备的 /etc/hosts，添加：
192.168.56.103  gitlab.voidking.com

然后，这台设备就可以通过域名 http://gitlab.voidking.com 访问gitlab服务了。
6.3.2. 本地DNS配置域名解析本地DNS配置域名解析：gitlab.voidking.com -&gt; 192.168.56.103然后，同一局域网内所有使用本地DNS的设备就可以通过域名 http://gitlab.voidking.com 访问gitlab服务了。
6.4. https域名访问参考《好好学GitLab：CentOS7安装配置GitLab》添加SSL一节，生成证书，配置gitlab使用证书，并且添加到浏览器。
7. 使用GitLab1、创建项目Create a Project，Project name填入voidking，选择Public，勾选Initialize repository with a README。
2、clone项目
git config --global http.sslVerify falsegit clone https://gitlab.voidking.com/root/voidking.git

3、上传代码
git add .git commit -m &quot;do something&quot;git push

8. 修改root密码参考文档：

重置 GitLab 管理员密码
重置用户的密码

1、进入rails控制台
docker exec -it vk-gitlab /bin/bashgitlab-rails console

2、重置密码
user = User.find(1)newpassword=&#x27;xxx&#x27;user.password=newpassworduser.password_confirmation=newpassworduser.save!exit


]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>git</tag>
        <tag>gitlab</tag>
        <tag>好好学GitLab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：使用Docker安装配置MySQL</title>
    <url>/dev-docker-mysql/</url>
    <content><![CDATA[1. 前言本文中，我们学习使用Docker安装配置MySQL的方法。
相关文档：

《Docker入门篇》
《CentOS7设置mysql开机自启动》
《CentOS安装多版本MySQL》
Server System Variables
Docker安装最新版MySQL8（mysql-8.0.31）教程
MySQL Product Archives
MySQL8.0 清华镜像站



2. 安装配置MySQL2.1. 安装MySQL5.7.x1、登录dockerhub查看需要的mysql版本。
2、下载mysql镜像（以mysql5.7.28为例）
docker pull mysql:5.7.28

3、准备mysql配置
docker run --name tmp-mysql -d -e MYSQL_ROOT_PASSWORD=voidking mysql:5.7.28mkdir -p /opt/mysql/conf.ddocker cp tmp-mysql:/etc/mysql/conf.d/. /opt/mysql/conf.ddocker rm tmp-mysql -f

4、启动mysql服务
docker run --name vk-mysql -d \    -p 3306:3306 \    -v /opt/data/mysql:/var/lib/mysql \    -v /opt/mysql/conf.d:/etc/mysql/conf.d \    -e MYSQL_ROOT_PASSWORD=voidking \    mysql:5.7.28

以上命令：

命名容器为vk-mysql，后台运行
映射宿主机3306端口到容器3306端口
映射宿主机 /opt/data/mysql 目录到容器 /var/lib/mysql ，用于持久化mysql数据库数据
映射宿主机 /opt/mysql/conf.d 目录到容器 /etc/mysql/conf.d ，用户持久化mysql配置
设置数据库密码为voidking

更多启动参数参考How to use this image。其中有一个 MYSQL_ROOT_HOST 也是一个常用参数，设置为 % 表示允许所有客户端连接，但是经验证不添加也可以。
PS：mysql启动脚本路径为 /usr/local/bin/docker-entrypoint.sh
5、验证安装
docker psdocker logs vk-mysql

2.2. MySQL5.7.x 配置文件说明2.2.1. 默认配置文件路径
配置文件路径为 /etc/mysql/my.cnf
配置文件会先后引用 /etc/mysql/conf.d/ 和 /etc/mysql/mysql.conf.d 目录中的以cnf结尾的配置文件
/etc/mysql/conf.d/ 目录中包含 docker.cnf、mysql.cnf、mysqldump.cnf
/etc/mysql/mysql.conf.d 目录中包含 mysqld.cnf

2.2.2. 默认配置内容my.cnf
!includedir /etc/mysql/conf.d/!includedir /etc/mysql/mysql.conf.d/

docker.cnf
[mysqld]skip-host-cacheskip-name-resolve

mysql.cnf
[mysql]

mysqldump.cnf
[mysqldump]quickquote-namesmax_allowed_packet  = 16M

mysqld.cnf
[mysqld]pid-file    = /var/run/mysqld/mysqld.pidsocket      = /var/run/mysqld/mysqld.sockdatadir     = /var/lib/mysql#log-error  = /var/log/mysql/error.log# By default we only accept connections from localhost#bind-address   = 127.0.0.1# Disabling symbolic-links is recommended to prevent assorted security riskssymbolic-links=0

2.2.3. 自定义配置文件示例：custom.cnf
[mysqld]general_log_file=/var/log/mysql/mysql.loggeneral_log = 1

上面的配置，会开启sql日志。
更多配置参考文档Server System Variables
2.3. 安装MySQL8.0.x1、登录dockerhub查看需要的mysql版本。
2、下载mysql镜像（以mysql8.0.28为例）
docker pull mysql:8.0.28

3、准备mysql配置
docker run --name tmp-mysql -d -e MYSQL_ROOT_PASSWORD=voidking mysql:8.0.28mkdir -p /opt/mysql/conf.ddocker cp tmp-mysql:/etc/mysql/conf.d/. /opt/mysql/conf.ddocker rm tmp-mysql -f

4、启动mysql服务
docker run --name vk-mysql -d \    -p 3306:3306 \    -v /opt/mysql/data:/var/lib/mysql \    -v /opt/mysql/mysql-files:/var/lib/mysql-files \    -v /opt/mysql/conf.d:/etc/mysql/conf.d \    -v /opt/mysql/log:/var/log/mysql \    -e MYSQL_ROOT_PASSWORD=voidking \    mysql:8.0.28

以上命令：

命名容器为vk-mysql，后台运行
映射宿主机3306端口到容器3306端口
映射宿主机 /opt/data/mysql 目录到容器 /var/lib/mysql ，用于持久化mysql数据库数据
映射宿主机 /opt/mysql/mysql-files 目录到容器 /var/lib/mysql-files ，用于导出导入mysql文件数据（secure_file_priv）
映射宿主机 /opt/mysql/conf.d 目录到容器 /etc/mysql/conf.d ，用户持久化mysql配置
映射宿主机 /opt/mysql/log 目录到容器 /var/log/mysql ，用户持久化mysql日志
设置数据库密码为voidking

更多启动参数参考How to use this image。其中有一个 MYSQL_ROOT_HOST 也是一个常用参数，设置为 % 表示允许所有客户端连接，但是经验证不添加也可以。
PS：mysql启动脚本路径为 /usr/local/bin/docker-entrypoint.sh
5、验证安装
docker psdocker logs vk-mysql

2.4. MySQL8.0.x 配置文件说明2.4.1. 默认配置文件路径
配置文件路径为 /etc/mysql/my.cnf
配置文件会引用 /etc/mysql/conf.d/ 目录中的以cnf结尾配置文件
/etc/mysql/conf.d/ 目录中包含 docker.cnf、mysql.cnf

2.4.2. 默认配置内容my.cnf
[mysqld]pid-file        = /var/run/mysqld/mysqld.pidsocket          = /var/run/mysqld/mysqld.sockdatadir         = /var/lib/mysqlsecure-file-priv= NULL# Custom config should go here!includedir /etc/mysql/conf.d/

docker.cnf
[mysqld]skip-host-cacheskip-name-resolve

mysql.cnf
[mysql]

2.4.3. 自定义配置文件示例：custom.cnf
[mysqld]general_log_file=/var/log/mysql/mysql.loggeneral_log = 1

上面的配置，会开启sql日志。
更多配置参考文档Server System Variables
3. 测试访问MySQL3.1. 容器内访问MySQLdocker exec -it vk-mysql /bin/bashmysql -uroot -p

3.2. 其他容器访问MySQL1、启动mysql client
docker run --name mysql-client -d \    -v /data/mysql-client:/data/mysql-client     -e MYSQL_ROOT_PASSWORD=mysql123 \    mysql:5.7.28# ordocker run --name mysql-client -d \    -v /data/mysql-client:/data/mysql-client     -e MYSQL_ROOT_PASSWORD=mysql123 \    mysql:8.0.28

2、使用mysql client访问mysql已知docker宿主机IP为192.168.56.130。
docker exec -it mysql-client /bin/bashmysql -h 192.168.56.130 -P 3306 -u root -p

3.3. 宿主机上访问MySQLcentos7上访问mysql server5.7.x
yum install -y mariadb.x86_64 mariadb-libs.x86_64mysql -h 127.0.0.1 -uroot -p

centos7上访问mysql server8.0.x
wget https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpmcurl -L https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 -o /etc/pki/rpm-gpg/RPM-GPG-KEY-mysqlrpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-mysqlrpm -ivh mysql80-community-release-el7-3.noarch.rpm # 安装repo源yum makecacheyum list | grep mysqlyum list mysql-community-client --showduplicates | sort -ryum install mysql-community-client-8.0.28-1.el7.x86_64 --setopt=protected_multilib=false

ubuntu18上访问mysql server5.7.x
#apt install mysql-client-core-8.0apt install mysql-client-core-5.7mysql -h 127.0.0.1 -uroot -p


4. 开放访问1、开放端口
# centosfirewall-cmd --add-port=3306/tcp --permanentsystemctl reload firewalld# 或者systemctl stop firewalld# ubuntu18ufw allow 3306

2、安装mysql-client，方法同上
3、访问mysql-server已知docker宿主机IP为192.168.56.130。
mysql -h 192.168.56.130 -P 3306 -u root -p
输入密码，即可连接到vk-mysql容器服务。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>mysql</tag>
        <tag>数据库</tag>
        <tag>好好学MySQL</tag>
      </tags>
  </entry>
  <entry>
    <title>使用LastPass管理密码</title>
    <url>/dev-manage-pwd-by-lastpass/</url>
    <content><![CDATA[1. 前言之前整理过一篇《使用KeePass管理密码》，也实际使用了一段时间的KeePass，但是发现这个工具并不能很好地满足自己的需求。因为KeePass适合用来本地存储密码，但是用来自动填充密码，还是不够好用。
本文中，将会学习使用更加方便的LastPass，用来存储管理安全级别为商密和普密的密码。


2. 简单使用1、访问LastPass官网，创建账号，然后在chrome安装LastPass插件。
2、在chrome登录lastpass账号。
3、正常访问网站，登录后会提示保存密码到lastpass，下次登录即可自动填充密码。
3. 导出导入密码3.1. keepass导出密码假设之前使用keepass管理密码，需要进行导出。1、使用keepass打开数据库文件
2、File，Export，KeePass XML，Export to，OK。
3.2. lastpass导入密码1、点击lastpass图标，Account Options，Advance，Import，Other。
2、Source选择KeePass
3、使用打开xml密码文件，复制，粘贴到Centent。
4、UPLOAD，IMPORT SELECTED或者IMPORT ALL。
3.3. lastpass导出密码1、点击lastpass图标，Account Options，Advance，Export，LastPass CSV File。
2、复制页面的文本，保存为csv文件。
4. 后记和keepass相比，lastpass确实更加实用。个人经验是使用lastpass代替chrome的密码管理，保存管理商密和普密；使用keepass或者其他方法保存核密。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>chrome</tag>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S节点关机后会发生什么？</title>
    <url>/dev-k8s-node-shutdown/</url>
    <content><![CDATA[1. 问题描述当一个k8s节点关机后，k8s会对pod做什么操作？答：k8s发现节点不可用之后，过一段时间节点还没有恢复，会在其他节点上拉起挂掉节点上的pod。
这个回答对，但是只对了一半。因为这个说法只适用于deployment，并不适用于statefulset。


2. 正确解答参考文档：

K8s 节点断开连接后，本在运行的 Pod 会如何？
节点非体面关闭
Kubernetes 1.24: 节点非体面关闭特性进入 Alpha 阶段

k8s节点关闭分为节点体面关闭和节点非体面关闭。
2.1. 节点体面关闭kubelet 会尝试检测节点系统关闭事件并终止在节点上运行的所有 Pod。
在节点终止期间，kubelet 保证 Pod 遵从常规的 Pod 终止流程。
节点体面关闭特性依赖于 systemd，因为它要利用 systemd 抑制器锁机制， 在给定的期限内延迟节点关闭。
节点体面关闭特性受 GracefulNodeShutdown 特性门控控制， 在 1.21 版本中是默认启用的。
在体面关闭节点过程中，kubelet 分两个阶段来终止 Pod：

终止在节点上运行的常规 Pod。
终止在节点上运行的关键 Pod。

2.2. 节点非体面关闭节点关闭的操作可能无法被 kubelet 的节点关闭管理器检测到， 是因为该命令不会触发 kubelet 所使用的抑制锁定机制，或者是因为用户错误的原因， 即 ShutdownGracePeriod 和 ShutdownGracePeriodCriticalPod 配置不正确。
当某节点关闭但 kubelet 的节点关闭管理器未检测到这一事件时， 在那个已关闭节点上、属于 StatefulSet 的 Pod 将停滞于终止状态，并且不能移动到新的运行节点上。 这是因为已关闭节点上的 kubelet 已不存在，亦无法删除 Pod， 因此 StatefulSet 无法创建同名的新 Pod。 如果 Pod 使用了卷，则 VolumeAttachments 不会从原来的已关闭节点上删除， 因此这些 Pod 所使用的卷也无法挂接到新的运行节点上。 所以，那些以 StatefulSet 形式运行的应用无法正常工作。 如果原来的已关闭节点被恢复，kubelet 将删除 Pod，新的 Pod 将被在不同的运行节点上创建。 如果原来的已关闭节点没有被恢复，那些在已关闭节点上的 Pod 将永远滞留在终止状态。
为了缓解上述情况，用户可以手动将具有 NoExecute 或 NoSchedule 效果的 node.kubernetes.io/out-of-service 污点添加到节点上，标记其无法提供服务。 如果在 kube-controller-manager 上启用了 NodeOutOfServiceVolumeDetach 特性门控， 并且节点被通过污点标记为无法提供服务，如果节点 Pod 上没有设置对应的容忍度， 那么这样的 Pod 将被强制删除，并且该在节点上被终止的 Pod 将立即进行卷分离操作。 这样就允许那些在无法提供服务节点上的 Pod 能在其他节点上快速恢复。
在非体面关闭期间，Pod 分两个阶段终止：

强制删除没有匹配的 out-of-service 容忍度的 Pod。
立即对此类 Pod 执行分离卷操作。

3. statefulset处理如果没有配置节点体面关闭，直接关机了一个k8s节点，怎样恢复节点上的statefulset pod？答：强制删除statefulset pod（从k8s apiserver中删除）。例如：
kubectl delete pod mysql-2 -n mysql --force

4. 后记很多问题并不是想象的那样，不能想当然，还是需要好好读一遍k8s文档。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：当K8S遇到PM2</title>
    <url>/dev-k8s-pm2/</url>
    <content><![CDATA[1. 问题描述某个前端项目使用PM2进行node进程的管理，把该项目打包放到了docker镜像中，使用docker run命令可以正常启动。但是，把docker镜像放入K8S集群后，启动报错，[PM2][ERROR] Process failed to launch spawn E2BIG。


2. 解决方案查阅资料发现，上面的问题，是因为环境变量过多引起的。K8S启动时会给容器注入环境变量，K8S集群中的项目数越多，环境变量也就越多。而pm2在启动时会导入系统中的环境变量，当环境变量数量过多时，就会报错[PM2][ERROR] Process failed to launch spawn E2BIG。
使用env或者printenv命令查看容器中的变量，果然环境变量特别多。解决方案也就明确了：减少环境变量。
2.1. 修改pm2源码第一个解决方案是修改pm2源码，过滤掉环境变量。编辑/usr/local/lib/node_modules/pm2/lib/Common.js，修改process.env部分。
function filterDockerEnv(envObj)&#123;	let keys = Object.keys(envObj);	let new_env = &#123;&#125;;	let allowKeys = keys.filter(item =&gt; !item.startsWith(&quot;ENV_HOST_&quot;));	allowKeys.forEach(key =&gt; &#123;		new_env[key] = envObj[key];	&#125;);	return new_env;&#125;var newEnv = filterDockerEnv(env);// Change to double check  (dropped , &#123;pm_cwd: cwd&#125;)app.env = [&#123;&#125;, newEnv, app.env || &#123;&#125;].reduce(function(e1, e2)&#123;	return util._extend(e1, e2);&#125;);

2.2. 清除环境变量第二个方案是在pm2启动前清除系统中的环境变量。正常启动命令前，先执行一段清除系统变量的脚本。
for i in `env | grep -E -i &#x27;SERVICE|HOST|ADDR|PORT&#x27; | sed &#x27;s/=.*//&#x27;` ; do unset $i;done

第二种方案更加灵活，推荐使用这种方式。
3. 书签
一次NodeJS测试集群全线瘫痪的解决思路
一次环境变量引发的血案
[PM2][ERROR] Process failed to launch spawn E2BIG

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>k8s</category>
        <category>frontend</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>前端</tag>
        <tag>问题排查</tag>
        <tag>node</tag>
        <tag>好好学K8S</tag>
        <tag>pm2</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker容器内部使用Supervisor</title>
    <url>/dev-docker-supervisor/</url>
    <content><![CDATA[1. 问题描述使用docker run启动一个beego服务，命令为：
docker run --name bservice -d -p 8080:8080 voidking/bservice:v1 &quot;/bin/bash&quot; &quot;-c&quot; &quot;cd /opt/bservice &amp;&amp; nohup ./bservice&quot;
启动后，beego服务是容器内PID为1的前台进程。假设该服务不是那么健壮，出了bug会停止服务，那么容器也会随之停止。如果想要服务停止后自动启动，那么就需要supervisor出马了。


2. 解决方案参考《CentOS安装配置Supervisor》，在容器中安装supervisor。
1、在/etc/supervisor中新建bservice.conf文件
[program:bservice]directory=/opt/bservicecommand=/opt/bservice/bserviceuser=rootautostart=trueautorestart=truestdout_logfile=/var/log/supervisor/%(program_name)s.logstderr_logfile=/var/log/supervisor/%(program_name)s.log

2、测试启动
systemctl retart supervisordsupervisorctl statusps aux | grep bservice

3、编辑/etc/supervisord.conf，设置supervisor为前台进程
nodaemon=true

4、把容器保存为镜像
docker commit container_id voidking/bservice:v2

5、修改启动命令为
docker run --name bservice -d -p 8080:8080 voidking/bservice:v2 &quot;/bin/bash&quot; &quot;-c&quot; &quot;supervisord -c /etc/supervisord.conf&quot;

使用这种启动方式，哪怕服务因为意外停止，也会马上被supervisor拉起，提高了服务的可用性。
3. 扩展应用再来看另外一个场景：npm run start原本可以正常启动node服务进入后台运行，容器化后通过docker run启动，命令为：
docker run --name fservice -d -p 80:80 voidking/fservice:v1 &quot;/bin/bash&quot; &quot;-c&quot; &quot;cd /opt/fservice &amp;&amp; npm run start&quot;
尴尬的是，容器启动后就会自动退出，这是因为容器运行必须要一个前台进程。因此一个简单的解决办法是修改启动命令，添加tail -f：
docker run --name fservice -d -p 80:80 voidking/fservice:v1 &quot;/bin/bash&quot; &quot;-c&quot; &quot;cd /opt/fservice &amp;&amp; npm run start &gt; ./start.log &amp;&amp; tail -f ./start.log&quot;

但是，更好的办法还是使用supervisor进行node服务的管理。同样需要在容器中安装supervisor，然后进行配置。
1、在/etc/supervisor中新建fservice.conf文件
[program:fservice]directory=/opt/fservicecommand=npm run startuser=rootautostart=trueautorestart=truestdout_logfile=/var/log/supervisor/%(program_name)s.logstderr_logfile=/var/log/supervisor/%(program_name)s.log

2、测试启动
systemctl retart supervisordsupervisorctl statusps aux | grep fservice

3、编辑/etc/supervisord.conf，设置supervisor为前台进程
nodaemon=true

4、把容器保存为镜像
docker commit container_id voidking/fservice:v2

5、修改启动命令为
docker run --name bservice -d -p 8080:8080 voidking/fservice:v2 &quot;/bin/bash&quot; &quot;-c&quot; &quot;supervisord -c /etc/supervisord.conf&quot;]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：CentOS6.8上Python2.6.6升级Python2.7.15</title>
    <url>/dev-centos68-python26-to-python27/</url>
    <content><![CDATA[1. 前言
Python 2.6 is no longer supported by the Python core team, please upgrade your Python.

虽然Python2.6已经不再维护，但是CentOS6.8系统里默认的Python版本依然是2.6.6。这就很尴尬了，要么凑合用，但是没有pip命令，常规安装pip的方法还会失败。要么进行升级，但是整个过程很麻烦。本文记录一下2.6.6凑合用的方法，以及升级2.7.15的方法。


2. 凑合用参考python2.6安装pip。
curl https://bootstrap.pypa.io/2.6/get-pip.py -o get-pip.pypython get-pip.py

3. 升级参考centos6.5升级到python2.7。
1、安装编译依赖
yum -y install gcc openssl-devel bzip2-devel

2、下载安装包并解压
cd /optwget https://www.python.org/ftp/python/2.7.15/Python-2.7.15.tgztar xvzf Python-2.7.15.tgz

3、安装
cd Python-2.7.15./configure --enable-optimizationsmake &amp;&amp; make altinstall
configure时加上enable-optimizations，启用PGO优化，让Python在运行时能变得更快。make 加altinstall参数，避免覆盖原来安装在/usr/bin/python 的版本。
4、检查安装/usr/local/bin/python2.7 -V
5、修改系统默认python
mv /usr/bin/python /usr/bin/python2.6.6 ln -s /usr/local/bin/python2.7 /usr/bin/python

6、解决yum不支持python2.7的问题sed -i &#39;s/python/python2.6.6/&#39; /usr/bin/yum
7、安装pip
python -m ensurepippip -V

或者：
wget https://bootstrap.pypa.io/get-pip.pypython get-pip.py -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com

以上，升级完成。
4. 报错解决4.1. pippip -V 如果报错：pkg_resources.DistributionNotFound: The ‘pip==7.1.0’ distribution was not found and is required by the application
那么根据 python -m ensurepip 输出的版本，修改 /usr/bin/pip 文件里pip的版本。这里我的输出为 pip-9.0.3 ，那么修改结果如下：
#!/usr/bin/python# EASY-INSTALL-ENTRY-SCRIPT: &#x27;pip==7.1.0&#x27;,&#x27;console_scripts&#x27;,&#x27;pip&#x27;__requires__ = &#x27;pip==9.0.3&#x27;import sysfrom pkg_resources import load_entry_pointif __name__ == &#x27;__main__&#x27;:    sys.exit(        load_entry_point(&#x27;pip==9.0.3&#x27;, &#x27;console_scripts&#x27;, &#x27;pip&#x27;)()    )

再次执行 pip -V ，即可看到升级后的pip版本。
4.2. easy_installeasy_install -h 如果报错：pkg_resources.DistributionNotFound: The ‘distribute==0.6.10’ distribution was not found and is required by the application
那么安装distribute模块：
wget https://pypi.python.org/packages/source/d/distribute/distribute-0.6.10.tar.gztar -zxvf distribute-0.6.10.tar.gzcd distribute-0.6.10python setup.py install
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>python</tag>
        <tag>pip</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>LDAP入门篇</title>
    <url>/dev-ldap-start/</url>
    <content><![CDATA[1. LDAP简介
The LDAP(Lightweight Directory Access Protocol) is an open, vendor-neutral, industry standard application protocol for accessing and maintaining distributed directory information services over an Internet Protocol (IP) network. Directory services play an important role in developing intranet and Internet applications by allowing the sharing of information about users, systems, networks, services, and applications throughout the network. As examples, directory services may provide any organized set of records, often with a hierarchical structure, such as a corporate email directory. Similarly, a telephone directory is a list of subscribers with an address and a phone number.

简单来说，LDAP（轻量目录访问协议）是一个协议，用来解决多个系统的统一权限管理问题。
参考文档：

Lightweight Directory Access Protocol
LDAP 协议入门（轻量目录访问协议）



2. LDAP结构LDAP的核心是目录树，准确来说，是DIT（Directory Information Tree 目录信息树）。

LDAP directory servers present data arranged in tree-like hierarchies in which each entry may have zero or more subordinate entries. This structure is called the Directory Information Tree, or DIT.

LDAP 目录服务器将信息以树形的方式组织，每一项都可以包含 0 个或多个子项。这样的结构叫做目录信息树。
目录信息树和文件系统目录树有什么差别？答：目录信息树是信息存储的组织结构，文件系统目录树是文件存储的组织结构。
2.1. Entry在目录信息树中，每一行，都可以叫做一项（Entry），不论是叶子节点还是中间的节点。项包含一个 DN，一些属性，一些对象类。
2.2. Root DSE每个 LDAP 服务器必须对外暴露一个特殊的项，叫做 Root DSE（Root DSA-specific Entry），这个项的 DN 是空字符串。这个项是根节点，描述了 LDAP 服务器自身的信息和能力。例如 LDAP 服务器支持的功能，LDAP 协议版本等信息。
2.3. DNDN（Distinguished Name 分辨名）是用于唯一标识一个项，类似于关系型数据库中的主键。DN也表示项在目录信息树中的位置，DN字符串从左向右，各组成部分依次向树根靠近。
2.4. RDNRDN（Relative Distinguished Name 相对分辨名）就是键值对。DN 由若干个 RDN 组成，以逗号分隔。
2.5. CNCN（Common Name，通用名称）是LDAP（轻量级目录访问协议）目录服务中最基本且最重要的属性之一，用于标识目录中的对象。
2.6. OO（Organization 组织机构、公司），在 DN 中可能会包含 O=公司 这样的组成部分，这里的 O 指代组织机构。
2.7. OUOU（Organization Unit 组织单元、部门），在 DN 中可能会包含 OU=某某部门 这样的组成部分，这里的 OU 指代组织单元。
2.8. DCDC（Domain Component 域名组成）http://example.com 这样的域名，拆成 dc=example,dc=com 这样的形式。
2.9. Object Classes每个项里面包含若干个 Object Classes，Object Class指定了本项中必须、可能包含的属性，相当于 MySQL 中的建表语句。包含某个 Object Class 的项，必须满足 Object Class 中约定的规范，例如规定哪些属性必须存在。
3. 部署ldap serverLDAP是一个协议，约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 LDAP 协议的实现，有着众多版本。例如微软的 Active Directory 是 LDAP 在 Windows 上的实现，AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server，一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。
本节中，我们使用OpenLDAP安装部署一个ldap server。
参考文档：

openldap官网
Quickly Setup LDAP User Directory for Jira
docker下快速部署openldap与PHPLdapAdmin
docker下快速部署openldap与self-service-password

1、下载ldap镜像
docker pull bitnami/openldap:2.5.12-debian-10-r29

网上推荐较多的是osixia/openldap，这里我们选择的是bitnami/openldap。因为bitnami很靠谱，使命就是让开源软件的使用更加简单。
2、启动ldap
mkdir -p /opt/openldapchmod a+w /opt/openldapdocker run --name openldap -d \  -p 1389:1389 \  -p 1636:1636 \  -v /opt/openldap:/bitnami/openldap \  --env LDAP_ROOT=dc=voidking,dc=com \  --env LDAP_ADMIN_USERNAME=admin \  --env LDAP_ADMIN_PASSWORD=adminpassword \  --env LDAP_USERS=customuser \  --env LDAP_PASSWORDS=custompassword \  bitnami/openldap:2.5.12-debian-10-r29

admin用户对应的DN为cn=admin,dc=voidking,dc=com。
如果不指定LDAP_ROOT，那么默认为dc=example,dc=org，admin用户对应的DN为cn=admin,dc=example,dc=org。
3、查看ldap
docker ps | grep openldapdocker logs openldap

4. LDAP Clientldap client也有很多，本文中选择Apache Directory Studio。安装过程比较简单不再展开，这里重点说一下它的使用方法。
1、新建LDAP Connection
2、填写网络参数主机名填写openldap所在主机IP，端口填写1389，Check Network Parameter，没问题的话点击Next
3、填写鉴权参数Bind DN or user填入DN cn=admin,dc=voidking,dc=com，密码填入 adminpassword ，Check Authentication，没问题的话点击Finish
4、查看目录信息树数据
5. 接入LDAP当有平台需要接入LDAP时，一般需要提供以下的参数：

LDAP_SERVER_URI：ldap://192.168.56.102:1389
LDAP_BIND_DN：cn=admin,dc=voidking,dc=com
LDAP_BIND_PASSWORD：adminpassword
LDAP_USER_SEARCH_BASE：ou=staff,dc=voidking,dc=com
LDAP_USER_SEARCH_FILTER：(&amp;(objectClass=organizationalPerson)(sAMAccountName=%s))
LDAP_USER_ATTR_MAP：&#123;&quot;username&quot;=&quot;sAMAccountName&quot;,&quot;display&quot;=&quot;displayName&quot;,&quot;email&quot;=&quot;mail&quot;&#125;

其中，LDAP_USER_SEARCH_BASE、LDAP_USER_SEARCH_FILTER、LDAP_USER_ATTR_MAP 等不确定的参数，可以通过Apache Directory Studio登录LDAP，通过查看目录信息树来进行确认。
测试LDAP参数的方法：1、安装ldapsearch
apt-get install ldap-utils#yum install openldap-clientsldapsearch -v

2、测试LDAP参数
ldapsearch -x -H ldap://192.168.56.102:1389 \  -D &quot;cn=admin,dc=voidking,dc=com&quot; -W \  -b &quot;ou=staff,dc=voidking,dc=com&quot; \  &quot;(&amp;(objectClass=organizationalPerson)(sAMAccountName=%s))&quot;

3、简化测试LDAP参数
ldapsearch -x -H ldap://192.168.56.102:1389 \  -D &quot;cn=admin,dc=voidking,dc=com&quot; -W \  -b &quot;ou=staff,dc=voidking,dc=com&quot; \  &quot;(objectClass=*)&quot; \  namingContexts




]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>bitnami</tag>
        <tag>ldap</tag>
      </tags>
  </entry>
  <entry>
    <title>Redis数据备份和恢复</title>
    <url>/dev-redis-backup-and-recovery/</url>
    <content><![CDATA[1. Redis数据备份概述常用的Redis数据备份方法主要有RDB持久化和AOF持久化。
本文中，我们主要学习一下这两种备份方法的不同，以及RDB备份和恢复方法。


2. Redis数据备份原理2.1. RDB持久化RDB（Redis DataBase）持久化将 Redis 内存中的数据以快照的形式保存到磁盘上，以便在 Redis 重启或者出现故障时进行数据恢复。这种方式可以实现全量备份，数据恢复时比较快，但可能会造成数据丢失。
可以在 Redis 集群运行期间定期执行 RDB 快照备份，也可以手动执行备份命令。
RDB 持久化是通过 fork() 函数创建一个子进程，将 Redis 数据库的状态写入一个临时文件，再用该临时文件替换原有的 RDB 文件的方式实现的。RDB 持久化操作可以在 Redis 主进程不被阻塞的情况下完成。
RDB 持久化适用于需要快速备份和恢复 Redis 数据库状态的场景，例如进行数据迁移、备份和恢复等操作。但由于 RDB 持久化是按照时间间隔或者写操作数量触发的，因此可能会造成一定的数据丢失。
2.2. AOF持久化AOF（Append-Only File）持久化将所有写操作以追加的方式记录到一个文件中，即将 Redis 执行的每个写操作记录到 AOF 文件中。这种方式可以实现增量备份，数据恢复时比较慢，但可以保证数据的完整性。当 Redis 重启或者出现故障时，可以通过重放 AOF 文件中的写操作来恢复 Redis 数据库的状态。
AOF 持久化适用于对数据的完整性要求比较高的场景，例如对数据的实时性要求比较高的缓存场景。但由于 AOF 持久化会将所有写操作都记录下来，因此可能会占用较多的磁盘空间，也会影响 Redis 的性能。
3. RDB备份和恢复本节以集群模式部署的Redis为例，学习具体的RDB备份和恢复步骤。其他模式部署的Redis，备份和恢复方式类似。
3.1. Redis Cluster备份对于 Redis Cluster，我们需要为每个 master 节点创建一个 RDB 文件。可以使用以下方法为每个节点创建 RDB 文件：
1、生成备份文件使用 redis-cli 命令手动执行 BGSAVE
redis-cli -h &lt;host&gt; -p &lt;port&gt; -a &lt;password&gt; bgsave

或者在 redis.conf 配置文件中设置定期自动执行 BGSAVE
save &lt;seconds&gt; &lt;changes&gt;

配置完成后，每隔一段时间，Redis 会将内存中的数据快照保存到磁盘上。
2、收集备份文件从每个 master 节点收集生成的 RDB 文件（dump.rdb），并将其保存在一个安全的位置。这些文件包含了 Redis 集群的完整数据。
注意：在执行备份和恢复操作时，请确保集群处于低负载状态，以免影响集群性能。同时，根据数据量的大小，备份和恢复过程可能需要一定时间，请耐心等待。
3.2. Redis Cluster恢复要恢复 Redis Cluster，首先需要将备份的 RDB 文件分发到各个 master 节点。然后，按照以下步骤操作：
1、停止每个节点上的 Redis 服务
systemctl stop redis

2、放置备份文件将 RDB 文件复制到每个节点的 Redis 数据目录。默认数据目录通常为 /var/lib/redis，文件名应为 dump.rdb。
3、修改 redis.conf 配置文件
dbfilename dump.rdbdir /var/lib/redis

4、启动每个节点上的 Redis 服务
systemctl start redis

5、验证数据恢复是否成功可以使用 redis-cli 命令连接到 Redis Cluster，检查数据是否存在并正确恢复。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>chatgpt</tag>
        <tag>数据库</tag>
        <tag>问题排查</tag>
        <tag>redis</tag>
        <tag>缓存</tag>
      </tags>
  </entry>
  <entry>
    <title>Redis集群模式概述</title>
    <url>/dev-redis-cluster/</url>
    <content><![CDATA[1. Redis集群模式简介Redis集群是Redis的一种高可用解决方案，它通过在多个节点上分片存储数据，实现数据的高可用和扩展。Redis集群最初是在Redis 3.0版本中引入的。
Redis集群的节点由多个主节点和从节点组成，每个主节点都有多个从节点。集群中的每个节点都维护了整个集群的状态信息，节点之间通过Gossip协议进行通信，通过相互交换状态信息来保持集群中节点的一致性。Redis集群使用的哈希槽（hash slot）将键映射到不同的节点，实现了数据的分布式存储。
Redis集群的优点包括：

高可用：当集群中的某个节点故障时，集群仍然可以正常运行。
扩展性：Redis集群可以支持更多的数据和客户端请求，可以通过增加节点来实现横向扩展。
分布式：Redis集群可以将数据分散在不同的节点上，使得数据的读写负载得到了平衡，提高了Redis的性能。

但是Redis集群也存在一些限制和注意事项，例如：

Redis集群只能支持单个数据库空间，不能在不同节点之间存储不同的数据库。
Redis集群不支持事务嵌套和 Lua 脚本中的事务操作。
Redis集群使用的是哈希槽分片，因此在集群运行过程中，不能动态添加或删除哈希槽，否则会导致数据的迁移，对集群性能产生影响。

在实际应用中，如果需要使用Redis集群，需要对集群中的节点进行适当的配置和调优，以确保集群的高可用性、性能和可靠性。
本节内容来自ChatGPT。


2. Redis集群容错机制Redis集群的节点由多个主节点和从节点组成，每个主节点都有多个从节点，从节点和主节点的数据一致。从节点挂掉，不会影响redis集群数据的写入和读取，只会降低读取的性能和容错能力。从节点启动后会自动从主节点同步数据。主节点挂掉，Redis集群会将该主节点的从节点切换为新的主节点（理论上会几秒内完成，实测三分钟没有等到切换，可能是配置问题），以保证数据可用性和一致性，在新的主节点选出来之前，集群写能力不可用。
参考文档：

docker搭建3主3从redis集群(主从容错切换、主从扩容、主从缩容)
K8S部署Redis Cluster集群（三主三从模式）
redis集群节点宕机
深入理解redis cluster的failover机制
CLUSTER FAILOVER
Scaling with Redis Cluster
腾讯云开发者社区 - cluster failover

3. Redis集群操作命令cluster命令查看和操作redis集群。通过给定可选的参数 section ，可以让命令只返回某一部分的信息：

cluster info：打印redis集群的信息
cluster slots：列出集群的槽信息
cluster nodes：列出集群当前已知的所有节点（node），以及这些节点的相关信息
cluster meet &lt;ip&gt; &lt;port&gt;：将 ip 和 port 所指定的节点添加到集群当中，让它成为集群的一份子
cluster forget &lt;node_id&gt;：从集群中移除 node_id 指定的节点
cluster failover：该命令只能在群集slave节点执行，让slave节点进行一次人工故障切换，成为master。切换过程中redis会阻塞请求（10s），切换完成再处理请求。

4. Redis集群重启需求描述：192.168.56.101-103 三台机器，使用7001和7002端口，搭建了redis集群。现在想降低内存碎片率，因此计划对redis集群进行重启。
根据Redis集群容错机制，为了对上游调用的服务影响最小，重启流程如下：
1、查看主节点和从节点
redis-cli -c -h 192.168.56.101 -p 7001 -a &#x27;xxx&#x27; cluster nodesredis-cli --cluster check 192.168.56.101:7001 -a &#x27;xxx&#x27;

其中-c参数的作用是启用集群模式，这样可以自动重定向到正确的节点。
查看到主节点和从节点，以及它们的对应关系。
2、挨个重启从节点（一个节点重启成功再进行下一个）
# 登录192.168.56.101执行redis-cli -c -h 192.168.56.101 -p 7002 -a &#x27;xxx&#x27; shutdownredis-server /path/to/conf/7002.confredis-cli --cluster check 192.168.56.101:7002 -a &#x27;xxx&#x27;# 登录192.168.56.102执行redis-cli -c -h 192.168.56.102 -p 7002 -a &#x27;xxx&#x27; shutdownredis-server /path/to/conf/7002.confredis-cli --cluster check 192.168.56.102:7002 -a &#x27;xxx&#x27;# 登录192.168.56.103执行redis-cli -c -h 192.168.56.103 -p 7002 -a &#x27;xxx&#x27; shutdownredis-server /path/to/conf/7002.confredis-cli --cluster check 192.168.56.103:7002 -a &#x27;xxx&#x27;

3、手动故障转移，从节点升级为主节点
redis-cli -c -h 192.168.56.101 -p 7002 -a &#x27;xxx&#x27; cluster failover TAKEOVERredis-cli -c -h 192.168.56.102 -p 7002 -a &#x27;xxx&#x27; cluster failover TAKEOVERredis-cli -c -h 192.168.56.103 -p 7002 -a &#x27;xxx&#x27; cluster failover TAKEOVER

其中TAKEOVER可选，详情参考腾讯云开发者社区 - cluster failover
4、挨个重启从节点（原本的主节点）
# 登录192.168.56.101执行redis-cli -c -h 192.168.56.101 -p 7001 -a &#x27;xxx&#x27; shutdownredis-server /path/to/conf/7001.confredis-cli --cluster check 192.168.56.101:7001 -a &#x27;xxx&#x27;# 登录192.168.56.102执行redis-cli -c -h 192.168.56.102 -p 7001 -a &#x27;xxx&#x27; shutdownredis-server /path/to/conf/7001.confredis-cli --cluster check 192.168.56.102:7001 -a &#x27;xxx&#x27;# 登录192.168.56.103执行redis-cli -c -h 192.168.56.103 -p 7001 -a &#x27;xxx&#x27; shutdownredis-server /path/to/conf/7001.confredis-cli --cluster check 192.168.56.103:7001 -a &#x27;xxx&#x27;

5、手动故障转移，从节点升级为主节点
redis-cli -c -h 192.168.56.101 -p 7001 -a &#x27;xxx&#x27; cluster failover TAKEOVERredis-cli -c -h 192.168.56.102 -p 7001 -a &#x27;xxx&#x27; cluster failover TAKEOVERredis-cli -c -h 192.168.56.103 -p 7001 -a &#x27;xxx&#x27; cluster failover TAKEOVER

5. 节点不可用问题Redis集群模式，Java访问Redis集群报错：
io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN The cluster is down

排查方法：
redis-cli -c -h 192.168.56.101 -p 7001 -a &#x27;xxx&#x27; cluster nodesredis-cli --cluster check 192.168.56.101:7001 -a &#x27;xxx&#x27;# yum install -y ruby# cd redis-6.2.4/src# ./redis-trib.rb check 192.168.56.101:7001 -a &#x27;xxx&#x27;

发现报错：
[ERR] Nodes don&#x27;t agree about configuration!&gt;&gt;&gt; Check for open slots...[WARNING] Node 192.168.56.101:7002 has slots in importing state ...

这个报错实际上有两个问题：1）有一个节点报错ERR，处于不可用状态，经查是192.168.56.103:7001；2）192.168.56.101:7002节点有一些槽位处于导入状态还未完成。
对于192.168.56.101:7002，解决办法是重启redis节点。
redis-cli -c -h 192.168.56.101 -p 7002 -a &#x27;xxx&#x27; shutdownredis-server /path/to/conf/7002.confredis-cli --cluster check 192.168.56.101:7001 -a &#x27;xxx&#x27;
重启成功。
对于192.168.56.103:7001，也是首先尝试重启redis节点。重启失败，查看日志发现报错：
15914:C 12 Aug 2023 19:07:36.383 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo15914:C 12 Aug 2023 19:07:36.383 # Redis version=6.2.4, bits=64, commit=00000000, modified=0, pid=15914, just started15914:C 12 Aug 2023 19:07:36.383 # Configuration loaded15914:M 12 Aug 2023 19:07:36.384 * monotonic clock: POSIX clock_gettime15914:M 12 Aug 2023 19:07:36.384 # Unrecoverable error: corrupted cluster config file.

解决办法：删除数据目录中的文件，然后重新启动。
重新启动成功，但是该节点启动后独立了，不在原本的集群中，这时需要重新握手加入到集群。
redis-cli --cluster check 192.168.56.103:7002 -a &#x27;xxx&#x27;redis-cli -c -h 192.168.56.101 -p 7001 -a &#x27;xxx&#x27; cluster meet 192.168.56.103 7002redis-cli --cluster check 192.168.56.103:7002 -a &#x27;xxx&#x27;

参考文档：Redis [ERR] Nodes don’t agree about configuration!
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>database</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>chatgpt</tag>
        <tag>数据库</tag>
        <tag>问题排查</tag>
        <tag>redis</tag>
        <tag>缓存</tag>
      </tags>
  </entry>
  <entry>
    <title>Redis入门篇</title>
    <url>/dev-redis-start/</url>
    <content><![CDATA[1. Redis简介
Redis: The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker.

Redis是被数百万开发人员用作数据库、缓存、流引擎和消息代理的开源内存数据存储。
Redis特点：

Redis支持数据的持久化，可以将内存中的数据保存在磁盘中，重启的时候可以再次加载进行使用。
Redis不仅仅支持简单的key-value类型的数据，同时还提供list，set，zset，hash等数据结构的存储。
Redis支持数据的备份，即master-slave模式的数据备份。

Redis优势：

性能极高 – Redis能读的速度是110000次/s，写的速度是81000次/s 。
丰富的数据类型 – Redis支持二进制案例的 Strings， Lists， Hashes， Sets 及 Ordered Sets 数据类型操作。
原子 – Redis的所有操作都是原子性的，意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务，即原子性，通过MULTI和EXEC指令包起来。
丰富的特性 – Redis还支持 publish/subscribe， 通知， key 过期等等特性。

参考文档：

Redis官网
Redis源码
Redis在线测试
Redis - Commands
Redis命令参考
Redis教程
Redis【数据结构类型篇】



2. 安装Redis2.1. Redis安装模式Redis有四种安装（部署）模式：

单机模式：只有一个节点。简单，但是可用性低。
主从复制：全部节点读，一个节点作为主节点负责写，写数据后复制到从节点。主节点挂掉时，我们可以把从节点手动变成主节点，这样应用方就恢复可用了。但是，应用方需要感知主节点变化并随着变化，否则无法写。
哨兵模式：在主从复制的基础上，哨兵实现了自动化的故障恢复。哨兵模式包括哨兵集群和数据集群，哨兵集群由一个或多个哨兵节点组成，不存储数据；数据集群包括主节点和从节点，存储数据。访问redis集群的数据都是经过哨兵集群的，哨兵集群负责数据的转发。哨兵集群高可用，数据集群高可用，主节点挂掉发生切换，应用方无感知。
集群模式：单机、主从和哨兵模式中，单个节点的存储能力是有上限的，访问能力是有上限的。集群模式把数据进行分片存储，当节点存储能力达到上限时，我们可以增加节点，然后重新分片实现槽位迁移。集群模式具有高可用、可扩展、分布式、容错等特性。

参考文档：

Redis的四种模式，单机、主从、哨兵、集群

2.2. Redis单机模式本节中，我们使用docker安装一个redis节点。
1、下载redis镜像，这里选择版本 redis:5.0.14-alpine3.15
docker pull redis:5.0.14-alpine3.15

2、启动redis
docker run --name redis -d \  -p 6379:6379 \  redis:5.0.14-alpine3.15

PS：启动redis并设置密码
docker run --name redis -d \  -p 6379:6379 \  redis:5.0.14-alpine3.15 \  --requirepass &quot;xxx&quot;

参考文档：

Installing Redis
《K8S中安装Redis》

2.3. 自定义Redis配置1、获取 redis.conf访问对应版本的github redis.conf，获取 redis.conf
2、准备redis5.0的配置文件 
mkdir -p /opt/redis/vim /opt/redis/redis.conf

按需修改，建议修改的配置为：

requirepass：删除注释，配置一个密码
bind：改为 0.0.0.0

3、使用配置文件启动redis
docker run --name redis -d \  -p 6379:6379 \  -v /opt/redis/redis.conf:/etc/redis/redis.conf \  redis:5.0.14-alpine3.15 \  redis-server /etc/redis/redis.conf

2.4. 安装Redis主从模式参考文档《K8S中安装配置Redis》
2.5. 安装Redis哨兵模式参考文档《K8S中安装配置Redis》
2.6. 安装Redis集群模式参考文档：

《Redis集群模式概述》
《K8S中安装配置Redis》

3. 测试Redis3.1. 容器内测试docker exec -it redis /bin/shredis-cli&gt; ping

3.2. 宿主机测试1、启动测试容器
docker run --name redis-client -d redis:5.0.14-alpine3.15

2、测试
docker exec -it redis-client redis-cli -h 192.168.56.102 -p 6379&gt; ping

4. 重启Redis4.1. docker redisdocker restart redis

4.2. bin redis方法一：
/usr/local/bin/redis-server stop/usr/local/bin/redis-server start /opt/redis/redis.conf# /usr/local/bin/redis-server restart

方法二：
redis-cli -h 127.0.0.1 -p 6379 shutdownredis-server

方法三：
ps -ef | grep rediskill -9 xxxredis-server

5. Redis可视化工具5.1. RedisInsightRedisInsight is supported on multiple operating systems: Linux, Windows, and macOS.
RedisInsight supports all Redis deployments. Whether you use Redis Open Source, Redis Stack, Redis Enterprise Software, Redis Enterprise Cloud, or Amazon ElastiCache, RedisInsight makes it easy to interact with your data and your application.
官方网站：RedisInsight
5.2. vscode-database-clientDatabase Client for Visual Studio Code是Visual Studio Code的数据库客户端扩展，支持MySQL/MariaDB、PostgreSQL、SQLite、Redis、ClickHouse、达梦以及ElasticSearch的管理，且可以作为一个SSH客户端。
6. Redis数据结构参考文档：Redis【数据结构类型篇】Redis的Key一定是String，Value的数据结构类型包括String、List、Hash、Set、ZSet，每种类型都有自己的操作命令。Redis的不同数据结构类型适用于不同的场景。
6.1. String特点：String类型是redis最常用的数据结构类型，存储的值为字符串，也可以是数据。
应用场景：i. 做与统计有关的业务，如新浪微博（微信朋友圈）中的点赞功能。相关命令：incr、incrby、decr、decrby。
ii. 解决多线程的线程安全问题。redis的key是单线程模式，这就意味一瞬间只有一个线程能够持有这个key，所以可以使用redis解决部分涉及线程安全的业务，比如说抢购、秒杀。
6.2. List特点：

基于Linked List实现。
元素是字符串类型。
列表头尾增删快，中间增删慢，增删元素是常态。
从左至右，从0开始，从右至左，从-1开始。
元素可以重复出现。
最多包含2^32-1元素。

应用场景：i. 处理顺序类业务。如新浪微博评论、论坛回帖楼层等。ii. 聊天室。
6.3. Hash特点：

hash内容由field和与之关联的value组成map键值对组成
key、field和value是字符串类型
一个hash中最多包含2^32-1键值对

应用场景：i. 节省资源redis每创建一个键，都会为这个键储存一些附加的管理信息（比如这个键的类型，这个键最后一次被访问的时间等等），因此redis的key相对于值来说，更珍贵！！！reids数据库中的键越多，redis数据库服务器在储存附加管理信息方面耗费的内存就越多，在获取key对应的value值时cpu的开销也会更多。Hash结构可以将具有关联关系的一组key-value，存储到同一个hash结构中，从而减少key的数量。因此能使用hash的时候尽量使用hash，这也是hash类型的应用场景。
Hash类型不适用的场景：i. 对某一个field设置过期时间如果我们仅仅只对一个字段设置过期，就不能使用hash。因为Redis的key的过期功能只能对键操作，而Hash结构不能单独对某一个filed设置过期功能。
6.4. Set特点：

无序的、无重复的
元素是字符串类型
最多包含2^32-1元素

应用场景：i. 新浪微博的共同关注当用户访问另一个用户的时候，会显示出两个用户共同关注哪些相同的用户。将每个用户关注的用户放在集合中，求交集即可。
6.5. ZSet特点：

类似Set集合
有序的、无重复的
元素是字符串类型
每一个元素都关联着一个浮点数分值（Score），并按照分值从小到大的顺序排列集合中的元素。注意：分值可以相同
最多包含2^32-1元素

适用场景：i. 适用于需要有序且唯一的业务或操作：网易音乐排行榜每首歌的歌名作为元素（唯一、不重复）；每首歌的播放次数作为分值；使用zrevrange 命令来获取播放次数最多的歌曲
7. Redis常用命令参考文档：Redis命令参考
特别注意：redis命令不区分大小写。
7.1. 数据库操作7.1.1. 建立连接redis-cli -h 192.168.56.102 -p 6379# docker exec -it redis-client redis-cli -h 192.168.56.102 -p 6379
请求客户端与服务器建立连接。
7.1.2. 退出连接quit
请求服务器关闭与当前客户端的连接。一旦所有等待中的回复(如果有的话)顺利写入到客户端，连接就会被关闭。
7.1.3. 设置密码通过设置配置文件中 requirepass 项的值(使用命令 CONFIG SET requirepass password )，可以使用密码来保护 Redis 服务器。
如果开启了密码保护的话，在每次连接 Redis 服务器之后，就要使用 AUTH 命令解锁，解锁之后才能使用其他 Redis 命令。
如果 AUTH 命令给定的密码 password 和配置文件中的密码相符的话，服务器会返回 OK 并开始接受命令输入。
另一方面，假如密码不匹配的话，服务器将返回一个错误，并要求客户端需重新输入密码。
1、设置密码
config set requirepass secret_password   # 将全局密码设置为 secret_passwordquit

2、使用密码
pingauth secret_password                     # 输入正确的密码ping 

设置了密码保护之后，如果想要直接通过redis-cli直接执行redis命令，可以加上-a参数。
docker exec -it redis-client redis-cli -h 192.168.56.102 -p 6379 -a &#x27;secret_password&#x27; ping

7.1.4. 新增用户并授权ACL SETUSER alice on +@read +@allchannels#ACL SETUSER alice on allkeys +get +setACL LISTACL GETUSER aliceACL SETUSER alice &gt;alice_passwordAUTH alice alice_password# ACL DELUSER alice

参考文档：ACL（Redis Access Control List）
7.1.5. 指定用户连接Redis方法一：
redis-cli -h 192.168.56.102 -p 6379&gt; AUTH alice alice_password

方法二：
redis-cli -h 192.168.56.102 -p 6379 -u alice -p alice_password

方法三：
redis-cli -u redis://alice:alice_password@192.168.56.102:6379

参考文档：how to login redis 6.0+ use username and password
7.1.6. 切换数据库使用select切换到指定的数据库，数据库索引号 index 用数字值指定，以 0 作为起始索引值。
SET db_number 0SELECT 1 GET db_numberSET db_number 1GET db_number

7.1.7. 统计信息以一种易于解释（parse）且易于阅读的格式，返回关于 Redis 服务器的各种信息和统计数值。通过给定可选的参数 section ，可以让命令只返回某一部分的信息：

info server： 一般 Redis 服务器信息
info clients： 已连接客户端信息
info memory： 内存信息
info persistence： RDB 和 AOF 的相关信息
info stats： 一般统计信息
info replication： 主/从复制信息
info cpu： CPU 计算量统计信息
info commandstats： Redis 命令统计信息
info cluster： Redis 集群信息
info keyspace： 数据库相关的统计信息
info all： 返回所有信息
info default： 返回默认选择的信息

当不带参数直接调用 INFO 命令时，使用 default 作为默认参数。
info memoryinfo stats

7.2. 测试7.2.1. pingping使用客户端向 Redis 服务器发送一个 PING ，如果服务器运作正常的话，会返回一个 PONG 。通常用于测试与服务器的连接是否仍然生效，或者用于测量延迟值。
7.2.2. echo打印一个特定的信息 message ，测试时使用。
echo &quot;test&quot;

7.3. String类型操作7.3.1. 设置keyselect 3set key_name key_value

7.3.2. 查询keyselect 3get key_namekeys *

7.3.3. 遍历keySCAN cursor [MATCH pattern] [COUNT count]SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代（incrementally iterate）一集元素（a collection of elements）：

SCAN 命令用于迭代当前数据库中的数据库键。
SSCAN 命令用于迭代集合键中的元素。
HSCAN 命令用于迭代哈希键中的键值对。
ZSCAN 命令用于迭代有序集合中的元素（包括元素成员和元素分值）。

7.3.4. 查看key数量dbsize：返回当前数据库的 key 的数量。
7.4. Hash类型操作
hkeys key：返回哈希表 key 中的所有域。
hget key field：返回哈希表 key 中给定域field的值
hlen key：返回哈希表 key 中域的数量。
hgetall key：返回哈希表 key 中，所有的域和值。
hexists key field：查看哈希表 key 中，给定域 field 是否存在。
hset key field value：将哈希表 key 中的域 field 的值设为 value 。
hdel key field [field ...]：删除哈希表 key 中的一个或多个指定域，不存在的域将被忽略。

7.5. Redis集群操作参考文档：《Redis集群模式概述》
8. 内存碎片率说明使用info memory可以查看内存碎片率，内存碎片率计算方法为：mem_fragmentation_ratio = used_memory_rss / used_memory
内存碎片率大小说明：

mem_fragmentation_ratio &lt; 1 表示Redis内存分配超出了物理内存，操作系统正在进行内存交换，内存交换会引起非常明显的响应延迟；
mem_fragmentation_ratio &gt; 1 是合理的；
mem_fragmentation_ratio &gt; 1.5 说明Redis消耗了实际需要物理内存的150%以上，其中50%是内存碎片率，可能是操作系统或Redis实例中内存管理变差的表现

手动清理内存碎片方法：
memory purge

但是，对于redis5.0.14，这个内存碎片率的说明并不适用，经测试查看到的内存碎片率为13-15，远大于1。参考文档Commands - INFO和Redis 5.0.11 high memory fragmentation ratio可知：

allocator_frag_ratio:: Ratio between allocator_active and allocator_allocated. This is the true (external) fragmentation metric (not mem_fragmentation_ratio).


you are looking at mem_fragmentation_ratio which is not actually fragmentation. It’s the ratio of used memory to RSS (resident physical memory), We don’t rename if for the sake of backwards compatibility.RSS includes shared libraries, code, stack and other things, so while this metric is useful, it’s not actually fragmentation (which has risk of rising to high values).If you wanna look at fragmentation, you should look at allocator_frag_ratio instead.But another point is that your db is nearly empty, so although the ratio seems high, it’s not actually a lot of memory.allocator_frag_bytes shows just about 400kb.You only wanna be concerned if that number is high, and in that case you can enable activedefrag

因此，对于redis5.0.14和更高版本，内存碎片率应该查看 allocator_frag_ratio 而不是 mem_fragmentation_ratio。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>database</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>数据库</tag>
        <tag>redis</tag>
        <tag>缓存</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker Compose实现滚动更新</title>
    <url>/dev-docker-compose-rolling-update/</url>
    <content><![CDATA[1. 前言本文中，基于Docker Compose实现服务滚动更新。具体来说，webapp想要从v1.0更新到v2.0，更新过程中服务不能中断，如果v2.0有问题那么就停止更新并回退。
参考文档：

《Docker-Compose入门篇》
谁说docker-compose不能水平扩展容器、服务多实例？



2. 滚动更新思路1、增加一个新版本容器2、验证新版本容器3、删除老版本容器，增加新版本容器
3. docker-compose配置3.1. docker-compose.ymldocker-compose.yml 定义如下：
version: &quot;3&quot;services:  webapp:    image: &quot;voidking/nginx:v1.0&quot;    deploy:      replicas: 2  nginx:    image: nginx:latest    volumes:      - type: bind        source: /root/rollingupdate/nginx.conf        target: /etc/nginx/nginx.conf    depends_on:      - webapp    ports:      - &quot;80:80&quot;

3.2. 准备nginx配置多个容器之间，通过nginx进行负载均衡，负载均衡基于 Docker’s embedded DNS。
nginx.conf 定义如下：
user  nginx;events &#123;  worker_connections   1000;&#125;http &#123;  server &#123;    listen 80;    location / &#123;      proxy_pass http://webapp:80;    &#125;  &#125;&#125;

4. 滚动更新操作1、修改webapp版本修改 docker-compose.yml ，webapp镜像改为 voidking/nginx:v2.0
2、使用新版本扩容出一个容器
docker-compose up -d --scale webapp=3 --no-recreate

3、验证新版本容器
4、删除老版本容器
docker ps | grep &quot;voidking/nginx:v1.0&quot; | awk &#x27;&#123;print $1&#125;&#x27; | xargs docker rm -f 

5、扩容新版本容器
docker-compose up -d --scale webapp=2 --no-recreate

当容器数量比较多时，4和5可以进行改成删一个旧容器，加一个新容器，删一个旧容器，加一个新容器。。。
5. 自动化脚本实现未完待续。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>docker-compose</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker Compose构建镜像并启动容器</title>
    <url>/dev-docker-compose-with-build/</url>
    <content><![CDATA[1. 前言本文中，我们学习使用Compose构建镜像，并拉起容器。
参考文档：

《Docker-Compose入门篇》
《Dockerfile入门篇》
Docker用户指南
Dockerfile参考

2. webapp + redis1、创建测试目录
mkdir composetestcd composetest

2、在项目目录中创建一个名为app.py的文件，内容为：
import timeimport redisfrom flask import Flaskapp = Flask(__name__)cache = redis.Redis(host=&#x27;redis&#x27;, port=6379)def get_hit_count():    retries = 5    while True:        try:            return cache.incr(&#x27;hits&#x27;)        except redis.exceptions.ConnectionError as exc:            if retries == 0:                raise exc            retries -= 1            time.sleep(0.5)@app.route(&#x27;/&#x27;)def hello():    count = get_hit_count()    return &#x27;Hello World! I have been seen &#123;&#125; times.\n&#x27;.format(count)
在此示例中，redis是应用程序网络上的redis容器的主机名，使用默认端口6379。
3、创建requirements.txt文件，内容为：
flaskredis

4、创建Dockerfile，内容为：
FROM python:3.7-alpineWORKDIR /codeENV FLASK_APP app.pyENV FLASK_RUN_HOST 0.0.0.0RUN apk add --no-cache gcc musl-dev linux-headersCOPY requirements.txt requirements.txtRUN pip install -r requirements.txtCOPY . .CMD [&quot;flask&quot;, &quot;run&quot;]

该文件说明：

从Python 3.7镜像像开始构建镜像。
将工作目录设置为/code。
设置flask命令使用的环境变量。
安装gcc，以便诸如MarkupSafe和SQLAlchemy之类的Python包可以编译加速。
复制requirements.txt并安装Python依赖项。
复制当前目录中的所有文件到工作目录。
将容器的默认命令设置为flask run。

5、创建docker-compose.yml，内容为：
version: &#x27;3&#x27;services:  web:    build: .    ports:      - &quot;15000:5000&quot;  redis:    image: &quot;redis:alpine&quot;
该Compose文件定义了两个服务：Web和Redis。Web服务从Dockerfile构建镜像，启动后容器在5000端口提供服务，然后将容器端口5000绑定到主机端口15000。
6、使用Compose构建和运行应用
docker-compose up

7、测试应用
curl http://127.0.0.1:15000

3. webapp + redis + 挂载1、编辑docker-compose.yml，挂载当前目录到/code
version: &#x27;3&#x27;services:  web:    build: .    ports:      - &quot;5000:5000&quot;    volumes:      - .:/code    environment:      FLASK_ENV: development  redis:    image: &quot;redis:alpine&quot;

2、运行应用
docker-compose up

3、修改app.py
import timeimport redisfrom flask import Flaskapp = Flask(__name__)cache = redis.Redis(host=&#x27;redis&#x27;, port=6379)def get_hit_count():    retries = 5    while True:        try:            return cache.incr(&#x27;hits&#x27;)        except redis.exceptions.ConnectionError as exc:            if retries == 0:                raise exc            retries -= 1            time.sleep(0.5)@app.route(&#x27;/&#x27;)def hello():    count = get_hit_count()    return &#x27;Hello World 2.0! I have been seen &#123;&#125; times.\n&#x27;.format(count)

4、测试应用
curl http://127.0.0.1:15000


]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
        <category>storage</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>网络</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>存储</tag>
        <tag>docker-compose</tag>
        <tag>dockerfile</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker-Compose入门篇</title>
    <url>/dev-docker-compose/</url>
    <content><![CDATA[1. Docker-Compose简介Docker-Compose（简称Compose）是用于定义和运行多容器Docker应用程序的工具。通过Compose，可以使用YAML文件来配置应用程序的服务。然后使用一个命令，就可以从配置中创建并启动所有服务。
本文中，我们来学习一下Compose的基础用法。
参考文档：

Overview of Docker Compose
Docker 入门教程
docker/compose



2. 安装Compose在CentOS7机器上，假设已经了Docker，参考Install Docker Compose安装Compose-Docker。如果没有安装Docker，那么参考Docker入门进行安装。
1、安装docker-compose 1.24.1 版本
curl -L &quot;https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose

1、安装docker-compose 2.34.0
wget https://github.com/docker/compose/releases/download/v2.34.0/docker-compose-linux-x86_64 -O /usr/local/bin/docker-compose

2、添加执行权限
chmod +x /usr/local/bin/docker-composeln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

3、验证安装
docker-compose --version

3. Compose运行容器1、定义 docker-compose.yml
version: &#x27;3&#x27;services:  nginx:    image: nginx:latest    container_name: vk-nginx    ports:      - &quot;80:80&quot;      - &quot;443:443&quot;

2、启动容器在 docker-compose.yml 文件所在目录，执行命令
docker-compose up -d

执行之后，会启动一个名为 vk-nginx 的容器，主机端口80映射到容器端口80，主机端口443映射到容器端口443。
上面的配置和启动命令，实际上相当于
docker run --name vk-nginx -d \  -p 80:80 \  -p 443:443 \  nginx:latest

4. Compose常用命令4.1. 创建/销毁容器# 创建并运行容器docker-compose up# 创建并后台运行容器docker-compose up -d# 关闭并删除容器docker-compose down# 关闭并删除容器，连带删除容器挂载卷docker-compose down -vdocker-compose down --volumes

4.2. 启动/停止容器# 关闭容器但不删除容器docker-compose stop# 启动容器docker-compose start

4.3. 查看容器# 查看容器docker-compose psdocker-compose ps -a# 查看web服务的环境变量docker-compose run web env

4.4. 指定compose配置文件执行 docker-compose 时，默认配置文件是当前目录中的 docker-compose.yml 或 docker-compose.yaml ，某些情况下我们想要手动指定配置文件，可以使用 -f 参数或者 COMPOSE_FILE 环境变量。
docker-compose -f my-compose.yaml up -ddocker-compose -f my-compose1.yaml -f my-compose2.yaml up -dCOMPOSE_FILE=my-compose.yml docker-compose up -dCOMPOSE_FILE=my-compose1.yml,my-compose2.yml docker-compose up -d

5. Compose常用配置5.1. 自定义启动命令version: &#x27;3&#x27;services:  test:    image: alpine:latest    container_name: test    command: [&#x27;tail&#x27;,&#x27;-f&#x27;,&#x27;/dev/null&#x27;]

5.2. 使用环境变量version: &#x27;3&#x27;services:  test:    image: alpine:latest    container_name: test    command: [&#x27;tail&#x27;,&#x27;-f&#x27;,&#x27;/dev/null&#x27;]    environment:      MYSQL_HOST: &#x27;192.166.56.100&#x27;      MYSQL_PORT: &#x27;3306&#x27;      MYSQL_USER: &#x27;root&#x27;      MYSQL_PASSWORD: &#x27;mysql123&#x27;

5.3. 容器挂掉后自动拉起version: &#x27;3&#x27;services:  test:    image: alpine:latest    container_name: test    command: [&#x27;tail&#x27;,&#x27;-f&#x27;,&#x27;/dev/null&#x27;]    restart: always

5.4. 使用volumedocker使用持久化存储的时候，有两种方式：

bind mount方式：挂载宿主机的绝对路径（目录和文件都可以）到容器
volume方式：挂载volume（本质是目录）到容器

使用bind mount方式，如果宿主文件夹是空的，不光容器内的文件不会被复制到宿主机器上，容器的内容还会被覆盖为空，对于一些容器内路径文件夹存放config文件的情况，宿主目录为空会导致容器内的对应目录被清空，造成启动错误的问题。使用volume方式，容器内的文件会被保留在volume中（对应的_data文件夹）内部，也就是文件被复制到宿主机器上.
两种方式都可以使用-v(–volume)和–mount参数，但是，在创建bind mount的时候，使用–volume会自动创建宿主方的文件夹，而–mount不会，因此使用–mount映射不存在的目录，会报错。
docker-compose指定的时候，有一些区别：

使用bind mount的时候，宿主这边可以使用相对路径了(./)，不需要显示指定($PWD)
可以在指定volume名称的同时创建volume，需要使用volume:的配置项

以下是一个使用volumes的示例：
services:  ...  dolphinscheduler-master:    volumes:      - dolphinscheduler-logs:/opt/dolphinscheduler/logs      - dolphinscheduler-shared-local:/opt/soft      #- /xxx/mysql-connector.jar:/opt/dolphinscheduler/libs/mysql-connector.jar      - type: bind        source: /xxx/mysql-connector.jar        target: /opt/dolphinscheduler/libs/mysql-connector.jar

参考文档：

关于使用docker volume挂载的注意事项

5.5. 使用expose和ports参考文档：

Difference Between Expose and Ports in Docker Compose
docker compose run

5.5.1. exposeexpose暴露的端口，可由连接到同一网络的其他服务访问，但不会在宿主机上发布。例如：
services:  myapp1:    ...    expose:      - &quot;3000&quot;      - &quot;8000&quot;  myapp2:    ...    expose:      - &quot;5000&quot;

docker ps结果如下：
CONTAINER ID   IMAGE    COMMAND     CREATED     STATUS      PORTS               NAMES8673c14f18d1   ...      ...         ...         ...         3000/tcp, 8000/tcp  bael_myapp1bc044e180131   ...      ...         ...         ...         5000/tcp            bael_myapp2

5.5.2. portsports暴露的端口，可由连接到同一网络的其他服务访问，并在宿主机上发布。例如：
services:  myapp1:    ...    ports:    - &quot;3000&quot;                             # container port (3000), assigned to random host port    - &quot;3001-3005&quot;                        # container port range (3001-3005), assigned to random host ports    - &quot;8000:8000&quot;                        # container port (8000), assigned to given host port (8000)    - &quot;9090-9091:8080-8081&quot;              # container port range (8080-8081), assigned to given host port range (9090-9091)    - &quot;127.0.0.1:8002:8002&quot;              # container port (8002), assigned to given host port (8002) and bind to 127.0.0.1    - &quot;6060:6060/udp&quot;                    # container port (6060) restricted to UDP protocol, assigned to given host (6060)

docker ps结果如下：
CONTAINER ID   ... PORTS                                                                        NAMESe8c65b9eec91   ... 0.0.0.0:51060-&gt;3000/tcp, 0.0.0.0:51063-&gt;3001/tcp, 0.0.0.0:51064-&gt;3002/tcp,   bael_myapp1                   0.0.0.0:51065-&gt;3003/tcp, 0.0.0.0:51061-&gt;3004/tcp, 0.0.0.0:51062-&gt;3005/tcp,                    0.0.0.0:8000-&gt;8000/tcp, 0.0.0.0:9090-&gt;8080/tcp, 0.0.0.0:9091-&gt;8081/tcp                   127.0.0.1:8002-&gt;8002/tcp, 0.0.0.0:6060-&gt;6060/udp

ports长语法：
services:   myapp1:  ...  ports:  # - &quot;127.0.0.1:6060:6060/udp&quot;  - target: 6060    host_ip: 127.0.0.1    published: 6060    protocol: udp    mode: host

5.5.3. 小结这两种暴露方式，最大的区别在于是否在宿主机上暴露端口，是否对外部网络提供服务。使用docker ps查看容器时，根据PORTS字段，我们也能判断出该容器是否在宿主机上暴露端口。
但是，个别情况下，虽然我们看到的是端口是3000/tcp，但是依然可能在宿主机上暴露端口。可能是docker/docker-compose某个版本的bug，也可能是yaml文件中的version不匹配，也可能是其他什么原因。这种情况下，只能找到docker-compose.yml，查看端口映射关系，然后查看宿主机端口开放情况。
5.6. 共享IPCversion: &#x27;3&#x27;services:  first:    image: busybox    ipc: shareable    container_name: share_container    command: [&#x27;tail&#x27;,&#x27;-f&#x27;,&#x27;/dev/null&#x27;]  second:    image: busybox    ipc: &#x27;container:share_container&#x27;    command: [&#x27;tail&#x27;,&#x27;-f&#x27;,&#x27;/dev/null&#x27;]    depends_on:      - &#x27;first&#x27;


6. Docker-Compose网络问题6.1. 问题描述宿主机执行 docker-compose up -d 之后，宿主机突然无法从其他主机 ping 通了。
6.2. 问题排查查看路由，发现路由中存在和主机网络冲突的路由。
ip routeroute -n

查看docker网段使用情况，发现冲突的路由是由docker compose创建的。
docker network listdocker network inspect xxx

6.3. 原因分析和解决办法原因分析：docker-compose up的时候，会自动创建一个网桥，占用一个网段，并且添加路由规则。如果这个网段和现有网段发生了冲突，就会导致一些非预期的网络问题，比如主机突然访问不通了。
解决办法：指定容器网段。详情参考docker-compose up使用自定义的网段的两种方式
例如指定compose启动的容器网段为 192.168.51.0/24
version: &#x27;2.3&#x27;networks:  harbor:    driver: bridge    ipam:      driver: default      config:      - subnet: 192.168.51.0/24        gateway: 192.168.51.1services:  registry:    image: goharbor/registry-photon:v2.3.3    container_name: registry    restart: always    ...    networks:      - harbor






]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
        <category>storage</category>
        <category>network</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>存储</tag>
        <tag>问题排查</tag>
        <tag>docker-compose</tag>
      </tags>
  </entry>
  <entry>
    <title>Alpine入门篇</title>
    <url>/dev-alpine-start/</url>
    <content><![CDATA[1. Alpine简介Alpine Linux是基于musl libc和 busybox 的面向安全的轻量级Linux发行版。Alpine的VM镜像大小113M，Alpine的Docker镜像大小只有2M！
Alpine由非商业组织维护的，支持广泛场景的Linux发行版，它特别为资深/重度Linux用户而优化，关注安全，性能和资源效能。Alpine 镜像可以适用于更多常用场景，并且是一个优秀的可以适用于生产的基础系统/环境。
第一次接触alpine，是使用它跑单测。gitlab设置代码提交后触发单测，而这个单测，可以使用alpine来进行。但是因为时区问题，跑出的单测结果和预期不同。因此，本文以修改Alpine时区为引子，简单学习了解一下Alpine。
PS：与Alpine相似的一个小系统CirrOS的VM镜像大小只有12M，CirrOS的Docker镜像大小只有10M。


2. Alpine包管理Alpine采用了 musl libc 和 busybox 以减小系统的体积和运行时资源消耗，但功能上比 busybox 又完善的多，因此得到开源社区越来越多的青睐。
在保持瘦身的同时，Alpine还提供了自己的包管理工具apk。参考文档：

Alpine Linux package management
Alpine Linux下的包管理工具。

常用命令：
# 更新最新本地镜像源apk update # 升级软件apk upgrade # 指定升级部分软件包apk add --upgrade xxx # 安装包apk add xxx# 搜索包，支持正则apk search xxxapk search -v &#x27;xx*&#x27;apk search -v -d &#x27;xxx&#x27;# 查看包详细信息apk info -a xxx# 查看包列表apk show# 卸载并删除包apk del xxx

使用apk add命令时，往往下载缓慢或者下载失败。参考清华大学Alpine镜像使用帮助，修改镜像源。
sed -i &#x27;s/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g&#x27; /etc/apk/repositories

3. 编译依赖包build-basebuild-base里面包含gcc、libc、make、g++
apk add --no-cache build-baseapk info -a build-base

4. 修改时区Alpine修改时区，主要参考修改使用Alpine Linux的Docker容器的时区。
1、下载apline镜像
docker pull alpine:3.7.3

2、启动apline容器&amp;登录进入容器
docker run -d --name alpine_timezone alpine:3.7.3 sleep 3000docker exec -it alpine_timezone /bin/sh

3、安装timezone
apk add -U tzdatals /usr/share/zoneinfo

4、拷贝需要的时区文件到localtime
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

5、验证时区
date

6、精简镜像
apk del tzdata

7、保存镜像
exitdocker commit alpine_timezone voidking/alpine:v1.0docker logindocker push

以上，完成了Alpine镜像时区的修改。
此外，还有一种更简单的方法，在启动Alpine容器时映射宿主机的时区文件。
docker run --name alpine -it -d -v /etc/localtime:/etc/localtime alpine:3.7.3

或者更简单一些，使用Dockerfile构建新镜像，把宿主机/etc/localtime拷贝到alpine镜像中。
5. 问题排查5.1. pip包安装失败python:3.8.8-alpine3.12 中，安装python包bcrypt==3.2.2，报错：
  unable to execute &#x27;gcc&#x27;: No such file or directory  error: command &#x27;gcc&#x27; failed with exit status 1  ----------------------------------------  ERROR: Failed building wheel for cffi  Running setup.py clean for cffiFailed to build cffiInstalling collected packages: pycparser, wheel, setuptools, cffi    Running setup.py install for cffi: started    Running setup.py install for cffi: finished with status &#x27;error&#x27;    ERROR: Command errored out with exit status 1:

原因：从bcrypt文档可知，bcrypt有一些依赖需要提前安装
解决办法：安装gcc依赖
apk add --update musl-dev gcc cargo

再次安装bcrypt==3.2.2，报错：
  ERROR: Failed building wheel for cffi  Running setup.py clean for cffiFailed to build cffi

解决办法：安装libffi-dev
apk add libffi-dev

参考文档：

bcrypt
libffi / ffi.h issue on alpine-python:3.6-onbuild

5.2. 软件包下载失败apk add libffi-devel报错：
ERROR: unsatisfiable constraints:  libffi-devel (missing):    required by: world[libffi-devel]

这是因为软件仓库没有这个软件，可以在Alpine’s package search网站上搜索，看看包是否存在，叫什么名字。
这里可以搜索关键字libffi*，发现在alpine中，没有libffi-devel，只有libffi-dev，安装它即可。
参考文档How to resolve missing dependencies in Docker Alpine
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>docker</tag>
        <tag>问题排查</tag>
        <tag>alpine</tag>
        <tag>busybox</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：CentOS7部署beego项目</title>
    <url>/dev-centos7-beego-env/</url>
    <content><![CDATA[1. 前言《beego入门篇——上、中、下》三篇文章中，记录了beego的基本用法。假设已经完成了beego项目的代码，此时我们需要把项目部署到服务器上，本文就研究一下怎样在CentOS7环境里部署beego服务。


2. 环境安装2.1. golang安装golang，主要参考centos7安装golang环境。
1、golang官网查找安装包
2、安装golangwget https://dl.google.com/go/go1.12.7.linux-amd64.tar.gz
3、解压至/usr/local目录tar -C /usr/local -xzf go1.12.7.linux-amd64.tar.gz
4、配置环境变量vim /etc/profile，添加：
export GOROOT=/usr/local/goexport GOPATH=$HOME/goexport PATH=$PATH:$GOROOT/bin:$GOPATH/bin

source /etc/profile使修改生效。
5、查看安装
go versiongo env

2.2. beego安装1、安装beegogo get -v github.com/astaxie/beego
如果下载缓慢，可以设置GOPROXY。
export GOPROXY=https://goproxy.ioexport GO111MODULE=on

2、安装bee工具go get -v github.com/beego/bee
3、创建测试项目testbee new test
4、运行项目
cd $GOPATH/src/testbee run

5、访问项目curl http://localhost:8080
2.3. 数据库依赖1、安装go-sqlite3
go get -v github.com/mattn/go-sqlite3

3. 部署项目3.1. 测试项目1、下载vkbeego
cd $GOPATH/src/git clone https://github.com/voidking/vkbeego.git

2、运行项目
cd vkbeegobee run
第一次运行后会在项目下生成db.sqlite3文件，里面是user表。如果已经有了db.sqlite3文件，则会进行校验。或者使用bee migrate命令生成表结构，具体参考bee 工具命令详解。
3、访问项目curl http://localhost:8080
其他机器如果无法访问，就先关闭防火墙。systemctl stop firewalld
3.2. 编译部署1、编译命令go build项目下生成名为vkbeego的二进制文件。
2、运行项目./vkbeego
3、后台运行nohup ./vkbeego &gt; nohup.out 2&gt;&amp;1 &amp;
4、关闭fg，然后Ctrl+C。
以上，就在CentOS7上部署好了beego项目。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>beego</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo更换主题为Next</title>
    <url>/dev-hexo-theme-next/</url>
    <content><![CDATA[1. 前言使用Yilia主题很久了，自己也进行了很多个性化的定制。今日始，决定把博客主题换成更加炫酷强大的Next，本文记录一下替换过程。


2. 下载主题1、进入hexo/themes目录下，克隆next主题git clone https://github.com/voidking/hexo-theme-next.git next
2、编辑hexo/_config.yml
theme: next

3. 个性化3.1. 风格Next主题支持多种风格，默认使用Muse风格。这里切换成自己更喜欢的Pisces风格，编辑next/_config.yml
scheme: Gemini

3.2. 导航编辑next/_config.yml，启用about、tags、categories、archives。
menu:  home: / || home  about: /about/ || user  tags: /tags/ || tags  categories: /categories/ || th  archives: /archives/ || archive

3.3. tags原Yilia主题中，是没有tags页面的，因此需要创建。1、创建tags页面hexo new page tags
2、编辑tags/index.md
---title: tagstype: &quot;tags&quot;layout: &quot;tags&quot;comments: false---

3.4. 头像和签名百年老店，需要logo。1、把avatar.jpg放入next/source/images目录
2、编辑next/_config.yml，启用avatar
# Sidebar Avataravatar:  # Replace the default image and set the url here.  url: /images/avatar.jpg  # If true, the avatar will be dispalyed in circle.  rounded: false  # If true, the avatar will be rotated with the cursor.  rotated: false

不过，黑白主题配这个头像，有种搞绿化的感觉，哈哈。
3、编辑hexo/_config.yml，修改签名
# Sitetitle: VoidKingsubtitle: 好好学习，天天向上！description: 学而不思则罔，思而不学则殆！author: VoidKinglanguage: zh-CNtimezone:

3.5. favicon1、把favicon.png放入next/source/images目录
2、编辑next/_config.yml，配置favicon
favicon:  small: /images/favicon.png  medium: /images/favicon.png

3.6. scrollpercent在阅读文章过程中，scroll to top按钮显示当前滚动的百分比。
back2top:  enable: true  # Back to top in sidebar.  sidebar: false  # Scroll percent label in b2t button.  scrollpercent: true

3.7. 社交链接编辑next/_config.yml，配置社交链接
# Social Linkssocial:  E-Mail: mailto:voidking@qq.com || envelope  GitHub: https://github.com/voidking || github  Weibo: http://weibo.com/voidking  || weibo  Twitter: https://twitter.com/voidking_com || twitter# `Follow me on GitHub` banner in the top-right corner.github_banner:  enable: true  permalink: https://github.com/voidking  title: Follow me on GitHub

3.8. 关闭更新时间编辑next/_config.yml，关闭文章的更新时间。
# Post meta display settingspost_meta:  item_text: true  created_at: true  updated_at:    enable: false    another_day: false  categories: true

4. 功能强化4.1. 评论原Yilia主题的评论插件是自己添加的livere，而Next主题支持livere。
编辑next/_config.yml，启用livere
livere_uid: MTAyMC8zODU3Mi8xNTEwMA==

4.2. 分享1、编辑next/_config.yml，添加needmoreshare2分享
needmoreshare2:  enable: true  postbottom:    enable: true    options:      iconStyle: box      boxForm: horizontal      position: bottomCenter      networks: Weibo,Wechat,Douban,QQZone,Twitter,Facebook  float:    enable: true    options:      iconStyle: box      boxForm: horizontal      position: topRight      networks: Weibo,Wechat,Douban,QQZone,Twitter,Facebook

2、修复微信分享bugneedmoreshare2有一个bug，微信分享不能用，显示loading wechat image编辑next/source/lib/needsharebutton/needsharebutton.js，如下修改：
# var imgSrc = &quot;https://api.qinco.me/api/qr?size=400&amp;content=&quot; + encodeURIComponent(myoptions.url);var imgSrc = &quot;http://api.qrserver.com/v1/create-qr-code/?size=150x150&amp;data=&quot; + encodeURIComponent(myoptions.url);

4.3. 搜索1、hexo目录下，安装搜索插件npm install hexo-generator-searchdb --save
2、编辑hexo/_config.yml，添加
# local searchsearch:  path: search.xml  field: post  format: html  limit: 10000

3、编辑next/_config.yml，启用搜索功能
# Local Searchlocal_search:  enable: true  # If auto, trigger search by changing input.  # If manual, trigger search by pressing enter key or search button.  trigger: auto  # Show top n results per article, show all results by setting to -1  top_n_per_article: 1  # Unescape html strings to the readable one.  unescape: false  # Preload the search data when the page loads.  preload: false

4.4. 阅读量统计1、编辑next/_config.yml，启用不蒜子统计
busuanzi_count:  enable: true  total_visitors: true  total_visitors_icon: user  total_views: true  total_views_icon: eye  post_views: true  post_views_icon: eye

2、修复404问题不蒜子的原有js地址已经过期，因此插件需要修改。编辑next/layout/_third-party/analytics/busuanzi-counter.swig，如下修改
&lt;!-- &lt;script async src=&quot;https://dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js&quot;&gt;&lt;/script&gt; --&gt;&lt;script async src=&quot;//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js&quot;&gt;&lt;/script&gt;

4.5. 文章置顶1、安装支持置顶的插件
npm uninstall hexo-generator-index --savenpm install hexo-generator-index-pin-top --save

2、next主题配置支持显示置顶编辑文件next/layout/_macro/post.swig，&lt;div class=&quot;post-meta&quot;&gt;中添加：
&#123;% if post.top %&#125;  &lt;i class=&quot;fa fa-thumb-tack&quot;&gt;&lt;/i&gt;  &lt;font color=&quot;RED&quot;&gt;置顶&lt;/font&gt;  &lt;span class=&quot;post-meta-divider&quot;&gt;|&lt;/span&gt;&#123;% endif %&#125;

3、需要置顶的文章头部添加top字段
top: true

4.6. seo1、编辑next/_config.yml，修改keywords
keywords: &quot;VoidKing, Hankin, 好好学习的好, 郝锦&quot;

2、启用seo优化
canonical: trueseo: trueindex_with_subtitle: true

3、启用百度统计
baidu_analytics: b759ac2a7fa45129e3ef060bf68259f0

4.7. MathJax编辑next/_config.yml，启用mathjax。这里指定某些页面启用mathjax，而不是全局。
# Math Formulas Render Supportmath:  per_page: true  mathjax:    enable: true    mhchem: false  katex:    enable: false    copy_tex: false

4.8. 背景动画编辑next/_config.yml，启用背景动画
# Canvas-nestcanvas_nest:  enable: true  onmobile: true # Display on mobile or not  color: &quot;0,0,255&quot; # RGB values, use `,` to separate  opacity: 0.5 # The opacity of line: 0~1  zIndex: -1 # z-index property of the background  count: 99 # The number of lines

4.9. 烟火动画1、在next/source/js/src目录下新建fireworks.js文件
&quot;use strict&quot;;function updateCoords(e) &#123;    pointerX = (e.clientX || e.touches[0].clientX) - canvasEl.getBoundingClientRect().left,    pointerY = e.clientY || e.touches[0].clientY - canvasEl.getBoundingClientRect().top&#125;function setParticuleDirection(e) &#123;    var t = anime.random(0, 360) * Math.PI / 180,    a = anime.random(50, 180),    n = [ - 1, 1][anime.random(0, 1)] * a;    return &#123;        x: e.x + n * Math.cos(t),        y: e.y + n * Math.sin(t)    &#125;&#125;function createParticule(e, t) &#123;    var a = &#123;&#125;;    return a.x = e,    a.y = t,    a.color = colors[anime.random(0, colors.length - 1)],    a.radius = anime.random(16, 32),    a.endPos = setParticuleDirection(a),    a.draw = function() &#123;        ctx.beginPath(),        ctx.arc(a.x, a.y, a.radius, 0, 2 * Math.PI, !0),        ctx.fillStyle = a.color,        ctx.fill()    &#125;,    a&#125;function createCircle(e, t) &#123;    var a = &#123;&#125;;    return a.x = e,    a.y = t,    a.color = &quot;#F00&quot;,    a.radius = 0.1,    a.alpha = 0.5,    a.lineWidth = 6,    a.draw = function() &#123;        ctx.globalAlpha = a.alpha,        ctx.beginPath(),        ctx.arc(a.x, a.y, a.radius, 0, 2 * Math.PI, !0),        ctx.lineWidth = a.lineWidth,        ctx.strokeStyle = a.color,        ctx.stroke(),        ctx.globalAlpha = 1    &#125;,    a&#125;function renderParticule(e) &#123;    for (var t = 0; t &lt; e.animatables.length; t++) &#123;        e.animatables[t].target.draw()    &#125;&#125;function animateParticules(e, t) &#123;    for (var a = createCircle(e, t), n = [], i = 0; i &lt; numberOfParticules; i++) &#123;        n.push(createParticule(e, t))    &#125;    anime.timeline().add(&#123;        targets: n,        x: function(e) &#123;            return e.endPos.x        &#125;,        y: function(e) &#123;            return e.endPos.y        &#125;,        radius: 0.1,        duration: anime.random(1200, 1800),        easing: &quot;easeOutExpo&quot;,        update: renderParticule    &#125;).add(&#123;        targets: a,        radius: anime.random(80, 160),        lineWidth: 0,        alpha: &#123;            value: 0,            easing: &quot;linear&quot;,            duration: anime.random(600, 800)        &#125;,        duration: anime.random(1200, 1800),        easing: &quot;easeOutExpo&quot;,        update: renderParticule,        offset: 0    &#125;)&#125;function debounce(e, t) &#123;    var a;    return function() &#123;        var n = this,        i = arguments;        clearTimeout(a),        a = setTimeout(function() &#123;            e.apply(n, i)        &#125;,        t)    &#125;&#125;var canvasEl = document.querySelector(&quot;.fireworks&quot;);if (canvasEl) &#123;    var ctx = canvasEl.getContext(&quot;2d&quot;),    numberOfParticules = 30,    pointerX = 0,    pointerY = 0,    tap = &quot;mousedown&quot;,    colors = [&quot;#FF1461&quot;, &quot;#18FF92&quot;, &quot;#5A87FF&quot;, &quot;#FBF38C&quot;],    setCanvasSize = debounce(function() &#123;        canvasEl.width = 2 * window.innerWidth,        canvasEl.height = 2 * window.innerHeight,        canvasEl.style.width = window.innerWidth + &quot;px&quot;,        canvasEl.style.height = window.innerHeight + &quot;px&quot;,        canvasEl.getContext(&quot;2d&quot;).scale(2, 2)    &#125;,    500),    render = anime(&#123;        duration: 1 / 0,        update: function() &#123;            ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)        &#125;    &#125;);    document.addEventListener(tap,    function(e) &#123;        &quot;sidebar&quot; !== e.target.id &amp;&amp; &quot;toggle-sidebar&quot; !== e.target.id &amp;&amp; &quot;A&quot; !== e.target.nodeName &amp;&amp; &quot;IMG&quot; !== e.target.nodeName &amp;&amp; (render.play(), updateCoords(e), animateParticules(pointerX, pointerY))    &#125;,    !1),    setCanvasSize(),    window.addEventListener(&quot;resize&quot;, setCanvasSize, !1)&#125;&quot;use strict&quot;;function updateCoords(e) &#123;    pointerX = (e.clientX || e.touches[0].clientX) - canvasEl.getBoundingClientRect().left,    pointerY = e.clientY || e.touches[0].clientY - canvasEl.getBoundingClientRect().top&#125;function setParticuleDirection(e) &#123;    var t = anime.random(0, 360) * Math.PI / 180,    a = anime.random(50, 180),    n = [ - 1, 1][anime.random(0, 1)] * a;    return &#123;        x: e.x + n * Math.cos(t),        y: e.y + n * Math.sin(t)    &#125;&#125;function createParticule(e, t) &#123;    var a = &#123;&#125;;    return a.x = e,    a.y = t,    a.color = colors[anime.random(0, colors.length - 1)],    a.radius = anime.random(16, 32),    a.endPos = setParticuleDirection(a),    a.draw = function() &#123;        ctx.beginPath(),        ctx.arc(a.x, a.y, a.radius, 0, 2 * Math.PI, !0),        ctx.fillStyle = a.color,        ctx.fill()    &#125;,    a&#125;function createCircle(e, t) &#123;    var a = &#123;&#125;;    return a.x = e,    a.y = t,    a.color = &quot;#F00&quot;,    a.radius = 0.1,    a.alpha = 0.5,    a.lineWidth = 6,    a.draw = function() &#123;        ctx.globalAlpha = a.alpha,        ctx.beginPath(),        ctx.arc(a.x, a.y, a.radius, 0, 2 * Math.PI, !0),        ctx.lineWidth = a.lineWidth,        ctx.strokeStyle = a.color,        ctx.stroke(),        ctx.globalAlpha = 1    &#125;,    a&#125;function renderParticule(e) &#123;    for (var t = 0; t &lt; e.animatables.length; t++) &#123;        e.animatables[t].target.draw()    &#125;&#125;function animateParticules(e, t) &#123;    for (var a = createCircle(e, t), n = [], i = 0; i &lt; numberOfParticules; i++) &#123;        n.push(createParticule(e, t))    &#125;    anime.timeline().add(&#123;        targets: n,        x: function(e) &#123;            return e.endPos.x        &#125;,        y: function(e) &#123;            return e.endPos.y        &#125;,        radius: 0.1,        duration: anime.random(1200, 1800),        easing: &quot;easeOutExpo&quot;,        update: renderParticule    &#125;).add(&#123;        targets: a,        radius: anime.random(80, 160),        lineWidth: 0,        alpha: &#123;            value: 0,            easing: &quot;linear&quot;,            duration: anime.random(600, 800)        &#125;,        duration: anime.random(1200, 1800),        easing: &quot;easeOutExpo&quot;,        update: renderParticule,        offset: 0    &#125;)&#125;function debounce(e, t) &#123;    var a;    return function() &#123;        var n = this,        i = arguments;        clearTimeout(a),        a = setTimeout(function() &#123;            e.apply(n, i)        &#125;,        t)    &#125;&#125;var canvasEl = document.querySelector(&quot;.fireworks&quot;);if (canvasEl) &#123;    var ctx = canvasEl.getContext(&quot;2d&quot;),    numberOfParticules = 30,    pointerX = 0,    pointerY = 0,    tap = &quot;mousedown&quot;,    colors = [&quot;#FF1461&quot;, &quot;#18FF92&quot;, &quot;#5A87FF&quot;, &quot;#FBF38C&quot;],    setCanvasSize = debounce(function() &#123;        canvasEl.width = 2 * window.innerWidth,        canvasEl.height = 2 * window.innerHeight,        canvasEl.style.width = window.innerWidth + &quot;px&quot;,        canvasEl.style.height = window.innerHeight + &quot;px&quot;,        canvasEl.getContext(&quot;2d&quot;).scale(2, 2)    &#125;,    500),    render = anime(&#123;        duration: 1 / 0,        update: function() &#123;            ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)        &#125;    &#125;);    document.addEventListener(tap,    function(e) &#123;        &quot;sidebar&quot; !== e.target.id &amp;&amp; &quot;toggle-sidebar&quot; !== e.target.id &amp;&amp; &quot;A&quot; !== e.target.nodeName &amp;&amp; &quot;IMG&quot; !== e.target.nodeName &amp;&amp; (render.play(), updateCoords(e), animateParticules(pointerX, pointerY))    &#125;,    !1),    setCanvasSize(),    window.addEventListener(&quot;resize&quot;, setCanvasSize, !1)&#125;;

2、添加next/layout/_third-party/fireworks.swig，内容如下：
&#123;% if theme.fireworks %&#125;   &lt;canvas class=&quot;fireworks&quot; style=&quot;position: fixed;left: 0;top: 0;z-index: 1; pointer-events: none;&quot; &gt;&lt;/canvas&gt;    &lt;script type=&quot;text/javascript&quot; src=&quot;//cdn.bootcss.com/animejs/2.2.0/anime.min.js&quot;&gt;&lt;/script&gt;    &lt;script type=&quot;text/javascript&quot; src=&quot;/js/src/fireworks.js&quot;&gt;&lt;/script&gt;&#123;% endif %&#125;

3、编辑next/layout/_layout.swig，在&lt;/body&gt;上面添加：
&#123;% include &#x27;_third-party/fireworks.swig&#x27; %&#125;

4、编辑next/_config.yml，添加并启用firework
# Fireworksfireworks: true

5. 后记至此，完成了Hexo主题从Yilia到Next的替换。进行了一些个性化设置，并且应用了Next的很多新特性。更多关于Next主题的内容，需要时再去学习使用。
6. 书签theme-next/hexo-theme-next
Next
iissnan/hexo-theme-next
IIssNan’s Notes
Elegant Theme for Hexo
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：使用client-go在K8S集群发布项目</title>
    <url>/dev-k8s-client-go-deploy/</url>
    <content><![CDATA[1. 问题简述怎样在K8S集群中发布一个新的项目？怎样判断一个项目是发布成功了还是失败了？怎样访问一个项目？怎样实现灰度发布？怎样实现发布过程中的并发度控制、暂停、继续、取消？怎样实现回滚？
本文中，会努力寻求这些问题的答案。
参考文档：

Pod 的生命周期
管理资源
Service
10 Most Common Reasons Kubernetes Deployments Fail (Part 1)
10 Most Common Reasons Kubernetes Deployments Fail (Part 2)



2. 准备镜像首先准备两个镜像，分别是nginx:v1.0和nginx:v2.0。
1、启动并进入容器
docker run --name nginx -d -p 80:80 voidking/nginx:latestdocker exec -it nginx /bin/bash

有些小伙伴会有疑问，为什么不使用docker run启动并进入容器？docker run -it voidking/nginx:latest /bin/bash
因为如果使用这种方式进入容器，修改内容后commit镜像，那么镜像无法正常启动。
docker ps -a --no-trunc使用该命令可以看到，voidking/nginx:latest的默认启动命令为：nginx -g &#39;daemon off;&#39;而commit后的镜像，默认启动命令被修改为：/bin/bash看到这里就知道，因为自定义的启动命令覆盖掉了原有的启动命令，所以镜像无法正常启动。为什么会覆盖呢？因为CMD指令允许用户指定容器的默认的执行命令，此命令会在容器启动且docker run没有指定其他命令时运行。当docker run指定其他命令时，则会覆盖默认的执行命令。更多内容参考Dockerfile RUN，CMD，ENTRYPOINT命令区别。
2、修改index.html
cd /usr/share/nginx/htmlsed &#x27;s/nginx!/nginx!v1.0/&#x27; index.htmlsed -i &#x27;s/nginx!/nginx!v1.0/&#x27; index.htmlcat index.html

3、commit/push镜像
docker ps -ldocker commit 6efcd878d0d1 voidking/nginx:v1.0docker logindocker push voidking/nginx:v1.0

3. kubectl在使用go-client之前，我们先来学习一下使用kubectl怎样发布项目、获取状态、访问项目、更新项目。
3.1. 发布项目以发布一个nginx项目为例。
1、准备deployment yaml文件nginx.yaml
apiVersion: apps/v1kind: Deploymentmetadata:  name: nginxspec:  replicas: 1  selector:    matchLabels:      app: nginx  template:    metadata:      labels:        app: nginx    spec:      containers:      - name: nginx        image: voidking/nginx:v1.0        ports:        - containerPort: 80        imagePullPolicy: Always

2、发布项目
kubectl apply -f nginx.yaml

3.2. 获取状态kubectl get deployments nginxkubectl get deployments nginx -o jsonkubectl get pods -l app=nginxkubectl get pods -l app=nginx -o jsonkubectl describe deployments nginxkubectl describe pods -l app=nginx
根据获取到的信息，判断发布成功还是失败。
3.3. 访问项目1、准备service yaml文件nginx-svc.yaml
apiVersion: v1kind: Servicemetadata:  name: nginxspec:  ports:  - port: 80    protocol: TCP    targetPort: 80  selector:    app: nginx  sessionAffinity: None  type: NodePort  #type: LoadBalancer

2、查看服务IP和端口
kubectl get svc nginx

3、访问项目
minikube service nginx

执行上面的命令后，会自动打开浏览器。或者使用curl命令访问：curl http://192.168.99.100:32329/
4、从容器内部访问项目（三种方式）
kubectl get podskubectl exec -it hello-node-6747d458b6-d757m /bin/bash# wget -O- 10.97.151.28curl 10.97.151.28curl nginx.defaultenv | grep NGINXcurl $NGINX_SERVICE_HOST:$NGINX_SERVICE_PORT

如果env命令没有显示出nginx service信息，那么可以重建pod：
kubectl get pod hello-node-6747d458b6-d757m -n default -o yaml | kubectl replace --force -f -

PS：使用kubectl进入容器的方法
kubectl get podskubectl exec -it pod-name /bin/bashkubectl exec -it pod-name -c container-name /bin/bash

3.4. 更新发布1、修改nginx.yaml中的配置，升级镜像为voidking/nginx:2.0
apiVersion: apps/v1kind: Deploymentmetadata:  name: nginxspec:  replicas: 1  selector:    matchLabels:      app: nginx  template:    metadata:      labels:        app: nginx    spec:      containers:      - name: nginx        image: voidking/nginx:v2.0        ports:        - containerPort: 80        imagePullPolicy: Always

2、更新发布
kubectl apply -f nginx.yaml

3、访问项目
curl http://192.168.99.100:32329/

4. client-go《使用client-go操作K8S》一文中，配置好了k8s-client-go项目，可以使用这个项目对k8s集群进行一些基本操作。在此基础上，本节会实现一些更高阶的操作。
4.1. 判断发布状态根据deployment和pod的状态，怎样判断一次发布成功还是失败？首先，要知道deployment和pod都有哪些状态。
1、通过kubectl命令获取发布数据
kubectl get deployments hello-node -o jsonkubectl get deployments nginx -o jsonkubectl get pods -l app=hello-node -o jsonkubectl get pods -l app=nginx -o json

2、获取到的发布数据发布成功的json数据：hello-node.json发布失败的json数据：nginx.json
3、具体可用状态有哪些
# deployment部分deployment.generationdeployment.spec.replicasdeployment.status.observedGenerationdeployment.status.replicasdeployment.status.readyReplicas/unavailableReplicasdeployment.status.updatedReplicasdeployment.status.conditions[*].type&#123;Available,Progressing&#125; &amp;&amp; deployment.status.conditions[*].status&#123;True,False&#125;# pod部分pod.status.phase&#123;Pending,Running&#125;pod.status.conditions[*].type&#123;Initialized,Ready,ContainersReady,PodScheduled&#125; &amp;&amp; pod.status.conditions[*].status&#123;True,False&#125;pod.status.containerStatuses[*].state.&#123;waiting,running&#125;

4、判断条件组合参考k8s-client-go/common/deployment.go文件的GetDeploymentStatus函数。
4.2. 灰度发布常见的发布方案包括蓝绿发布、滚动发布和灰度发布（金丝雀发布），更多内容参考微服务部署：蓝绿部署、滚动部署、灰度发布、金丝雀发布。参考Spinnaker第四节-对接k8s、基于 Spinnaker 的 K8S 灰度发布、灰度发布/蓝绿发布，使用Ingress，可以很好地实现灰度发布。但是，在没有Ingress的情况下，该怎样实现灰度发布？
最简单的方案是发布一个新的deployment，把service的流量分流到新的deployment，测试通过后删除老的deployment，流量就全部到了新的deployment。具体实现参考k8s-client-go/common/deploy.go文件的GrayDeploy和UpdateDeploy函数。
但是，如果只允许维护一个deployment，该怎么处理？创建灰度的deployment，测试通过后删除灰度的deployment，然后正常更新原有的deployment。具体实现参考k8s-client-go/common/deploy.go文件的GrayDeploy2和UpdateDeploy2函数。
5. 后记并发度控制、暂停、继续、取消、回滚等等功能，未完待续。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>golang</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>client-go</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Golang包管理工具</title>
    <url>/dev-golang-gpm/</url>
    <content><![CDATA[1. Golang包管理工具简介大一点的项目，通常会依赖很多第三方包。Golang把所有的第三方包都放在GOPATH/src目录下，且每个包仅保留一个版本。如果两个项目依赖不同版本的第三方包，就会产生问题。
为了解决这个问题，go在1.5版本引入了vendor属性（默认关闭），并在1.6版本中默认开启了vendor属性。简单来说，vendor属性就是让go编译时，优先从项目源码树根目录下的vendor目录查找代码（可以理解为切了一次GOPATH），如果vendor中有，则不再去GOPATH中去查找。
但是vendor目录又带来了新的问题：（1）vendor目录中依赖包没有版本信息。这样依赖包脱离了版本管理，对于升级、问题追溯，会有点困难。（2）如何方便的得到本项目依赖了哪些包，并方便的将其拷贝到vendor目录下？
为了解决这些问题，开发者在vendor基础上开发了多个管理工具，比较常用的有godep、govendor、glide，以及官方的dep和gomod。本文中，我们来了解一下godep、govendor和gomod，并重点学习gomod。
参考文档：

go依赖包管理工具对比
golang包管理解决之道——go modules初探
再探go modules：使用与细节



2. Golang包管理工具对比2.1. godepgodep的使用者众多，如docker、kubernetes、coreos等go项目很多都是使用godep来管理其依赖，当然原因可能是早期也没的工具可选。
godep早期版本并不依赖vendor，所以对go的版本要求很松，go 1.5之前的版本也可以用，只是行为上有所不同。在vendor推出以后，godep也改为使用vendor了。
godep使用很简单，当项目编写完成，使用GOPATH的依赖包测试没问题的时候，执行 godep save ，该命令会做两件事：（1）扫描项目的代码，将项目依赖的包及该包的版本号（即git commit）记录到Godeps/Godeps.json文件中。（2）将依赖的包从GOPATH/src中拷贝到vendor目录（忽略依赖包的.git目录）。对于不支持vendor的早期版本，则会拷贝到Godeps/_workspace/里。
其他常用命令：
# 安装godepgo get -u -v github.com/tools/godep# 安装新的包go get github.com/voidking/k8s-client-goimport &quot;github.com/voidking/k8s-client-go&quot;godep save# 更新依赖包go get -u github.com/voidking/k8s-client-gogodep update github.com/voidking/k8s-client-go# orgodep update github.com/voidking/...# 下载godeps.json里的包到GOPATH/srcgodep restore

2.2. govendorgovendor是在vendor之后出现的，功能相对godep多一点，不过就核心问题的解决来说基本是一样的。govendor生成vendor目录的时候需要2条命令：govendor init 生成vendor/vendor.json，此时文件中只有本项目的信息。govendor add +external 更新vendor/vendor.json，并拷贝GOPATH下的第三方包到vendor目录中。
其他常用命令：
# 安装govendorgo get -u github.com/kardianos/govendor# 获取指定版本的包govendor fetch golang.org/x/net/context@v1# 查看依赖的包govendor list# 查看哪些包使用了fmtgovendor list -v fmt

2.3. gomodgo modules随着golang1.11的发布和我们见面了，这是官方提倡的新的包管理，乃至项目管理机制，可以不再需要GOPATH的存在。
现在modules机制仍在早期阶段，所以golang提供了一个环境变量“GO111MODULE”，默认值为auto，如果当前目录里有go.mod文件，就使用go modules，否则使用旧的GOPATH和vendor机制，因为在modules机制下go get只会下载go modules，这一行为会在以后版本中成为默认值，这里我们保持auto即可，如果你想直接使用modules而不需要从GOPATH过度，那么把“GO111MODULE”设置为on。
modules和传统的GOPATH不同，不需要包含例如src，bin这样的子目录，一个源代码目录甚至是空目录都可以作为module，只要其中包含有go.mod文件。
1、启用modules机制linux and mac 中：
export GO111MODULE=on

windows powershell 中：
$env:GO111MODULE = &quot;on&quot;

2、初始化test项目为module
go mod init test
初始化之后，目录下会自动生成go.mod文件。当我们使用go build，go test以及go list时，go会自动得更新go.mod文件，将依赖关系写入其中。
3、整理依赖
go mod tidy -v


这条命令会自动下载依赖的module到cache，清除无用的module。
生成go.mod的require部分，和go.sum文件。go.sum是一个构建状态跟踪文件，它会记录当前module所有的顶层和间接依赖，go modules根据这些记录去寻找对应的依赖。
依赖的module下载到GOPATH/pkg/cache/和GOPATH/pkg/mod/目录下。

4、Goland启用gomodFile，Settings，Go，Go Modules，勾选Enable Go Modules(vgo) integration，Apply。
其他常用命令：
# 帮助go help mod# 已有go.mod，下载依赖到cachego mod download# 拷贝依赖到vendor（没有则先下载）go mod vendor# 清空cachego clean -modcache

2.4. 小结综上，推荐使用gomod，这是官方提倡的新的包管理工具。
3. 使用gomod进行包管理3.1. 安装client-go假设一个项目依赖client-go，那么参考官方说明进行安装。
1、查看k8s版本
kubectl version
Client Version表示kubectl的版本，Server Version表示apiserver的版本（假设版本为v1.15.0）。
2、查找对应版本的client-go在kubernetes/client-go上查找k8s v1.15.0对应的client-go版本，找到版本为12.0
3、安装对应版本的client-go
go get k8s.io/client-go/12.0/kubernetes
这里有个坑，国内无法访问k8s.io，需要科学上网（恰好我的搬瓦工服务器也访问不到k8s.io）。因此，这里使用GOPROXY的方式来解决，参考一键解决 go get golang.org/x 包失败。
# export GOPROXY=https://goproxy.cnexport GOPROXY=https://goproxy.ioexport GO111MODULE=ongo get k8s.io/client-go/12.0/kubernetes


失败了，返回404，猜测是12.0版本在代理服务器不存在，使用另外一种下载方式。
go get k8s.io/client-go@kubernetes-1.15.0
此处下载了两个版本的client-go，我们使用v0.0.0版本。
4、安装client-go相关依赖client-go默认下载到$GOPATH/pkg/mod/k8s.io目录下，把k8s.io目录移动到$GOPATH/src目录下。然后把&#99;&#108;&#105;&#x65;&#110;&#116;&#45;&#103;&#x6f;&#x40;&#118;&#x30;&#x2e;&#48;&#x2e;&#48;重命名为client-go，然后在client-go目录下执行安装依赖的命令：
export GOPROXY=https://goproxy.ioexport GO111MODULE=ongo mod vendor
完成之后，所有的依赖都下载到了client-go/vendor目录下，一个完整的client-go依赖库就准备好了。
3.2. 依赖库版本管理client-go依赖库准备好了，但是另一个问题来了：假设项目vk-project依赖client-go和库A，client-go依赖库A，两个依赖库A版本不一致，那么vk-project在编译时，会同时依赖两个不同的库A，导致编译出的二进制包有问题。
此时，一个简单的办法是把GOPATH/src/k8s.io/client-go/vendor目录剪切覆盖vk-project/vendor目录，然后把GOPATH/src/k8s.io/client-go目录移动到vk-project/vendor/k8s.io/目录下。这么做的话，client-go就和自己的依赖是平级的存在了。
但是，这么做有另一个问题：依赖库版本没有记录。因为没有版本记录，那么在上传vk-project项目时，就要把依赖全部上传，否则别人下载了vk-project也没办法正确安装依赖。
这里，使用gomod进行依赖库版本管理。
go mod init vk-projectgo mod tidy -vgo mod vendor

上传代码时忽略vendor目录，在gitignore中添加：
/vendor/

3.3. 安装依赖1、下载项目后，进入项目目录
cd vk-project

2、下载依赖
go mod download

3、拷贝依赖到vendor（可选）
go mod vendor



]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>client-go</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：使用client-go操作K8S</title>
    <url>/dev-k8s-client-go/</url>
    <content><![CDATA[1. client-go简介直观上看，用户可以使用kubectl、客户端库或者REST请求来访问K8S API。而实际上，无论是kubectl还是客户端库，都是封装了REST请求的工具。
client-go作为一个客户端库，能够调用K8S API，实现对K8S集群中资源对象（包括deployment、service、ingress、replicaSet、pod、namespace、node等）的增删改查等操作。
源码地址：kubernetes/client-go接口文档：kubernetes - GoDoc


2. 通过REST访问K8S《K8S入门篇》一文中使用kubectl对minikube进行管理，本节中直接使用REST请求来对minikube进行管理。参考Access Clusters Using the Kubernetes API，来测试一下。
kubectl处理API服务器的定位和身份验证。如果要使用curl或wget等http客户端或浏览器直接访问REST API，可以通过多种方式查找和验证API服务器：（1）在代理模式下运行kubectl（推荐）。建议使用此方法，因为它使用存储的apiserver位置并使用自签名证书验证API服务器的标识。使用此方法无法进行中间人（MITM）攻击。（2）直接向http客户端提供位置和凭据。这适用于被代理混淆的客户端代码。为了防止中间人攻击，需要将根证书导入浏览器。使用Go或Python客户端库可以在代理模式下访问kubectl。
启动minikube，查看集群配置
minikube startkubectl config view

查看kubectl访问了什么url，获得了什么信息。
kubectl get nodes -v=8kubectl get nodes -v=9

2.1. kubectl代理模式充当反向代理模式下运行kubectl，它处理定位API服务器和进行身份验证。
kubectl proxy --port=8080 &amp;curl http://localhost:8080/api/
此时使用浏览器进行访问，同样可以拿到数据。
2.2. grep/cut方法# Check all possible clusters, as you .KUBECONFIG may have multiple contexts:kubectl config view -o jsonpath=&#x27;&#123;&quot;Cluster name\tServer\n&quot;&#125;&#123;range .clusters[*]&#125;&#123;.name&#125;&#123;&quot;\t&quot;&#125;&#123;.cluster.server&#125;&#123;&quot;\n&quot;&#125;&#123;end&#125;&#x27;# Select name of cluster you want to interact with from above output:# export CLUSTER_NAME=&quot;some_server_name&quot;export CLUSTER_NAME=&quot;minikube&quot;# Point to the API server refering the cluster nameAPISERVER=$(kubectl config view -o jsonpath=&quot;&#123;.clusters[?(@.name==\&quot;$CLUSTER_NAME\&quot;)].cluster.server&#125;&quot;)# Gets the token valueTOKEN=$(kubectl get secrets -o jsonpath=&quot;&#123;.items[?(@.metadata.annotations[&#x27;kubernetes\.io/service-account\.name&#x27;]==&#x27;default&#x27;)].data.token&#125;&quot;|base64 -d)# Explore the API with TOKENcurl -X GET $APISERVER/api --header &quot;Authorization: Bearer $TOKEN&quot; --insecure


2.3. jsonpath方法APISERVER=$(kubectl config view --minify -o jsonpath=&#x27;&#123;.clusters[0].cluster.server&#125;&#x27;)TOKEN=$(kubectl get secret $(kubectl get serviceaccount default -o jsonpath=&#x27;&#123;.secrets[0].name&#125;&#x27;) -o jsonpath=&#x27;&#123;.data.token&#125;&#x27; | base64 --decode )curl $APISERVER/api --header &quot;Authorization: Bearer $TOKEN&quot; --insecure


3. client-go操作K8Sclient-go实际上是封装了REST请求的客户端库，能够简化我们的工作。
参考k8s-client-go项目，对k8s进行一些基本操作，包括连接k8s，创建更新deployment，查看deployment状态，查看pod状态等。在使用该项目之前，需要获取k8s集群的配置信息：kubectl config view把配置信息拷贝一下，粘贴到项目中的admin.conf文件中即可。
此外，k8s-client-go项目依赖client-go，因此需要参考《Golang包管理工具》安装好依赖。
4. client-go客户端类型概述client-go 提供了几种不同类型的客户端，分别适用于不同的需求：

REST Client：它是所有其他客户端的基础，提供底层的 HTTP/RESTful 交互能力。当我们需要自定义请求或处理非标准的 API 路径时，可以使用 REST Client。
Typed Clients：它们是针对 Kubernetes API 中定义的具体资源类型的客户端，如 Pods、Services 等。这些客户端通常是类型安全的，可以享受静态类型语言的一些好处。
ClientSet：这是为 Kubernetes API 的每一个版本生成的一组客户端，合并了所有核心组的 Typed Clients。ClientSet 是多个 Typed Client 的集合。如果需要处理标准资源，如 Deployments、Pods 等，可以使用 ClientSet。
Dynamic Client：使用动态客户端，可以以非类型化的方式处理资源，这对于编写通用代码或处理自定义资源（CRDs）时非常有用。
Discovery Client：用于发现 API 服务器支持的 API 资源集合，可以检索 API 组、版本和资源信息。
Informers and Listers：Informers 是一种高级抽象，用于监听资源事件，并保持资源的本地缓存。Listers 作为其配套使用，从 Informers 维护的本地缓存中获取资源数据。
Controller-Runtime Client：尽管不是 client-go 的直接一部分，controller-runtime 客户端通常与 client-go 一起使用，特别是在构建 Kubernetes Controllers 和 Operators 时。它简化了客户端的交互，并对各种 API 操作提供了更高层次的抽象。

客户端初始化方法参考代码：k8sclient.go
5. 后记本文中学习了client-go的安装方法，然后借助k8s-client-go项目学习了client-go的基本用法。接下来，使用client-go实现在K8S集群中发布项目，go！go！go！
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>golang</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>client-go</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>Helm入门篇</title>
    <url>/dev-helm-start/</url>
    <content><![CDATA[1. Helm简介
Helm is the best way to find, share, and use software built for Kubernetes.

helm是k8s的包管理工具，就像yum之于centos，apt之于ubuntu。不同的是，yum管理的是软件包，helm管理的是配置好的k8s资源包。这种配置好的k8s资源包，按照chart的包格式进行包装。以下是wordpress的chart包结构：
wordpress/  Chart.yaml          # A YAML file containing information about the chart  LICENSE             # OPTIONAL: A plain text file containing the license for the chart  README.md           # OPTIONAL: A human-readable README file  requirements.yaml   # OPTIONAL: A YAML file listing dependencies for the chart  values.yaml         # The default configuration values for this chart  charts/             # A directory containing any charts upon which this chart depends.  templates/          # A directory of templates that, when combined with values,                      # will generate valid Kubernetes manifest files.  templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes

Helm2包括两个组件：客户端（Helm Client）和服务端（Tiller）。Helm发送指令给Tiller(gRPC协议)，Tiller主要用于管理各种应用发布的版本，并且与k8s进行交互。不过，在 Helm3 中 Tiller 被移除掉了，Helm直接与k8s进行交互，版本相关的数据存储在k8s中。
Helm3中命令也发生了变化：

helm delete 更名为 helm uninstall 
helm inspect 更名为 helm show 
helm fetch 更名为 helm pull

旧的命令依然可以使用。
参考文档：

helm/helm
Helm Quickstart Guide
使用Helm
Helm 从入门到实践 | 从 0 开始制作一个 Helm Charts
Helm用户与开发者指南。



2. 体验Helm越来越喜欢用katacoda来学习新技能，Helm也可以在上面通过实践学习。
Helm相关教程：

Helm Package Manager - BY JAVAJON
Helm Package Manager - BY KATACODA

3. 安装Helm参考 Installing Helm，安装helm。
3.1. macosbrew install helmhelm version

3.2. linux1、查找需要的release版本
2、下载并安装
wget https://get.helm.sh/helm-v3.2.4-linux-amd64.tar.gztar -xzvf helm-v3.2.4-linux-amd64.tar.gzmv linux-amd64/helm /usr/local/bin/helmln -s /usr/local/bin/helm /usr/bin/helmhelm version

4. Helm常用命令4.1. 使用条件helm命令使用条件：

安装配置好k8s集群
安装配置好kubectl，可以正常访问k8s
安装好helm

helm和kubectl一样，可以在一台机器上管理多个k8s集群，使用kubectl config use-context xxx切换一下集群即可。
4.2. 查看帮助helm -hhelm get -h

4.3. 从 Artifact Hub 查找charthelm search hub mysql

Artifact Hub中存放了大量不同的仓库。
4.4. 添加chart仓库到本地# helm repo add stable https://kubernetes-charts.storage.googleapis.com/# helm repo add stable https://charts.helm.sh/stable# helm repo remove stablehelm repo add stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/chartshelm repo updatehelm repo list

执行helm repo add，实际上是在本地创建或更新一个名为repositories.yaml的文件，该文件存储了所有已添加的仓库的详细信息，包括名称和URL。
repositories.yaml 文件路径为：

在Linux上：$HOME/.config/helm
在macOS上：$HOME/Library/Preferences/helm
在Windows上：%USERPROFILE%\AppData\Roaming\Helm

4.5. 本地查找chart列表helm search repo stablehelm search repo stable/mysqlhelm search repo stable/mysql -l

4.6. 查看chart详情helm show chart stable/mysqlhelm show all stable/mysql

4.7. 查看chart渲染后的yamlhelm template mysql stable/mysql

4.8. 模拟安装软件helm install mysql stable/mysql --version 0.3.5 --debug --dry-run

4.9. 安装软件以安装mysql为例（适用于 k8s v1.16- 版本）
helm install mysql stable/mysql# orhelm install stable/mysql --generate-namehelm status mysql

安装指定chart版本的mysql
helm search repo stable/mysql -lhelm install mysql stable/mysql --version 0.3.5

k8s v1.16+ 版本，extensions/v1beta1 API version中Deployment资源已经被移除了，因此这里的mysql chart仅适用于1.16以下的版本。详情参考Deprecated APIs Removed In 1.16: Here’s What You Need To Know
如果想要在 k8s v1.16+ 版本中使用这个mysql chart，请参考《K8S中安装Mysql》一文中的【helm安装单节点mysql（5.7.x）】一节。
4.10. 查看k8s中安装的charthelm listhelm list -Ahelm list -n mysqlhelm status mysql -n mysql

4.11. 查看k8s中安装的chart详情helm get manifest mysql -n mysql

该命令能够打印出所有上传到k8s的资源清单。每个文件以---开头表示YAML文件的开头，然后是自动生成的注释行，表示哪个模板文件生成了这个YAML文档。
4.12. 卸载软件以卸载mysql为例
helm lshelm uninstall mysqlhelm status mysql

卸载指定namespace下的软件
helm ls -Ahelm uninstall xxx -n yyy

4.13. 修改默认配置4.13.1. 方法一：下载chart（推荐）下载chart，修改配置文件。这种方法是最推荐的方法，原因有二：

有时不止需要修改values.yaml，还需要修改其他yaml文件
安装之后有完整留档，方便以后的配置调整和升级

以修改mysql的默认配置为例：
1、获取chart并解压
helm fetch stable/mysql # orhelm fetch stable/mysql --version 0.3.5tar -xzvf mysql-0.3.5.tgz

2、编辑values
cd mysqlvim values.yaml

配置项改为我们需要的配置。
3、执行安装在values所在目录下，执行命令
kubectl create ns mysqlhelm install mysql -f values.yaml . -n mysql

安装过程中有两种方式传递配置数据：

--values (或 -f)：使用 YAML 文件覆盖配置。可以指定多次，优先使用最右边的文件。
--set：通过命令行的方式对指定项进行覆盖。优先级更高。

4.13.2. 方法二：导出配置文件导出配置文件，修改配置文件
以修改mysql的默认配置为例：
1、获取默认values
helm show values stable/mysql &gt; mysql-values.yaml

2、编辑values编辑mysql-values.yaml
3、执行安装
kubectl create ns mysqlhelm install mysql stable/mysql -f mysql-values.yaml -n mysql

4.13.3. 方法三：执行命令时指定配置执行helm install命令时，指定配置
以修改mysql的默认配置为例：
helm install mysql stable/mysql -n mysql --set persistence.size=10Gi

4.14. 修改配置后更新helm upgrade mysql stable/mysql -f mysql-values.yaml -n mysql

4.15. 创建chart并打包1、创建chart
helm create wecms

2、验证chart
helm lint wecms

3、打包chart
helm package wecms

4、使用chart安装
helm install wecms ./wecms-0.1.0.tgz

更多关于创建chart的内容，参考文档《自己动手制作Helm Chart》
4.16. 生成渲染好的资源清单helm template wecms wecms &gt; wecms_manifest.yaml# orhelm template wecms wecms-0.1.0.tgz &gt; wecms_manifest.yaml

5. Helm基础知识参考文档《Helm官方文档摘录》
6. 常用chart仓库
github - MySQL packaged by Bitnami
artifacthub - MySQL packaged by Bitnami
github - Bitnami package for Redis(R)
artifacthub - Bitnami package for Redis(R)

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>bitnami</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中安装配置StorageClass</title>
    <url>/dev-k8s-storageclass/</url>
    <content><![CDATA[1. StorageClass简介StorageClass 为管理员提供了描述存储 “类” 的方法。 不同的类型可能会映射到不同的服务质量等级或备份策略，或是由集群管理员制定的任意策略。 Kubernetes 本身并不清楚各种类代表的什么。这个类的概念在其他存储系统中有时被称为 “配置文件”。
参考文档：

K8S官方文档 - 存储
K8S官方文档 - 存储类
《K8S中的Volume》



2. 安装配置StorageClass概述要使用 StorageClass，我们就得安装对应的自动配置程序，比如存储后端使用的是 nfs-server，那么我们就需要使用到一个 nfs-client 的自动配置程序，我们也叫它 Provisioner，这个程序使用我们已经配置好的 nfs 服务器，来自动创建持久卷，也就是自动帮我们创建 PV。
参考文档：

github - kubernetes-sigs/sig-storage-lib-external-provisioner
github - kubernetes-retired/external-storage
github - kubernetes-sigs/nfs-subdir-external-provisioner
StorageClass
利用NFS动态提供Kubernetes后端存储卷
nacos - nfs storageclass yaml
搭建NFS Server

3. 安装NFS参考文档《Linux中安装NFS》，在存储节点安装nfs-server，K8S所有节点安装nfs-client。
nfs-server安装完成后，对外服务路径为：
192.168.56.101:/data/nfs

4. 特别注意配置好 nfs-server 后，可以直接用它的IP来配置 nfs-client-provisioner 。但是，更好的办法是给 nfs-server 配置一个主机名，用主机名来配置 nfs-client-provisioner 。使用主机名的好处是如果 nfs-server 需要迁移，IP 发生了变更，那么不会影响到 nfs-client-provisioner，也就不会影响到 storageclass。
参考文档：

Update PersistentVolume NFS IP
kubernetes PersistenceVolume NFS IP change

5. 安装nfs-provisioner1、编写sa spec，nfs-client-sa.yaml
apiVersion: v1kind: ServiceAccountmetadata:  name: nfs-client-provisioner---kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1metadata:  name: nfs-client-provisioner-runnerrules:  - apiGroups: [&quot;&quot;]    resources: [&quot;persistentvolumes&quot;]    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;delete&quot;]  - apiGroups: [&quot;&quot;]    resources: [&quot;persistentvolumeclaims&quot;]    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;update&quot;]  - apiGroups: [&quot;storage.k8s.io&quot;]    resources: [&quot;storageclasses&quot;]    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]  - apiGroups: [&quot;&quot;]    resources: [&quot;events&quot;]    verbs: [&quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;]  - apiGroups: [&quot;&quot;]    resources: [&quot;endpoints&quot;]    verbs: [&quot;create&quot;, &quot;delete&quot;, &quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;patch&quot;, &quot;update&quot;]---kind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata:  name: run-nfs-client-provisionersubjects:  - kind: ServiceAccount    name: nfs-client-provisioner    namespace: defaultroleRef:  kind: ClusterRole  name: nfs-client-provisioner-runner  apiGroup: rbac.authorization.k8s.io

2、编写nfs-client-provisioner的deployment spec，nfs-client.yaml
kind: DeploymentapiVersion: apps/v1metadata:  name: nfs-client-provisionerspec:  replicas: 1  strategy:    type: Recreate  selector:    matchLabels:      app: nfs-client-provisioner  template:    metadata:      labels:        app: nfs-client-provisioner    spec:      serviceAccountName: nfs-client-provisioner      containers:        - name: nfs-client-provisioner          #image: quay.io/external_storage/nfs-client-provisioner:latest          image: registry.cn-beijing.aliyuncs.com/mydlq/nfs-subdir-external-provisioner:v4.0.0          volumeMounts:            - name: nfs-client-root              mountPath: /persistentvolumes          env:            - name: PROVISIONER_NAME              value: fuseim.pri/ifs            - name: NFS_SERVER              value: 192.168.56.101            - name: NFS_PATH              value: /data/nfs      volumes:        - name: nfs-client-root          nfs:            server: 192.168.56.101            path: /data/nfs

3、编写sc spec，nfs-client-sc.yaml
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata:  name: nfs-storage  annotations:    &quot;storageclass.kubernetes.io/is-default-class&quot;: &quot;true&quot;provisioner: fuseim.pri/ifs
这里的fuseim.pri/ifs是deployment中定义的 PROVISIONER_NAME 变量
4、创建资源对象
kubectl apply -f nfs-client-sa.yamlkubectl apply -f nfs-client.yamlkubectl apply -f nfs-client-sc.yaml

5、查看pod状态
kubectl describe pods nfs-client-provisioner-7975f9b954-7fw5l
报错：
Output: mount: wrong fs type, bad option, bad superblock on 192.168.56.101:/data/nfs       missing codepage or helper program, or other error       (for several filesystems (e.g. nfs, cifs) you might       need a /sbin/mount.&lt;type&gt; helper program)

这是因为nfs-client-provider依赖宿主机上的/sbin/mount.&lt;type&gt;，所以所有的节点都需要安装nfs和rpcbind。详情参考nfs挂载报错误wrong fs type, bad option, bad superblock
6. 安装nfs-provisioner简化版1、添加chart repo
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/

2、获取chart
helm search repo nfs-subdir-external-provisioner/nfs-subdir-external-provisioner -lhelm pull nfs-subdir-external-provisioner/nfs-subdir-external-provisioner --version 4.0.18tar -xzvf nfs-subdir-external-provisioner-4.0.18.tgzvim nfs-subdir-external-provisioner/values.yaml # 按需修改配置

3、执行安装
helm install nfs-provisioner ./nfs-subdir-external-provisioner \  --set nfs.server=192.168.56.101 \  --set nfs.path=/data/nfs \  --set image.repository=registry.cn-beijing.aliyuncs.com/mydlq/nfs-subdir-external-provisioner \  --set image.tag=v4.0.0

参考文档：github - kubernetes-sigs/nfs-subdir-external-provisioner
7. 设置默认StorageClass如果没有设置默认的storageclass，可以参考改变默认 StorageClass，设置nfs-storage为默认storageclass
kubectl patch storageclass nfs-storage -p &#x27;&#123;&quot;metadata&quot;: &#123;&quot;annotations&quot;:&#123;&quot;storageclass.kubernetes.io/is-default-class&quot;:&quot;true&quot;&#125;&#125;&#125;&#x27;

8. 启用selflink1.20.x之后的k8s版本，selflink已经弃用了。而quay.io/external_storage/nfs-client-provisioner:latest的实现基于selflink，因此在使用时会报错。
解决办法：apiserver添加启动参数--feature-gates=RemoveSelfLink=false。详情参考文档《K8S中安装KubeSphere》
如果是k8s1.24.x以上的版本，是不支持修改启动参数RemoveSelfLink的，否则api-server无法启动。解决办法：替换nfs-client-provisioner的镜像版本为新版本（上文中已经替换）。
9. 使用StorageClass参考文档配置 Pod 以使用 PersistentVolume 作为存储
9.1. 创建 PersistentVolumeClaim1、定义pvc.yaml
apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: busybox-pv-claimspec:  storageClassName: nfs-storage  accessModes:    - ReadWriteOnce  resources:    requests:      storage: 3Gi

2、创建pvc
kubectl apply -f pvc.yamlkubectl get pvckubectl get pv # 不会创建出对应的pv

注意：nfs-privisioner提供的存储，在申请时是可以超过真实存储容量的。比如NFS的真实存储只有10GB，我们申请的PVC是100GB或者1000GB，也可以申请成功，当PVC绑定给Pod后，能够创建出对应PV。
9.2. 创建Pod1、定义pod.yaml
apiVersion: v1kind: Podmetadata:  creationTimestamp: null  labels:    run: busybox  name: busyboxspec:  containers:  - command:    - sleep    - &quot;3000&quot;    image: busybox:1.25    name: busybox    resources: &#123;&#125;    volumeMounts:    - mountPath: &quot;/data&quot;      name: busybox-pv-storage  volumes:  - name: busybox-pv-storage    persistentVolumeClaim:      claimName: busybox-pv-claim  dnsPolicy: ClusterFirst  restartPolicy: Neverstatus: &#123;&#125;

2、创建pod
kubectl apply -f pod.yamlkubectl get podskubectl get pvckubectl get pv # 创建出了对应的pv

3、验证挂载
kubectl exec -it busybox /bin/shdf -hls /datatouch /data/test.txt

然后，登录到nfs-server的主机192.168.56.101，查看写入结果。
9.3. volumeClaimTemplates对于statefulset，需要使用volumeClaimTemplates来声明使用storageclass，详情参考StatefulSet
10. 安装配置多个StorageClass如果k8s集群中的存储全部使用同一个nfs-server，那么这个nfs-server的网络压力会很大。为了分担单个nfs-server的压力，我们可以在多个主机上安装配置nfs-server，然后在k8s中软件部署时规划好哪些软件使用哪个nfs-server。
假设现在我们在192.168.56.102主机上也安装好了nfs-server，下面我们在k8s集群中添加它的配置。
1、再创建一个nfs-client-provisioner，sa不用变
kind: DeploymentapiVersion: apps/v1metadata:  name: nfs-client-provisioner-102spec:  replicas: 1  strategy:    type: Recreate  selector:    matchLabels:      app: nfs-client-provisioner-102  template:    metadata:      labels:        app: nfs-client-provisioner-102    spec:      serviceAccountName: nfs-client-provisioner      containers:        - name: nfs-client-provisioner          image: quay.io/external_storage/nfs-client-provisioner:latest          volumeMounts:            - name: nfs-client-root              mountPath: /persistentvolumes          env:            - name: PROVISIONER_NAME              value: fuseim.pri/ifs-102            - name: NFS_SERVER              value: 192.168.56.102            - name: NFS_PATH              value: /data/nfs      volumes:        - name: nfs-client-root          nfs:            server: 192.168.56.102            path: /data/nfs

2、再创建一个storageclass
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata:  name: nfs-storage-102provisioner: fuseim.pri/ifs-102

3、使用192.168.56.102存储数据当我们使用nfs-storage-102这个storageclass时，就能够存储数据到 192.168.56.102 这台主机上了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>storage</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>存储</tag>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>好好学K8S</tag>
        <tag>nfs</tag>
      </tags>
  </entry>
  <entry>
    <title>自己动手制作Helm Chart</title>
    <url>/dev-make-helm-chart/</url>
    <content><![CDATA[1. 前言本文中，我们学习制作Helm Chart，用来简化imagepullsecret-patcher的部署。
参考文档：

《Helm入门篇》
Helm - Getting Started
《K8S配置使用imagePullSecrets》
imagepullsecret-patcher
imagepullsecret-patcher deploy-example



2. 第一个Chart1、生成chart模板
helm create imagepullsecret-patcher

2、删除所有templates
rm -rf imagepullsecret-patcher/templates/*

3、准备资源清单

imagepullsecret-patcher/templates/namespace.yaml
imagepullsecret-patcher/templates/rbac.yaml
imagepullsecret-patcher/templates/deployment.yaml

具体资源清单内容参见：make-helm-chart/v1/imagepullsecret-patcher
这份资源清单来自《K8S配置使用imagePullSecrets》，yaml文件中没有使用任何变量。
4、检查chart
helm lint imagepullsecret-patcher

5、执行安装
helm install imagepullsecret-patcher ./imagepullsecret-patcher

chart安装成功，但是出现了一个问题：chart所属namespace是default，但是真实资源所属namespace是imagepullsecret-patcher（在yaml文件中指定的）。
3. 第一个Chart存在的问题第一个Chart，存在几个问题：

helm install 的namespace和真实资源的namespace不同
deployment等资源的name是固定的，不能在values.yaml中或者安装时自定义
dockerconfigjson是固定的，不能在values.yaml中或者安装时自定义
imagepullsecret-patcher/values.yaml 中的变量完全无用
chart的version和app的version不符合预期

为了解决上面几个问题，我们先来补充下helm的知识。参考文档《Helm官方文档摘录》
4. 第二个Chart第二个Chart，解决第一个Chart中存在的问题。
1、准备资源清单

imagepullsecret-patcher/templates/rbac.yaml
imagepullsecret-patcher/templates/deployment.yaml
imagepullsecret-patcher/templates/_helpers.tpl
imagepullsecret-patcher/templates/NOTES.txt
imagepullsecret-patcher/values.yaml

具体资源清单内容参见：make-helm-chart/v2/imagepullsecret-patcher
2、检查chart
helm lint imagepullsecret-patcher

3、执行安装
helm install imagepullsecret-patcher ./imagepullsecret-patcher \    -n imagepullsecret-patcher --create-namespace \    --set imageCredentials.registry=harbor.voidking.com \    --set imageCredentials.username=haojin \    --set imageCredentials.password=haojin123

4、打包chart
helm package imagepullsecret-patcher
生成chart包 imagepullsecret-patcher-0.1.0.tgz
5. 分享Chart仓库参考文档：

Chart仓库指南
Helm上传Chart到Artifact Hub
Helm 进阶到骨灰玩家，请收藏！
bitnami/charts
bitnami/index.yaml

5.1. 创建Chart仓库1、在github创建一个公共仓库
未完待续
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>helm</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中的Volume</title>
    <url>/dev-k8s-volume/</url>
    <content><![CDATA[1. Volume简介Container 中的文件在磁盘上是临时存放的，这会带来两个问题：1）当容器崩溃时文件会丢失；2）同一Pod中运行多个容器并共享文件时，容器重启时状态丢失。Kubernetes 卷（Volume） 这一抽象概念能够解决这两个问题。
Docker卷的本质是目录，存在与一个或多个容器中，由docker挂载到容器，但不属于联合文件系统，因此能够绕过“联合文件系统”提供一些用于持续存储或共享数据的特性。K8S卷的本质也是目录，其中可能存有数据，Pod 中的容器可以访问该目录中的数据。 所采用的特定的卷类型将决定该目录如何形成的、使用何种介质保存数据以及目录中存放的内容。
Kubernetes中的卷分为两类：临时卷和持久卷。临时卷会遵从 Pod 的生命周期，和Pod一起创建和删除；持久卷的生命周期独立于pod的生命周期。
Kubernetes中常用的卷类型包括：

configMap：临时卷/投射卷，常用来存储配置数据
secret：临时卷/投射卷，常用来存储配置数据，使用base64加密数据
downwardAPI：临时卷/投射卷，让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。
emptyDir：临时卷，存储pod中的数据，pod删除时数据会被删除
hostPath：持久卷，将主机节点上的文件或目录挂载到pod中（存在许多安全风险，尽量避免使用）
local：持久卷，创建pv时指定pv所在节点，使用pvc的pod会被调度到指定节点，将主机节点上的文件或目录挂载到pod中
nfs：持久卷，将 NFS (网络文件系统) 挂载到的pod中。 
persistentVolumeClaim：持久卷申领，不需要知道持久卷细节。
projected：投射卷能将多个卷来源映射到同一目录上。

持久卷（PersistentVolume，PV） 是集群中的一块存储，可以由管理员事先制备， 或者使用存储类（Storage Class）来动态制备。 持久卷是集群资源，就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样， 也是使用卷插件来实现的，只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。 此 API 对象中记述了存储的实现细节，无论其背后是 NFS、iSCSI 还是特定于云平台的存储系统。
持久卷申领（PersistentVolumeClaim，PVC） 表达的是用户对存储的请求。概念上与 Pod 类似。 Pod 会耗用节点资源，而 PVC 申领会耗用 PV 资源。Pod 可以请求特定数量的资源（CPU 和内存）；同样 PVC 申领也可以请求特定的大小和访问模式 （例如，可以要求 PV 卷能够以 ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany 模式之一来挂载，参见访问模式）。
PVC与PV是一一对应关系，不能一个PVC挂载多个PV，也不能一个PV挂载多个PVC。
参考文档：

K8S官方文档 - 存储
K8S官方文档 - 卷
K8S官方文档 - 持久卷
K8S官方文档 - 临时卷
K8S官方文档 - 投射卷
K8S官方文档 - Downward API
阿里云ACK - 存储基础知识
《K8S中安装配置StorageClass》



2. ConfigMap/Secret为什么是临时卷？临时卷会遵从 Pod 的生命周期，和Pod一起创建和删除。
ConfigMap本身是一个API对象，用来将非机密性的数据保存到键值对中。使用时，Pods可以将其用作环境变量、命令行参数或者存储卷中的配置文件。当容器挂载 ConfigMap 时，ConfigMap的信息会被节点本地临时存储，然后挂载到容器的文件系统中。当Pod删除时，容器的文件系统没了，本地临时存储中的ConfigMap信息也没了，因此ConfigMap是临时卷。
Secret同理。
其实，如果说ConfigMap/Secret是持久卷，也没有什么问题，因为ConfigMap/Secret不会随着Pod删除而删除。
不用太纠结于这种概念性的东西，了解即可。
3. 使用configmap我们有四种方式来使用 ConfigMap 配置 Pod 中的容器：

容器的环境变量
在容器命令和参数内
在只读卷里面添加一个文件，让应用来读取
编写代码在 Pod 中运行，使用 Kubernetes API 来读取 ConfigMap

参考文档：

K8S官方文档 - ConfigMap
Pod中使用ConfigMap

3.1. 生成configmap3.1.1. 基于文字生成configmapkubectl create cm game-demo \--from-literal=sleep_seconds=3600 \--from-literal=player_initial_lives=3 \--from-literal=ui_properties_file_name=user-interface.properties \--dry-run=client -oyaml &gt; configmap.yaml

3.1.2. 基于环境变量配置文件生成configmap1、准备环境变量配置文件 env.sh
sleep_seconds=3600player_initial_lives=3ui_properties_file_name=user-interface.properties

2、生成config文件
kubectl create cm game-demo \--from-env-file=env.sh \--dry-run=client -oyaml &gt; configmap.yaml

3.1.3. 基于文件生成configmap1、准备配置文件 game.properties 和 user-interface.properties
enemy.types=aliens,monstersplayer.maximum-lives=5

color.good=purplecolor.bad=yellowallow.textmode=true 

2、生成configmap文件
kubectl create cm game-demo \--from-file=game.properties=game.properties \--from-file=user-interface.properties=user-interface.properties \--dry-run=client -oyaml &gt; configmap.yaml

3.2. pod中使用configmap简单示例1、configmap.yaml
apiVersion: v1kind: ConfigMapmetadata:  name: game-demodata:  # 类属性键；每一个键都映射到一个简单的值  sleep_seconds: &quot;3600&quot;  player_initial_lives: &quot;3&quot;  ui_properties_file_name: &quot;user-interface.properties&quot;  # 类文件键  game.properties: |    enemy.types=aliens,monsters    player.maximum-lives=5      user-interface.properties: |    color.good=purple    color.bad=yellow    allow.textmode=true 

2、pod.yaml
apiVersion: v1kind: Podmetadata:  name: configmap-demo-podspec:  containers:    - name: demo      image: alpine      command: [&quot;sleep&quot;, &quot;$(SLEEP_SECONDS)&quot;]      env:        # 定义环境变量        - name: SLEEP_SECONDS # 请注意这里和 ConfigMap 中的键名是不一样的          valueFrom:            configMapKeyRef:              name: game-demo           # 这个值来自 ConfigMap              key: sleep_seconds # 需要取值的键        - name: PLAYER_INITIAL_LIVES # 请注意这里和 ConfigMap 中的键名是不一样的          valueFrom:            configMapKeyRef:              name: game-demo           # 这个值来自 ConfigMap              key: player_initial_lives # 需要取值的键        - name: UI_PROPERTIES_FILE_NAME          valueFrom:            configMapKeyRef:              name: game-demo              key: ui_properties_file_name      volumeMounts:      - name: config        mountPath: &quot;/config&quot;        readOnly: true  volumes:  # 你可以在 Pod 级别设置卷，然后将其挂载到 Pod 内的容器中  - name: config    configMap:      # 提供你想要挂载的 ConfigMap 的名字      name: game-demo      # 来自 ConfigMap 的一组键，将被创建为文件      items:      - key: &quot;game.properties&quot;        path: &quot;game.properties&quot;      - key: &quot;user-interface.properties&quot;        path: &quot;user-interface.properties&quot;

ConfigMap 不会区分单行属性值和多行类似文件的值，重要的是 Pods 和其他对象如何使用这些值。
上面的例子定义了一个卷并将它作为 /config 文件夹挂载到 demo 容器内， 创建两个文件，/config/game.properties 和 /config/user-interface.properties， 尽管 ConfigMap 中包含了四个键。 这是因为 Pod 定义中在 volumes 节指定了一个 items 数组。 如果你完全忽略 items 数组，则 ConfigMap 中的每个键都会变成一个与该键同名的文件， 因此你会得到四个文件。
PS：使用 envFrom 可以将 ConfigMap 的所有数据定义为容器的环境变量。
4. 使用secretPod 可以用三种方式之一来使用 Secret：

作为容器的环境变量。
作为挂载到一个或多个容器上的卷 中的文件。
由 kubelet 在为 Pod 拉取镜像时使用。

创建 Secret 时，我们可以使用 Secret 资源的 type 字段，或者与其等价的 kubectl 命令行参数（如果有的话）为其设置类型。 Secret 类型有助于对 Secret 数据进行编程处理。
Kubernetes 提供若干种内置的类型，用于一些常见的使用场景。 针对这些类型，Kubernetes 所执行的合法性检查操作以及对其所实施的限制各不相同。
如果 type 值为空字符串，则被视为 Opaque 类型（用户定义的任意数据）。
Secret使用base64加密，可以使用echo &quot;YWRtaW4=&quot; | base64 -d命令进行解密。
参考文档：

K8S官方文档 - Secret

4.1. 生成secret4.1.1. 基于文字生成secretkubectl create secret generic mysecret \--from-literal=username=admin \--from-literal=password=1f2d1e2e67df \--dry-run=client -oyaml &gt; secret.yaml

4.1.2. 基于环境变量配置文件生成secret1、准备环境变量配置文件 env.sh
username=adminpassword=1f2d1e2e67df

2、生成secret
kubectl create secret generic mysecret \--from-env-file=env.sh \--dry-run=client -oyaml &gt; secret.yaml

4.1.3. 基于文件生成secretkubectl create secret generic mysecret \--from-file=ssh-privatekey=path/to/id_rsa \--from-file=ssh-publickey=path/to/id_rsa.pub \--dry-run=client -oyaml &gt; secret.yaml

4.2. pod中使用secret简单示例1、secret.yaml
apiVersion: v1kind: Secretmetadata:  name: mysecrettype: Opaquedata:  username: YWRtaW4=  password: MWYyZDFlMmU2N2Rm

2、pod.yaml
apiVersion: v1kind: Podmetadata:  name: secret-demo-podspec:  containers:  - name: mycontainer    image: alpine    command: [&quot;sleep&quot;, &quot;3600&quot;]    env:    - name: SECRET_USERNAME      valueFrom:        secretKeyRef:          name: mysecret          key: username          optional: false # 此值为默认值；意味着 &quot;mysecret&quot;                          # 必须存在且包含名为 &quot;username&quot; 的主键    - name: SECRET_PASSWORD      valueFrom:        secretKeyRef:          name: mysecret          key: password          optional: false # 此值为默认值；意味着 &quot;mysecret&quot;                          # 必须存在且包含名为 &quot;password&quot; 的主键    volumeMounts:    - name: foo      mountPath: &quot;/etc/foo&quot;  volumes:  - name: foo    secret:      secretName: mysecret      defaultMode: 0400  restartPolicy: Never

容器中看到的环境变量，和文件中的内容，都是明文。
PS：使用 envFrom 可以将 Secret 的所有数据定义为容器的环境变量。
4.3. imagePullSecrets参考文档 《K8S配置使用imagePullSecrets》
5. 使用emptyDir参考文档K8S官方文档 - 卷 - emptyDir configuration example
apiVersion: v1kind: Podmetadata:  name: test-pdspec:  containers:  - image: registry.k8s.io/test-webserver    name: test-container    volumeMounts:    - mountPath: /cache      name: cache-volume  volumes:  - name: cache-volume    emptyDir:      sizeLimit: 500Mi

6. 使用hostPath参考文档K8S官方文档 - 卷 - hostPath configuration example
apiVersion: v1kind: Podmetadata:  name: test-pdspec:  containers:  - image: registry.k8s.io/test-webserver    name: test-container    volumeMounts:    - mountPath: /test-pd      name: test-volume  volumes:  - name: test-volume    hostPath:      # directory location on host      path: /data      # this field is optional      type: Directory

7. 使用nfs7.1. 直接使用nfs参考文档K8S官方文档 - 卷 - nfs
apiVersion: v1kind: Podmetadata:  name: test-podspec:  containers:  - image: busybox    name: busybox    command: [&quot;sleep&quot;, &quot;3600&quot;]    volumeMounts:    - mountPath: /my-nfs-data      name: test-volume  volumes:  - name: test-volume    nfs:      server: my-nfs-server.example.com      path: /my-nfs-volume      readOnly: true

7.2. nfs作为pv参考文档：

K8S官方文档 - Persistent Volumes
K8S官方文档 - Persistent Volumes - Reclaiming
Kubernetes 中 PV 和 PVC 的状态变化

1、pv.yaml
apiVersion: v1kind: PersistentVolumemetadata:  name: nfs-pv  labels:    release: &quot;stable&quot;    environment: &quot;dev&quot;spec:  capacity:    storage: 5Gi  volumeMode: Filesystem  accessModes:    - ReadOnlyMany  persistentVolumeReclaimPolicy: Retain  storageClassName: slow  mountOptions:    - hard    - nfsvers=4  nfs:    server: my-nfs-server.example.com    path: /my-nfs-volume

需要注意的是，persistentVolumeReclaimPolicy使用的Retain，当pvc删除后，真实存储设备上的数据依然存在，pv不能被其他pvc申领；当pv删除后，真实存储设备上的数据依然存在。
如果使用Delete，那么当pvc删除后，pv也会被删除，真实存储设备上的数据也会被删除。pv自动删除可能会删除失败，需要手动删除，手动删除pv后，真实存储设备上的数据依然存在。
如果使用Recycle，那么当pvc删除后，真实存储设备上的数据会被删除（rm -rf /thevolume/*），pv可以被新的pvc申领；如果只删除pv，真实存储设备上的数据不会被删除。回收策略Recycle 已被废弃。取而代之的建议方案是使用动态制备。
2、pvc.yaml
apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: nfs-pvcspec:  accessModes:    - ReadOnlyMany  volumeMode: Filesystem  resources:    requests:      storage: 5Gi  storageClassName: slow  selector:    matchLabels:      release: &quot;stable&quot;    matchExpressions:      - &#123;key: environment, operator: In, values: [dev]&#125;

3、pod.yaml
apiVersion: v1kind: Podmetadata:  name: test-podspec:  containers:  - image: busybox    name: busybox    command: [&quot;sleep&quot;, &quot;3600&quot;]    volumeMounts:    - mountPath: /my-nfs-data      name: test-volume  volumes:  - name: test-volume    persistentVolumeClaim:      claimName: nfs-pvc

8. 使用storageclass参考文档《K8S中安装配置StorageClass》
9. subPath说明有时，在单个 Pod 中共享卷以供多方使用是很有用的。 volumeMounts.subPath 属性可用于指定所引用的卷内的子路径，而不是其根路径。
例如：
apiVersion: v1kind: Podmetadata:  name: my-lamp-sitespec:    containers:    - name: mysql      image: mysql      env:      - name: MYSQL_ROOT_PASSWORD        value: &quot;rootpasswd&quot;      volumeMounts:      - mountPath: /var/lib/mysql        name: site-data        subPath: mysql    - name: php      image: php:7.0-apache      volumeMounts:      - mountPath: /var/www/html        name: site-data        subPath: html    volumes:    - name: site-data      persistentVolumeClaim:        claimName: my-lamp-site-data
上面的例子中，Pod 使用同一共享卷。其中 PHP 应用的代码和相关数据映射到卷的 html 文件夹，MySQL 数据库存储在卷的 mysql 文件夹。
使用 subPathExpr 字段可以基于 downward API 环境变量来构造 subPath 目录名。 subPath 和 subPathExpr 属性是互斥的。例如：
apiVersion: v1kind: Podmetadata:  name: pod1spec:  containers:  - name: container1    env:    - name: POD_NAME      valueFrom:        fieldRef:          apiVersion: v1          fieldPath: metadata.name    image: busybox:1.28    command: [ &quot;sh&quot;, &quot;-c&quot;, &quot;while [ true ]; do echo &#x27;Hello&#x27;; sleep 10; done | tee -a /logs/hello.txt&quot; ]    volumeMounts:    - name: workdir1      mountPath: /logs      # 包裹变量名的是小括号，而不是大括号      subPathExpr: $(POD_NAME)  restartPolicy: Never  volumes:  - name: workdir1    hostPath:      path: /var/log/pods
在这个示例中，Pod 使用 subPathExpr 在 hostPath 卷 /var/log/pods 中创建目录 pod1。 hostPath 卷采用来自 downwardAPI 的 Pod 名称生成目录名。 宿主目录 /var/log/pods/pod1 被挂载到容器的 /logs 中。
如果我们想要挂在一个configmap或者secret配置文件到pod，但是不想覆盖pod中的文件所在目录，这时也需要使用subPath。
spec:  containers:    volumeMounts:    - name: nginx-config      mountPath: /etc/nginx/nginx.conf      subPath: nginx.conf  volumes:  - name: nginx-config    configMap:      name: nginx-config      items:      - key: &quot;nginx.conf&quot;        path: &quot;nginx.conf&quot;

参数说明：

volumes.configMap.items.key 表示configmap中的data key
volumes.configMap.items.path 表示要将key映射到的文件的相对路径（相对于volume）
containers.volumeMounts.mountPath 表示volume在容器中的挂载路径
containers.volumeMounts.subPath 表示volume的子路径

本例中，configmap作为volume，可以认为是根目录/，key nginx.conf 被映射为volume的相对路径nginx.conf，绝对路径就是 /nginx.conf。容器中挂载volume，使用volume的子路径nginx.conf（绝对路径/nginx.conf），挂载到容器中的/etc/nginx/nginx.conf。
10. Mount PropagationMount Propagation（挂载卷的传播）允许将容器安装的卷共享到同一 Pod 中的其他容器，甚至共享到同一节点上的其他 Pod。
卷的挂载传播特性由 containers[*].volumeMounts 中的 mountPropagation 字段控制。 它的值包括：

None - 此卷挂载将不会感知到主机后续在此卷或其任何子目录上执行的挂载变化。这是默认模式。该模式等同于 mount(8) 中描述的 rprivate 挂载传播选项。
HostToContainer - 此卷挂载将会感知到主机后续针对此卷或其任何子目录的挂载操作。换句话说，如果主机在此挂载卷中挂载任何内容，容器将能看到它被挂载在那里。该模式等同于 mount(8) 中描述的 rslave 挂载传播选项。
Bidirectional - 这种卷挂载和 HostToContainer 挂载表现相同。另外，容器创建的卷挂载将被传播回至主机和使用同一卷的所有 Pod 的所有容器。该模式等同于 mount(8) 中描述的 rshared 挂载传播选项。

参考文档：Kubernetes Volumes - Mount propagation
Mount Propagation很重要的一个应用是Pod挂载点自动恢复。例如，我们使用了JuiceFS CSI作为持久化驱动，当修改了JuiceFS的挂载配置后，需要重启CSI Pod和Mount Pod。而重启Mount Pod后，应用Pod的挂载点会失效，需要重启应用Pod。而如果配置了 mountPropagation: HostToContainer，应用Pod的挂载点就能自动恢复，不需要重启。
]]></content>
      <categories>
        <category>engineering</category>
        <category>storage</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>存储</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>CoreDNS入门篇</title>
    <url>/dev-coredns-start/</url>
    <content><![CDATA[1. CoreDNS简介CoreDNS是一个灵活的、插件化的DNS服务器，能够响应来自容器、虚拟机或任何需要DNS服务的设备的查询。CoreDNS具有强大的插件机制，可以按需添加功能。它是云原生计算基金会的一个顶级项目，并且是Kubernetes的默认DNS解析器。
参考文档：

CoreDNS官网
CoreDNS文档
github - coredns
阿里云 - CoreDNS配置说明



2. 核心特性和高级应用CoreDNS主要的插件包括：

cache插件：此插件实现了一个内部DNS消息缓存。
host插件：此插件承载给定路径的静态DNS记录（包括通配符）。它可以被用在任意协议的dns查询。

此外，CoreDNS能够通过插件提供许多其他识别和发现服务的功能。例如，autopath插件允许CoreDNS直接解析Kubernetes的服务名，这意味着客户端不需要事先知道服务的完整路径。
3. 如何安装CoreDNSCoreDNS的安装过程依赖于我们的操作环境以及确定的配置方式。位于GitHub仓库的CoreDNS核心库包含一个名为coredns的预编译二进制文件，使用包管理器（如apt-get或yum）可以在主机操作系统上直接安装此文件。然后，我们可以运行此二进制文件以启动CoreDNS。
但是实际上，CoreDNS通常作为容器运行，并通过Kubernetes集群管理。在这种情况下，我们需要将CoreDNS部署为一个Kubernetes Deployment，然后通过Service暴露其端口。这将允许我们的应用程序在Kubernetes集群内部或者从集群的边界外部访问CoreDNS服务。
4. 配置CoreDNS4.1. 配置概述CoreDNS的配置是通过一个名为Corefile的文件完成的。Corefile格式非常简单，它包含定义DNS服务行为的一系列插件块。每一个插件块由插件名和可选的插件参数组成。当CoreDNS运行时，它按照在Corefile中列出的顺序加载和运行所有插件。
下面是一个示例Corefile，显示了如何配置CoreDNS来使用主机文件、内部缓存，并自动解析Kubernetes服务：
.:53 &#123;    hosts /etc/hosts    cache 30    kubernetes cluster.local&#125;

4.2. 配置forword方法一：所有找不到的域名都去找forward
.:53 &#123;    hosts /etc/hosts    cache 30    kubernetes cluster.local    forward . 192.168.56.101 &#123;      prefer_udp    &#125;&#125;

方法二：指定域名去找forward
.:53 &#123;    hosts /etc/hosts    cache 30    kubernetes cluster.local&#125;voidking.com:53 &#123;    errors    cache 30    forward . 192.168.56.101 &#123;      prefer_udp    &#125;&#125;

4.3. 直接配置一个域名解析www.example.com 指定IP为 127.0.0.1
.:53 &#123;    errors    health &#123;       lameduck 15s    &#125;    ready        hosts &#123;      127.0.0.1 www.example.com      fallthrough    &#125;&#125;

5. 测试CoreDNS安装和配置CoreDNS之后，我们可以通过发送一个DNS查询来进行测试。
nslookup www.example.com localhostdig @localhost -p 53 www.example.com

6. 小结CoreDNS提供了一个强大和灵活的DNS服务解决方案。无论我们是在大型编排平台上（例如K8S）需要一个强大的可伸缩的DNS服务器，还是仅仅需要一个轻量级的可配置的本地DNS服务器，CoreDNS都很适合。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>dns</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中镜像拉取卡住问题</title>
    <url>/dev-k8s-image-pull-hang-up/</url>
    <content><![CDATA[1. 问题描述节点上已经存在某个镜像了，但是拉取镜像时卡住：Pulling image …


2. 问题原因kubelet的配置中，serializeImagePulls默认值true，被启用时会通知 kubelet 每次仅拉取一个镜像。如果有一个镜像拉取时卡住，那么其他镜像拉取时会等待前一个镜像完成，因此也会卡住。
参考文档Kubelet 配置 (v1beta1)
3. 解决办法一（推荐）问题解决思路：找到拉取镜像卡住的pod，解决它
# kubectl get pod -A -owide | grep nodenamekubectl get pod -A -o wide --field-selector spec.nodeName=nodenamekubectl get pod -A --field-selector spec.nodeName=slave8 | egrep -v &quot;Completed|Running&quot;

4. 解决办法二问题解决思路：拉取镜像改成并行。
1、修改kubelet配置
vim /var/lib/kubelet/config.yaml

添加：
serializeImagePulls: false

2、重启kubelet
systemctl restart kubelet






]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>好好学K8S</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S故障排查常用命令</title>
    <url>/dev-k8s-trouble-shooting-command/</url>
    <content><![CDATA[1. 前言本文记录K8S故障排查常用命令，备忘。


2. 查看资源对象详情2.1. 查找namespace下所有资源kubectl api-resources --verbs=list --namespaced -o name | \    xargs -n 1 kubectl get --show-kind --ignore-not-found -n &lt;your-namespace&gt;

2.2. 查看Pod详细信息kubectl get pods $pod_name -n $namespace -oyamlkubectl describe pods $pod_name -n $namespace 

2.3. 查看节点详情kubectl describe node $node_name

2.4. 查看Deployment详情kubectl get deployment $controller_name -n $namespace -oyamlkubectl describe deployment $controller_name -n $namespace

2.5. 查看Service详情kubectl get service $service_name -n $namespacekubectl get service $service_name -n $namespace -oyaml

查看endpoints资源，service选择到了哪些pod和端口  
kubectl get endpoints $service_name

3. 资源用量统计3.1. 查看Node资源使用kubectl top nodeskubectl top nodes --sort-by=&#x27;cpu&#x27;kubectl top nodes --sort-by=&#x27;memory&#x27;

3.2. 查看Pod资源使用查看所有Pod的资源使用情况：
kubectl top pods -Akubectl top pods -A --sort-by=&#x27;cpu&#x27;kubectl top pods -A --sort-by=&#x27;memory&#x27;

查看单个Pod的资源使用情况：
kubectl top pod -n $namespace $pod_name

3.3. 查看Pod资源申请kubectl get po -A \    -o custom-columns=&quot;name:metadata.name,namespace:metadata.namespace,requests-cpu:spec.containers[*].resources.requests.cpu,requests-memory:spec.containers[*].resources.requests.memory&quot;kubectl get po -A --field-selector status.phase==Running \    -o custom-columns=&quot;name:metadata.name,namespace:metadata.namespace,requests-cpu:spec.containers[*].resources.requests.cpu,requests-memory:spec.containers[*].resources.requests.memory&quot;kubectl get po -A --field-selector status.phase==Running,spec.nodeName=worker0 \    -o custom-columns=&quot;name:metadata.name,namespace:metadata.namespace,requests-cpu:spec.containers[*].resources.requests.cpu,requests-memory:spec.containers[*].resources.requests.memory&quot;kubectl get po -A \    -o=jsonpath=&quot;&#123;range .items[*]&#125;&#123;.metadata.namespace&#125;:&#123;.metadata.name&#125;&#123;&#x27;\n&#x27;&#125;&#123;range .spec.containers[*]&#125;  &#123;.name&#125;:&#123;.resources.requests.cpu&#125;&#123;&#x27;\n&#x27;&#125;&#123;end&#125;&#123;&#x27;\n&#x27;&#125;&#123;end&#125;&quot;kubectl get po -A \    -o=jsonpath=&quot;&#123;range .items[*]&#125;&#123;.metadata.namespace&#125;:&#123;.metadata.name&#125;&#123;&#x27;\n&#x27;&#125;&#123;range .spec.containers[*]&#125;  &#123;.name&#125;:&#123;.resources.requests.memory&#125;&#123;&#x27;\n&#x27;&#125;&#123;end&#125;&#123;&#x27;\n&#x27;&#125;&#123;end&#125;&quot;

3.4. 查看Pod资源限制kubectl get po -A \    -o custom-columns=&quot;name:metadata.name,namespace:metadata.namespace,limits-cpu:spec.containers[*].resources.limits.cpu,limits-memory:spec.containers[*].resources.limits.memory&quot;kubectl get po -A --field-selector status.phase==Running \    -o custom-columns=&quot;name:metadata.name,namespace:metadata.namespace,limits-cpu:spec.containers[*].resources.limits.cpu,limits-memory:spec.containers[*].resources.limits.memory&quot;kubectl get po -A --field-selector status.phase==Running,spec.nodeName=worker0 \    -o custom-columns=&quot;name:metadata.name,namespace:metadata.namespace,limits-cpu:spec.containers[*].resources.limits.cpu,limits-memory:spec.containers[*].resources.limits.memory&quot;kubectl get po -A \    -o=jsonpath=&quot;&#123;range .items[*]&#125;&#123;.metadata.namespace&#125;:&#123;.metadata.name&#125;&#123;&#x27;\n&#x27;&#125;&#123;range .spec.containers[*]&#125;  &#123;.name&#125;:&#123;.resources.limits.cpu&#125;&#123;&#x27;\n&#x27;&#125;&#123;end&#125;&#123;&#x27;\n&#x27;&#125;&#123;end&#125;&quot;kubectl get po -A \    -o=jsonpath=&quot;&#123;range .items[*]&#125;&#123;.metadata.namespace&#125;:&#123;.metadata.name&#125;&#123;&#x27;\n&#x27;&#125;&#123;range .spec.containers[*]&#125;  &#123;.name&#125;:&#123;.resources.limits.memory&#125;&#123;&#x27;\n&#x27;&#125;&#123;end&#125;&#123;&#x27;\n&#x27;&#125;&#123;end&#125;&quot;

4. Pod操作4.1. 查看容器日志查看容器标准输出日志
kubectl logs $pod_name -n $namespace kubectl logs $pod_name -c $container_name -n $namespacekubectl logs --tail=100 $pod_name -c $container_name -n $namespacekubectl logs -f $pod_name -c $container_name -n $namespacekubectl logs --tail=100 -l app=test -n $namespace

查看crashed容器标准输出日志
kubectl logs --previous $pod_name -c $container_name -n $namespace 

查看deployment的所有容器标准输出日志
kubectl logs -f deployment/$deployment_name -n $namespace

4.2. 查看容器内部日志kubectl exec $pod_name -n $namespace -- cat /var/log/cassandra/system.logkubectl exec $pod_name -c $container_name -n $namespace -- cat /var/log/cassandra/system.log

4.3. 执行命令1、登录容器
kubectl exec -it pod-name /bin/bashkubectl exec -it pod-name -c container-name /bin/bashkubectl exec -it pod-name -c container-name sh

2、直接执行命令
kubectl exec pod-name envkubectl exec pod-name -- envkubectl exec pod-name -it -- envkubectl exec -n default pod-name -it -- env# 命令带参数时必须加双横线kubectl exec pod-name -- sh -c &#x27;echo $&#123;LANG&#125;&#x27;

4.4. 拷贝文件拷贝pod内容到宿主机
kubectl cp $podname:/tmp/$filename .

拷贝宿主机内容到pod
kubectl cp $filename $podname:/tmp/

前提是容器里需要有tar命令，否则执行会报错：
OCI runtime exec failed: exec failed: unable to start container process: exec: &quot;tar&quot;: executable file not found in $PATH: unknowncommand terminated with exit code 126

5. 验证测试5.1. Pod测试为了验证dns解析是否正确、service访问是否正常，最好的方法就是在k8s集群中启动一个pod进行测试。
#kubectl run test --image=busybox:1.25 --command sleep 3600kubectl run test --image=alpine:3.7.3 --command sleep 3600kubectl run test --image=debian:buster --command sleep 3600

注意：用于测试的容器镜像不要使用busybox镜像，测试结果可能不准确。比如，使用最新版busybox的nslookup解析可能解析失败，但是换个低版本的busybox可能就正常了；使用telnet的测试端口提示Connection closed by foreign host，nc测试也是没有结果，但是换成其他镜像就可以正常连接。
5.2. 自定义测试镜像1、准备Dockerfile
FROM debian:busterRUN sed -i &#x27;s#http://deb.debian.org#http://mirrors.tuna.tsinghua.edu.cn#g&#x27; /etc/apt/sources.listRUN sed -i &#x27;s#http://security.debian.org/debian-security#http://mirrors.tuna.tsinghua.edu.cn/debian-security#g&#x27; /etc/apt/sources.listRUN apt update &amp;&amp; apt install -y telnet netcat curl

2、构建镜像&amp;上传镜像
docker build -t voidking/debian:buster .docker push voidking/debian:buster

3、启动测试pod
kubectl run test --image=voidking/debian:buster --command sleep 3600

6. 查看IP范围6.1. service cidr怎样查看一个k8s集群的service ip范围？
kubeadm config view | grep Subnetkubectl get pods -n kube-system kube-apiserver-master -oyaml | grep service-cluster-ip-range

6.2. pod cidr怎样查看一个k8s集群的pod ip范围？
kubeadm config view | grep Subnetkubectl cluster-info dump | grep -i cidr

如果上面两个方法都找不到，那么还可以通过网络组件的日志来查看，以weave为例。
docker ps | grep weavedocker logs &lt;weave-container-id&gt; | grep ipalloc-range

7. 集群操作7.1. 查看集群信息kubectl cluster-infokubectl cluster-info dump

7.2. 查看集群状态kubectl get cskubectl get componentstatuses

NAME                 STATUS      MESSAGE                                                                                       ERRORcontroller-manager   Unhealthy   Get &quot;http://127.0.0.1:10252/healthz&quot;: dial tcp 127.0.0.1:10252: connect: connection refusedscheduler            Unhealthy   Get &quot;http://127.0.0.1:10251/healthz&quot;: dial tcp 127.0.0.1:10251: connect: connection refusedetcd-0               Healthy     &#123;&quot;health&quot;:&quot;true&quot;&#125;
如果看到上面的信息，不要紧张。通过kubeadm安装的k8s集群，都是这样，可以忽略。如果非要解决，那么可以修改kube-controller-manager.yaml和kube-scheduler.yaml中的command启动参数，添加address参数。
spec:  containers:  - command:    - kube-scheduler    - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf    - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf    - --bind-address=127.0.0.1    - --kubeconfig=/etc/kubernetes/scheduler.conf    - --leader-elect=true    - --port=0    - --address=127.0.0.1

7.3. 查看集群事件kubectl get ev

7.4. 备份集群资源清单#!/bin/bash# 定义命名空间# NAMESPACES=$(kubectl get ns -o jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#x27;)NAMESPACES=&quot;default test&quot;# 定义备份的资源类型RESOURCE_TYPES=&quot;service deploy configmap secret job cronjob replicaset daemonset statefulset pvc ingress&quot;# 定义备份文件存放的根目录BACKUP_ROOT_PATH=yaml# 确保根目录存在mkdir -p $BACKUP_ROOT_PATH# 循环遍历所有命名空间for ns in $NAMESPACES; do  # 为每个命名空间创建一个目录  NAMESPACE_BACKUP_PATH=&quot;$BACKUP_ROOT_PATH/$ns&quot;  mkdir -p $NAMESPACE_BACKUP_PATH  echo &quot;Backup namespace: $ns to directory: $NAMESPACE_BACKUP_PATH&quot;  # 循环遍历所有资源类型  for type in $RESOURCE_TYPES; do    echo &quot;Backup type: $type in namespace: $ns&quot;    # 获取资源列表    item_list=$(kubectl -n $ns get $type -o jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#x27; || echo &quot;&quot;)    # 循环遍历资源并导出 YAML    for item in $item_list; do      file_name=&quot;$&#123;NAMESPACE_BACKUP_PATH&#125;/$&#123;type&#125;_$&#123;item&#125;.yaml&quot;      echo &quot;Backup $item to $file_name&quot;      kubectl -n $ns get $type $item -o yaml | kubectl neat &gt; $file_name    done  donedone


]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>alpine</tag>
        <tag>busybox</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
        <tag>debian</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S故障排查手册</title>
    <url>/dev-k8s-trouble-shooting/</url>
    <content><![CDATA[1. 故障排查整体思路故障排查三步走：1、分类。故障排查的第一步是分类，缩小问题范围。2、套路。不同问题，有不同的标准排查方法，详情可以参考阿里云ACK - 故障排除。3、发散。百度+谷歌+ChatGPT，搜寻各种可能的解决办法并进行尝试。
本文中，我们主要学习故障排查的套路（标准流程）。
参考文档：

阿里云ACK - 故障排除
阿里云ACK - 节点异常问题排查
腾讯云TKE - 故障处理
腾讯云TKE - Pod 异常排查概述
应用故障排查
Kubernetes 网络异常分类及排错指南
k8s实践指南-排错案例-分析 ExitCode 定位 Pod 异常退出原因



2. 问题分类K8S中的问题可以分为下面几类：

节点异常
Pod异常
DNS解析异常
Service异常
Ingress异常
存储异常
组件异常

3. 节点异常问题排查3.1. 排查思路
3.2. 查看节点状态kubectl describe nodes $nodename

3.3. 查看ntpsystemctl status chronydsystemctl restart chronydjournalctl -u chronyd

3.4. 重启kubelet和dockersystemctl stop kubeletsystemctl stop dockersystemctl stop docker.socketsystemctl stop containerdsystemctl daemon-reloadsystemctl start containerdsystemctl start dockersystemctl start kubelet

3.5. PLEG问题Pod生命周期事件生成器PLEG（Pod Lifecycle Event Generator）会记录Pod生命周期中的各种事件，如容器的启动、终止等。PLEG is not healthy 异常通常是由于节点上的运行时进程异常或者节点Systemd版本缺陷导致。
4. Pod异常问题排查未完待续。。。
5. DNS解析异常问题排查未完待续。。。
6. Service异常问题排查未完待续。。。
7. Ingress异常问题排查未完待续。。。
8. 存储异常问题排查未完待续。。。
9. 组件异常问题排查未完待续。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>alpine</tag>
        <tag>busybox</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
        <tag>debian</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S问题记录</title>
    <url>/dev-k8s-problem/</url>
    <content><![CDATA[1. 前言本文记录使用K8S过程中遇到的问题、解决办法和一些原理。问题排查方法参考《kubectl命令——故障排查篇》。


2. kubelet不停重启2.1. 问题描述K8S的一个worker节点磁盘不足，关机进行磁盘扩容，物理扩容后开机，执行esize2fs /dev/vdb，扩容完成。
docker ps，发现没有容器被启动。kubectl status kubelet，发现kubelet不停进行重启，每次都启动失败。kubectl status docker，正常docker正常running。
重启机器，问题依旧。
2.2. 排查解决journalctl -xeu kubelet -rjournalctl -xeu docker -r

kubelet日志没有报错，docker日志中报错：
level=error msg=&quot;xxx cleanup: failed to delete container from containerd: no such container


FROM ChatGPT：该错误日志表示 Docker 清理容器时失败，原因是没有找到相应的容器。


可能原因及解决方法：

容器不存在：检查容器是否已被删除或者已经退出，如果是则不需要处理该错误。
容器正在运行：如果容器正在运行，可能是由于正在执行某些任务而无法清理。此时可以尝试停止容器后再进行清理。
Docker daemon 出现故障：在某些情况下，Docker daemon 可能会出现故障导致无法清理容器。尝试重启 Docker daemon 可能会解决问题。
操作系统出现故障：在某些情况下，操作系统可能会出现故障导致无法清理容器。尝试重启操作系统可能会解决问题。


解决办法：手动清理容器，然后重启机器。
docker container prunereboot

3. 新增节点flannel启动失败3.1. 问题描述K8S集群新增了一个节点，flannel pod自动调度上去了，但是并没有启动成功。查看kubelet日志，报错为：[failed to find plugin “flannel” in path [/opt/cni/bin]]W0523 20:49:19.343813   12586 cni.go:239] Unable to update cni config: no valid networks found in /etc/cni/net.d
3.2. 解决办法从其他正常节点拷贝一个flannel文件到这个问题节点上的 /opt/cni/bin 目录。
4. 节点上的pod被驱逐4.1. 问题描述因为磁盘压力，节点上的pod被驱逐了。但是，实际上节点还有很多磁盘空间。
4.2. 解决办法1、查找kubelet配置文件路径
systemctl status kubelet -l

找到Drop-In配置文件路径，一般为：
/usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf或者：/etc/systemd/system/kubelet.service.d/10-kubeadm.conf

2、修改kubelet配置文件配置文件中添加：
KUBELET_EXTRA_ARGS=&quot;--eviction-hard=memory.available&lt;100Mi,nodefs.available&lt;5%,imagefs.available&lt;15%,nodefs.inodesFree&lt;5%&quot;

3、重启kubelet
systemctl daemon-reloadsystemctl restart kubelet

4.3. 扩展阅读节点压力驱逐是 kubelet 主动终止 Pod 以回收节点上资源的过程。
kubelet 监控集群节点的内存、磁盘空间和文件系统的 inode 等资源。 当这些资源中的一个或者多个达到特定的消耗水平， kubelet 可以主动地使节点上一个或者多个 Pod 失效，以回收资源防止饥饿。
在节点压力驱逐期间，kubelet 将所选 Pod 的 PodPhase 设置为 Failed。这将终止 Pod。
我们可以为 kubelet 指定自定义驱逐条件，以便在作出驱逐决定时使用。驱逐条件分为软驱逐条件和硬驱逐条件。软驱逐条件将驱逐条件与管理员所必须指定的宽限期配对。 在超过宽限期之前，kubelet 不会驱逐 Pod。如果没有指定的宽限期，kubelet 会在启动时返回错误。硬驱逐条件没有宽限期。当达到硬驱逐条件时， kubelet 会立即杀死 pod，而不会正常终止以回收紧缺的资源。
kubelet 具有以下默认硬驱逐条件：

memory.available&lt;100Mi
nodefs.available&lt;10%
imagefs.available&lt;15%
nodefs.inodesFree&lt;5%（Linux 节点）

参考文档：节点压力驱逐
5. 节点上的镜像被清理5.1. 问题描述节点上的镜像，被清理掉了，但是并没有人进行过手动清理操作，也没有定时任务。手动拉取镜像，过一段时间还是会被清理掉。
5.2. 问题原因参考文档：

Garbage Collection
K8S kubelet 资源垃圾回收机制
关于Kubernetes image垃圾镜像容器的回收

Kubernetes 对节点上的所有镜像提供生命周期管理服务，这里的所有镜像是真正意义上的所有镜像，不仅仅是通过 Kubelet 拉取的镜像。当磁盘使用率超过设定上限 HighThresholdPercent 时，Kubelet 就会按照 LRU 清除策略逐个清理掉那些没有被任何 Pod 容器（包括已经死亡的容器）所使用的镜像，直到磁盘使用率降到设定下限 LowThresholdPercent 或没有空闲镜像可以清理。此外，在进行镜像清理时，会考虑镜像的生存年龄，对于年龄没有达到最短生存年龄 MinAge 要求的镜像，暂不予以清理。
在磁盘使用率超出设定上限后：首先，通过 CRI 容器运行时接口读取节点上的所有镜像以及 Pod 容器；然后，根据现有容器列表过滤出那些已经不被任何容器所使用的镜像；接着，按照镜像最近被使用时间排序，越久被用到的镜像越会被排在前面，优先清理；最后，就按照排好的顺序逐个清理镜像，直到磁盘使用率降到设定下限（或者已经没有空闲镜像可以清理）。
需要注意的是，Kubelet 读取到的镜像列表是节点镜像列表，而读取到的容器列表却仅包括由其管理的容器（即 Pod 容器，包括 Pod 内的死亡容器）。因此，那些用户手动 docker run 起来的容器，对于 Kubelet 垃圾回收来说就是不可见的，也就不能阻止对相关镜像的垃圾回收。当然，Kubelet 的镜像回收不是 force 类型的回收，虽然会对用户手动下载的镜像进行回收动作，但如果确实有运行的（或者停止的任何）容器与该镜像关联的话，删除操作就会失败（被底层容器运行时阻止删除）。
影响镜像垃圾回收的关键参数有：
--image-gc-high-threshold：磁盘使用率上限，有效范围 [0-100]，默认 85--image-gc-low-threshold： 磁盘使用率下限，有效范围 [0-100]，默认 80--minimum-image-ttl-duration：镜像最短应该生存的年龄，默认 2 分钟

需要注意的是，1.21 版本之后 image-gc-high-threshold 替换为了 eviction-hard，image-gc-low-threshold 替换为了 eviction-mininum-reclaim。
6. 业务服务响应很慢6.1. 问题描述某个业务服务的Pod响应很慢，发现它的requests资源配置很低，limits资源配置很高。调大requests后响应速度明显变快了，是什么原理？
6.2. 原理解析requests是长期允许，保证资源；limits是临时允许，并不保证资源。上面的问题中，因为requests配置的很低，所以只能保证requests中配置的资源，并不能保证用到用到limits中配置的资源。
6.3. 扩展阅读“requests”和”limits”在Kubernetes中的原理是通过Linux的cgroups（control groups）来实现资源管理和隔离。cgroups是Linux内核提供的一种机制，它允许对进程组进行资源限制、优先级调整和统计。Kubernetes利用cgroups将资源限制和隔离应用到容器级别。
“requests”定义了容器所需的最小资源数量。Kubernetes调度器使用这个值来决定在哪个节点上运行容器，并确保节点上有足够的资源满足容器的请求。”requests”的目的是为了确保容器能够正常运行而不会遇到资源不足的问题。
而”limits”定义了容器允许使用的资源的上限。Kubernetes使用这个值来监控容器的资源使用情况，并保护节点的稳定性。如果容器试图使用超过其限制的资源量，Kubernetes会采取相应的措施，如终止容器或重新调度到其他节点。”limits”的目的是为了防止容器使用过多的资源，从而保护整个集群的稳定性。
尽管可以在容器中设置超过”requests”的资源使用量，但这并不是一个推荐的做法。当容器超过其”requests”的资源使用量时，它可能会影响其他容器的性能，导致资源竞争和不稳定的情况。超过”requests”的使用量只是暂时允许，Kubernetes会尽力满足容器的需求，但不保证持续提供额外的资源。
7. 节点资源充足但是无法调度7.1. 问题描述某个节点CPU、内存资源充足，也没有污点和亲和性配置，但是Pod无法调度到上面。
7.2. 排查解决除了检查资源之外，再检查下节点上Pod数量的限制。
kubectl describe node node01

Non-terminated Pods 要小于 Allocatable.pods ，如果Pod数量已经达到了节点上Pod数量限制，那么需要调大这个上限。
vim /var/lib/kubelet/config.yaml # maxPods默认110，调大它systemctl restart kubelet

8. kubectl top命令执行报错8.1. 问题描述执行kubectl top pod，报错：error: Metrics API not available 
在Kubernetes Dashboard上，也发现Pod的CPU使用率和内存使用都无法显示。
8.2. 问题分析参考kubectl top node error: metrics not available yet可知，之所以出现上面的问题，是因为K8S集群没有安装 metrics-server 。
8.3. 解决办法解决办法：安装 metrics-server 。
通用安装方法：
wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yamlkubectl apply -f components.yaml

如果是sealos安装的K8S集群，那么可以使用 metrics-server 集群镜像安装。
sealos run labring/metrics-server:v0.6.2 --cmd=&quot;\    helm upgrade -i metrics-server charts/metrics-server \    -n kube-system \    -f /root/metrics-server/values.yaml&quot;

其中 /root/metrics-server/values.yaml 内容为：
defaultArgs:  - --cert-dir=/tmp  - --kubelet-preferred-address-types=InternalIP  - --kubelet-use-node-status-port  - --metric-resolution=15s  - --kubelet-insecure-tls

9. kubectl get响应慢9.1. 问题描述执行 kubectl get xxx ，响应很慢，十几秒，总会几条出现提示：
I0819 11:17:11.769943 2075052 request.go:668] Waited for 1.159893511s due to client-side throttling, not priority and fairness, request: GET:https://192.168.56.101:6443/apis/enterprisesearch.k8s.elastic.co/v1beta1?timeout=32sI0819 11:17:31.979933 2075052 request.go:668] Waited for 9.271822241s due to client-side throttling, not priority and fairness, request: GET:https://192.168.56.101:6443/apis/events.openfunction.io/v1alpha1?timeout=32s

而且每次request后面的内容都不同。
9.2. 解决办法解决办法：kube-apiserver 的启动参数中添加 --feature-gates=APIPriorityAndFairness=false
具体操作方法：
# master节点cd /etc/kubernetes/manifestsvim kube-apiserver.yaml

添加启动参数：
spec:  containers:  - command:    - kube-apiserver    - --feature-gates=APIPriorityAndFairness=false    - ...

但是，上面的办法只能解决部分问题。kubectl get pod正常了，但是kubectl get all还是有问题。
终极解决办法：升级kubectl到v1.22版本以上。从提示其实也能看出是 client-side 侧的限流，但是 kubectl 并没有修改限流配置的方法，只能升级版本。
参考文档：

Kubectl Error - Waited for … due to client-side throttling, not priority and fairness, request
API Priority and Fairness
Crossplane 支持的自定义资源数量突破了 Kubernetes 的限制
How to avoid “Waited for Xs due to client-side throttling, not priority and fairness”
kubectl get all - command return - Throttling request
k8smaster-请求受到了节流
kube-apiserver

10. 某个节点无法访问coredns10.1. 问题描述新增了一个节点X，处于Ready状态，但是发现节点X和coredns不通。具体表现为 telnet 10.96.0.10 53 不通，nslookup www.baidu.com 10.96.0.10 也无法得到解析结果。与此同时，节点A是正常的，telnet 10.96.0.10 53 通，nslookup www.baidu.com 10.96.0.10 可以得到解析结果。
10.2. 问题排查解决1、检查节点状态所有节点都处于Ready状态。
2、检查pod网络组件查看节点X的flannel和kube-proxy，都运行正常。
3、确认问题边界

节点X ping 节点A和节点B，通
节点X telnet 另外的 service，不通
节点X ping coredns pod ip，不通
节点X ping 本机 pod ip，通
节点X ping 节点A pod ip，通
节点X ping 节点B pod ip，不通

这就有意思了，节点X和主机A pod通，和主机B pod不通，大概率是CNI的问题。
4、检查所有主机的cni插件
kubectl get nodeskubectl get pod -n kube-system -owide | grep flannel

10个节点，发现只有5个节点有flannel，但是这些没有flannel的节点也处于Ready状态，也是个bug。
继续排查发现没有flannel的节点存在污点，因此给flannel添加容忍，使每个节点都部署上CNI插件。
至此，问题解决。
10.3. 根因分析CNI插件的根本作用是调整节点上网络的转发规则。虽然节点X上的CNI插件正常，但是有5个节点缺少CNI插件，那么当节点X上调整网络转发规则时，会漏掉这5个节点上的pod，也就是说节点X上没有网络转发规则可以访问到它们。
11. 某个节点上的pod全部pending11.1. 问题描述节点X处于Ready状态，pod也可以调度到上面，但是所有调度到上面的pod全部处于pending状态，也没有任何events输出。
11.2. 解决办法解决办法：重启节点X的kubelet。
12. read eth0/speed: invalid argument12.1. 问题描述个别GPU节点上，运行 gpu-feature-discovery 报错：
E0927 10:43:40.329641       1 network.go:146] failed to read net iface attribute speed: read /host-sys/class/net/eth0/speed: invalid argument

登录GPU节点，执行：
cat /host-sys/class/net/eth0/speed
同样会报错 invalid argument
12.2. 解决办法升级内核。
参考文档：

Can’t read a specific file under /sys filesystem

13. 无法删除pod情况一13.1. 问题描述一些Pod处于Terminating状态，但是一直无法被删除。使用--force和--grace-period强制删除，会报出警告：
warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.

13.2. 解决办法kubectl describe pod xxx -n yyy
可以看到具体的pod无法被删除的原因，比如：
Warning  Unhealthy  12s (x3182 over 8h)  kubelet  Readiness probe errored: rpc error: code = Unknown desc = Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

根据提示检查docker状态，发现docker没有正常运行，启动它即可。
14. 无法删除pod情况二14.1. 问题描述一些Pod处于Terminating状态，但是一直无法被删除。使用--force和--grace-period强制删除，依然会卡住，无法删除。
14.2. 解决办法kubectl describe pod xxx -n yyy
依然看不到无法删除pod的原因，只能看到Status为Terminating，State为Terminated，Reason为Error。
解决办法：编辑pod，删除 metadata.finalizers 字段。
参考文档：Finalizers
15. 无法删除CRD15.1. 问题描述kubectl delete crd ingresses.networking.internal.knative.dev
卸载knative时，删除CRD ingresses.networking.internal.knative.dev ，无法删除。
15.2. 解决办法kubectl edit crd ingresses.networking.internal.knative.dev

删除其中的 metadata.finalizers 字段。
参考文档：Finalizers
16. 日志不存在16.1. 问题描述systemctl status kubelet，发现大量报错：
11月 28 15:48:31 node1 kubelet[1663]: E1122 15:48:31.502542    1663 cri_stats_provider.go:669] &quot;Unable to fetch container log stats&quot; err=&quot;failed to get fsstats for \&quot;/var/log/pods/calico-system_csi-node-driver-jbf9l_969616b6-784a-4154-8161-24206f4501cc/calico-csi/21.log\&quot;: no such file or directory&quot; containerName=&quot;calico-csi&quot;11月 28 15:48:31 node1 kubelet[1663]: E1122 15:48:31.502610    1663 cri_stats_provider.go:669] &quot;Unable to fetch container log stats&quot; err=&quot;failed to get fsstats for \&quot;/var/log/pods/kube-system_kube-proxy-9vh2d_f52465f9-229f-4895-9c23-3366b9d71c11/kube-proxy/21.log\&quot;: no such file or directory&quot; containerName=&quot;kube-proxy&quot;11月 28 15:48:31 node1 kubelet[1663]: E1122 15:48:31.502645    1663 cri_stats_provider.go:669] &quot;Unable to fetch container log stats&quot; err=&quot;failed to get fsstats for \&quot;/var/log/pods/calico-system_calico-node-68w9g_d33768de-9b24-43f6-ba60-ec9917889847/calico-node/0.log\&quot;: no such file or directory&quot; containerName=&quot;calico-node&quot;

16.2. 问题排查解决查看报错中的日志路径：
/var/log/pods/calico-system_csi-node-driver-jbf9l_969616b6-784a-4154-8161-24206f4501cc/calico-csi/21.log -&gt; /var/lib/docker/containers/8b352f0c0582b0c048449fef8305fde15ce4017dbb37abfaedcd698e9e1d2ffb/8b352f0c0582b0c048449fef8305fde15ce4017dbb37abfaedcd698e9e1d2ffb-json.log

日志路径是一个软链，而真实日志已经不存在了。
解决办法为删除所有失效软链：
find /var/log/pods -type l ! -exec test -e &#123;&#125; \; -printfind /var/log/pods -type l ! -exec test -e &#123;&#125; \; -delete

然后重启kubelet
systemctl restart kubeletsystemctl status kubelet

17. 流量经过ingress偶发502问题17.1. 问题描述100个并发压测一个接口，直接压测service，全部200；通过域名压测时，无论是在内网压测还是外网压测，都会出现1%的502。
服务pod日志中，没有502。ingress access.log 中，出现502。ingress errer.log 中，出现104。
2023/12/09 02:35:50 [error] 7960#7960: *60866823 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 172.20.0.100, server: test.voidking.com, request: &quot;POST /api/v1/user/login HTTP/1.1&quot;, upstream: &quot;http://100.107.80.144:80/api/v1/user/login&quot;, host: &quot;test.voidking.com&quot;2023/12/09 02:36:05 [error] 7373#7373: *60866826 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 172.20.0.100, server: test.voidking.com, request: &quot;POST /api/v1/user/login HTTP/1.1&quot;, upstream: &quot;http://100.82.122.191:80/api/v1/user/login&quot;, host: &quot;test.voidking.com&quot;

17.2. 问题排查解决1、调整服务pod资源、ingress pod资源、coredns pod资源，都没有效果。2、去掉tls证书，没有效果。3、调整ingress内核参数，没有效果。4、调整ingress upstream-keepalive-timeout 参数为 4 ，502数量降低到了 0.1% ，效果明显！调整 ingress upstream-keepalive-timeout 参数为 30，502数量降低到了 0.1% 。
注意：要在ingress controller的configMap中修改 upstream-keepalive-timeout 配置，而不是ingress的annotations（经验证无效）。
参考文档：

如何在 Kubernetes 中进行 ingress-nginx 配置优化以及HTTP请求速率限制
我们是如何解决偶发性的 502 错误的

17.3. 扩展阅读502错误是网关错误，504错误是网关超时。有个比较好的比喻：502是电话打通没人接，504是电话打不通。
什么是keepalive？HTTP/1.1中的Keep-Alive是一种机制，它允许在单个TCP连接上发送多个HTTP请求和响应，而不需要为每个请求/响应对都建立一个新的连接。在HTTP/1.0中，每个HTTP请求都需要建立一个新的TCP连接，这会导致一些性能上的开销。HTTP/1.1引入了Keep-Alive以解决这个问题。
那么 keepalive_timout 和 502 错误之间有什么关系？因为网站的架构不是浏览器直接连接后端的应用服务器，而是中间有nginx服务器做反向代理，浏览器和nginx服务器之间建立keepalive连接，nginx再和后端的应用服务器建立keepalive连接，所以这是两种不同的keepalive连接。
我们把浏览器和nginx之间的keepalive连接叫做ka1，把nginx和应用服务器之间的keepalive连接叫做ka2。如果ka1的超时设置为100秒，也就是说如果100秒之内没有新内容要传输，就把nginx和浏览器之间的连接断掉。而同时，我们把ka2设置为50秒，也就是说如果nginx和应用服务器之间没有新内容要传输，那么就把应用服务器和nginx之间的连接断掉。那么这时候就会产生一个问题：前50秒没有传输内容，在第51秒的时候，浏览器向nginx发了一个请求，这时候ka1还没有断掉，因为没有到100秒的时间，所以这是没有问题的，但是当nginx试图向应用服务器发请求的时候就出问题了，ka2断了！因为ka2的超时设置是50秒，这时候已经超了，所以就断了，这时候nginx无法再从应用服务器获得正确响应，只好返回浏览器502错误！
因此，解决办法也就明确了：让ka1小于ka2。
18. udp包问题18.1. 问题描述ffprobe rtsp://user:password@192.168.56.200:8080/api/to/stream
在宿主机上执行上面这条命令，可以正常获取到视频流。启动一个容器，执行上面这条命令，可以正常获取到视频流。使用同一个镜像启动Pod，执行上面这条命令，获取到视频流时会卡住。
正常输出：
ffprobe version 4.2.7-0ubuntu0.1 Copyright (c) 2007-2022 the FFmpeg developers  ...  libavutil      56. 31.100 / 56. 31.100  libavcodec     58. 54.100 / 58. 54.100  libavformat    58. 29.100 / 58. 29.100  libavdevice    58.  8.100 / 58.  8.100  libavfilter     7. 57.100 /  7. 57.100  libavresample   4.  0.  0 /  4.  0.  0  libswscale      5.  5.100 /  5.  5.100  libswresample   3.  5.100 /  3.  5.100  libpostproc    55.  5.100 / 55.  5.100Input #0, rtsp, from &#x27;rtsp://user:password@192.168.56.200:8080/api/to/stream&#x27;:  Metadata:    title           : Media Presentation  Duration: N/A, start: 0.000000, bitrate: N/A    Stream #0:0: Video: hevc (Main), yuv420p(tv), 1280x720, 32 fps, 25 tbr, 90k tbn, 32 tbc    Stream #0:1: Audio: pcm_alaw, 8000 Hz, 1 channels, s16, 64 kb/s


卡住的输出：
ffprobe version 4.2.7-0ubuntu0.1 Copyright (c) 2007-2022 the FFmpeg developers  ...  libavutil      56. 31.100 / 56. 31.100  libavcodec     58. 54.100 / 58. 54.100  libavformat    58. 29.100 / 58. 29.100  libavdevice    58.  8.100 / 58.  8.100

18.2. 解决办法指定 tcp 可以正常获取到视频流。
ffprobe rtsp://user:password@192.168.56.200:8080/api/to/stream -rtsp_transport tcp

从问题表现，只能推测出calico网络插件对于udp包的处理存在问题，具体原因还需要探索。
19. 卸载istio后无法创建pod问题19.1. 问题描述卸载istio后，创建了一个deployment，但是k8s无法创建出这个deployment对应的pod，到不了调度阶段，看不到pod。
查看deployment的描述，发现报错：
Warning  FailedCreate      24m (x17 over 29m)    deployment-controller  Error creating: Internal error occurred: failed calling webhook &quot;rev.object.sidecar-injector.istio.io&quot;: Post &quot;https://istiod.istio-system.svc:443/inject?timeout=10s&quot;: no endpoints available for service &quot;istiod&quot;

19.2. 原因分析出现这个问题的原因是卸载 Istio 后，ValidatingWebhookConfiguration 还保留在 Kubernetes 系统中。当 Kubernetes API server 内部试图调用 webhook 但无法找到相应的 webhook 服务端点时，会出现此错误。
19.3. 解决办法1、列出k8s集群中所有的 MutatingWebhookConfiguration 和 ValidatingWebhookConfiguration
kubectl get mutatingwebhookconfigurationskubectl get validatingwebhookconfigurations

找到与 Istio 相关的 webhook 配置。
2、删除与 Istio 相关的 webhook 配置
kubectl delete mutatingwebhookconfigurations &lt;your-mutating-webhook-config-name&gt;kubectl delete validatingwebhookconfigurations &lt;your-validating-webhook-config-name&gt;

删除这些 webhook 配置之后，deployment 对应的 pods 就能够正常创建了。
20. pod网段、service网段与docker网段冲突问题20.1. 问题描述部署k8s时规划网段不合理，给pod或service规划的网段为 172.17.0.0/16 ，刚好和docker的默认网段 172.17.0.0/16 冲突了。
ip addr show docker0kubectl describe pod -n kube-system $(kubectl get pods -n kube-system | grep kube-apiserver | awk &#x27;&#123;print $1&#125;&#x27;) | grep service-cluster-ip-rangekubectl get ippool -oyaml | grep cidr

20.2. 解决办法如果刚开始部署k8s集群就发现这个问题，那么给pod或service修改一个网段，重新部署即可。
如果已经使用了一段时间后才发现这个问题，那么修改起来就比较麻烦了。这里有两个解决办法：方法一：修改docker默认网段（验证可行）
1、停止kubelet和docker
systemctl stop kubeletsystemctl stop docker

2、编辑docker配置
vim /etc/docker/daemon.json

添加一行docker网关ip配置：
&quot;bip&quot;: &quot;192.168.254.1/24&quot;,

3、启动docker和kubelet
systemctl daemon-reloadsystemctl start dockersystemctl start kubelet

4、查看修改后的docker0 IP
ip addr show docker0

方法二：修改pod或service的网段（未验证）参考文档：

k8s更换svc和pod网段
How to make the pod CIDR range larger in kubernetes cluster deployed with kubeadm?
Migrate from one IP pool to another

21. 怎样扩展service网段？21.1. 问题描述发现knative controller报错：
failed to create private K8s Service: Internal error occurred: failed to allocate a serviceIP: range is full

21.2. 解决办法1、确认serviceIP是否已经用完
kubectl describe pod -n kube-system $(kubectl get pods -n kube-system | grep kube-apiserver | awk &#x27;&#123;print $1&#125;&#x27;) | grep service-cluster-ip-rangekubectl get svc -A | wc -l

如果发现 --service-cluster-ip-range=10.96.0.0/22 ，那么最多可以创建 1022 个 serviceIP。当实际svc数量大于1022时，那么证明serviceIP确实用完了。
2、修改apiserver配置，扩展service网段在master节点执行：
vim /etc/kubernetes/manifests/kube-apiserver.yaml

修改 serviceIP 范围为 10.96.0.0/20
3、检查 kube-apiserver 是否重启（配置发生修改时会自动重启）
kubectl get pods -n kube-system | grep kube-apiserver

4、验证修改
kubectl describe pod -n kube-system $(kubectl get pods -n kube-system | grep kube-apiserver | awk &#x27;&#123;print $1&#125;&#x27;) | grep service-cluster-ip-range

22. 怎样修改Service NodePort范围？22.1. 问题描述在Kubernetes中，默认情况下，NodePort类型的Service可以使用的端口范围是30000-32767。在某些情况下，我们可能需要使用更大的端口范围，此时该怎样修改Service NodePort的端口范围？
22.2. 修改方法1、登录master节点，修改 apiserver 配置
vim /etc/kubernetes/manifests/kube-apiserver.yaml

添加 --service-node-port-range 参数：
- command:  - kube-apiserver  - ...  - --service-node-port-range=10000-60000

修改后，会自动重建 apiserver pod注意：最小端口建议大于1024，这样可以避免占用系统保留端口。
2、查看新的 apiserver pod
kubectl get pod -n kube-system

23. pvc内容未更新问题23.1. 问题描述已知条件：

多个pod挂载了同一个pvc
pvc后端基于cephfs，挂载路径为 /data -&gt; cephfs:/path/to/data
cephfs挂载到了一台主机A，挂载路径为 /path -&gt; cephfs:/path

问题：在主机A上更新了 /path/to/data 中的内容后，从pod中看，/data 中的内容并没有更新；而新创建的pod，/data 中的内容是有更新的。
23.2. 问题排查解决同步情况验证：1、更新主机A /path/to/data 中的内容，发现老的pod /data 中的内容不会更新，新的pod /data 中的内容会更新。2、更新新的pod /data 中的内容，发现老的pod /data 中的内容不会更新，主机A /path/to/data 中的内容会更新。3、更新老的pod /data 中的内容，发现新的pod /data 中的内容不会更新，主机A /path/to/data 中的内容不会更新，但是其他老的pod /data 中的内容会更新。
以上问题表现，有两个怀疑：1）K8S某些节点有问题，没有处理好cephfs同步问题；2）cephfs存在问题，多副本间没有实现一致性。但是，检查了K8S节点，没有发现问题；检查cephfs日志，也没有发现问题。
意外发现：老的 pod /data 中内容更新后会同步到主机A上的 /path/to/data.bak 目录！
由此问题就清晰了：有位同学在主机A上更新  /path/to/data 中的内容时，并不是直接修改该目录中的内容，而是直接把该目录重命名为了 /path/to/data.bak，然后重建了一个目录 /path/to/data ，最终导致内容同步出现问题。这是因为cephfs挂载时读取文件名，会转换成一个统一标志符再进行挂载，哪怕重命名了，挂载的依然是原本的目录，所以老的pod挂载的目录，从 /path/to/data 变成了 /path/to/data.bak ，新的pod挂载的目录是 /path/to/data 
24. pod中执行bash速度很慢24.1. 问题描述在pod中，执行 bash 会卡住，要10s左右才能执行成功，切到新的bash shell。
24.2. 问题排查解决1、更换pod镜像，问题依然存在。证明不是镜像的问题。2、更换Docker版本，升级内核版本，更换操作系统，问题依然存在。证明不是它们的问题。3、执行bash时抓包，并没有网络流量。证明不是因为访问网络耗时导致的速度慢。4、执行 bash 时速度慢，执行 sh 时速度很快。证明执行速度慢和命令有关。5、删除 .profile、.bashrc 等文件，执行 bash 速度快了一些，大概需要 5s 左右。证明 bash 速度慢和 bash 初始化有关。6、执行 env 发现 pod 中有上万条环境变量，unset 这些环境变量后执行 bash 不再卡住。证明执行 bash 卡住的根本原因是 pod 环境变量过多。
问题原因：k8s中，默认情况下，同一个 namespace 的 service 信息会作为环境变量注入到 pod 中。解决办法：pod 中不要默认注入 service 信息。
apiVersion: v1kind: Podmetadata:  name: my-podspec:  enableServiceLinks: false  containers:  - name: my-container    image: my-image

此外，同一个namespace下的service过多，还可能引起pod无法启动，报错：
standard_init_linux.go:211: exec user process caused &quot;argument list too long&quot;

解决办法：pod 中不要默认注入 service 信息。参考文档：standard_init_linux.go:211: exec user process caused argument list too long
25. 服务不可用说明25.1. 问题描述当服务A不可用时，怎样给服务A添加一个不可用说明？
25.2. 解决办法创建一个说明服务B，当服务A不可用时，调整服务A的ingress配置，流量打到服务B。说明服务B定义参考：xxx-unavailable.yaml
26. 修改Ingress配置不生效问题26.1. 问题描述修改了一个 ingress 配置，但是测试发现没有生效。查看ingress-nginx日志，发现报错：
Failed to determine a suitable ClusterIP Endpoint for Service &quot;default/demo&quot;: service &quot;default/demo&quot; does not exist

26.2. 问题排查解决问题原因：这个报错的原因是 ingress 中配置使用了 default namespace 中的 demo service，但是这个service已经不存在了。
解决办法：找到使用 demo service 的 ingress，修改或删除它
kubectl get ingress -n default -oyaml | grep demo -A10

27. etcd 报错 database space exceeded27.1. 问题描述执行 kubectl 命令时报错：
Error from server: etcdserver: mvcc: database space exceeded

27.2. 解决办法参考文档：Etcdserver: mvcc: database space exceeded
ETCDCTL_API=3 etcdctl \--endpoints=https://[127.0.0.1]:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key \endpoint status --write-out=&quot;json&quot; | egrep -o &#x27;&quot;revision&quot;:[0-9]*&#x27; | egrep -o &#x27;[0-9].*&#x27;ETCDCTL_API=3 etcdctl \--endpoints=https://[127.0.0.1]:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key \compact 359226442ETCDCTL_API=3 etcdctl \--endpoints=https://[127.0.0.1]:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key \compact defrag --cluster



]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>好好学K8S</tag>
        <tag>etcd</tag>
        <tag>ingress</tag>
        <tag>kubectl</tag>
        <tag>sealos</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中的调度</title>
    <url>/dev-k8s-schedule/</url>
    <content><![CDATA[1. 调度是啥？在 K8S 中，调度 是指将 Pod 放置到合适的节点上，以便对应节点上的 Kubelet 能够运行这些 Pod。调度器通过 K8S 的监测（Watch）机制来发现集群中新创建且尚未被调度到节点上的 Pod，把它调度到一个合适的节点上运行。K8S调度分为两个阶段：过滤和打分。过滤阶段会将所有满足 Pod 调度需求的节点选出来；在打分阶段会为 Pod 从所有可调度节点中选取一个最合适的节点。最后，会将pod调度到得分最高的节点上。
我们可以约束一个 Pod 限制其只能在特定的节点上运行，或优先在特定的节点上运行。具体方法包括：

nodeName
label和nodeSelector
亲和性与反亲和性
Pod 拓扑分布约束

我们也可以约束一些 Pod 不能在特定的节点上运行。具体方法是使用污点（taint）和容忍（tolerations）。
参考文档：

Kubernetes 调度器
将 Pod 指派给节点
Pod 拓扑分布约束
污点和容忍度



2. 调度规则每个节点具有每种资源类型的最大容量：可为 Pod 提供的 CPU 和内存量。调度程序确保对于每种资源类型，调度的容器的资源请求的总和小于节点的容量。哪怕节点上实际CPU或内存的资源使用量非常低，但如果容量检查失败，则调度程序仍然拒绝在该节点上放置 Pod。
参考文档：管理容器的计算资源
3. nodeNamepod想要指定调度到某个node，可以直接在spec下制定nodeName字段
spec:  nodeName: node1

4. label和nodeSelector4.1. 添加labelkubectl label nodes node1 key1=value1,key2=value2

4.2. 添加nodeSelectornode添加label后，pod想要指定调度到这个node，可以在spec下添加nodeSelector字段
spec:  nodeSelector:    key1: value1

4.3. 删除label如果不再使用某个label，可以进行删除。
kubectl label nodes node1 key2-

5. 亲和性与反亲和性5.1. 节点亲和性节点亲和性(Node Affinity)是一种 Pod 调度器机制，用于指定 Pod 可以调度到哪些节点上运行。它可以通过指定节点的标签来实现，从而确保 Pod 运行在它所需要的节点上，以实现更好的资源利用率和性能表现。
1、节点添加label
kubectl label nodes node1 key1=value1,key2=value2

2、spec添加affinitynode添加label后，pod想要指定调度到这个node，也可以在spec下添加affinity字段
spec:  affinity:    nodeAffinity:      requiredDuringSchedulingIgnoredDuringExecution:        nodeSelectorTerms:        - matchExpressions:          - key: key1            operator: In            values:            - value1            - valuex      preferredDuringSchedulingIgnoredDuringExecution:      - weight: 1        preference:          matchExpressions:          - key: key2            operator: In            values:            - value2            - valuey

5.2. 节点反亲和性节点反亲和性(Node Anti-Affinity)？不存在这种机制。
5.3. pod亲和性Pod 亲和性(Pod Affinity)是一种 Pod 调度器机制，用于指定多个 Pod 之间的亲和性关系，以便它们可以被调度到同一节点上运行。这种亲和性配置可以用于实现分布式应用程序的高可用性和资源利用率，以及避免数据传输延迟等问题。
服务A和服务B的pod想要部署在同一个主机上：
apiVersion: apps/v1kind: Deploymentmetadata:  name: my-app-a  namespace: namespace-aspec:  replicas: 3  selector:    matchLabels:      app: my-app-a  template:    metadata:      labels:        app: my-app-a    spec:      affinity:        podAffinity:          requiredDuringSchedulingIgnoredDuringExecution:            - labelSelector:                matchExpressions:                - key: app                  operator: In                  values:                  - my-app-b              topologyKey: &quot;kubernetes.io/hostname&quot;              namespaceSelector:                 matchLabels:                  name: namespace-b      containers:      - name: my-container        image: nginx---apiVersion: apps/v1kind: Deploymentmetadata:  name: my-app-b  namespace: namespace-bspec:  replicas: 3  selector:    matchLabels:      app: my-app-b  template:    metadata:      labels:        app: my-app-b    spec:      affinity:        podAffinity:          requiredDuringSchedulingIgnoredDuringExecution:            - labelSelector:                matchExpressions:                - key: app                  operator: In                  values:                  - my-app-a              topologyKey: &quot;kubernetes.io/hostname&quot;              namespaceSelector: &#123;&#125;      containers:      - name: my-container        image: nginx

注意：

上面的例子中配置的亲和，是双向的亲和，服务A亲和服务B，服务B也亲和服务A。但是，如果两个服务都不可用时，可能会出现互相等待的情况。因此，大多数情况下最好配置单向的亲和。
默认情况下，Pod 亲和性规则是在同一命名空间内生效的。如果我们希望实现跨命名空间的亲和性，我们需要使用 namespaceSelector 来指定目标 Pod 所在的命名空间。这样可以确保亲和性规则适用于其他命名空间中的 Pod。空的 namespaceSelector: &#123;&#125; 表示匹配所有命名空间。

5.4. pod反亲和性Pod 反亲和性(Pod Anti-Affinity)是一种 Pod 调度器机制，用于指定多个 Pod 之间的反亲和性关系，以便它们不会被调度到同一节点上运行。这种反亲和性配置可以用于实现分布式应用程序的高可用性和避免资源冲突，以及确保容器之间的相互独立性。
参考文档Pod 打散调度
相同服务的pod必须打散，强反亲和：
apiVersion: apps/v1kind: Deploymentmetadata:  name: nginxspec:  replicas: 2  selector:    matchLabels:      app: nginx  template:    metadata:      labels:        app: nginx    spec:      affinity:        podAntiAffinity:          requiredDuringSchedulingIgnoredDuringExecution:          - topologyKey: kubernetes.io/hostname            labelSelector:              matchLabels:                app: nginx      containers:      - name: nginx        image: nginx

相同服务的pod尽量打散，弱反亲和：
apiVersion: apps/v1kind: Deploymentmetadata:  name: nginxspec:  replicas: 2  selector:    matchLabels:      app: nginx  template:    metadata:      labels:        app: nginx    spec:      affinity:        podAntiAffinity:          preferredDuringSchedulingIgnoredDuringExecution:          - podAffinityTerm:              topologyKey: kubernetes.io/hostname            weight: 100

6. Pod 拓扑分布约束未完待续。。。
7. 污点和容忍7.1. 添加taintkubectl taint nodes node1 key1=value1:NoSchedulekubectl taint nodes node1 key2=value2:PreferNoSchedulekubectl taint nodes node1 key3=value3:NoExecute

7.2. 添加tolerationsnode添加taint后，pod想要调度到这个node，需要在spec下添加tolerations字段：
spec:  tolerations:  - key: &quot;key1&quot;    operator: &quot;Equal&quot;    value: &quot;value1&quot;    effect: &quot;NoSchedule&quot;  - key: &quot;key2&quot;    operator: &quot;Exists&quot;    effect: &quot;PreferNoSchedule&quot;  - key: &quot;key3&quot;    operator: &quot;Equal&quot;    value: &quot;value3&quot;    effect: &quot;NoExecute&quot;

7.3. 删除taintkubectl taint nodes node1 key1:NoSchedule-

8. node封锁如果node存在问题，或者node需要升级维护，这时需要对node进行封锁，并且驱除pod。
8.1. 封锁node，不允许分配podkubectl cordon nodename

8.2. 从指定node驱除podkubectl drain nodename --ignore-daemonsets

9. 解除node的封锁，允许分配podkubectl uncordon nodename




]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S中的Pod水平自动扩缩</title>
    <url>/dev-k8s-horizontal-pod-autoscale/</url>
    <content><![CDATA[1. K8S中的Pod水平自动扩缩简介在容器编排和管理技术中，Kubernetes（K8S）已经成为最为流行和广泛使用的平台之一。Kubernetes不仅可以帮助管理容器化应用程序的部署和扩展，还可以通过自动化的方式来实现资源的动态调整，以满足应用程序的需求。其中，Pod水平的自动扩缩是Kubernetes中一个重要且强大的特性，它可以根据配置的规则和指标自动调整Pod的数量，以便在不同的负载情况下保持应用程序的稳定性和性能。
参考文档：Pod 水平自动扩缩


2. 什么是Pod水平自动扩缩？Pod水平自动扩缩是指根据一定的条件和指标，自动增加或减少Pod的数量，以便在不同的负载情况下适应应用程序的需求。这是一种实现弹性伸缩的方法，可以确保应用程序在高峰时期具有足够的资源，而在低谷时期避免资源浪费。
自动扩缩的核心概念：

指标（Metrics）：在Pod水平自动扩缩中，指标是评估应用程序性能和负载的关键数据。这些指标可以是CPU利用率、内存使用率、网络流量等。Kubernetes可以根据这些指标的变化来判断是否需要调整Pod的数量。
副本数（Replica Count）：副本数指的是当前运行的Pod数量。通过增加或减少Pod的副本数，可以实现自动扩缩的效果。
控制器（Controller）：Kubernetes中的控制器负责管理Pod的生命周期和数量。常见的控制器包括Deployment、ReplicaSet等。自动扩缩通常是通过调整控制器的副本数来实现的。
伸缩策略（Scaling Policy）：伸缩策略定义了在不同指标条件下应该如何调整Pod的副本数。例如，当CPU利用率超过80%时，增加副本数以应对高负载。

3. 配置Pod水平自动扩缩要配置Pod水平自动扩缩，需要以下几个步骤：
1、选择合适的指标：根据应用程序的性质和需求，选择合适的指标作为自动扩缩的依据。常见的指标包括CPU利用率、内存使用率、请求吞吐量等。
2、配置伸缩策略：通过Kubernetes的HorizontalPodAutoscaler资源对象来定义伸缩策略。在伸缩策略中，需要设置目标指标值、最小副本数、最大副本数等参数。
3、应用伸缩策略：Kubernetes会根据伸缩策略自动监测指标，并在需要时调整Pod的副本数。这可以确保应用程序始终具有所需的性能和资源。
4. 实际案例假设我们有一个名为my-app-deployment的Deployment，它运行着一个Web应用程序，并且我们希望根据CPU利用率来自动调整Pod的数量。下面是如何创建和配置一个HorizontalPodAutoscaler的实际步骤：
1、创建Deployment：创建一个Deployment来运行我们的应用程序。
apiVersion: apps/v1kind: Deploymentmetadata:  name: my-app-deploymentspec:  replicas: 3  selector:    matchLabels:      app: my-app  template:    metadata:      labels:        app: my-app    spec:      containers:        - name: my-app-container          image: my-app-image

2、创建HorizontalPodAutoscaler hpa.yaml：创建一个HorizontalPodAutoscaler对象，指定目标CPU利用率、最小副本数和最大副本数。
apiVersion: autoscaling/v2beta2kind: HorizontalPodAutoscalermetadata:  name: my-app-hpaspec:  scaleTargetRef:    apiVersion: apps/v1    kind: Deployment    name: my-app-deployment  minReplicas: 2  maxReplicas: 10  metrics:    - type: Resource      resource:        name: cpu        target:          type: Utilization          averageUtilization: 50
在上述例子中，我们创建了一个名为my-app-hpa的HorizontalPodAutoscaler对象，它将目标Deployment设置为my-app-deployment，并且定义了CPU利用率的目标值为50%。最小副本数设置为2，最大副本数设置为10。
3、应用HorizontalPodAutoscaler
kubectl apply -f hpa.yaml
现在，Kubernetes将根据CPU利用率自动监测并调整my-app-deployment的Pod副本数，以确保CPU利用率保持在50%左右。
5. 总结Pod水平自动扩缩是Kubernetes中一个强大的特性，可以根据应用程序的负载情况自动调整Pod的数量，以确保应用程序始终具有所需的性能和稳定性。通过选择合适的指标、配置伸缩策略和应用伸缩策略，可以在不同负载条件下实现自动化的资源调整，从而更好地应对不断变化的需求。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>shell</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：Kustomize工具</title>
    <url>/dev-kubectl-kustomize/</url>
    <content><![CDATA[1. Kustomize简介Kustomize是一个独立的工具，用来通过kustomization文件定制Kubernetes对象。从 1.14 版本开始，kubectl 也开始支持使用 kustomization 文件来管理 Kubernetes 对象。要查看包含 kustomization 文件的目录中的资源，执行下面的命令：
kubectl kustomize &lt;kustomization_directory&gt;

要应用这些资源，使用 –kustomize 或 -k 参数来执行 kubectl apply：
kubectl apply -k &lt;kustomization_directory&gt;

参考文档：

Kustomize
使用 Kustomize 对 Kubernetes 对象进行声明式管理
kustomize - kustomization
kustomize - PatchesJson6902
kubernetes-sigs/kustomize
kustomize - Examples
Demo: change image names and tags



2. Kustomize作用2.1. 生成资源ConfigMap 和 Secret 包含其他 Kubernetes 对象（如 Pod）所需要的配置或敏感数据。 ConfigMap 或 Secret 中数据的来源往往是集群外部，例如某个 .properties 文件或者 SSH 密钥文件。 Kustomize 提供 secretGenerator 和 configMapGenerator，可以基于文件或字面值来生成 Secret 和 ConfigMap。
2.2. 设置贯穿性字段在项目中为所有 Kubernetes 对象设置贯穿性字段是一种常见操作。 贯穿性字段的一些使用场景如下：

为所有资源设置相同的名字空间
为所有对象添加相同的前缀或后缀
为对象添加相同的标签集合
为对象添加相同的注解集合

2.3. 组织和定制资源一种常见的做法是在项目中构造资源集合并将其放到同一个文件或目录中管理。Kustomize提供基于不同文件来组织资源并向其应用补丁或者其他定制的能力。
Kustomize 支持组合不同的资源。kustomization.yaml 文件的 resources 字段定义配置中要包含的资源列表。我们可以将 resources 列表中的路径设置为资源配置文件的路径。
补丁文件（Patches）可以用来对资源执行不同的定制。Kustomize 通过 patchesStrategicMerge 和 patchesJson6902 支持不同的打补丁机制。下文【使用Kustomize】一节中会有补丁机制的具体用法。
3. 安装Kustomizekubectl中集成的kustomize功能不全，因此最好还是单独安装kustomize。
brew install kustomize

4. 使用Kustomize4.1. 生成资源清单kustomize build &lt;kustomization_directory&gt;

4.2. patchesStrategicMerge打补丁patchesStrategicMerge 的内容是一个文件路径的列表，其中每个文件都应可解析为策略性合并补丁（Strategic Merge Patch）。 补丁文件中的名称必须与已经加载的资源的名称匹配。建议构造规模较小的、仅做一件事情的补丁。例如，构造一个补丁来增加 Deployment 的副本个数；构造另外一个补丁来设置内存限制。
示例：
# base/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: my-deploymentspec:  replicas: 3# overlays/patch.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: my-deploymentspec:  template:    spec:      containers:        - name: my-container          image: nginx:latest# kustomization.yamlresources:  - base/deployment.yamlpatchesStrategicMerge:  - overlays/patch.yaml

在这个示例中，patchesStrategicMerge 使用 YAML 格式的补丁。它修改了 my-deployment 的 spec.template.spec.containers，将其中的镜像更改为 nginx:latest。补丁会与原始资源合并，生成最终的部署资源。
4.3. patchesJson6902打补丁4.3.1. patchesJson6902补丁机制概述patchesJson6902 补丁机制使用 JSON Patch 格式，它描述了如何对现有资源进行精确的更改。JSON Patch 是一个标准的 RFC 6902 文档，它定义了一组操作，如添加、替换、移动、复制和删除，以便对 JSON 对象进行修改。使用 patchesJson6902，我们可以提供一个或多个 JSON Patch 文件，这些文件将按顺序应用于原始资源，生成最终的结果。
4.3.2. 示例1// patches/patch.json[  &#123; &quot;op&quot;: &quot;replace&quot;, &quot;path&quot;: &quot;/spec/replicas&quot;, &quot;value&quot;: 5 &#125;]// kustomization.yamlresources:  - base/deployment.yamlpatchesJson6902:  - target:      group: apps      version: v1      kind: Deployment      name: my-deployment    path: patches/patch.json

示例1中，patchesJson6902 使用 JSON Patch 格式的补丁。它使用 replace 操作将 my-deployment 的 spec.replicas 更改为 5。补丁会被应用到原始资源上，生成具有修改的最终部署资源。
4.3.3. 示例2// kustomization.yamlresources:  - base/deployment.yamlpatchesJson6902:  - target:      group: apps      version: v1      kind: Deployment      name: my-deployment    patch: |-      - op: replace        path: /spec/replicas        value: 5

示例2实现的效果，和示例1相同。
4.3.4. 示例3// patches/patch.yaml- op: replace  path: /spec/replicas  value: 5// kustomization.yamlresources:  - base/deployment.yamlpatchesJson6902:  - target:      group: apps      version: v1      kind: Deployment      name: my-deployment    path: patches/patch.yaml

示例3中，patchesJson6902 使用 YAML Patch 格式的补丁。实现了和示例1完全相同的效果。
注意：patchesJson6902 默认是不支持 YAML 格式的补丁文件的，但是Kustomize做了额外的实现以支持YAML格式的补丁文件。
4.3.5. 示例4- target:    group: networking.k8s.io    kind: Ingress    name: test    version: v1  patch: |-    - op: add      path: /metadata/annotations/argocd.argoproj.io~1sync-options      value: Skip

示例4中，使用 patchesJson6902 在 Ingress 资源中添加 argocd.argoproj.io/sync-options: Skip ，使argocd忽略该资源的同步。需要注意的是，annotations的key中的/，被改成了~1。
4.4. 修改镜像需求：使用kustomize修改pod中使用的镜像。
1、创建测试yaml
cat &lt;&lt;EOF &gt;kustomization.yamlresources:- pod.yamlEOFcat &lt;&lt;EOF &gt;pod.yamlapiVersion: v1kind: Podmetadata:  name: myapp-pod  labels:    app: myappspec:  containers:  - name: myapp-container    image: busybox:1.29.0    command: [&#x27;sh&#x27;, &#x27;-c&#x27;, &#x27;echo The app is running! &amp;&amp; sleep 3600&#x27;]  initContainers:  - name: init-mydb    image: busybox:1.28.0    command: [&#x27;sh&#x27;, &#x27;-c&#x27;, &#x27;until nslookup mydb; do echo waiting for mydb; sleep 2; done;&#x27;]EOF

2、不修改镜像，生成资源清单
kustomize build .

3、设置修改镜像（1）修改image busybox:1.29.0 为 alpine:3.6
kustomize edit set image busybox:1.29.0=alpine:3.6

执行后，kustomization.yaml中会写入：
images:- name: busybox:1.29.0  newName: alpine  newTag: 3.6

（2）修改image busybox 为 alpine:3.6，无论 busybox 版本是什么
kustomize edit set image busybox=alpine:3.6

执行后，kustomization.yaml中会写入：
images:- name: busybox  newName: alpine  newTag: 3.6

4、设置修改镜像后，生成资源清单
kustomize build .


5. Kustomize问题记录5.1. 部分资源清单前缀未修改已知 kustomization.yaml 内容为：
namePrefix: prod-apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- deployment.yaml- pvc.yaml

生产资源清单之后，发现其中一个 deployment 中的claimName并没有添加 prod- 前缀。
解决办法：经过多次测试，发现是因为该 deployment 配置了namespace，去掉后问题解决。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>问题排查</tag>
        <tag>argocd</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：kubectl命令——资源对象增删查改篇</title>
    <url>/dev-kubectl-curd/</url>
    <content><![CDATA[1. K8S资源对象是啥？在 Kubernetes 系统中，Kubernetes 对象 是持久化的实体。 Kubernetes 使用这些实体去表示整个集群的状态，具体包括pod、replicaset、deployment、service、configmap、secret等等。
参考文档：

理解 Kubernetes 对象
Kubernetes 对象管理



2. 资源对象管理方式资源对象的管理有三种方式：指令式命令、指令式对象配置和声明式对象配置。其中，指令式对象配置和声明式对象配置都要依赖资源对象的yaml/json定义文件。
2.1. 指令式命令使用指令式命令时，用户可以在集群中的活动对象上进行操作。用户将操作传给 kubectl 命令作为参数或标志。这是开始或者在集群中运行一次性任务的推荐方法。因为这种方式直接在活跃对象上操作，所以它不提供以前配置的历史记录。例如，创建一个nginx deployment：
kubectl create deployment nginx --image nginx

2.2. 指令式对象配置在指令式对象配置中，kubectl 命令指定操作（创建、更新和删除），可选标志和至少一个文件名。指定的文件必须包含 YAML 或 JSON 格式的对象的完整定义。例如，创建配置文件中定义的对象（前提是写好了nginx.yaml）：
kubectl create -f nginx.yaml

通过覆盖更新配置文件中定义的对象：
kubectl replace -f nginx.yaml

删除两个配置文件中定义的对象：
kubectl delete -f nginx.yaml -f redis.yaml

2.3. 声明式对象配置使用声明式对象配置时，用户对本地存储的对象配置文件进行操作，但是用户未定义要对该文件执行的操作。 kubectl 会自动检测每个文件的创建、更新和删除操作。这使得配置可以在目录上工作，根据目录中配置文件对不同的对象执行不同的操作。例如，处理 configs 目录中的所有对象配置文件，创建并更新活跃对象。可以首先使用 diff 子命令查看将要进行的更改，然后再进行应用：
kubectl diff -f configs/kubectl apply -f configs/

与指令式对象配置相比的优点：

对活动对象所做的更改即使未合并到配置文件中，也会被保留下来。
声明性对象配置更好地支持对目录进行操作并自动检测每个文件的操作类型（创建，更新，删除）。

与指令式对象配置相比的缺点：

声明式对象配置难于调试并且出现异常时结果难以理解。
使用 diff 产生的部分更新会创建复杂的合并和补丁操作。

3. 资源对象的yaml/json文件详解3.1. yaml/json文件构成k8s资源对象的yaml文件分成四部分，apiVersion、kind、metadata和spec。

apiVersion和kind是关联的，参考kubectl api-resources。
metadata必填name、namespace、labels。
pod.spec主要填containers的name和image；deployment.spec主要填replicas、template和selector；service.spec主要填selector、ports和type。

3.2. yaml/json文件编写编写k8s资源对象的yaml/json文件，有三种方法：

纯手工编写（难度最高）
使用--dry-run生成yaml/json文件，然后修改
从线上导出yaml/json文件，然后修改

3.3. 生成文件模板生成deployment的yaml/json文件模板（推荐）：
kubectl create deployment vk-deploy --image=nginx --dry-run -o yaml &gt; deploy-name.yamlkubectl create deployment vk-deploy --image=nginx --dry-run -o json &gt; deploy-name.json

更多内容，参考Kubernetes kubectl create deployment 命令详解。
注意，我们并不需要在deployment中指定容器对外暴露的ports，因为该字段只是一个提示作用。

List of ports to expose from the container. Exposing a port here gives the system additional information about the network connections a container uses, but is primarily informational. Not specifying a port here DOES NOT prevent that port from being exposed. Any port which is listening on the default “0.0.0.0” address inside a container will be accessible from the network. Cannot be updated.

生成deployment的yaml文件模板（即将废弃）：
kubectl run vk-deploy --image=nginx --dry-run -o yaml
会出现提示：kubectl run –generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run –generator=run-pod/v1 or kubectl create instead.因为官方不推荐使用 run-pod/v1 以外的其他生成器，其他生成器不久后就会弃用。更多内容参考kubectl run。
3.4. 导出文件模板从K8S集群中导出已经存在的资源对象的yaml文件（已经废弃）：
kubectl get deploy/deployment-name -o yaml --export &gt; deploy-name.yaml
出现提示：Flag –export has been deprecated, This flag is deprecated and will be removed in future.该flag 1.14版本弃用，1.18版本移除，目前已经不可用。
替代方案：
kubectl krew install neatkubectl get deploy/deployment-name -o yaml | kubectl neat

3.5. -h帮助在生成yaml文件的过程中，如果忘记了参数，可以使用kubectl xxx -h命令来获取帮助。
kubectl -hkubectl create -hkubectl create deployment -h

-h除了可以查看生成yaml文件的命令帮助，还可以用来查看所有kubectl子命令的帮助，非常实用。
3.6. explain帮助编写yaml文件的过程中，如果忘记了某些结构和字段，可以使用kubectl explain命令来获取帮助。
1、查看资源包含哪些字段以查看deployment的yaml包含哪些字段为例：
kubectl explain deploymentkubectl explain deployment --api-version=apps/v1kubectl explain deployment --api-version=apps/v1 --recursive=true

2、查看子字段以查看节点亲和性字段为例：
kubectl explain deployment.spec.template.spec.affinitykubectl explain deployment.spec.template.spec.affinity.nodeAffinity...kubectl explain deployment.spec.template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions

必须说明的是，explain不止可以查看k8s内置资源结构，还可以查看crd资源结构，例如：
kubectl explain milvus --api-version=milvus.io/v1beta1kubectl explain milvus.spec --api-version=milvus.io/v1beta1kubectl explain milvus.spec.components --api-version=milvus.io/v1beta1kubectl explain milvus.spec.dependencies --api-version=milvus.io/v1beta1

milvus安装配置方法，参考《K8S中安装配置Milvus》
3.7. yaml/json文件验证kubectl create --validate -f deploy-name.yaml --dry-run=client

需要注意的是，--dry-run=client参数要带着，否则验证成功后，这个文件直接就提交给k8s了。
4. pod yaml生成pod的yaml文件模板比较常用，这里单独做一下记录。
kubectl run vkpod --image=busybox --restart=Never --dry-run=client -oyamlkubectl run vk-pod --image=busybox --dry-run=client -o yaml --command -- sleep 1000kubectl run vk-pod --image=busybox -l &#x27;name=vk-pod,env=dev&#x27; --dry-run=client -o yamlkubectl run vk-pod --image=busybox --labels=&#x27;name=vk-pod,env=dev&#x27; --dry-run=client -o yaml

更多内容，参考Kubernetes kubectl run 命令详解和kubectl 的用法约定。
5. 查看资源对象5.1. 查看可用api版本kubectl api-versions

5.2. 查看资源缩写kubectl describekubectl api-resources
建议记住常用资源的SHORTNAMES，可以提升输入效率。此外，记住常用资源的APIGROUP，可以提高编写yaml文件时的效率。
5.3. 查看所有资源对象kubectl get allkubectl get all -o wide

5.4. 查看指定的资源对象以deployment资源对象为例，其他资源对象操作方法相同。
查看所有空间的所有deployment，deployment可以缩写为deploy
kubectl get deployment --all-namespaces

查看指定空间下的所有deployment，默认空间default
kubectl get deploykubectl get deploy -n voidkingkubectl get deploy -n voidking -o wide

查看deployment，显示label
kubectl get deploy --show-labels

根据label筛选deployment
kubectl get deploy -l &#x27;env=dev&#x27;kubectl get deploy --selector=&#x27;env=dev&#x27;kubectl get deploy --selector=&quot;env=dev,type=frontend&quot;

查看指定的deployment
kubectl get deploy/deployment-namekubectl get deploy/deployment-name -o widekubectl get deploy/deployment-name -o yaml

查看deployment详细信息
kubectl describe deploy/deployment-name

查看deployment实时变化
kubectl get deploy/deployment-name --watch

5.5. 根据label筛选资源对象筛选所有label包含 env=dev 的资源对象
kubectl get all -l &#x27;env=dev&#x27;# orkubectl get all --selector &#x27;env=dev&#x27;

5.6. 根据field筛选资源对象对于Pod资源来说，以下是一些可用于--field-selector的字段：

metadata.name
metadata.namespace
status.phase（例如，Running，Pending，Succeeded，Failed，Unknown）
status.podIP
spec.nodeName（节点名）

这是一个不完全的列表，支持的字段可能会随着Kubernetes版本的更新而变化。
筛选所有Running状态的Pod
kubectl get pod -A --field-selector status.phase==Running

5.7. 根据jsonpath筛选资源对象筛选所有Ready状态的Pod
#!/bin/bashkubectl get pods -A -o jsonpath=&#x27;&#123;range .items[*]&#125;&#123;.metadata.namespace&#125;&#123;&quot;\t&quot;&#125;&#123;.metadata.name&#125;&#123;&quot;\n&quot;&#125;&#123;end&#125;&#x27; &gt; pods.txtwhile read line;do  namespace=$(echo &quot;$line&quot; | awk &#x27;&#123;print $1&#125;&#x27;)  name=$(echo &quot;$line&quot; | awk &#x27;&#123;print $2&#125;&#x27;)  # ready 取值 True/False  ready=$(kubectl get pods $name -n $namespace -o jsonpath=&quot;&#123;.status.conditions[?(@.type==&#x27;Ready&#x27;)].status&#125;&quot;)  if [[ $ready == &quot;True&quot; ]];then    echo &quot;$&#123;namespace&#125;/$&#123;name&#125; is ready!&quot;  else    echo &quot;$&#123;namespace&#125;/$&#123;name&#125; is not ready!&quot;  fidone &lt; pods.txt

更多jsonpath的内容，参考文档《JSONPath入门篇》
6. 创建资源对象以deployment资源对象为例，其他资源对象操作方法相同。
6.1. 指令式命令kubectl create deployment vk-deploy --image=nginx

6.2. 指令式对象配置指令式对象配置，需要依赖定义好的资源对象yaml文件。
kubectl create -f deploy.yaml

6.3. 声明式对象配置和指令式对象配置唯一不同的是，kubectl create 改为 kubectl apply。
7. 更新资源对象以deployment资源对象为例，其他资源对象操作方法相同。
7.1. 基本操作# 编辑集群中的资源kubectl edit deployment deployment-name# 比较manifest与集群中当前资源的不同kubectl diff -f deploy.yaml# 应用最新定义kubectl replace -f deploy.yamlkubectl apply -f deploy.yaml# 添加labelkubectl label deployment deployment-name new-label=awesome# 添加annotationkubectl annotate deployment deployment-name icon-url=http://goo.gl/XXBTWq 

7.2. patch操作7.2.1. patch简介kubectl patch: Update field(s) of a resource using strategic merge patch.
用法：
kubectl patch (-f FILENAME | TYPE NAME) -p PATCH

JSON and YAML formats are accepted.
--type有三个选项：

strategic：策略更新，也是默认的选项。根据k8s crd 资源对象的字段定义（patchStrategy）决定如何该如何更新，不指定patchStrategy时，默认是replace，当前大多资源对象在代码中已经指定patchStrategy为merge。
merge：合并更新，有相同的字段就替换，没有相同的字段就合并。和代码中指定patchStrategy为merge效果相同。
json：json更新，要求--patch参数是一个json列表 [&#123;&quot;op&quot;:&quot;&quot;,&quot;path&quot;:&quot;&quot;,&quot;value&quot;:&quot;&quot;&#125;]。

merge之坑：

如果一个字段的值为数组，无论你有没有指定 --type=merge，新的元素都将覆盖掉原先所有的列表元素。
merge 想要删除一个 key，只能通过将 key 设置为 null，但有些 key 对应的 value 是不能为 null 的。

当 --patch 参数是 json 格式时，path 里的斜杠就会与 key 冲突，此时可以将 波浪线和斜杠 替换成如下转义字符。

~（波浪线）对应的是 ~0
/（斜杠）对应的是 ~1

这个规则和kustomize中的patchesJson6902类似。
参考文档：

kubectl patch
详解 kubectl patch 命令 – strategic、json 和 merge 区别
《Kustomize工具》

7.2.2. patch添加节点亲和性1、准备亲和性配置，例如 data.yaml
spec:  template:    spec:      # affinity:      #   nodeAffinity:      #     requiredDuringSchedulingIgnoredDuringExecution:      #       nodeSelectorTerms:      #         - matchExpressions:      #             - key: nodetype      #               operator: In      #               values:      #                 - infra      nodeSelector:        nodetype: infra      tolerations:        - key: &quot;nodetype&quot;          operator: &quot;Equal&quot;          value: &quot;infra&quot;          effect: &quot;NoSchedule&quot;

或者json格式的配置文件，也是支持的。
2、添加节点亲和性
kubectl patch deployment deployment-name --type=merge -p &quot;$(cat data.yaml)&quot;# orkubectl patch deployment deployment-name --type=merge --patch-file data.yaml

3、批量添加节点亲和性
kubectl get deployment -n $namesapce -o=jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#x27; | xargs kubectl patch deployment --type=merge --patch-file data.yaml -n $namesapcekubectl get statefulset -n $namesapce -o=jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#x27; | xargs kubectl patch statefulset --type=merge --patch-file data.yaml -n $namesapce

7.2.3. patch删除存活探针kubectl patch deployment deployment-name --type json \    -p=&#x27;[&#123;&quot;op&quot;: &quot;remove&quot;, &quot;path&quot;: &quot;/spec/template/spec/containers/0/livenessProbe&quot;&#125;]&#x27;

7.2.4. patch修改env需求：某namespace中存在大量deployment，现在需要修改所有的deployment，容器名为 c1 的 env 字段中原本的 ENV: dev修改为 ENV: test，其他 env 字段保持不变。
实现：
kubectl get deployment -n $namesapce -o=jsonpath=&#x27;&#123;.items[*].metadata.name&#125;&#x27; | xargs kubectl patch deployment -n $namesapce -p &#x27;&#123;&quot;spec&quot;:&#123;&quot;template&quot;:&#123;&quot;spec&quot;:&#123;&quot;containers&quot;:[&#123;&quot;name&quot;:&quot;c1&quot;,&quot;env&quot;:[&#123;&quot;name&quot;:&quot;ENV&quot;,&quot;value&quot;:&quot;test&quot;&#125;]&#125;]&#125;&#125;&#125;&#125;&#x27;

7.3. set操作参考文档：

kubectl set
kubectl set env

kubectl set 命令的作用和 kubectl patch 的命令类似，都是用于修改K8S中的资源定义。按照本人的理解，set是patch的简化和补充。比如，使用patch修改image相对复杂，而使用set就很简单；再比如，使用patch修改env很麻烦，使用set就很简单。
使用set实现批量修改env的需求：
kubectl set env deployments -n $namesapce --all --containers=&quot;c1&quot; ENV=test

注意：set对自定义资源无效，此时还是需要更加强大的patch。
8. 删除资源对象以deployment资源对象为例，其他资源对象操作方法相同。
删除单个deployment：
kubectl delete deployment deployment-name

批量删除deployment：
kubectl get deployment -n &lt;your-namespace&gt; -oname | \    xargs kubectl delete -n &lt;your-namespace&gt;

9. 扩缩容扩缩容针对pod对象，需要操作deployment对象。
方法一：扩缩容命令。
kubectl scale --replicas=2 deployment deployment-name

方法二：更新yaml文件。
10. 重启podk8s中，没有pod重启命令，只有pod重建命理。而我们配置的pod重启策略，本质上也是重建pod，只不过pod名称相同，保留了event记录。
验证方法：创建一个100秒重启的pod，启动后在其中创建一个文件。当pod重启后，文件消失，pod在宿主机上对应的容器id也全部改变了。
11. 重建pod参考文档Using Kubectl to Restart a Kubernetes Pod
方法一：删除pod并重建
kubectl get pod &lt;pod_name&gt; -n &lt;namespace&gt; -o yaml | kubectl replace --force -f -

方法二：删除pod利用保活重建（前提是有replicaset）
kubectl delete pod &lt;pod_name&gt; -n &lt;namespace&gt;

方法三：通过扩缩实现重建pod
kubectl scale deployment &lt;deployment_name&gt; --replicas=0 -n &lt;namespace&gt;kubectl scale deployment &lt;deployment_name&gt; --replicas=2 -n &lt;namespace&gt;

方法四：通过rollout实现重建pod
kubectl rollout restart deployment &lt;deployment_name&gt; -n &lt;namespace&gt;

方法五：通过docker停止容器实现重建pod前提是保证pod的重启策略是Always。登录到pod所在宿主机，通过docker命令停止pod的pause容器。
docker ps | grep xxxdocker stop yyy

pause容器被停止后，kubectl get pods xxx看到的是pod重启。kubectl describe pod xxx会看到：Pod xxx changed, it will be killed and re-created.是的，实际上pod的所有容器都会被重建。
12. 创建service/暴露服务12.1. 命令式指令为deployment创建clusterip，暴露clusterip的80端口，对应pod的80端口
kubectl expose deployment deployment-name --port=80 --name svc-name

为deployment创建clusterip，暴露clusterip的6789端口，对应pod的80端口
kubectl expose deployment deployment-name --target-port=80 --port=6789kubectl expose -f vk-deploy.yaml --target-port=80 --port=6789

创建nodeport，暴露clusterip的80端口，对应pod的80端口，暴露node的随机端口
kubectl expose deployment deployment-name --port=80 --type=NodePort --name svc-name

创建clusterip，暴露clusterip的80端口，对应pod的80端口
kubectl create service clusterip svc-name --tcp=80:80

创建nodeport，暴露clusterip的80端口，对应pod的pod端口，暴露node的30080端口
kubectl create service nodeport svc-name --tcp=80:80 --node-port=30080


12.2. 命令式配置文件生成service的yaml文件模板（推荐方法）：
kubectl create service clusterip vk-svc --tcp=&quot;5678:80&quot; --dry-run -o yaml &gt; service.yaml

创建service
kubectl create -f service.yaml

更多内容，参考Kubernetes kubectl create service 命令详解和Service。
12.3. 声明式配置文件创建service
kubectl apply -f service.yaml

13. kubectl port-forward对外暴露服务，我们会使用nodeport或者loadbalance类型的service。如果是临时测试，还可以使用kubectl port-forward。
kubectl port-forward 通过端口转发映射本地端口到指定的应用端口，从而访问集群中的应用程序。
# 本地端口映射pod端口kubectl port-forward pod/$pod_name -n $namespace $node_port:$pod_portkubectl port-forward pod/$pod_name -n $namespace --address 0.0.0.0 $node_port:$pod_port# 本地端口映射service端口kubectl port-forward service/$service_name -n $namespace $node_port:$service_portkubectl port-forward service/$service_name -n $namespace --address 0.0.0.0 $node_port:$service_port

注意：使用该转发功能依赖socat，需要提前安装否则会报错：unable to do port forwarding: socat not found
yum install -y socat

14. 版本回退# 查看发布历史，看到的版本号按照递增顺序分配，越大的版本号表示越新的版本kubectl rollout history deployment deployment-name# 查看发布历史详情kubectl rollout history deployment deployment-name --revision=1# 查看发布状态kubectl rollout status deployment deployment-name# 回退到上一个版本kubectl rollout undo deployment deployment-name# 回退到指定版本kubectl rollout undo deployment deployment-name --to-revision=1

rollout表示roll out，这里翻译为发布。
回退到指定replicaset对应的版本：
# 查看想要回退到的replicaset详情kubectl describe replicaset deployment-name-xxx # 找到 Annotations 中的 deployment.kubernetes.io/revision ，例如值为2，那么回退命令为kubectl rollout undo deployment deployment-name --to-revision=1

15. configmap和secret1、创建configmap
kubectl create configmap special-config --from-literal=special.how=very --from-literal=special.type=charmkubectl create configmap game-config-3 --from-file=&lt;my-key-name&gt;=&lt;path-to-file&gt;

2、创建secret
# 在命令中指定key和valuekubectl create secret generic db-user-pass --from-literal=username=voidking --from-literal=password=&#x27;vkpassword&#x27;kubectl get secret db-user-pass -o yaml# 在文件中指定valueecho -n &#x27;voidking&#x27; &gt; ./username.txtecho -n &#x27;vkpassword&#x27; &gt; ./password.txtkubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txtkubectl create secret generic db-user-pass --from-file=username=./username.txt --from-file=password=./password.txtkubectl get secret db-user-pass -o yaml

在yaml文件中看到的username和password，都是经过base64加密的字符串。
# 加密echo -n &#x27;voidking&#x27; | base64# 解密echo &#x27;dm9pZGtpbmc=&#x27; | base64 --decode

16. 创建多个资源如果要创建ns，然后在ns中创建configmap，最后创建deployment（依赖configmap），该怎么操作？方法一：挨个apply
kubectl apply -f ns.yamlkubectl apply -f cm.yamlkubectl apply -f deploy.yaml

方法二：按顺序合并，然后apply
cat ns.yaml &lt;(echo &quot;---&quot;) cm.yaml &lt;(echo &quot;---&quot;) deploy.yaml &gt; ns-cm-deploy.yamlkubecl apply -f ns-cm-deploy.yaml

方法三：多次apply
kubectl apply -f .kubectl apply -f .

方法四：添加编号
mv ns.yaml 00-ns.yamlmv cm.yaml 01-cm.yamlmv deploy.yaml 02-deploy.yamlkubectl apply -f .

17. 强制删除namspace17.1. 问题描述有时，通过kubectl删除了一个namespace之后，namespace并不会消失，而是一直处于Terminating状态。
17.2. 解决办法方法一：先删除namespace中的资源
kubectl get ns &lt;your-namespace&gt; -oyamlkubectl api-resources --verbs=list --namespaced -o name | \    xargs -n 1 kubectl get --show-kind --ignore-not-found -n &lt;your-namespace&gt;kubectl delete &lt;resource-type&gt; &lt;resource-name&gt; --namespace=&lt;your-namespace&gt;

方法二：强制删除
kubectl delete ns &lt;your-namespace&gt; --force --grace-period=0

方法三：强制删除plus
kubectl patch namespace &lt;your-namespace&gt; -p &#x27;&#123;&quot;metadata&quot;:&#123;&quot;finalizers&quot;: []&#125;&#125;&#x27; --type=merge

方法四：方法三的API调用版
1、获取namepace配置
kubectl get ns &lt;your-namespace&gt; -o json &gt; namespace.json

2、编辑namespace配置编辑namespace.json，从 finalizers 字段中删除 kubernetes。
3、提交namespace配置
kubectl proxy --port=6880 &amp;curl -k -H &quot;Content-Type: application/json&quot; -X  PUT --data-binary @namespace.json http://127.0.0.1:6880/api/v1/namespaces/$&#123;NAMESPACE&#125;/finalizekubectl get ns










]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>shell</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：kubectl命令——配置篇</title>
    <url>/dev-kubectl-config/</url>
    <content><![CDATA[1. kubectl简介
Kubectl is a command line interface for running commands against Kubernetes clusters. 

没错，kubectl是一个命令行工具，用来控制K8S集群。kubectl该怎么读？可以参考HowToPronounce-kubectl，郝同学喜欢读作kubecontrol。
kubectl命令格式为：
kubectl [command] [TYPE] [NAME] [flags]

更多内容，参考：

Overview of kubectl
kubectl Cheat Sheet
The Kubectl Book
Kubernetes kubectl 命令表

《K8S入门篇》一文中，已经学习了kubectl的安装方法，并且使用了一些简单命令。本文学习kubectl的配置方法和配置技巧。


2. 安装kubectlcurl -LO https://dl.k8s.io/release/v1.22.15/bin/linux/amd64/kubectlchmod a+x kubectlmv kubectl /usr/local/bin/ln -s /usr/local/bin/kubectl /usr/bin/kubectlkubectl -h

3. 获取kubeconfig要想正常使用kubectl命令，首先要拿到用户认证配置信息（kubeconfig）。
kubeconfig获取方式：

使用kubeadm安装的K8S集群，默认的kubeconfig配置文件路径为 /etc/kubernetes/admin.conf
使用minikube安装的K8S集群，默认的kubeconfig配置文件路径为 $&#123;HOME&#125;/.kube/config
用户默认使用的kubeconfig配置文件路径为 $&#123;HOME&#125;/.kube/config
阿里云/腾讯云等云平台提供的K8S集群，从集群管理页面上获取kubeconfig配置，保存为 $&#123;HOME&#125;/.kube/config 即可

4. 指定配置文件如果有多个集群的多个kubeconfig配置文件，那么我们可能需要指定一个配置文件。
4.1. 指定默认配置文件export KUBECONFIG=~/.kube/config

4.2. 查看kubeconfig配置内容kubectl config view

4.3. 指定配置文件和contextkubectl config --kubeconfig=/root/vk-kube-config use-context voidking@kubenertes

4.4. 指定单次命令的配置文件kubectl get deployments --kubeconfig=/root/.kube/config

5. 指定默认的namespacekubectl config get-contextskubectl config set-context --current --namespace=voidkingkubectl config set-context $(kubectl config current-context) --namespace=voidking

6. 配置整合6.1. 同时使用多个配置export KUBECONFIG=~/.kube/config:~/Download/new-config

要保证多个config的cluster name、user name、context name不同，否则无法区分它们，会造成配置混乱。如果多个config的cluster name、user name、context name相同，可以自行修改一下，改成不同的name，修改name不会影响对集群的操作。
6.2. 多个配置整合成一个配置KUBECONFIG=~/.kube/config:~/Downloads/new-config kubectl config view --merge --flatten &gt; ~/.kube/newconfigexport KUBECONFIG=~/.kube/newconfig

6.3. 查看和使用配置kubectl config get-contextskubectl config use-context voidking@kubenertes

6.4. 删除某个集群配置kubectl config delete-context xxxkubectl config delete-user xxxkubectl config delete-cluster xxx

7. 配置整合2.0多个kubeconfig合并成一个kubeconfig后，可以通过kubectl config实现在多个k8s集群间切换，很方便。
但是，因为很多集群的cluster name、user name、context name是相同的，所以每当有新的k8s集群加入时，或者kubeconfig过期需要重新加入时，我们都需要修改对新的config中的name进行修改，也是比较麻烦。
这里提供另外一种配置整合的方法：config文件改为软链，软链到真实config，通过修改软链切换配置。
1、准备真实config
mkdir -p ~/.kubeconfig/dev/mkdir -p ~/.kubeconfig/test/vim ~/.kubeconfig/dev/configvim ~/.kubeconfig/test/config

2、准备修改软链脚本 change-kubeconfig.sh
#!/bin/bashCLUSTER=$1if [[ -z &quot;$CLUSTER&quot; ]];then    config_path=$(ls -l $HOME/.kube/config | awk &#x27;&#123;print $NF&#125;&#x27;)    echo &quot;Current kubeconfig is $config_path&quot;    echo &quot;You can specify a cluster to change the kubeconfig!&quot;    exit 1fiif [[ ! -e &quot;$HOME/.kubeconfig/$CLUSTER/config&quot; ]];then    echo &quot;Config file $HOME/.kubeconfig/$CLUSTER/config does not exist!&quot;    exit 1filn -sf $HOME/.kubeconfig/$CLUSTER/config $HOME/.kube/configecho &quot;Kubeconfig has changed to $HOME/.kubeconfig/$CLUSTER/config&quot;

3、脚本配置
chmod a+x change-kubeconfig.shmkdir $HOME/.scriptsmv change-kubeconfig.sh $HOME/.scriptscat &lt;&lt;EOF | tee -a $HOME/.bash_profile# kubeconfig scriptalias changek=&quot;\$HOME/.scripts/change-kubeconfig.sh&quot;EOF

4、使用脚本
source .bash_profilechangek devkubectl get nodeschangek testkubectl get nodes


8. 命令自动补全8.1. linux系统配置yum install -y bash-completionsource /usr/share/bash-completion/bash_completionsource &lt;(kubectl completion bash)echo &quot;source /usr/share/bash-completion/bash_completion&quot; &gt;&gt; ~/.bash_profileecho &quot;source &lt;(kubectl completion bash)&quot; &gt;&gt; ~/.bash_profile

配置后，输入kubectl get s后，两次按下tab键，会提示可以选择的资源。输入kubectl get sec后，按下tab键，会自动补全命令。
8.2. mac系统配置brew install bash-completionsource &lt;(kubectl completion zsh)echo &quot;source &lt;(kubectl completion zsh)&quot; &gt;&gt; ~/.bash_profile

9. 快捷命令9.1. kubectl缩写为kwhich kubectlalias k=&quot;/usr/local/bin/kubectl&quot;complete -F __start_kubectl k

建议把配置写入 .bash_profile ，登录shell后别名自动生效。
9.2. 命令合并alias kg=&quot;kubectl get&quot;alisa kd=&quot;kubectl describe&quot;
kubectl get合并为kg，kubectl describe合并为kd。
]]></content>
      <categories>
        <category>engineering</category>
        <category>k8s</category>
        <category>cloudnative</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>k8s</tag>
        <tag>shell</tag>
        <tag>好好学K8S</tag>
        <tag>kubectl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学K8S：K8S入门篇</title>
    <url>/dev-k8s-start/</url>
    <content><![CDATA[1. K8S简介Docker最开始是一个管理容器的工具，后来发展成为容器云。容器云以容器为资源分割和调度的基本单位，封装整个软件运行时环境，为开发者和系统管理员提供用于构建、发布和运行分布式应用的平台。当容器云专注于资源共享与隔离、容器编排与部署时，它更接近传统的IaaS；当容器云渗透到应用支撑与运行时环境时，它是一种PaaS。
但是，如果想要将Docker应用于具体的业务实现，是存在困难的——编排、管理和调度等各个方面，都不容易。于是，人们迫切需要一套管理系统，对Docker及容器进行更高级更灵活的管理。于是，Kubernetes出现了。Kubernetes这个单词来自于希腊语，含义是舵手或领航员。缩写为K8S，8代表“ubernete”这8个字符。
更多内容，参考10分钟看懂Docker和K8S和浙大SEL实验室的《Docker 容器与容器云》。


2. K8S架构K8S起源于Borg，Borg是谷歌内部的大规模集群管理系统，负责对谷歌内部很多核心服务的调度和管理。Borg 的目的是让用户能够不必操心资源管理的问题，让他们专注于自己的核心业务，并且做到跨多个数据中心的资源利用率最大化。Borg 主要由 BorgMaster、Borglet、borgcfg 和 Scheduler 组成。

BorgMaster 是整个集群的大脑，负责维护整个集群的状态，并将数据持久化到 Paxos 存储中；
Scheduer 负责任务的调度，根据应用的特点将其调度到具体的机器上去；
Borglet 负责真正运行任务（在容器中）；
borgcfg 是 Borg 的命令行工具，用于跟 Borg 系统交互，一般通过一个配置文件来提交任务。

K8S借鉴了Borg的设计理念，整体架构跟 Borg 非常像，一个K8S系统，通常称为一个K8S集群（Cluster）。这个集群主要包括两个部分：一个Master节点（主节点）和一群Node节点（计算节点）。Master节点主要还是负责管理和控制。Node节点是工作负载节点，里面是具体的容器。
Master节点包括API Server、Scheduler、Controller manager、Etcd等。

API Server提供了资源操作的唯一入口，并提供认证、授权、访问控制、API 注册和发现等机制；
Scheduler负责资源的调度，按照预定的调度策略将 Pod 调度到相应的机器上；
Controller manager负责维护集群的状态，比如故障检测、自动扩展、滚动更新等；
Etcd保存了整个集群的状态。

Node节点包括Docker、Kubelet、Proxy、Pod等。

Docker负责管理容器。
Kubelet负责维护容器的生命周期，同时也负责 Volume（CVI）和网络（CNI）的管理；
Proxy负责为Pod对象提供代理。
Pod是Kubernetes最基本的操作单元。一个Pod代表着集群中运行的一个进程，它内部封装了一个或多个紧密相关的容器。除了Pod之外，K8S还有一个Service的概念，一个Service可以看作一组提供相同服务的Pod的对外访问接口。

除了核心组件，还有一些推荐的 Add-ons：

kube-dns 负责为整个集群提供 DNS 服务
Ingress Controller 为服务提供外网入口
Heapster 提供资源监控
Dashboard 提供 GUI
Federation 提供跨可用区的集群
Fluentd-elasticsearch 提供集群日志采集、存储与查询

更多内容，参考Kubernetes 架构原理。
3. Minikube安装Minikube是在个人计算机上的虚拟机中运行单节点Kubernetes集群的工具。因为minikube要启动virtualbox虚拟机，在虚拟机中运行k8s集群。所以这里有两个安装思路：思路一：在centos7虚拟机中安装virtualbox，以及minikube。思路二：在本机win10中安装virtualbox，以及minikube。
对于思路一，因为virtualbox不支持Intel处理器的嵌套虚拟化，因此minikube无法在centos7虚拟机中再启动虚拟机，该思路不可行。
本文接下来使用思路二进行安装，默认virtualbox已经安装完成。安装方法参考

Install Minikube and Docker with VirtualBox on Windows 10 Home
Kubernetes 学习笔记之 MiniKube 安装

3.1. 安装Docker Toolbox1、下载Docker Toolbox，双击安装。
2、启动Docker Toolbox打开Docker quickstart terminal，Docker会进行初始化。
3.2. 镜像加速器参考《Docker镜像站的配置和使用》配置镜像加速器，本文中使用阿里云的镜像加速器。
1、访问阿里云镜像加速器，得到一个专属的加速地址。
2、对于Docker Toolbox用户，首先进入default虚拟机docker-machine ssh default
3、设置加速地址
sudo sed -i &quot;s|EXTRA_ARGS=&#x27;|EXTRA_ARGS=&#x27;--registry-mirror=https://wrqtu3hz.mirror.aliyuncs.com |g&quot; /var/lib/boot2docker/profileexit

4、验证配置
docker-machine restart defaultdocker info
如果出现Registry Mirrors和加速地址，则表明设置成功。
常用命令：
# 重启电脑后，需要手动启动default虚拟机docker-machine start default# default虚拟机IP发生变动时，需要重新生成配置docker-machine regenerate-certs defaultdocker-machine enveval $(&quot;C:\Program Files\Docker Toolbox\docker-machine.exe&quot; env)

3.3. 安装Minikube1、下载阿里云版minikube v1.2.0，放入k8s目录，重命名为minikube.exe，并加入环境变量。其他版本修改下载的url即可，版本号可以在github aliyun minikube中找到。
之所以使用阿里云版minikube，是因为原版的minikube在国内启动失败，无法下载Minikube ISO镜像。
2、验证安装右键+Shift打开Powershell，输入minikube version
3、启动minikube
minikube start# orminikube start --vm-driver=virtualbox --registry-mirror=https://registry.docker-cn.com


4、验证启动minikube status
5、启动minikube dashboardminikube dashboard

其他安装方式参考Install Minikube。
3.4. 安装kubectl1、下载kubectl，同样放到k8s目录。
2、验证安装kubectl version
4. Hello Minikube参考Hello Minikube，目标：

将部署hello world应用程序部署到Minikube。
运行应用程序。
查看应用日志。

1、使用git bash启动minikubeminikube start
2、准备镜像
# 准备文件docker pull node:6.14.2vi server.jsvi Dockerfile# 创建镜像docker build -t voidking/hello-node .# 登录上传# docker loginwinpty docker logindocker push voidking/hello-node

其中server.js写入内容为：
var http = require(&#x27;http&#x27;);var handleRequest = function(request, response) &#123;  console.log(&#x27;Received request for URL: &#x27; + request.url);  response.writeHead(200);  response.end(&#x27;Hello World!&#x27;);&#125;;var www = http.createServer(handleRequest);www.listen(8080);

Dockerfile写入内容为：
FROM node:6.14.2EXPOSE 8080COPY server.js .CMD node server.js

3、创建管理Pod的Deployment，Pod根据提供的Docker镜像运行容器。
kubectl create deployment hello-node --image=voidking/hello-node

4、查看Deployments和Pods
kubectl get deploymentskubectl get podskubectl describe pod hello-node

由上图可以看出，创建pod的过程中，花费时间最长的是下载镜像的步骤，大约10分钟。这里有个疑问，为什么没有使用本地的镜像？
5、创建服务，将Pod公开到公共Internet
kubectl expose deployment hello-node --type=LoadBalancer --port=8080kubectl get servicesminikube service hello-node

在支持负载均衡器的云提供商上，将配置外部IP地址以访问服务。在Minikube上，LoadBalancer类型通过minikube service命令使服务可访问。打开浏览器，访问：http://192.168.99.103:30378/ ，可以看到 Hello World!
6、查看Pod和Servicekubectl get pod,svc -n kube-system

7、删除service和deployment（可选）
kubectl delete service hello-nodekubectl delete deployment hello-node

8、关闭集群，删除集群（可选）
minikube stopminikube delete

5. minikube常用命令# 进入节点服务器minikube ssh# 执行节点服务器命令，例如查看节点 docker infominikube ssh -- docker info# 查看插件，启用插件minikube addons listminikube addons enable heapsterminikube addons disable heapster

6. 后记至此，跑通了Minikube的第一个Deployment，k8s的万里长征开始了第一步。接下来继续深入学习，目标是拿到双证：Certified Kubernetes Administrator (CKA) Program和Certified Kubernetes Application Developer (CKAD) Program，具体要求参考curriculum。
7. 书签
Learn Kubernetes Basics
Interactive Tutorial - Creating a Cluster
Installing Kubernetes with Minikube
Kubernetes 学习路径
学习 Kubernetes 和容器技术体系的最佳方法

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>k8s</tag>
        <tag>好好学K8S</tag>
        <tag>virtualbox</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：beego入门篇——下</title>
    <url>/dev-beego-start-2/</url>
    <content><![CDATA[1. 前言本文中研究学习beego的静态文件配置使用方法，请求返回值的json化处理，使用goland调试beego框架，数据库从sqlite迁移到mysql。
参考文档：

beego官网
beego开发文档



2. 静态文件配置使用bee工具生成的vkbeego项目，index.tpl中包含css。实际项目中，静态文件是和模板文件分离的，下面分离出index.tpl中的css，改成引用的方式。
1、图片准备index.tpl中的两个base64格式的图片，另存为icon.png和background.png，放到在static/img目录下。
2、在static/css目录下新建index.css文件，内容从index.tpl中拷贝，详见源码v0.0.2。
3、修改views/index.tpl文件为：
&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;  &lt;title&gt;Beego&lt;/title&gt;  &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot;&gt;  &lt;link rel=&quot;icon&quot; href=&quot;/static/img/icon.png&quot; /&gt;  &lt;link rel=&quot;stylesheet&quot; href=&quot;/static/css/index.css&quot;&gt;&lt;/head&gt;&lt;body&gt;  &lt;!--keep the same--&gt;&lt;/body&gt;&lt;/html&gt;

启动beego，此时看到的首页和原首页相同。
3. json处理当今，json已经成为了Web开发中前后端交互的标准格式。本节中修改《beego入门篇——中》一文中的增删查改接口，使返回数据变成json格式。为节省篇幅，下文中省略了文件头部。
1、修改controllers/user/add.go，内容为：
// omit the headfunc (c *AddController) Get() &#123;    var username = c.GetString(&quot;username&quot;)    fmt.Println(username)    c.Ctx.WriteString(&quot;Please add user by post request.&quot;)&#125;func (c *AddController) Post() &#123;    var username = c.GetString(&quot;username&quot;)    var password = c.GetString(&quot;password&quot;)    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    exist := o.QueryTable(&quot;user&quot;).Filter(&quot;UserName&quot;, username).Exist()    if exist&#123;        c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 1, &quot;ext&quot;: &quot;Username has been existed!&quot;&#125;        c.ServeJSON()        return    &#125;    var user = new(models.User)    user.Username = username    user.Password = password    id,err := o.Insert(user)    if err == nil &#123;        fmt.Println(id)        c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 0, &quot;userid&quot;: id,&quot;ext&quot;: &quot;User has been added!&quot;&#125;        c.ServeJSON()    &#125;else &#123;        c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 2, &quot;ext&quot;: &quot;Write to database failed!&quot;&#125;        c.ServeJSON()    &#125;    //c.Ctx.WriteString(&quot;add user: &quot; + username)    return&#125;

2、修改controllers/user/list.go，内容为：
// omit the headfunc (c *ListController) Get() &#123;    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    var qs orm.QuerySeter    qs = o.QueryTable(&quot;user&quot;)    var users []*models.User    //num, err := qs.All(&amp;users)    num, err := qs.All(&amp;users,&quot;Id&quot;, &quot;Username&quot;)    fmt.Printf(&quot;Returned Rows Num: %d, %s \n&quot;, num, err)    fmt.Println(users[0].Username)    c.Data[&quot;users&quot;] = users    c.TplName = &quot;user/list.tpl&quot;&#125;func (c *ListController) Post() &#123;    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    var qs orm.QuerySeter    qs = o.QueryTable(&quot;user&quot;)    var users []*models.User    //num, err := qs.All(&amp;users)    qs.All(&amp;users,&quot;Id&quot;, &quot;Username&quot;)    c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 0, &quot;ext&quot;: &quot;success&quot;, &quot;userlist&quot;: users&#125;    c.ServeJSON()    return&#125;

3、修改controllers/user/update.go，内容为：
// omit the headfunc (c *UpdateController) Post() &#123;    //id,_ := c.GetInt(&quot;id&quot;)    username := c.GetString(&quot;username&quot;)    password := c.GetString(&quot;password&quot;)    new_password := c.GetString(&quot;new_password&quot;)    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    exist := o.QueryTable(&quot;user&quot;).Filter(&quot;UserName&quot;, username).Exist()    if !exist&#123;        c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 1, &quot;ext&quot;: &quot;Username doesn&#x27;t exist!&quot;&#125;        c.ServeJSON()        return    &#125;    var user models.User    o.QueryTable(&quot;user&quot;).Filter(&quot;Username&quot;,username).Filter(&quot;Password&quot;,password).One(&amp;user)    if o.Read(&amp;user) == nil &#123;        user.Password = new_password        if num, err := o.Update(&amp;user); err == nil &#123;            if num != 0 &#123;                c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 0, &quot;ext&quot;: &quot;Password has been updated!&quot;&#125;                c.ServeJSON()            &#125;        &#125;    &#125;else &#123;        c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 2, &quot;ext&quot;: &quot;Username or password is wrong!&quot;&#125;        c.ServeJSON()    &#125;    return&#125;

4、修改controllers/user/del.go，内容为：
// omit the headfunc (c *DelController) Get() &#123;    c.Ctx.WriteString(&quot;Please use post request.&quot;)&#125;func (c *DelController) Post() &#123;    id,_ := c.GetInt(&quot;id&quot;)    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    num,err := o.QueryTable(&quot;user&quot;).Filter(&quot;Id&quot;,id).Delete()    fmt.Printf(&quot;Returned Rows Num: %d, %s \n&quot;, num, err)    if num != 0 &#123;        c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 0, &quot;ext&quot;: &quot;User has been deleted!&quot;&#125;        c.ServeJSON()    &#125;else&#123;        c.Data[&quot;json&quot;] = map[string]interface&#123;&#125;&#123;&quot;code&quot;: 1, &quot;ext&quot;: &quot;User Id doesn&#x27;t exist! Can&#x27;t delete!&quot;&#125;        c.ServeJSON()    &#125;    return&#125;

修改之后，四个接口都能够返回json格式的数据，使用Postman测试通过，可供js进行调用。
4. 调试以调试controllers/user/add.go中的Post方法为例。1、设置调试环境Run，Edit Configuration，Add New Configuration，Go Build。Run kind选择Directory，Directory、Output directory、Working directory都选择项目目录，OK。点击工具栏的Debug按钮，进入调试模式。
2、在Post方法中添加断点在代码左侧，单击添加断点。
3、测试调试使用Postman，发送Post请求到 http://localhost:8080/user/add ，请求参数包括username和password。查看Goland面板，可以看到面板中显示出了请求的参数等信息。使用F8进入下一步，F7进入函数，Shift+F8退出函数。
5. 数据库迁移前文中，使用sqlite数据库，而在生产环境中，大多是使用mysql。本节中研究学习数据库的迁移，把sqlite数据库中的数据移动到mysql数据库中。
5.1. 表结构迁移1、创建数据库使用mysql时，beego不会自动生成数据库，因此需要手动创建vkbeego数据库。
2、修改models/model.go文件，修改注册驱动和注册数据库为mysql：
import (    _ &quot;github.com/go-sql-driver/mysql&quot;)orm.RegisterDriver(&quot;mysql&quot;, orm.DRMySQL)orm.RegisterDataBase(&quot;default&quot;, &quot;mysql&quot;, &quot;root:voidking@tcp(192.168.56.104:3306)/vkbeego?charset=utf8&quot;)

3、生成表结构运行bee run，即可在beego数据库中生成表结构。
5.2. 表结构迁移2.01、创建beego/database/migrations/vkbeego.sql文件，内容为：
CREATE TABLE IF NOT EXISTS `user` (    `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,    `username` varchar(255) NOT NULL DEFAULT &#x27;&#x27; ,    `password` varchar(255) NOT NULL DEFAULT &#x27;&#x27; );

2、生成表结构创建数据库，修改models/model.go使用mysql后，执行命令：bee migrate [-driver=mysql] [-conn=&quot;root:voidking@tcp(127.0.0.1:3306)/vkbeego&quot;]
5.3. 数据迁移sqlite和mysql的SQL语句不兼容，参考sqlite导入到mysql进行数据迁移，此处不再展开。
6. 后记以上，学习了beego静态文件配置、json处理、调试和数据库迁移。beego还有好多内容值得学习，比如模块使用、进程监控、应用部署、第三方库等等，但是入门篇到这里就告一段落，see you。
源码分享：v0.0.2
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>beego</tag>
        <tag>debug</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：beego入门篇——中</title>
    <url>/dev-beego-start-1/</url>
    <content><![CDATA[1. 前言本文中研究学习beego对于数据库的增删查改。beego的官方ORM DEMO不是很好，demo无法跑通，也无法自动创建表结构。因此，本文重新设计了学习流程。
参考文档：

beego官网
beego开发文档



2. 环境准备1、安装ORMgo get github.com/astaxie/beego/orm
2、安装驱动
go get -u github.com/mattn/go-sqlite3go get -u github.com/go-sql-driver/mysqlgo get -u github.com/lib/pq

3. First Model本节中创建一个名为User的Model，只有三个字段，Id、Name和Age。运行代码，创建sqlite数据库和user表结构，并且向user表中插入一条数据。
1、在vkbeego/models目录中创建main.go文件，内容为：
package mainimport (    &quot;fmt&quot;    &quot;github.com/astaxie/beego/orm&quot;    _ &quot;vkbeego/routers&quot;    //_ &quot;github.com/go-sql-driver/mysql&quot;    _ &quot;github.com/mattn/go-sqlite3&quot;)type User struct &#123;    Id      int    Name    string    Age     int&#125;func create_table()&#123;    // 数据库别名    name := &quot;default&quot;    // drop table 后再建表    force := true    // 打印执行过程    verbose := true    // 遇到错误立即返回    err := orm.RunSyncdb(name, force, verbose)    if err != nil &#123;        fmt.Println(err)    &#125;&#125;func init() &#123;    //orm.RegisterDriver(&quot;mysql&quot;, orm.DRMySQL)    //orm.RegisterDataBase(&quot;default&quot;, &quot;mysql&quot;, &quot;root:root@/orm_test?charset=utf8&quot;)    orm.RegisterDriver(&quot;sqlite3&quot;, orm.DRSqlite)    orm.RegisterDataBase(&quot;default&quot;, &quot;sqlite3&quot;, &quot;db.sqlite3&quot;)    orm.RegisterModel(new(User))    create_table()&#125;func main() &#123;    o := orm.NewOrm()    o.Using(&quot;default&quot;) // 默认使用 default，你可以指定为其他数据库    user := new(User)    user.Name = &quot;Hankin&quot;    fmt.Println(o.Insert(user))&#125;

2、运行代码在models目录，go run main.go，控制台输出创建表的信息。

3、查看执行效果查看vkbeego/models目录，发现出现了db.sqlite3文件。如果运行代码时右键run，那么db.sqlite3文件会出现在根目录下。使用navicat for sqlite，打开db.sqlite3数据库，可以看到创建的表和数据。
4. 增加数据以上，已经实现了向数据库中添加数据，但是，Model的定义和注册、数据库的连接、插入数据的逻辑都在同一个文件中，非常混乱。因此需要对代码进行拆分，调整结构。
1、删除First Model删除models目录下的main.go和db.sqlite3。
2、在models目录下新建models.go，内容为：
package modelsimport (    &quot;fmt&quot;    &quot;github.com/astaxie/beego/orm&quot;    _ &quot;github.com/mattn/go-sqlite3&quot;)type User struct &#123;    Id      int    Username    string    Password     string&#125;func create_table()&#123;    // 数据库别名    name := &quot;default&quot;    // drop table 后再建表    force := false    // 打印执行过程    verbose := true    // 遇到错误立即返回    err := orm.RunSyncdb(name, force, verbose)    if err != nil &#123;        fmt.Println(err)    &#125;&#125;func init() &#123;    orm.RegisterModel(new(User))    orm.RegisterDriver(&quot;sqlite3&quot;, orm.DRSqlite)    orm.RegisterDataBase(&quot;default&quot;, &quot;sqlite3&quot;, &quot;db.sqlite3&quot;)    create_table()&#125;

3、在routers/user.go中添加路由：
beego.Router(&quot;/user/add&quot;, &amp;user.AddController&#123;&#125;)

4、在controllers/user目录中新建文件add.go，内容为：
package userimport (    &quot;fmt&quot;    &quot;github.com/astaxie/beego&quot;    &quot;github.com/astaxie/beego/orm&quot;    &quot;vkbeego/models&quot;)type AddController struct &#123;    beego.Controller&#125;func (c *AddController) Get() &#123;    var username = c.GetString(&quot;username&quot;)    fmt.Println(username)    c.Ctx.WriteString(&quot;Please add user by post request.&quot;)&#125;func (c *AddController) Post() &#123;    var username = c.GetString(&quot;username&quot;)    var password = c.GetString(&quot;password&quot;)    //fmt.Println(username)    //fmt.Println(password)    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    var user = new(models.User)    user.Username = username    user.Password = password    o.Insert(user)    c.Ctx.WriteString(&quot;add user: &quot; + username)&#125;

5、测试启动beego，使用Postman，发送Post请求到 http://localhost:8080/user/add ，请求参数包括username和password，成功写入参数到数据库。
5. 查看数据1、在routers/user.go中添加路由：
beego.Router(&quot;/user/list&quot;, &amp;user.ListController&#123;&#125;)

2、在controllers/user目录中新建文件list.go，内容为：
package userimport (    &quot;fmt&quot;    &quot;github.com/astaxie/beego&quot;    &quot;github.com/astaxie/beego/orm&quot;    &quot;vkbeego/models&quot;)type ListController struct &#123;    beego.Controller&#125;func (c *ListController) Get() &#123;    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    var qs orm.QuerySeter    qs = o.QueryTable(&quot;user&quot;)    var users []*models.User    //num, err := qs.All(&amp;users)    num, err := qs.All(&amp;users,&quot;Id&quot;, &quot;Username&quot;)    fmt.Printf(&quot;Returned Rows Num: %d, %s \n&quot;, num, err)    fmt.Println(users[0].Username)    c.Data[&quot;users&quot;] = users    c.TplName = &quot;user/list.tpl&quot;&#125;

3、在views/user目录中新建list.tpl文件，内容为：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;User List&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;User List&lt;/h2&gt;    &#123;&#123;range .users&#125;&#125;    &lt;p&gt;        ID为：&#123;&#123;.Id&#125;&#125;，用户名为：&#123;&#123;.Username&#125;&#125;    &lt;/p&gt;    &#123;&#123;end&#125;&#125;    &#123;&#123;range $index,$user := .users&#125;&#125;    &lt;p&gt;        Index为：&#123;&#123;$index&#125;&#125;，ID为：&#123;&#123;$user.Id&#125;&#125;，用户名为：&#123;&#123;$user.Username&#125;&#125;    &lt;/p&gt;    &#123;&#123;end&#125;&#125;&lt;/body&gt;&lt;/html&gt;

4、测试启动beego，使用Postman，发送Get请求到 http://localhost:8080/user/list ，即可看到用户列表。
6. 修改数据1、在routers/user.go中添加路由：
beego.Router(&quot;/user/update&quot;, &amp;user.UpdateController&#123;&#125;)

2、在controllers/user目录中新建文件update.go，内容为：
package userimport (    &quot;fmt&quot;    &quot;github.com/astaxie/beego&quot;    &quot;github.com/astaxie/beego/orm&quot;    &quot;vkbeego/models&quot;)type UpdateController struct &#123;    beego.Controller&#125;func (c *UpdateController) Get() &#123;    c.Ctx.WriteString(&quot;Please use post request.&quot;)&#125;func (c *UpdateController) Post() &#123;    //id,_ := c.GetInt(&quot;id&quot;)    username := c.GetString(&quot;username&quot;)    password := c.GetString(&quot;password&quot;)    new_password := c.GetString(&quot;new_password&quot;)    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    exist := o.QueryTable(&quot;user&quot;).Filter(&quot;UserName&quot;, username).Exist()    if !exist&#123;        c.Ctx.WriteString(&quot;Username doesn&#x27;t exist!&quot;)        return;    &#125;    var user models.User    o.QueryTable(&quot;user&quot;).Filter(&quot;Username&quot;,username).Filter(&quot;Password&quot;,password).One(&amp;user)    if o.Read(&amp;user) == nil &#123;        user.Password = new_password        if num, err := o.Update(&amp;user); err == nil &#123;            fmt.Println(num)        &#125;    &#125;else &#123;        c.Ctx.WriteString(&quot;Username or password is wrong!&quot;)        return;    &#125;    c.Ctx.WriteString(&quot;Password has been updated!&quot;)&#125;

3、测试启动beego，使用Postman，发送Post请求到 http://localhost:8080/user/update 进行密码的更新，请求参数包括username、password和new_password。
7. 删除数据1、在routers/user.go中添加路由：
beego.Router(&quot;/user/del&quot;, &amp;user.DelController&#123;&#125;)

2、在controllers/user目录中新建文件del.go，内容为：
package userimport (    &quot;fmt&quot;    &quot;github.com/astaxie/beego&quot;    &quot;github.com/astaxie/beego/orm&quot;)type DelController struct &#123;    beego.Controller&#125;func (c *DelController) Get() &#123;    c.Ctx.WriteString(&quot;Please use post request.&quot;)&#125;func (c *DelController) Post() &#123;    id,_ := c.GetInt(&quot;id&quot;)    var o = orm.NewOrm()    o.Using(&quot;default&quot;)    num,err := o.QueryTable(&quot;user&quot;).Filter(&quot;Id&quot;,id).Delete()    fmt.Printf(&quot;Returned Rows Num: %d, %s \n&quot;, num, err)    if num != 0 &#123;        c.Ctx.WriteString(&quot;User has been deleted!&quot;)    &#125;else&#123;        c.Ctx.WriteString(&quot;User Id doesn&#x27;t exist! Can&#x27;t delete!&quot;)    &#125;&#125;

3、测试启动beego，使用Postman，发送Post请求到 http://localhost:8080/user/del 进行删除数据，请求参数为id。
8. 结构体和表上面的代码中，我们并没有指定结构体和数据库表的对应关系，那么它们是怎样关联起来的呢？参考beego框架之orm模块——mysql、BeegoOrm 映射自定义表名以及自定义字段，可知在beego的models模块里，会自动处理结构体和数据库表的对应关系，比如：
type User struct&#123;    // 默认情况对应数据库的表名为：user   MyName  string    // 默认情况对应数据库里user表字段为：my_name   MyAge   string    // 默认情况对应数据库里user表字段为：my_age&#125;orm.RegisterModel(new(User))

新的需求：结构体名为User对应数据库的表名为test_userMyName的成员对应数据库的字段名为MyNameMyAge的成员对应数据库的字段名为MyAge
那么该User的结构体又该如何定义呢？新需求的User结构体：
type User struct&#123;   MyName  string `orm:&quot;column(MyName)&quot;`   MyAge   string `orm:&quot;column(MySex)&quot;`&#125;orm.RegisterModel(new(User))// 自定义表名（系统自动调用）func (u *User) TableName() string &#123;    return &quot;test_user&quot;&#125;

9. 后记至此，beego的增删查改操作基本都已经涉及，更高级的查询在使用时再去学习。
源码分享：v0.0.1
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>beego</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：beego入门篇——上</title>
    <url>/dev-beego-start-0/</url>
    <content><![CDATA[1. beego简介beego 是一个快速开发 Go 应用的 HTTP 框架，他可以用来快速开发 API、Web 及后端服务等各种应用，是一个 RESTful 的框架，主要设计灵感来源于 tornado、sinatra 和 flask 这三个框架，但是结合了 Go 本身的一些特性（interface、struct 嵌入等）而设计的一个框架。
参考文档：

beego官网
beego开发文档
GoWeb开发实战(Beego框架实现项目)
Qihoo360/wayne



2. beego架构2.1. beego模块beego 是基于八大独立的模块构建的，是一个高度解耦的框架。当初设计 beego 的时候就是考虑功能模块化，用户即使不使用 beego 的 HTTP 逻辑，也依旧可以使用这些独立模块。这些模块分别是cache、config、context、httplibs、logs、orm、session和toolbox。
2.2. beego执行逻辑beego 是一个典型的 MVC 架构，它的执行逻辑如下图所示：
2.3. beego项目结构一般的 beego 项目的目录如下所示：
├── conf│   └── app.conf├── controllers│   ├── admin│   └── default.go├── main.go├── models│   └── models.go├── static│   ├── css│   ├── ico│   ├── img│   └── js├── routers│   ├── router.go└── views    ├── admin    └── index.tpl

MVC中的M在models目录、V在views目录、C在controllers目录，main.go是入口文件。
3. beego安装1、安装beego安装包go get github.com/astaxie/beego
2、安装bee工具命令行执行 go get github.com/beego/bee
3、创建新项目vkbeego命令行执行 bee new vkbeego$GOPATH/src目录下就生成了vkbeego项目。
4、运行项目在vkbeego目录下执行 bee run
5、访问项目浏览器访问 http://localhost:8080
4. 入口文件main.go是beego项目的入口文件，内容为：
package mainimport (    _ &quot;vkbeego/routers&quot;    &quot;github.com/astaxie/beego&quot;)func main() &#123;    beego.Run()&#125;
入口文件中引入了routers模块和beego模块，然后在main函数中调用了beego模块的Run()函数。整个初始化过程如下图：
5. 路由路由模块负责路由转发，转发请求给对应的控制器。beego的入口文件中引入了routers模块，查看routers/router.go文件，内容为：
package routersimport (    &quot;vkbeego/controllers&quot;    &quot;github.com/astaxie/beego&quot;)func init() &#123;    beego.Router(&quot;/&quot;, &amp;controllers.MainController&#123;&#125;)&#125;

路由注册beego.Router，这个函数的功能是映射 URL 到 Controller，第一个参数是 URL （用户请求的地址），这里我们注册的是 /，也就是我们访问的不带任何参数的 URL，第二个参数是对应的 Controller，也就是我们即将把请求分发到那个控制器来执行相应的逻辑，我们可以执行类似的方式注册如下路由：
beego.Router(&quot;/test&quot;, &amp;controllers.TestController&#123;&#125;)

6. 控制器控制器接收到请求后，解析请求参数，负责实现具体的处理逻辑。上面的代码，在router中注册了路由，但是，我们并没有TestController控制器，因此需要创建该控制器。打开controllers/default.go文件，添加：
type TestController struct &#123;    beego.Controller&#125;func (c *TestController) Get() &#123;    c.Data[&quot;Website&quot;] = &quot;beego.me&quot;    c.Data[&quot;Email&quot;] = &quot;astaxie@gmail.com&quot;    c.Data[&quot;IsMale&quot;] = true    c.Data[&quot;LuckyNumbers&quot;] = [...]int&#123;0,1,3,4&#125;    c.TplName = &quot;test.tpl&quot;&#125;

新建views/test.tpl文件，内容为：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Test&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;p&gt;Test Page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

浏览器访问 http://localhost:8080/test ，即可看到“Test Page”。
7. 模板引擎模板引擎负责页面的渲染，比如上面的test.tpl就是一个模板，模板引擎把控制器处理后的数据传递给模板，然后根据模板结构生成html页面。TestController把Website等参数传递给test.tpl，但是test.tpl并没有显示，这里对test.tpl进行改造，显示参数。
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Test&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;Test Page&lt;/h2&gt;    &lt;p&gt;Website: &#123;&#123;.Website&#125;&#125;&lt;/p&gt;    &lt;p&gt;Email: &#123;&#123;.Email&#125;&#125;&lt;/p&gt;    &lt;p&gt;        Gender:        &#123;&#123;if .IsMale&#125;&#125;        Male        &#123;&#123;else&#125;&#125;        Female        &#123;&#123;end&#125;&#125;    &lt;/p&gt;    &lt;p&gt;        Lucky Numbers:        &#123;&#123;range .LuckyNumbers&#125;&#125;        &#123;&#123;.&#125;&#125;&amp;nbsp;&amp;nbsp;        &#123;&#123;end&#125;&#125;    &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

更多详细的模板语法参考beego 模板语法指南。
8. 路径美化以上，已经实现了基本的页面访问逻辑，但是路径不友好，比如所有的路由都写在routers/router.go中，所有的处理逻辑都写在controller/default.go中，所有的模板都写在views路径下。本节中对路径进行美化，使之看起来更加友好合理。假设现在要添加两个路由，分别用来显示登录页面和注册页面。以此为例，说明路径该怎样设计。
8.1. 总体设计因为登录和注册都属于用户管理，因此可以把它们归类到一个模块user，设计路径结构如下。
vkbeego├─controllers│  │  default.go│  └─user│      login.go│      reg.go├─routers│   router.go│   user.go└─views  │  index.tpl  │  test.tpl  └─user      login.tpl      reg.tpl

8.2. 路由一个模块对应一个路由文件，因此新建routers/user.go文件，内容为：
package routersimport (    &quot;vkbeego/controllers/user&quot;    &quot;github.com/astaxie/beego&quot;)func init() &#123;    beego.Router(&quot;/user/login&quot;, &amp;user.LoginController&#123;&#125;)    beego.Router(&quot;/user/reg&quot;, &amp;user.RegController&#123;&#125;)&#125;

8.3. 控制器一个模块对应一个控制器目录，因此新建controllers/user目录，目录中创建文件login.go和reg.go。
package userimport (    &quot;github.com/astaxie/beego&quot;)type LoginController struct &#123;    beego.Controller&#125;func (c *LoginController) Get() &#123;    c.TplName = &quot;user/login.tpl&quot;&#125;

package userimport (    &quot;github.com/astaxie/beego&quot;)type RegController struct &#123;    beego.Controller&#125;func (c *RegController) Get() &#123;    c.TplName = &quot;user/reg.tpl&quot;&#125;

当前login.go和reg.go中，只有一个Get请求的控制器，负责页面显示。在实际项目中，这两个文件中还可以添加Post请求的控制器，用来处理Post请求。
8.4. 模板一个模块对应一个视图目录，因此新建views/user目录，目录中创建文件login.tpl和reg.tpl。
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Login&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;p&gt;Login Page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Register&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;p&gt;Register Page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

执行命令 bee run，然后就可以浏览器访问URL了。
9. 后记本文中，首先学习了beego的模块构成、执行逻辑和项目结构，然后使用bee工具构建了第一个beego项目vkbeego，接着学习了beego的路由、控制器和模板引擎，最后美化了项目路径。接下来，学习使用beego对数据库进行增删查改。
源码分享：v0.0.0
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>beego</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Gin框架入门篇</title>
    <url>/dev-gin-start/</url>
    <content><![CDATA[1. Gin框架简介Gin 是一个用 Go (Golang) 编写的 Web 框架。 它具有类似 martini 的 API，性能要好得多，得益于 httprouter，速度提高了 40 倍。 如果在使用Golang开发时需要性能和良好的生产力，Gin框架很合适。
参考文档：

Gin官方文档
W3Cschoool Gin教程



2. Gin/Beego/Iris对比Gin：

高性能：Gin的速度可以比较接近于最低级别的网页处理程序，比如 net/http 的速度。这是尽可能少的内存拷贝和更少的内存分配所带来的好处。
简单易用：对于基本的Web应用程序，Gin通过降低API的复杂性而达到高效易用。
特性全面：Gin提供了大量便利的功能，如中间件、路由、表单类型解析等。
社区活跃，资料较多。

Beego：

整合性强：Beego采用模块化设计，包括ORM、Web框架、日志等，基于此可以直接编写Web应用程序，无需选用其他框架。
简单直观：Beego的API使用起来简单方便，可以便利地应用于Web开发中。
自动化代码生成：Beego有自己的bee命令行工具，可以自动生成代码。
生态完备，有完善的框架自带功能以及不少配套工具，但相对资源消耗较大，社区活跃度一般。

Iris：

高性能：Iris框架是性能上最快的GoWeb框架，处理请求的速度非常迅速。
易于上手：Iris提供了很多示例和说明，让初学者容易上手。
提供了丰富的功能，比如认证，websockets，even sourcing等。
社区活跃度较低，国内被使用的情况不如Gin和Beego。

整体来说，Gin是一款轻量级、足够灵活、性能极高的框架，很适合中小型项目；Beego则功能全面，适合大型项目，底层封装偏重，上手较为复杂；而Iris性能最好，但是社区活跃度和文档相对较少。
3. Gin快速入门3.1. 安装配置GolangGin 需要 Go 1.13 及以上版本，Go安装配置方法参考文档《好好学Golang：Golang入门篇》
3.2. 安装Gin1、创建一个Golang项目
mkdir gin-learning &amp;&amp; cd gin-learninggo mod init gin-learning

2、安装Gin
go get -u github.com/gin-gonic/gin

3.3. example.go1、创建一个 example.go 文件，内容为
package mainimport &quot;github.com/gin-gonic/gin&quot;func main() &#123;    r := gin.Default()    r.GET(&quot;/ping&quot;, func(c *gin.Context) &#123;        c.JSON(200, gin.H&#123;            &quot;message&quot;: &quot;pong&quot;,        &#125;)    &#125;)    r.Run() // 监听并在 0.0.0.0:8080 上启动服务&#125;

2、启动服务
go run example.go

3、访问服务
curl localhost:8080/ping


]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>gin</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Golang中的编译</title>
    <url>/dev-golang-compile/</url>
    <content><![CDATA[1. 前言编译是将高级语言编写的源代码转换成机器能够执行的指令的过程。编译可以分为几个阶段，这包括词法分析、语法分析、语义分析、中间代码生成、优化和目标代码生成。对于Go语言（Golang）来说，编译不仅仅是构建程序的一部分，而且是其设计哲学的核心，确保了速度、安全性和跨平台的灵活性。
参考文档：

How To Build and Install Go Programs
Go 编译相关的命令详解
go build命令详解
Compile and install the application



2. Go编译器的特点
快速编译：Go编译器以其极快的编译速度而闻名。这得益于Go的简洁语法、依赖管理系统和编译器的设计。快速的编译对提高开发效率和迭代速度至关重要。
静态链接：Go编程语言创建的二进制文件是静态链接的，这意味着所有依赖库都被包含在最终的可执行文件中。因此，我们可以在没有Go环境的系统上运行它们，无需担心依赖问题。
跨平台编译：使用Go，我们可以轻松地为不同的操作系统和架构编译程序。通过设置GOOS和GOARCH环境变量，Go编译器可以产生针对几乎任何平台的编译输出。
内存安全：Go的编译器会在编译时进行内存安全的检查，例如自动内存管理（GC）。这一机制有效防止了像野指针或内存泄漏这样的问题。
报错和警告：Go编译器非常严格，对于一些可能在运行时引起问题的代码段，如未使用的变量或导入的包，将无法通过编译，直到这些问题被修复。

3. 编译命令3.1. 标准编译命令编译Go代码非常简单，使用go build命令即可。在大多数情况下，这一命令无需任何参数，它会自动处理所有文件依赖和包引用。
go build

此命令将编译当前目录中的Go源文件。如果当前目录是包含Go代码的单个包的目录，它将产生一个可执行文件。
go build 需要的是一个路径，让编译器可以找到哪些需要编译的go文件。当省略所有参数时，就意味着使用当前目录进行编译。下面的几条命令是等价的：
go buildgo build .go build main.go

3.2. 自定义输出使用-o标志，我们可以为生成的二进制文件指定一个名字及路径。
go build -o myapp

3.3. 交叉编译Go语言（Golang）支持交叉编译，即在一个平台上编译生成另一个平台可执行的程序。这意味着我们可以在Mac上为Linux系统编译二进制文件，也可以在Windows上为ARM处理器编译程序，等等。Go语言交叉编译非常容易实现，因此被广泛应用于生成各种不同系统和架构的可执行程序，使得开发者可以轻松为多个平台构建应用。
要构建特定平台的应用程序，我们可以设置GOOS和GOARCH环境变量，分别表示操作系统和处理器架构。常见的GOOS值有：

darwin（Apple MacOS）
linux
windows
freebsd

常见的GOARCH值有：

amd64（64位x86架构）
386 （32位x86架构）
arm （ARM架构）
arm64（64位ARM架构）

例如，我们可以为Windows 64位系统生成可执行文件，即便我们正在使用Linux或macOS。
GOOS=windows GOARCH=amd64 go build

这将在我们的非Windows系统上生成一个Windows可执行程序。
交叉编译注意事项：1、安装golang依赖确保在进行交叉编译之前所有项目依赖都已经正确安装，运行go mod tidy 可以帮助我们清理和同步项目依赖。此外，我们的项目代码也应该没有平台相关的依赖，否则可能需要根据目标平台进行适配。
2、安装目标平台的库和工具链交叉编译时，可能需要注意目标平台的特定库或工具链支持。例如，当我们在64位系统上编译32位程序时，相应32位的库应该是可用的。还有，某些依赖C语言库（如CGO程序）的Go程序可能需要额外配置或安装相应的跨平台C语言库和工具链。
3、目标平台测试建议在目标平台上测试编译结果，以确保程序的正常运行。
4. 简单编译示例4.1. 编译Linux二进制文件GOOS=linux GOARCH=amd64 go build -o myapp myapp.go
生成一个Linux下可运行的可执行文件myapp。
注意：如果是在Windows中执行编译，需要使用set命令配置环境变量：
C:\&gt; set GOOS=linuxC:\&gt; set GOARCH=amd64

4.2. 编译Windows二进制文件GOOS=windows GOARCH=amd64 go build -o myapp.exe myapp.go
生成一个名为myapp.exe的Windows可执行文件。
4.3. 编译Mac二进制文件GOOS=darwin GOARCH=amd64 go build -o myapp myapp.go
生成一个Mac系统可运行的二进制文件myapp。
4.4. 编译ARM二进制文件GOOS=linux GOARCH=arm GOARM=7 go build -o myapp myapp.go
这个命令指定了GOARM环境变量，它用于定义ARM的具体版本。比如，GOARM=7通常用于诸如Raspberry Pi 2/3这样的较新ARM设备上。
5. 复杂编译示例CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags=&quot;-w -s&quot; -o /go/bin/pod-data-retorer

编译命令释义：
1、 CGO_ENABLED=0：

CGO_ENABLED 环境变量控制是否启用CGo。CGo允许Go代码调用C代码。
设置为0表示禁用CGo，这有助于确保生成的可执行文件能在没有C库（特别是在别的平台上没有C环境的情况下）的环境中运行。

2、 GOOS=linux 和 GOARCH=amd64：

这两个变量定义了目标编译平台的操作系统和处理器架构。
GOOS=linux 指定目标操作系统是Linux。
GOARCH=amd64 指定目标处理器架构是64位x86架构。

3、go build -a -installsuffix cgo：

go build 是Go语言的标准编译命令。
-a 选项告诉Go编译器重建所有涉及到的东西，包括所有的包（即使它们没有变化）。
-installsuffix cgo 是用来给输出的文件名添加一个后缀，以区分不同编译配置下的编译结果。由于我们设置了 CGO_ENABLED=0，所以这里使用的后缀是 cgo。

4、-ldflags=&quot;-w -s&quot;：

ldflags 表示链接器（linker）标志。
-w 和 -s 是链接选项，分别用于减小生成的二进制文件大小：      - -w 告诉编译器忽略DWARF调试信息，从而减小了生成文件的大小。      - -s 用于去除符号表和调试信息，进一步减小二进制文件的大小。

5、-o /go/bin/pod-data-retorer：

-o 选项用来指定输出的二进制文件名。
/go/bin/pod-data-retorer 是输出的二进制文件的完整路径和名称。这里指定了文件会被放在/go/bin/目录下，并且命名为pod-data-retorer。

综上所述，这条命令会生成一个静态链接的、没有C库依赖、文件尺寸优化的Linux 64位平台的可执行文件pod-data-retorer，并且会放在/go/bin目录下。轻量化和去除C库依赖的特点使得生成的可执行文件特别适合在容器化环境（如Docker）中运行，因为它能够减少容器镜像的大小，并且降低对环境的依赖。
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：上下文Context</title>
    <url>/dev-golang-context/</url>
    <content><![CDATA[1. Golang Context简介在Go语言中，context 是一个在Go语言程序中传递截止时间、取消信号以及其它请求作用域的值的包。context 包为并发操作提供了一种统一的方式来传递取消信号和元数据。经常用于控制不同Go协程之间的超时和取消操作，管理函数或方法的生命周期。
参考文档：上下文Context


2. Context用途context 的主要用途有以下几点：
1、超时控制：通过在 context 中设置超时来控制任务的执行时长，例如 HTTP 请求或数据库查询，以防止一些操作耗时过长影响系统性能。2、取消操作：在多个协程之间同步取消信号，如一个用户请求的取消操作需要传递给所有相关的处理协程。3、传递请求相关的数据：虽然不是 context 的主要用途，但它也可用于在API的调用链中传递请求特定的数据，如身份验证令牌等。
3. Context关键概念3.1. Context类型Context 类型： context 包中定义了 Context 接口，它包括以下方法：

Deadline()：返回 context 截止时间。
Done()：返回一个 channel，context 取消或者达截止时间时，channel 会被关闭。
Err()：返回 context 结束的错误，如果 context 被取消就返回 Canceled 错误，如果超时则返回 DeadlineExceeded 错误。
Value(key interface&#123;&#125;)：获取与 key 对应的值，用于传递请求特定的数据。

3.2. Context对象创建 Context 对象：

context.Background()：返回一个空的 Context，通常用于整个 Context 树的根节点。它不可以取消，也没有截止时间。我们并不需要，也不能主动去关闭它。它通常是用在main函数或顶层的初始化函数中，提供了一个基础的 Context，该Context通常被扩展为更复杂的 Context。
context.TODO()：返回一个空的 Context，用于占位。通常在代码的某个部分还没有决定应该使用哪个 Context 时使用，或者在当前没有可用的 Context，但我们预计将来某个时间点需要向下传递一个实际的 Context 时使用。
context.WithCancel(parent Context)：基于父 Context 创建新的可以主动取消的 Context 。
context.WithDeadline(parent Context, deadline time.Time)：基于父 Context 创建新的带有截止时间的 Context，当达到截止时间时，Context 会自动取消，也可以主动取消。
context.WithTimeout(parent Context, timeout time.Duration)：基于父 Context 和超时时间创建新的 Context。

3.3. Context树Context 树是指在Go的context包中通过Context对象的派生形成的层次结构。在这个层次结构中，每个Context对象都可能有父Context，同时也可以有一个或多个子Context。这种结构类似于一颗树，其中最顶层的Context通常是context.Background()或context.TODO()，它们通常作为所有Context层次结构的根节点。
创建子Context的过程一般通过context.WithCancel、context.WithDeadline、context.WithTimeout和context.WithValue四个函数实现。每个函数在创建子Context时都会复制父Context的行为（诸如超时、取消信号等），同时可以增加额外的行为或信息。
例如，如果我们有一个HTTP请求的处理逻辑，我们可能会在这个请求的开始处创建一个根Context（通过context.WithTimeout来设置整个请求的超时），然后随着我们的代码调用更深层的函数，或者启动新的goroutine来处理请求的不同部分，我们会从根Context派生子Context。
具体来说，如果一个数据库查询依赖于这个请求，我们可以再从这个根Context派生一个子Context（可能还会设置一个特定于数据库操作的超时时间），并将其传递给执行数据库操作的函数。如果需要取消整个请求处理，只需要取消最顶层的Context，整个Context树下所有的Context都会收到取消信号。
以下展示了Context树的简单结构示意：
Root Context (Background or TODO)│├── Child Context A (WithCancel or WithTimeout)│   ├── Grandchild Context A.1 (WithCancel or WithValue)│   └── Grandchild Context A.2 (WithCancel)│└── Child Context B (WithDeadline)    └── Grandchild Context B.1 (WithCancel or WithDeadline)        └── Great-Grandchild Context B.1.1 (WithCancel or WithValue)

在这个结构中：

取消Root Context会导致整个树的Context都被取消。
取消Child Context A只会影响A的子树，即Grandchild Context A.1与Grandchild Context A.2，不会影响Child Context B子树。
子Context会继承父Context的截止时间和取消信号，并可以拓展自己的值与超时设置。

在并发编程和复杂的应用逻辑中，管理Context树可以帮助开发者更有效地处理超时、取消操作以及传递关键的请求信息。
4. Context最佳实践
不要将 Context 存放在结构体中，而应明确地传递。
务必传递而非忽略 Context，即使代码还不需要它。
即使函数允许，也不要传递 nil Context，如果不确定使用哪个 Context，请使用 context.TODO()。
Context 的 Value 应该用得极其谨慎，它不是传递可选参数的手段。

一个典型的使用场景是在服务器处理请求时，使用 context.WithCancel 或 context.WithTimeout 在入口处产生一个 Context，然后将这个 Context 传递给每个涉及到的协程，让这些并发执行的操作都可以响应取消或超时事件。这样、单一请求的依赖操作可以被统一管理和控制。
5. Context使用示例5.1. 创建可取消的Contextpackage mainimport (  &quot;context&quot;  &quot;fmt&quot;  &quot;time&quot;)func operation(ctx context.Context, duration time.Duration) &#123;  select &#123;  case &lt;-time.After(duration):    fmt.Println(&quot;Operation finished successfully&quot;)  case &lt;-ctx.Done():    fmt.Println(&quot;Operation canceled:&quot;, ctx.Err())  &#125;&#125;func main() &#123;  // 创建一个可取消的context  ctx, cancel := context.WithCancel(context.Background())  // 启动一个协程执行任务  go operation(ctx, 5*time.Second)  // 假设1秒后我们想取消操作  time.Sleep(1 * time.Second)  cancel() // 触发取消操作  // 等待足够长的时间以确保打印信息  time.Sleep(3 * time.Second)&#125;

在这个例子中，我们通过context.WithCancel创建了一个可取消的Context。在operation函数中，我们使用select监听Context的Done通道是否关闭。一秒后，执行cancel()函数，此时Done通道关闭，程序输出”Operation canceled”，而不会等到5秒的超时时间。
5.2. 创建带超时的Contextpackage mainimport (  &quot;context&quot;  &quot;fmt&quot;  &quot;time&quot;)func operation(ctx context.Context) &#123;  select &#123;  case &lt;-time.After(5 * time.Second): // 假设操作需要5秒才能完成    fmt.Println(&quot;Operation finished successfully&quot;)  case &lt;-ctx.Done():    fmt.Println(&quot;Operation failed:&quot;, ctx.Err())  &#125;&#125;func main() &#123;  // 创建一个超时未5秒的context  ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)  defer cancel() // 确保所有路径下都会调用cancel  go operation(ctx)  // 等待operation执行或超时  &lt;-ctx.Done()  if ctx.Err() == context.DeadlineExceeded &#123;    fmt.Println(&quot;Operation timeout with deadline exceeded&quot;)  &#125;&#125;

在这个例子中，我们通过context.WithTimeout创建了一个带有2秒超时的Context。由于operation函数中的模拟操作需要5秒才能完成，Context会在2秒后因超时而取消。&lt;-ctx.Done()等待Context的取消信号，打印出超时信息。
5.3. Context传递值package mainimport (    &quot;context&quot;    &quot;fmt&quot;)func doSomething(ctx context.Context) &#123;  // 从Context中提取值  value := ctx.Value(&quot;key&quot;).(string)  fmt.Println(&quot;Value from context:&quot;, value)&#125;func main() &#123;  // 设置一个包含值的context  ctx := context.WithValue(context.Background(), &quot;key&quot;, &quot;go-12345&quot;)  // 调用函数并传递context  doSomething(ctx)&#125;

在这个例子中，我们使用context.WithValue向context添加了一个键值对。然后在doSomething函数中，我们从context中检索值并打印它。需要注意的是，如前所述，WithValue应该非常谨慎地使用，因为它不适合传递很多值，并且可能会导致依赖于context的代码变得难以理解和维护。
在实际应用中，context常常用于API调用链、数据库操作、HTTP请求处理等多级协程控制场景中。使用context能够优雅地管理多个协程间的超时、取消和信号传递。
5.4. Context传递停止信号ctx.Done() 返回只读chan类型的通道关闭信号。&lt;-ctx.Done() 用于等待ctx.Done方法返回的通道关闭信号。当context被取消或超时的情况下，Done通道会被关闭，这表示相关的操作应该停止执行。
我们可以通过监听这个Done通道来优雅地处理取消信号和超时事件。我们通常会在长时间运行或涉及到并发的操作中使用这种方法。
Done()用法的基本示例：
package mainimport (    &quot;context&quot;    &quot;fmt&quot;    &quot;time&quot;)func performTask(ctx context.Context) &#123;    for &#123;        select &#123;        case &lt;-ctx.Done(): // 监听context是否已被取消或超时            fmt.Println(&quot;Task was cancelled or has timed out:&quot;, ctx.Err())            return        default:            // 模拟正在进行的任务            fmt.Println(&quot;Task is running...&quot;)            time.Sleep(1 * time.Second)        &#125;    &#125;&#125;func main() &#123;    // 创建一个将在3秒后超时的context    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)    defer cancel() // 确保resources被释放    // 在新的goroutine中启动任务    go performTask(ctx)    // 等待ctx.Done()的信号，这表示context已被取消或者超时    &lt;-ctx.Done()    // 执行善后工作    fmt.Println(&quot;Main function: Context has been cancelled, performing cleanup...&quot;)    // 此时performTask的协程已退出，因为它监听到了ctx.Done()的关闭信号&#125;

在这个例子中，我们通过ctx.Done()来监控一个长时间运行的任务是否应该停止。当Context的超时时间到了（在本例中是3秒后），Done通道会关闭，同时select语句的case &lt;-ctx.Done()分支被选中，程序打印出取消或超时的信息，并且退出for循环，结束任务。在主函数（main）中，我们也监听了&lt;-ctx.Done()，以便等待Context的取消信号，进行必要的清理工作。
这种做法使得并发程序在需要响应停止指令（如取消或超时）时能够及时安全地停止正在执行的操作。
5.5. 多个子任务并发执行下面是多个子函数并发执行的示例，每个子函数都通过监听&lt;-ctx.Done()来响应取消信号：
package mainimport (  &quot;context&quot;  &quot;fmt&quot;  &quot;sync&quot;  &quot;time&quot;)// task 模拟一个简单的耗时任务func task(ctx context.Context, wg *sync.WaitGroup, id int) &#123;  defer wg.Done() // 等待组计数-1  // 模拟任务周期性执行某些操作  for &#123;    select &#123;    case &lt;-ctx.Done():      // 如果收到取消信号，打印取消信息并退出      fmt.Printf(&quot;Task %d is canceled: %v\n&quot;, id, ctx.Err())      return    default:      // 模拟周期性的工作      fmt.Printf(&quot;Task %d is running...\n&quot;, id)      time.Sleep(1 * time.Second)    &#125;  &#125;&#125;func main() &#123;  // 创建一个带有取消功能的Context  ctx, cancel := context.WithCancel(context.Background())  // 使用sync.WaitGroup等待所有子goroutine完成  var wg sync.WaitGroup  // 启动多个并发的子函数（任务）  for i := 1; i &lt;= 3; i++ &#123;    wg.Add(1) // 等待组计数+1    go task(ctx, &amp;wg, i)  &#125;  // 定时3秒后，取消所有任务  time.AfterFunc(3*time.Second, func() &#123;    fmt.Println(&quot;Cancelling all tasks...&quot;)    cancel()  &#125;)  // 等待所有任务结束  wg.Wait()  fmt.Println(&quot;All tasks have been cancelled.&quot;)&#125;

在这个程序中:
1、我们首先使用 context.WithCancel(context.Background()) 创建了一个可取消的父 Context，这个 Context 会被作为参数传给所有的任务（子函数）。
2、我们使用 sync.WaitGroup 确保主函数等待三个并发任务全部完成。每个任务的goroutine在开始时调用 wg.Add(1) 增加等待计数，当任务完成时调用 wg.Done() 递减计数。
3、task 函数表示需要执行的任务。它通过 select 语句监听两个事件：ctx.Done() 信号的关闭，表示任务应该取消；default 分支模拟正在进行的工作（这里简化为打印信息和等待1秒）。
4、在 main 函数中，我们使用 time.AfterFunc 设置了一个3秒后执行的定时器，在定时器触发时调用 cancel() 函数取消所有任务。
5、最后，wg.Wait() 阻塞主函数，直到所有goroutine执行 wg.Done()，这也就意味着所有任务都接收到取消信号并正常退出。
在上述示例中，当主函数中的定时器触发，所有任务都会因 ctx.Done() 的关闭而接收到取消信号并优雅地停止正在进行的工作。程序将输出”Task x is canceled”信息并退出。所有任务取消后，程序打印 “All tasks have been cancelled.”，然后结束。
6. 总结Context的本质作用，其实是控制函数或方法的生命周期。Context生命周期控制机制有两个关键概念：一个是ctx对象，一个是ctx对象的channel。
Context生命周期控制的流程可以概括为：1、父函数中，创建一个ctx对象，并且把该对象传递给子函数2、父函数中，可以通过定时或者手动取消，关闭ctx对象的channel3、父函数中关闭ctx对象的channel时，子函数就收到了关闭信号，子函数可以选择结束自己的生命周期4、如果子函数长时间运行，或者多个子函数并发，父函数中可以使用 &lt;-ctx.Done() 使主函数进入等待子函数执行
PS：多个子函数并发，还可以使用 sync.WaitGroup 等待子函数执行
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：协程Goroutine</title>
    <url>/dev-golang-goroutine/</url>
    <content><![CDATA[1. Golang协程简介在Go语言中，协程（coroutine）是通过goroutine实现的。goroutine是Go运行时管理的轻量级线程，由Go运行时环境调度，不是操作系统层面的线程。goroutine使得并发编程变得简洁易懂。
参考文档：

Golang 调度器设计思想、GMP 协程调度模型详解
调度器
Channel
同步原语与锁
Golang中Channel的实现原理



2. 创建goroutine当一个程序启动时，它仅仅是一个单独的goroutine（即主goroutine）。
创建一个新的goroutine非常简单，只需要在函数调用前加上关键字go。一旦goroutine被创建，它将与创建它的goroutine（简单理解为线程）并发（或并行）执行。
下面是一个goroutine的简单示例：
package mainimport (  &quot;fmt&quot;  &quot;time&quot;)// printNumbers 打印数字，它会在一个单独的goroutine中执行func printNumbers() &#123;  for i := 0; i &lt; 5; i++ &#123;    fmt.Printf(&quot;%d &quot;, i)    time.Sleep(50 * time.Millisecond) // 等待一小段时间  &#125;&#125;func main() &#123;  go printNumbers() // 开启一个新的goroutine  // main函数也做一些打印工作  for i := 0; i &lt; 5; i++ &#123;    fmt.Printf(&quot;%c &quot;, &#x27;a&#x27;+i)    time.Sleep(100 * time.Millisecond) // 等待一段比printNumbers更长的时间  &#125;&#125;

在这个例子中，main函数中的go printNumbers()语句启动了一个新的goroutine，而main函数自身也在执行打印操作。这两组打印操作会异步执行。这意味着main函数不会等待printNumbers函数完成，而是会立即开始它的循环和打印操作。在这个简单的程序中，我们可能会看到这两个循环的输出（数字和字母）交错在一起。
值得注意的是，main函数（主协程）必须运行足够长的时间，以允许别的goroutine有机会开始执行和完成它们的任务。如果main函数过早地结束，程序则会退出，并且所有在运行中的goroutine也会同时终止。在实际应用程序中，这通常通过等待组（sync.WaitGroup）、通道（channel）或其他同步机制来确保goroutine可以完成它们的任务。
3. 通道channel3.1. 基础概念在Go语言中，channel是一种内置的数据结构，可以让多个goroutines之间安全地进行数据通信。channel被用来在goroutines间进行通信和同步，它的设计理念来源于CSP（Communicating Sequential Processes）- 一种消息传递模型。
channel操作：

创建：使用make()函数创建channel，可以指定channel可存储的元素类型。例如，make(chan int)创建了一个可以传输整数类型的channel。
发送操作：使用channel &lt;- value语法向channel发送值。
接收操作：使用&lt;-channel语法从channel接收值。
关闭：使用内置的close()函数来关闭channel。关闭后不可再向其中发送数据，但仍可接收缓冲中剩余的数据。

channel类型：

无缓冲Channel（非缓冲Channel）： 在发送者和接受者准备好之前，不储存任何值。发送操作会阻塞，直到另一个goroutine在对应的channel上执行接收操作。
有缓冲Channel：可以存储一个或多个值，仅当缓冲满时，向channel发送数据才会阻塞；当缓冲为空时，接收操作才会阻塞。

3.2. 无缓冲channel示例在Go中，无缓冲的channel（或称作同步channel）是在没有空间保持任何元素的情况下进行通信的。一个goroutine在无缓冲channel上发送操作会阻塞，直到另一个goroutine执行接收操作，这样发送者和接收者必须同时准备好进行通信。
以下是一个使用无缓冲channel的示例。在这个例子中，有一个主goroutine和一个worker goroutine。它们将使用无缓冲channel来同步工作：主goroutine发送工作到channel，worker接收工作，执行，然后通知主goroutine工作已完成。
package mainimport (  &quot;fmt&quot;  &quot;time&quot;)func worker(done chan bool) &#123;  fmt.Print(&quot;Working...&quot;)  time.Sleep(time.Second)  fmt.Println(&quot;done&quot;)  // 发送一个值来通知工作已经完成  done &lt;- true&#125;func main() &#123;  // 创建一个无缓冲的channel  done := make(chan bool, 0)  // 启动worker goroutine并传递‘done’channel  go worker(done)  // 阻塞直到在channel上收到worker的完成通知  &lt;-done&#125;

在这个例子中，worker函数接受一个类型为bool的无缓冲channel done，它用于向主goroutine发送完成信号。在执行了一些工作（本例中模拟为一秒钟的Sleep调用）后，worker将true发送到done channel。对于done &lt;- true这个语句，在发送完成信号之前，这个worker goroutine 会阻塞，直到主goroutine执行&lt;-done接收操作。
同样，主程序中的&lt;-done将阻塞直到从done channel接收到值。这样，主程序就会等待worker goroutine完成它的工作并发送完成信号。一旦收到信号，主程序继续执行，程序结束。
这个过程演示了无缓冲channel在goroutine间同步执行的用法，确保main函数在工作完成之前不会退出。
3.3. 有缓冲channel示例下面是一个有缓冲channel的使用示例：
package mainimport (  &quot;fmt&quot;)func worker(id int, jobs &lt;-chan int, results chan&lt;- int) &#123;  for j := range jobs &#123;    fmt.Println(&quot;worker&quot;, id, &quot;processing job&quot;, j)    results &lt;- j * 2 // 假设的工作是将job值翻倍  &#125;&#125;func main() &#123;  jobs := make(chan int, 100)  results := make(chan int, 100)  // 创建3个worker协程。  for w := 1; w &lt;= 3; w++ &#123;    go worker(w, jobs, results)  &#125;  // 发送5个jobs然后关闭jobs channel。  for j := 1; j &lt;= 5; j++ &#123;    jobs &lt;- j  &#125;  close(jobs)  // 收集所有的处理结果。  for a := 1; a &lt;= 5; a++ &#123;    &lt;-results  &#125;&#125;

在这个例子中，我们创建了两个channel：jobs用于发送工作项到各个worker协程，results用于从worker协程收集处理结果。我们启动了3个worker协程，在它们中间分配工作项，并从jobs channel读取。每个worker处理一个工作项，然后将结果发送到results channel。
在main函数中，我们向jobs发送了5个工作项，并关闭了它，表示不会再发送新工作了。然后我们从results channel接收每个worker的计算结果。需要注意的是，我们不需要显式关闭results channel，因为当main函数结束时，程序会自动结束，所有channel将随之关闭。
在缓冲channel中，只有在尝试发送更多数据而channel已满时，发送操作才会阻塞；同样，只有channel为空时，接收操作才会阻塞。因为我们的channel缓冲区可存100个信号，而程序中只使用了5个信号，因此不会被阻塞。
4. 上下文Context参考文档：好好学Golang：上下文Context
5. 同步原语5.1. 基础概念在Go语言中，同步原语（Synchronization primitives）是一组用于协调多个协程（goroutines）之间访问共享资源或执行顺序的工具。Go语言的标准库sync为程序员提供了该领域的多个工具，其中包括互斥锁（Mutexes）、读写锁（RWMutexes）、WaitGroup、Cond等。
5.2. 同步原语工具概述5.2.1. Mutex（互斥锁）Mutex用于保护共享资源，防止同时进行的多个协程同时读写导致竞态条件（Race Condition）。

Lock()：锁定Mutex，任何其他试图锁定该Mutex的协程都将阻塞直到Mutex被解锁。
Unlock()：解锁Mutex，如果有其他协程在等待锁定这个Mutex，它们中的一个将能够锁定它并继续执行。

5.2.2. RWMutex（读写锁）RWMutex是一种特殊类型的Mutex，它允许多个协程同时读取资源，但只能有一个协程写入资源。

RLock()：锁定读锁，其他协程可以同时锁定读锁，但写锁在此状态下是无法被锁定的。
RUnlock()：解锁读锁。
Lock()：锁定写锁，需要等待直到所有的读锁被解锁。
Unlock()：解锁写锁。

5.2.3. WaitGroupWaitGroup用于等待协程集合完成执行。我们可以用它来确保程序在继续执行之前，一组相关的协程已经运行完毕。

Add(delta int)：增加WaitGroup的计数器。
Done()：减少WaitGroup的计数器，相当于Add(-1)。
Wait()：阻塞，直到计数器归零。

5.2.4. Cond（条件变量）Cond实现了条件变量的功能，它可以让一组协程等待某个条件为真。Cond常与Mutex或RWMutex一同使用。

NewCond(*sync.Locker)：创建Cond的新实例。
Wait()：等待条件变量满足，它在内部调用Unlock()并挂起当前协程，直到Signal()或Broadcast()被调用，然后重新锁定。
Signal()：唤醒一个等待该条件的协程。
Broadcast()：唤醒所有等待该条件的协程。

5.3. 同步原语工具示例5.3.1. WaitGroup示例package mainimport (  &quot;fmt&quot;  &quot;sync&quot;  &quot;time&quot;)// worker 模拟一个执行任务的functionfunc worker(id int, wg *sync.WaitGroup) &#123;  defer wg.Done() // 在函数退出之前，调用wg的Done()通知已完成  fmt.Printf(&quot;Worker %d starting\n&quot;, id)  time.Sleep(2 * time.Second) // 模拟耗时任务  fmt.Printf(&quot;Worker %d done\n&quot;, id)&#125;func main() &#123;  var wg sync.WaitGroup  // 启动多个worker协程  for i := 1; i &lt;= 3; i++ &#123;    wg.Add(1) // 添加一个计数    go worker(i, &amp;wg)  &#125;  wg.Wait() // 阻塞，直到wg计数器回到0  fmt.Println(&quot;All workers completed&quot;)&#125;

WaitGroup用于等待一组协程完成各自的工作。上面的代码中，每个协程在开始时调用wg.Add(1)，在工作完成时调用wg.Done()。main函数调用wg.Wait()等待所有协程完成。
5.3.2. Mutex示例package mainimport (  &quot;fmt&quot;  &quot;sync&quot;)var balance intvar mutex sync.Mutexfunc deposit(value int, wg *sync.WaitGroup) &#123;  mutex.Lock() // 获取互斥锁  balance += value  mutex.Unlock() // 释放互斥锁  wg.Done()&#125;func withdraw(value int, wg *sync.WaitGroup) &#123;  mutex.Lock() // 获取互斥锁  balance -= value  mutex.Unlock() // 释放互斥锁  wg.Done()&#125;func main() &#123;  var wg sync.WaitGroup  balance = 100 // 初始化账户余额  // 执行多个存款和取款操作  wg.Add(2)  go deposit(50, &amp;wg)  go withdraw(30, &amp;wg)  // 等待所有操作完成  wg.Wait()  fmt.Println(&quot;New Balance:&quot;, balance)&#125;

在这个例子中，balance代表一个银行账户的余额，多个协程尝试进行存款或取款操作。由于所有的存取款操作都通过互斥锁mutex来同步，因此无论协程的执行顺序如何，最终的账户余额都将是一致且正确的。使用sync.WaitGroup保证main函数会等待两个协程操作完成后才打印最终余额。
5.3.3. RWMutex示例package mainimport (  &quot;fmt&quot;  &quot;sync&quot;  &quot;time&quot;)var (  counter int  rwMutex sync.RWMutex)// reader 模拟一个读取操作func reader(id int, wg *sync.WaitGroup) &#123;  defer wg.Done()  rwMutex.RLock() // 获取读锁  fmt.Printf(&quot;Reader %d: %d\n&quot;, id, counter)  time.Sleep(1 * time.Millisecond) // 读取过程耗时  rwMutex.RUnlock()                // 释放读锁&#125;// writer 模拟一个写入操作func writer(id int, wg *sync.WaitGroup) &#123;  defer wg.Done()  rwMutex.Lock() // 获取写锁  counter++  fmt.Printf(&quot;Writer %d: %d\n&quot;, id, counter)  time.Sleep(1 * time.Millisecond) // 写入过程耗时  rwMutex.Unlock()                 // 释放写锁&#125;func main() &#123;  var wg sync.WaitGroup  // 启动读者  for i := 0; i &lt; 5; i++ &#123;    wg.Add(1)    go reader(i, &amp;wg)  &#125;  // 启动写者  for i := 0; i &lt; 3; i++ &#123;    wg.Add(1)    go writer(i, &amp;wg)  &#125;  wg.Wait() // 等待所有读者和写者结束&#125;

在上面的代码中，我们使用sync.RWMutex维护对counter读写操作的同步，允许多个读操作并行执行，但对写操作互斥访问。
5.3.4. Cond示例package mainimport (  &quot;fmt&quot;  &quot;sync&quot;  &quot;time&quot;)func main() &#123;  var mu sync.Mutex              // 锁，用于保护条件  cond := sync.NewCond(&amp;mu)     // 创建条件变量cond  var ready bool                // 条件：表示工作是否就绪  // 广播启动工作的协程  go func() &#123;    time.Sleep(2 * time.Second) // 模拟一些准备工作耗时    mu.Lock()                   // 获取锁，准备改变条件    ready = true                // 设置工作就绪    cond.Broadcast()            // 广播通知给所有等待的协程    mu.Unlock()                 // 释放锁  &#125;()  // 创建多个工作者协程等待条件变量  const nWorkers = 3  var wg sync.WaitGroup  wg.Add(nWorkers)  for i := 0; i &lt; nWorkers; i++ &#123;    go func(id int) &#123;      defer wg.Done()      mu.Lock()                  // 获取锁以检查条件      for !ready &#123;               // 循环，直到工作就绪为止        cond.Wait()           // 等待条件变量的信号      &#125;      fmt.Printf(&quot;Worker %d started\n&quot;, id)      mu.Unlock()                // 释放锁，让其他协程进入临界区    &#125;(i)  &#125;  wg.Wait()&#125;

在这段代码中，我们创建了一个条件变量cond。所有的协程启动时都会通过使用cond.Wait()等待（阻塞），直到条件准备就绪。一旦条件满足，我们用cond.Broadcast()通知所有等待中的协程。注意，在调用Wait和Broadcast之前和之后，我们都需要手动获取和释放相关的锁。
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Golang语法拾贝</title>
    <url>/dev-golang-grammar-pick-up/</url>
    <content><![CDATA[1. 前言本文记录使用Golang时遇到的一些语法问题，备忘。


2. 结构体2.1. 结构体定义方法在Go语言中，结构体（struct）是一种定义复合数据类型的方式，非常适合用来封装和组织相关属性。
结构体定义了一组具有零个或多个不同类型的变量，这些变量集合在一起，表示一个实体。在Go中，结构体的定义如下：
type 结构体名称 struct &#123;    字段1 数据类型    字段2 数据类型    // ...&#125;

例如，我们可以创建一个代表人的结构体：
type Person struct &#123;    Name string    Age  int    City string&#125;

在这个Person结构体中，有三个字段，分别存储着名字（Name）、年龄（Age）和城市信息（City）。
创建结构体实例的一个方法是按照字段的顺序提供字段值：
p := Person&#123;&quot;Alice&quot;, 30, &quot;London&quot;&#125;

另外也可以通过字段名初始化结构体，这使得顺序不再重要，增加了代码的可读性：
p := Person&#123;    Name: &quot;Bob&quot;,    Age:  25,    City: &quot;New York&quot;,&#125;

在上述两种初始化方式中，如果某些字段被省略，这些字段会被初始化为其类型的零值。
结构体是值类型，在函数调用时会复制相应的值。如果需要在函数间共享结构体数据，通常是通过指针来做的。在Go中，使用&amp;符号可以获取结构体实例的内存地址（指针），然后可以将该指针传递到函数中。
func updateAge(p *Person, newAge int) &#123;    p.Age = newAge&#125;// 使用p := Person&#123;Name: &quot;Charlie&quot;, Age: 20, City: &quot;Paris&quot;&#125;updateAge(&amp;p, 21)

结构体中也可以定义方法。方法是附加到结构体类型上的函数，它可以访问结构体中的字段。方法的定义如下：
func (接收器变量 结构体类型) 方法名(参数列表) 返回参数 &#123;    // 方法体&#125;

例如，给Person结构体添加一个方法来打印个人信息：
func (p Person) PrintInfo() &#123;    fmt.Printf(&quot;Name: %s, Age: %d, City: %s\n&quot;, p.Name, p.Age, p.City)&#125;// 使用p := Person&#123;Name: &quot;Dave&quot;, Age: 32, City: &quot;Miami&quot;&#125;p.PrintInfo()

结构体类型可以嵌套。即，一个结构体的字段可以是另一个结构体类型，这允许构造更复杂的数据模型，例如：
type Address struct &#123;    Street string    City   string&#125;type Employee struct &#123;    Name    string    Age     int    Address Address&#125;// 使用e := Employee&#123;    Name: &quot;Eve&quot;,    Age:  28,    Address: Address&#123;        Street: &quot;Main St&quot;,        City:   &quot;Springfield&quot;,    &#125;,&#125;

在Go中，结构体以及结构体上的字段和方法，如果首字母大写，则表示它们是导出的（即在包外部可见）。如果首字母小写，则它们是未导出的，只能在定义它们的包内部访问。
Go语言没有提供传统面向对象编程语言中的继承机制，而是通过接口（interface）和结构体嵌套（composition）来实现多态和代码复用。
2.2. 结构体转JSON（序列化）在Go语言中，encoding/json标准库提供了处理JSON数据的功能，包括将结构体转换为JSON（序列化）以及将JSON转换为结构体（反序列化）。
要将一个Go结构体转换为JSON字符串，可以使用json.Marshal函数。下面是一个简单的示例：
package mainimport (    &quot;encoding/json&quot;    &quot;fmt&quot;    &quot;log&quot;)type Person struct &#123;    Name string `json:&quot;name&quot;`    Age  int    `json:&quot;age&quot;`    City string `json:&quot;city&quot;`&#125;func main() &#123;    p := Person&#123;&quot;Alice&quot;, 30, &quot;New York&quot;&#125;        jsonData, err := json.Marshal(p)    if err != nil &#123;        log.Fatal(err)    &#125;        fmt.Println(string(jsonData))&#125;

输出将是：
&#123;&quot;name&quot;:&quot;Alice&quot;,&quot;age&quot;:30,&quot;city&quot;:&quot;New York&quot;&#125;

在此示例中，Person结构体定义了JSON字段名通过使结构体标签。当json.Marshal被调用时，它会检查每个结构体字段的这些标签，并使用它们作为JSON对象中的键名。
如果不想让某些字段出现在JSON中，可以使用json:&quot;-&quot;来排除这些字段，或者使字段名的首字母小写，这样在默认情况下它不会被序列化。
2.3. JSON转结构体（反序列化）将JSON数据转换为Go结构体的操作，是通过json.Unmarshal函数来完成的。首先让我们看一个如何实现的示例：
package mainimport (    &quot;encoding/json&quot;    &quot;fmt&quot;    &quot;log&quot;)type Person struct &#123;    Name string `json:&quot;name&quot;`    Age  int    `json:&quot;age&quot;`    City string `json:&quot;city&quot;`&#125;func main() &#123;    jsonData := []byte(`&#123;&quot;name&quot;:&quot;Bob&quot;,&quot;age&quot;:25,&quot;city&quot;:&quot;Los Angeles&quot;&#125;`)        var p Person    err := json.Unmarshal(jsonData, &amp;p)    if err != nil &#123;        log.Fatal(err)    &#125;        fmt.Printf(&quot;%+v\n&quot;, p)&#125;

输出将是：
&#123;Name:Bob Age:25 City:Los Angeles&#125;

在上面这个例子中，我们首先定义了一个JSON字符串，然后将其转换为字节切片（因为json.Unmarshal函数需要一个字节切片作为输入）。接下来创建了Person类型的变量p，并将其地址传递给json.Unmarshal，它将解析json数据，并填充到相应的结构体字段中。
值得注意的是，字段名在结构体标签中指定的JSON键必须与实际的JSON数据匹配。此外，如果JSON中的数据类型和结构体字段的数据类型不匹配，json.Unmarshal会报错。
使用结构体标签，我们可以控制序列化和反序列化的行为，例如忽略空值或者只在序列化时包含字段等：

omitempty选项会让字段在序列化JSON时，如果是空值，则忽略它。
omitempty也可以用在反序列化JSON时，如果JSON中某个字段缺失，则会保持该字段的零值。

下面的示例中，如果City字段为空，在序列化时它将不会出现在JSON中：
type Person struct &#123;    Name string `json:&quot;name&quot;`    Age  int    `json:&quot;age&quot;`    City string `json:&quot;city,omitempty&quot;`&#125;

处理结构体转换为JSON和JSON转换为结构体时，总是推荐进行错误检查，以防数据格式不正确或是类型不匹配，避免潜在的运行时问题。
2.4. 自定义结构体打印格式使用 fmt.Println 或者 logrus.Info 来打印一个结构体，它将默认调用结构体的 String() 方法（结构体实现了 fmt.Stringer 接口）或者直接打印结构体的值，不会直接打印字段名（key）。因此，如果想要自定义结构体的输出格式，那么需要重写 String() 方法。
示例：
type Person struct &#123;    Name string    Age  int&#125;p := Person&#123;&quot;Alice&quot;, 25&#125;// 打印结构体，只输出值fmt.Println(p)func (p Person) String() string &#123;    return fmt.Sprintf(&quot;Name: %s, Age: %d&quot;, p.Name, p.Age)&#125;// 将会调用 p.String() 并输出 &quot;Name: Alice, Age: 25&quot;fmt.Println(p)

2.5. 结构体的替代在Go语言中，结构体（struct）是一种自定义的数据类型，它允许我们组合不同的字段（属性），这些字段可以是不同的数据类型。使用结构体是一种强类型的方法来表示一组相关的数据。不过，在某些场景中，我们可能需要一种更灵活的数据结构来处理动态类型的数据或未知的数据模式。这时，可以使用map[string]interface&#123;&#125;和unstructured作为结构体的替代方案。
2.5.1. map[string]interface{}使用这种类型的映射来存储键值对，其中键是字符串类型，代表属性的名称，而值是interface&#123;&#125;类型，可以接受任何类型的值。这种方式的好处是我们可以存储任何结构的数据，但缺点是失去了类型安全，需要在使用时进行类型断言。
例子:
package mainimport (    &quot;fmt&quot;)func main() &#123;    // 创建一个map[string]interface&#123;&#125;的实例    person := make(map[string]interface&#123;&#125;)    // 向map中添加数据    person[&quot;name&quot;] = &quot;张三&quot;    person[&quot;age&quot;] = 25    person[&quot;hobbies&quot;] = []string&#123;&quot;读书&quot;, &quot;旅游&quot;&#125;    // 打印map的内容    fmt.Println(person)    // 读取map中的特定值    name := person[&quot;name&quot;].(string) // 类型断言    fmt.Println(&quot;名字：&quot;, name)&#125;
在这个例子中，我们创建了一个关于 person 的map，可以存储人名、年龄和爱好的数据。显然，这种方式比较灵活，但在读取map时，每次都需要对取出的值做类型断言，这样会带来运行时的风险。
2.5.2. unstructuredunstructured是来自Kubernetes的client-go库的一个类型，专门用来处理Kubernetes的API资源。它也是建立在map[string]interface&#123;&#125;基础上的，但提供了一套更为丰富的API来方便地访问和修改这些非结构化的数据。
例子:
package mainimport (   &quot;fmt&quot;   &quot;k8s.io/apimachinery/pkg/apis/meta/v1/unstructured&quot;   &quot;k8s.io/apimachinery/pkg/runtime/schema&quot;)func main() &#123;   // 创建一个Unstructured对象   u := unstructured.Unstructured&#123;       Object: map[string]interface&#123;&#125;&#123;           &quot;apiVersion&quot;: &quot;v1&quot;,           &quot;kind&quot;:       &quot;Pod&quot;,           &quot;metadata&quot;: map[string]interface&#123;&#125;&#123;               &quot;name&quot;: &quot;example-pod&quot;,           &#125;,           &quot;spec&quot;: map[string]interface&#123;&#125;&#123;               &quot;containers&quot;: []interface&#123;&#125;&#123;                   map[string]interface&#123;&#125;&#123;                       &quot;name&quot;:  &quot;web&quot;,                       &quot;image&quot;: &quot;nginx&quot;,                   &#125;,               &#125;,           &#125;,       &#125;,   &#125;   // 获取groupVersionKind信息   gvk := schema.GroupVersionKind&#123;       Version: &quot;v1&quot;,       Kind:    &quot;Pod&quot;,   &#125;   u.SetGroupVersionKind(gvk)   // 打印Unstructured对象的内容   fmt.Printf(&quot;Unstructured对象: %+v\n&quot;, u)   // 获取和访问spec中的容器信息   containers, _, _ := unstructured.NestedSlice(u.Object, &quot;spec&quot;, &quot;containers&quot;)   for _, container := range containers &#123;       fmt.Printf(&quot;容器: %+v\n&quot;, container)   &#125;&#125;

在这个例子中，我们创建了一个Kubernetes Pod资源的unstructured.Unstructured对象，并设置了它的API版本、资源类型和其他元信息。然后，我们通过特定的函数访问了spec.containers字段的内容。unstructured.Unstructured提供了一系列辅助函数来处理嵌套的字段，这使得访问和修改数据变得更加方便。
事实上，unstructured的核心仍然是一个map[string]interface&#123;&#125;，但是它提供了那些使得处理非结构化数据更易用的方法。而普通的map[string]interface&#123;&#125;则更为基础，适用于那些没有特殊库辅助方法的情况。
3. 函数与接口3.1. 函数与接口的定义方法Go 语言中的函数和方法是两个不同的概念，虽然它们的声明语法很类似，但是它们在 Go 中的作用和使用方式有所不同。
函数Go 语言定义函数使用关键字 func，下面是 Go 语言函数定义的通用语法：
func functionname(parametername type) returntype &#123;      // 函数体（具体的执行代码）&#125;

示例：
func add(x int, y int) int &#123;      return x+y&#125;
在上面的代码片段中，函数 add 接收两个 int 类型的输入参数，返回一个 int 类型的值。
方法方法是附加在一个给定类型的值上的一个函数，它能访问该类型的值的所有字段和方法。方法定义的语法如下：
func (t Type) methodName(parametername type) returntype &#123;      // 方法体（具体的执行代码）&#125;

Type 是 Go 语言中的一个类型，可以是内置的类型，例如 int、float32 等，也可以是用户自定义的类型。
示例：
type Rectangle struct &#123;      length int    width  int&#125;func (r Rectangle) Area() int &#123;      return r.length*r.width&#125;

在上面的代码片段中，Area 是作用在 Rectangle 类型上的一个方法，它使用 Rectangle 类型的 length 和 width 字段计算矩形的面积。
当我们有一个类型的实例时，我们可以使用 . 来调用它的方法：
r := Rectangle&#123;    length: 10,    width:  5,&#125;fmt.Println(r.Area()) // 输出 50

3.2. 指针接收器(t Type) 和 (t *Type) 都是 Go 语言方法的接收器（也称为接收者）类型，前者是值接收器，后者是指针接收器。
对于值接收器 (t Type)，在调用 t 的任何方法时，会将 t 的值复制一份，这意味着在方法体内对 t 的任何修改，只会影响复制的那份，不会影响原来的值。
示例：
type myType struct &#123;    value int&#125;func (t myType) setValue(val int) &#123;    t.value = val&#125;func main() &#123;    x := myType&#123;value: 10&#125;    x.setValue(20)    fmt.Println(x.value) // 输出 10&#125;

使用指针接收器 (t *Type) 时，不会复制原始值，而是直接获取到对象的内存地址，因此，在方法体内对 t 所做的任何修改，都会直接反映在原始值上。
示例：
type myType struct &#123;    value int&#125;func (t *myType) setValue(val int) &#123;    t.value = val&#125;func main() &#123;    x := &amp;myType&#123;value: 10&#125;    x.setValue(20)    fmt.Println(x.value) // 输出 20&#125;

选择使用值接收器还是指针接收器，主要取决于我们是否需要在方法内修改接收器的状态，以及我们是否希望避免复制（例如，当类型的值很大时，复制可能会很昂贵）。
4. 单元测试4.1. 运行测试假设 go.md 中的 module 为 pod-data-restorer ， xxx_test.go 中 package 为 podwebhook
1、测试运行整个包
go test pod-data-restorer/podwebhook

2、测试运行包中某个测试函数
go test -run TestFunctionName pod-data-restorer/podwebhook

4.2. 单元测试时输出日志在Go的单元测试中，标准的方式是使用来自 testing 包的 T 结构的 Log 方法和其变种如 Logf，Error，Errorf，Fatal，Fatalf 等。
这是一个例子：
func TestExample(t *testing.T) &#123;    t.Log(&quot;This is a log message during testing.&quot;)    t.Logf(&quot;Formatted log message with value: %d&quot;, 42)&#125;

注意：如果用 go test 运行测试，正常的 t.Log 或 t.Logf 输出是不会被显示的，只有当测试失败或者我们用 go test -v 运行测试，t.Log 的输出才会被显示。
如果我们需要日志在任何情况下都显示，考虑使用 t.Error 或 t.Errorf。这会使当前的测试失败，并输出我们提供的日志信息。
例如：
func TestExample(t *testing.T) &#123;    t.Error(&quot;This is an error message. The test will fail.&quot;)    t.Errorf(&quot;Formatted error message with value: %d&quot;, 42)&#125;

Fatal 和 Fatalf 方法也会使测试失败，但是它们会立即停止当前的测试函数，不会执行后面的代码。而 Error 和 Errorf 不会停止测试函数的执行。
例如：
func TestExample(t *testing.T) &#123;    t.Fatal(&quot;This is a fatal error message. The test will fail and stop.&quot;)    t.Fatalf(&quot;This will not be displayed because the test already stopped.&quot;)&#125;

5. 协程Goroutine参考文档：《好好学Golang：协程Goroutine》
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Golang操作MySQL</title>
    <url>/dev-golang-mysql/</url>
    <content><![CDATA[1. 前言在实际的开发中，操作数据库是必不可少的环节。Go语言通过标准库database/sql以及相对应的数据库驱动第三方库，让数据库操作变得简明而强大。本文中，我们将会学习安装MySQL数据库驱动，以及如何进行增删查改（CRUD）操作。


2. 安装MySQL数据库驱动go get -u github.com/go-sql-driver/mysql
3. 打开数据库连接使用sql.Open函数和 MySQL DSN 打开一个数据库连接。
package mainimport (  &quot;database/sql&quot;  &quot;fmt&quot;  &quot;log&quot;  _ &quot;github.com/go-sql-driver/mysql&quot;)func main() &#123;  // 数据库连接信息  dsn := &quot;root:mysql123@tcp(192.168.50.133:3306)/mysql&quot;  // Open函数会开启一个数据库连接，注意此时并不会立即校验用户名和密码的正确性  // 它仅仅是初始化一个连接的对象  db, err := sql.Open(&quot;mysql&quot;, dsn)  if err != nil &#123;    log.Fatalf(&quot;Error on initializing database connection: %s&quot;, err.Error())  &#125;  // 可以使用Ping方法来校验用户名和密码的正确性  err = db.Ping()  if err != nil &#123;    log.Fatalf(&quot;Error on opening database connection: %s&quot;, err.Error())  &#125;  defer db.Close()  // 查询示例  query := `SELECT version();` // 一个简单的查询，获取MySQL版本信息  var version string  err = db.QueryRow(query).Scan(&amp;version)  if err != nil &#123;    log.Fatalf(&quot;Query failed: %s&quot;, err.Error())  &#125;  fmt.Printf(&quot;Connected to MySQL database successfully! MySQL version: %s\n&quot;, version)&#125;

在写我们的MySQL连接字符串（DSN，即Data Source Name）时，确保我们指定了正确的用户名、密码、地址、端口和数据库名称。
注意事项：

代码中_ &quot;github.com/go-sql-driver/mysql&quot;的import表明引入了mysql驱动，但是并没有直接使用它，_的用法在Go中用来进行仅初始化该包而不实际使用包中任何函数、变量等。
需要具体根据我们的MySQL服务器的用户名(username), 密码 (password), 服务器地址 (127.0.0.1), 端口 (3306), 和数据库名(dbname)来编辑DSN。
db.Ping()是用来检查与数据库的连接是否仍然有效，或者在初次尝试时，确认能够建立连接。
最后，使用defer db.Close()来确保数据库连接被正确关闭，这是避免内存泄漏的一个重要习惯。defer关键字会安排随后的函数调用在函数返回时执行。
该示例中只执行了一个简单的SELECT查询来获取MySQL的版本信息，并将其打印到标准输出。在实际的应用程序中，我们可能需要执行更复杂的SQL查询和其他数据库操作。
在生产环境中，应该避免明文存储敏感信息如数据库的用户名和密码，可以考虑使用环境变量或加密存储这些信息。

3.1. 创建一个测试数据库和表create database test default character set utf8 collate utf8_general_ci;use test;create table if not exists `user` (  `id` int(8) not null auto_increment,  `name` varchar(32) not null,  `age` int(4) not null default 0,  primary key (id)) engine=innodb default charset=utf8;

4. 增删查改操作4.1. 创建（Create）创建记录通常使用INSERT语句。
func create(db *sql.DB) &#123;  query := `INSERT INTO user(name, age) VALUES(?, ?)`  result, err := db.Exec(query, &quot;John Doe&quot;, 28)  if err != nil &#123;    log.Fatal(err)  &#125;  id, err := result.LastInsertId()  if err != nil &#123;    log.Fatal(err)  &#125;  fmt.Printf(&quot;New record ID: %d\n&quot;, id)&#125;

4.2. 读取（Read）读取记录可以使用SELECT语句。查询单条结果可以使用QueryRow，查询多条结果可以使用Query。
func read(db *sql.DB) &#123;  query := `SELECT id, name, age FROM user WHERE age &gt; ?`  rows, err := db.Query(query, 18)  if err != nil &#123;    log.Fatal(err)  &#125;  defer rows.Close()  for rows.Next() &#123;    var id int    var name string    var age int    err = rows.Scan(&amp;id, &amp;name, &amp;age)    if err != nil &#123;      log.Fatal(err)    &#125;    fmt.Printf(&quot;ID: %d, Name: %s, Age: %d\n&quot;, id, name, age)  &#125;  // Check for errors from iterating over rows.  err = rows.Err()  if err != nil &#123;    log.Fatal(err)  &#125;&#125;

4.3. 更新（Update）更新记录通常使用UPDATE语句。
func update(db *sql.DB) &#123;  query := `UPDATE user SET age = ? WHERE name = ?`  result, err := db.Exec(query, 30, &quot;John Doe&quot;)  if err != nil &#123;    log.Fatal(err)  &#125;  count, err := result.RowsAffected()  if err != nil &#123;    log.Fatal(err)  &#125;  fmt.Printf(&quot;Affected rows: %d\n&quot;, count)&#125;

4.4. 删除（Delete）删除记录通常使用DELETE语句。
func delete(db *sql.DB) &#123;  query := `DELETE FROM user WHERE name = ?`  result, err := db.Exec(query, &quot;John Doe&quot;)  if err != nil &#123;    log.Fatal(err)  &#125;  count, err := result.RowsAffected()  if err != nil &#123;    log.Fatal(err)  &#125;  fmt.Printf(&quot;Removed rows: %d\n&quot;, count)&#125;

4.5. 完整示例以下是综合上述功能的一个完整示例：
package mainimport (  &quot;database/sql&quot;  &quot;fmt&quot;  &quot;log&quot;  _ &quot;github.com/go-sql-driver/mysql&quot;)func main() &#123;  // 数据库连接信息  dsn := &quot;root:mysql123@tcp(192.168.50.133:3306)/test&quot;  // Open函数会开启一个数据库连接，注意此时并不会立即校验用户名和密码的正确性  // 它仅仅是初始化一个连接的对象  db, err := sql.Open(&quot;mysql&quot;, dsn)  if err != nil &#123;    log.Fatalf(&quot;Error on initializing database connection: %s&quot;, err.Error())  &#125;  // 可以使用Ping方法来校验用户名和密码的正确性  err = db.Ping()  if err != nil &#123;    log.Fatalf(&quot;Error on opening database connection: %s&quot;, err.Error())  &#125;  defer db.Close()  create(db)  read(db)  update(db)  read(db)  delete(db)&#125;// create function implementation ...// read function implementation ...// update function implementation ...// delete function implementation ...

5. 注意事项
在任何数据库操作之后，如果出现错误，请确保进行错误处理（if err != nil）。
当使用rows进行查询时，我们必须通过rows.Close()来关闭rows，防止资源泄露。
使用defer来确保数据库连接被关闭。
在生产环境中，考虑使用连接池管理数据库连接，Go的database/sql包已经默认实现了连接池。
在实际场景中，多考虑使用事务管理(db.Begin())，以保证数据的一致性。
确保处理sql.ErrNoRows错误，当期望的行不存在时会返回此错误。

按照上文中的办法，即可完成在Go中对MySQL数据库进行基础的增删查改操作。在更高级的应用中，我们可能会涉及到更加复杂的事务、加锁策略和性能优化问题。
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Golang项目布局最佳实践</title>
    <url>/dev-golang-project-layout/</url>
    <content><![CDATA[1. 前言Go 官方没有项目布局（项目结构）要求，但是社区推崇清晰、模块化的布局，对于中大型项目推荐使用一些最佳实践。
如果我们尝试学习 Go，或者我们正在为自己建立一个 PoC 或一个玩具项目，关心项目布局是没啥必要的。建议从简单的事情开始，一个 main.go 文件绰绰有余。随着项目的增长，我们需要代码结构良好非常重要，否则我们最终会得到一个凌乱的代码，这其中就包含大量隐藏的依赖项和全局状态。这时候，我们就需要考虑项目布局和管理包/库的方法。
也就是说，项目布局应该取决于项目的大小、复杂程度、团队偏好和管理需求。
本文中，我们学习Go项目布局的最佳实践。
参考文档：

Standard Go Project Layout
golang 编程规范 - 项目目录结构
github - go-clean-arch
Golang 简洁架构实战



2. 获取一个标准项目布局git clone https://github.com/golang-standards/project-layout.git myproject

克隆 Standard Go Project Layout 项目，保留我们需要的内容，删除其他所有的内容！
3. 中大型项目布局说明以下是一个推荐的Go项目结构：
.├── cmd                  # 程序的主入口点所在的包，每个应用程序的入口文件都放在这个目录下的子目录中。│   └── myapp            # 程序名称作为目录名│       └── main.go      # 主程序文件（入口程序文件），只有一个入口程序文件时建议直接放入根目录├── pkg                  # 可以被外部应用程序导入使用的库代码（共享代码）。│   └── mypubliclib      # 可复用的公共库代码├── internal             # 只能被本项目使用的库代码（私有代码）。│   ├── api              # 存放API相关的代码，例如存放HTTP客户端和服务器逻辑。│   ├── service          # 核心业务逻辑实现│   ├── config           # 配置文件加载及解析│   └── pkg/myprivatelib # 可复用的私有库代码├── api                  # 存放API协议定义文件，如OpenAPI/Swagger规格、protobuf文件等。├── web                  # Web应用特定的组件，例如静态Web资产、服务器端模板等。├── assets               # 项目中使用的其他资源（图像、logo 等）。├── configs              # 配置文件模板或默认配置。├── scripts              # 存放构建、安装、分析、Linting等操作的脚本。├── deployments          # 存放部署相关的配置文件和脚本（比如Dockerfile、Kubernetes.yaml等）├── test                 # 外部测试的额外资料和数据├── docs                 # 设计和用户文档（除了 godoc 生成的文档）。├── examples             # 应用程序或公共库的示例程序。├── init                 # 系统初始化（systemd、upstart、sysv）和进程管理（runit、supervisord）配置。├── vendor               # 若采用了vendor模式，则包含应用的所有依赖项├── .gitignore           # 指定git忽略的非跟踪文件├── Dockerfile           # 用于构建Docker镜像├── go.mod               # 项目的模块依赖文件├── go.sum               # 包含特定模块版本内容的预期校验和└── README.md            # 项目的说明文件，通常包含安装、构建、操作说明等

注意，这里的test目录通常用于外部测试的额外资料和数据。而项目本身的单元测试文件，通常与被测试文件放在同一个目录。
4. 小型项目布局说明对于小型或者是短周期的项目，我们可能只需要以下几个组件：
.├── main.go           # 如果是一个应用，这里是入口文件├── go.mod            # 项目依赖文件├── go.sum            # 依赖的校验和├── README.md         # 项目说明文档└── somepkg           # 某些有意义的包    ├── file1.go          └── file2.go

5. gin框架项目布局说明对于gin框架，建议在【中大型项目布局说明】的基础上，增加几个包： 

internal/router
internal/server
internal/tools

示例项目：voidking/k8s-webhook
6. 后记在所有情况下，确保代码易于理解、模块化和可测试非常重要，这样的项目结构更容易让新加入的开发者迅速上手和维护。另外，随着项目的发展，结构可能需要根据新的需求进行调整，所以务必保持灵活性。
]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>gin</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Golang：Golang入门篇</title>
    <url>/dev-golang-start/</url>
    <content><![CDATA[1. Golang简介Golang，也称为Go，是由Google开发的一种静态强类型、编译型的系统级编程语言，具有语法简洁、并发支持、高效的内存管理和垃圾回收机制等特性。它由Ken Thompson、Rob Pike和Robert Griesemer于2007年设计，旨在解决当时Google面临的软件开发与维护问题。
Golang 语言语法与 C 语言相近，但功能上有：内存安全，GC（垃圾回收），结构形态及 CSP-style 并发计算。
本文中，我们开始学习Golang，主要参考：

Go官方网站
Go Tour，交互式的Go教程
Go by Example，通过实例学习Go语言
Effective Go，官方文档教我们编写高效的Go代码
Go 语言教程
Go标准库中文文档
Go程序设计语言（Go语言圣经）



2. 为什么选择GolangGolang的优势：

简洁且高效的编程语言：Go的语法设计非常简洁，上手快，编译速度快，性能接近C/C++。
并发编程：Go的并发是它的核心特性之一。使用Go协程（goroutines）和通道（channels）可以轻松编写并发应用程序。
丰富的标准库：Go拥有广泛的标准库，涵盖网络编程、加密、数据处理等各方面，大部分日常功能无需外部依赖。
跨平台编译：Go支持跨平台编译，可以方便地交叉编译，在不同的操作系统和架构上运行。

3. 安装配置Golang环境3.1. 通用安装配置方法1、访问Go官网，下载安装Golang环境。
2、把Golang的bin目录添加到Path环境变量。
3、访问Jetbrains-Go，下载安装Goland。
4、在Goland中配置好GOROOT和GOPATH。
3.2. MacOS安装配置Golang环境1、安装Golang方法一：使用 brew 安装
brew install golang

默认安装到 /usr/local/Cellar/go/ 目录。
方法二（推荐）：下载安装包安装访问Go官网，下载安装包，双击安装。
默认安装到 /usr/local/go/ 目录。
2、配置环境变量.bash_profile 中添加环境变量：
# golangexport GOROOT=&quot;/usr/local/go&quot;export GOPATH=&quot;$HOME/git/go&quot;export GOBIN=&quot;$HOME/git/go/bin&quot;export PATH=&quot;$PATH:$GOROOT/bin:$GOPATH/bin&quot;

4. helloworld按照国际惯例，学习新语言，先来一个helloworld。新建test.go文件，内容为：
package mainimport &quot;fmt&quot;func main() &#123;   fmt.Println(&quot;Hello, World!&quot;)&#125;

Shift+右键打开Powershell，使用go run test.go，运行代码。除了使用go run命令之外，还可以使用go build命令，先对代码进行编译，然后运行生成的二进制文件。
由上面这段代码可以看出，Go语言代码的基础组成有包声明、引入包、函数、语句和表达式。除此之外，还有注释、常量、变量、数组、结构体等。
5. Golang基础语法5.1. 程序结构Go程序的基本组织单元是包（packages）。每一个Go程序都是由一个或多个包组成的。main 包是每个可执行程序的入口点，程序从main函数开始运行。
5.2. 变量与数据类型Go是静态类型的语言，每个变量都有明确的类型。Go提供的基本类型包括bool、string，数值类型如int、float64等，以及byte和rune。
声明变量的一般格式是：
var variableName dataType = value

类型推导（使用:=运算符）允许你在声明变量时不显式指定数据类型，而是由编译器推导类型。
name := &quot;Go Programming&quot;

5.3. 函数函数是Go中的一类对象。函数的定义包括函数名、参数列表、返回值列表和函数体。
func functionName(parameterName dataType) returnType &#123;    // 函数体&#125;

5.4. 指针Go支持指针，意味着可以直接通过引用来读写内存地址中的数据。指针在Go中比在许多其他语言中使用起来更安全，因为Go隐藏了指针算术。
var x int = 1var p *int = &amp;x // p 是指向 x 的指针

5.5. 控制结构Go语言支持常见的控制结构，如if、else、for（用作循环，因为Go中没有while语句）、switch和select。
for i := 0; i &lt; 10; i++ &#123;    fmt.Println(i)&#125;

5.6. 结构体和接口结构体（structs）是定义复合数据类型的方式，而接口（interfaces）定义了对象的行为。在Go中，接口是隐式实现的，任何类型只要实现了接口中的方法即被视为实现了该接口。
type Shape interface &#123;    Area() float64&#125;type Rectangle struct &#123;    Width, Height float64&#125;func (r Rectangle) Area() float64 &#123;    return r.Width * r.Height&#125;

5.7. 并发Go的并发模型基于协程，称为goroutines，它们相对于线程来说更轻量级。channels用于协程之间的通信和同步。
func count(thing string, c chan string) &#123;    for i := 1; i &lt;= 5; i++ &#123;        c &lt;- thing    &#125;    close(c)&#125;func main() &#123;    c := make(chan string)    go count(&quot;sheep&quot;, c)    for msg := range c &#123;        fmt.Println(msg)    &#125;&#125;

6. Golang和C语法对比Golang的语法和C语言很像，这里和C进行对比，指出不同之处，方便记忆。

&#123;不能单独放在一行
表达式不需要分号结尾
变量声明赋值，var age int = 0
数据类型有差异
强制类型转换时括号的位置不同
支持多变量赋值，按顺序赋值
:=操作符也可以声明变量，但是只能在函数体内
const声明可以使用iota
if和for后面不需要圆括号
switch不需要break
循环都用for，没有while
函数定义，func func_name([params]) [return_types]&#123;&#125;
数组声明，var arr [10] float32; var arr2 = [3]float32&#123;1.0, 2.0, 3.4&#125;
结构体定义，type先行，下面有demo
切片（动态数组），var slice1 []int = make([]int, 3, 9)
切片append，copy
字典map定义，var map1 map[string]string = make(map[string]string)
字典delete条目
for range循环切片和字典
定义接口，下面有demo
错误处理，error接口，下面有demo
并发，go语句开启新线程，下面有demo

7. Golang命名规则文件名：Go语言的文件名通常是全小写，最好不要使用下划线和中划线，非要使用的话选下划线。例如，http_server.go。这种命名方式也是Go语言社区的普遍约定俗成的规则。Go测试文件中，是有特殊的命名规则的：如果你的文件名为abc.go，那么对应的测试文件应该命名为abc_test.go。同时，单元测试文件通常与被测试文件放在同一个目录，具体做法可以参考beego项目。这是Go标准库中普遍采用的方式，go test测试工具可以找到并运行这些测试文件。
包名：Go中的包名应当使用小写字母。如果包名由多个单词组成，那么一般不使用下划线或者中划线，而是直接将小写字母连在一起，例如httputil，ioutil等。
变量名、函数名、常量名：变量名和函数名通常使用驼峰命名法，当标识符的首字母大写时，其作用域为公共，可以被其他包访问；当首字母小写时，其作用域为包内私有。例如 ServerAddr 是公共的，而 serverAddr 是私有的。
结构体字段： 结构体的字段名也遵循驼峰命名法，并且一般会首字母大写以使其可以被其他包访问。
接口名：单个方法的接口应以“er”为后缀。例如 io.Reader、io.Writer等。
错误类型：在命名错误类型时，以 “Error” 结尾。
在Go语言中通常避免使用下划线，特别是在包命名，结构体，函数和变量命名中。一些特殊的情况，例如测试和协议缓冲区，可能会使用下划线。
8. Golang代码示例8.1. 结构体demopackage mainimport &quot;fmt&quot;type Books struct &#123;   title string   author string   subject string   book_id int&#125;func main() &#123;    // 创建一个新的结构体    fmt.Println(Books&#123;&quot;Go 语言&quot;, &quot;www.voidking.com&quot;, &quot;Go 语言教程&quot;, 6495407&#125;)    // 也可以使用 key =&gt; value 格式    fmt.Println(Books&#123;title: &quot;Go 语言&quot;, author: &quot;www.voidking.com&quot;, subject: &quot;Go 语言教程&quot;, book_id: 6495407&#125;)    // 忽略的字段为 0 或 空    fmt.Println(Books&#123;title: &quot;Go 语言&quot;, author: &quot;www.voidking.com&quot;&#125;)    // 结构体声明和赋值    var Book1 Books    Book1.title = &quot;Go语言入门篇&quot;    Book1.author = &quot;www.voidking.com&quot;    Book1.subject = &quot;Go语言&quot;    Book1.book_id = 123456    printBook(Book1)&#125;func printBook( book Books ) &#123;   fmt.Printf( &quot;Book title : %s\n&quot;, book.title);   fmt.Printf( &quot;Book author : %s\n&quot;, book.author);   fmt.Printf( &quot;Book subject : %s\n&quot;, book.subject);   fmt.Printf( &quot;Book book_id : %d\n&quot;, book.book_id);&#125;


8.2. 接口demopackage mainimport (    &quot;fmt&quot;)type Phone interface &#123;    call()&#125;type NokiaPhone struct &#123;&#125;func (nokiaPhone NokiaPhone) call() &#123;    fmt.Println(&quot;I am Nokia, I can call you!&quot;)&#125;type IPhone struct &#123;&#125;func (iPhone IPhone) call() &#123;    fmt.Println(&quot;I am iPhone, I can call you!&quot;)&#125;func main() &#123;    var phone Phone    phone = new(NokiaPhone)    phone.call()    phone = new(IPhone)    phone.call()&#125;

8.3. 错误处理demopackage mainimport (    &quot;fmt&quot;)// 定义一个 DivideError 结构type DivideError struct &#123;    dividee int    divider int&#125;// 实现 `error` 接口func (de *DivideError) Error() string &#123;    strFormat := `    Cannot proceed, the divider is zero.    dividee: %d    divider: 0`    return fmt.Sprintf(strFormat, de.dividee)&#125;// 定义 `int` 类型除法运算的函数func Divide(varDividee int, varDivider int) (result int, errorMsg string) &#123;    if varDivider == 0 &#123;        dData := DivideError&#123;            dividee: varDividee,            divider: varDivider,        &#125;        errorMsg = dData.Error()        return    &#125; else &#123;        return varDividee / varDivider, &quot;&quot;    &#125;&#125;func main() &#123;    // 正常情况    if result, errorMsg := Divide(100, 10); errorMsg == &quot;&quot; &#123;        fmt.Println(&quot;100/10 = &quot;, result)    &#125;    // 当被除数为零的时候会返回错误信息    if _, errorMsg := Divide(100, 0); errorMsg != &quot;&quot; &#123;        fmt.Println(&quot;errorMsg is: &quot;, errorMsg)    &#125;&#125;

8.4. 并发demopackage mainimport (    &quot;fmt&quot;    &quot;time&quot;)func say(s string) &#123;    for i := 0; i &lt; 5; i++ &#123;        time.Sleep(100 * time.Millisecond)        fmt.Println(s)    &#125;&#125;func main() &#123;    say(&quot;hello&quot;)    go say(&quot;world&quot;)&#125;

8.5. 单元测试demo1、准备项目
mkdir test-demo &amp;&amp; cd test-demogo mod init test-demomkdir pkg1

2、创建 pkg1/hello_test.go
package pkg1import (    &quot;fmt&quot;    &quot;testing&quot;)func TestHello(t *testing.T) &#123;    fmt.Println(&quot;Hello, World!&quot;)&#125;func TestHello2(t *testing.T) &#123;    fmt.Println(&quot;Hello, World! Again!&quot;)&#125;

3、执行单元测试
# 查找单个文件，执行匹配到正则表达式的函数go test -v pkg1/hello_test.go -run TestHellogo test -v pkg1/hello_test.go -run TestHello2# 执行单个文件中的所有函数go test -v pkg1/hello_test.go# 查找整个包，执行匹配到正则表达式的函数go test -v test-demo/pkg1 -run TestHello




]]></content>
      <categories>
        <category>engineering</category>
        <category>golang</category>
        <category>testing</category>
      </categories>
      <tags>
        <tag>golang</tag>
        <tag>好好学Golang</tag>
        <tag>测试</tag>
      </tags>
  </entry>
  <entry>
    <title>使用KeePass管理密码</title>
    <url>/dev-manage-pwd-by-keepass/</url>
    <content><![CDATA[1. 前言一直使用Chrome管理密码，简单方便，但是今天突然意识到，Chrome管理密码有一个大坑：只要知道开机密码，就可以明文查看所有密码！而且，还可以一键导出！不能忍，于是寻求更安全的保存密码的方式，同时也要足够方便，至少可以自动填充密码和同步密码。
当前流行的秘密管理工具主要包括KeePass、LastPass、1Password和Enpass，经过简单比较，郝同学决定选择KeePass，其免费、开源、功能强大、多平台使用，密码数据库保存在本地。


2. KeePass简介我们需要记住许多密码，包括Windows登录密码，电子邮件帐户网站的FTP密码，在线密码（如网站会员帐户）等等。此外，我们应该为每个帐户使用不同的密码。因为如果你在任何地方只使用一个密码并且有人获得这个密码就会出现问题，一个严重的问题。小偷可以访问您的电子邮件帐户，网站等，难以想象。KeePass是一个免费的开源密码管理器，可以帮助您以安全的方式管理密码。您可以将所有密码放在一个数据库中，该数据库使用一个主密钥或密钥文件锁定。因此，您只需记住一个主密码或选择密钥文件即可解锁整个数据库。使用当前已知的最佳和最安全的加密算法（AES和Twofish）对数据库进行加密。
3. 简单使用1、访问KeePass官网，下载对应平台的软件，本文中下载KeePass-2.42.1-Setup.exe，双击安装即可。
2、新建密码数据库File，New，OK，Input DBName，Save，Input Master password，Show expert options，Key file/provid，Use an existing file as key file，OK。这里的key file可选，使用key file会更加安全。
4、导入密码从Chrome中导出密码，File，Import，选择cvs文件导入即可。最后记得删除Chrome中的全部密码。
5、修改数据库后缀修改数据库后缀为常见文件后缀，增加安全性。
4. 自动填充密码参考Keepass用插件实现网页自动填写功能，进行配置。
4.1. Keepass插件1、下载KeePassHttp.plgx
2、将 KeePassHttp.plgx 移动到 keepass 安装目录的 Plugins 目录下，在任务栏右键退出 keepass，然后重新打开 keepass。
3、在keepass菜单栏点击Tools，KeePassHTTP Options，Advanced，勾选Always allow access to entries和Always allow updating entries，Save。
4.2. Chrome插件1、安装KeePassHttp-Connector
2、点击Chrome右上角的KeePassHttp-Connector图标，选择settings。选择连接数据库Connected Databases页面，点击右下角的Connect。keepass会弹出窗口，我们填入一个key，用来标记这个客户端。
4.3. 使用打开一个使用过的网站，首次使用自动填写功能，会有弹出框提醒。勾选Remember this decision，然后单击Allow。如果已经勾选Always allow access to entries和Always allow updating entries，则不会出现弹出框。
注册一个新网站，填入用户名密码后，KeePassHttp-Connector图标会变成红色，点击它可以选择保存新密码。
5. 同步密码参考通过 Google drive 备份与同步 Keepass 数据库，使各个设备的密码可以同步。
1、下载安装Google Drive
2、选择云端文件同步到本地。
3、把keepass数据库放入同步文件夹。
4、安卓设备下载安装keepass2android。
5、打开keepass2android，存储类型选择Google Drive，打开keepass数据库即可。
6. 后记使用KeePassHttp-Connector有一个坑，必须保证keepass处于unlock的状态，这就相当于在桌面摆着自己的密码！找到了替代方案，CKP - KeePass integration for Chrome，但是该插件不能添加新密码，也无法更新密码。方便安全难两全，就这么用着吧，也许以后能找到更好的办法。
7. 书签有什么值得推荐的密码管理软件？
怎么使用密码管理器如keepass更安全？
KeeWeb：一个开源且跨平台的密码管理工具
KeeWeb-WebApp
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>chrome</tag>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS7安装部署Jenkins</title>
    <url>/dev-centos7-install-jenkins/</url>
    <content><![CDATA[1. Jenkins简介Jenkins是一个开源软件项目，是基于Java开发的一种持续集成工具，用于监控持续重复的工作，旨在提供一个开放易用的软件平台，使软件的持续集成变成可能。
本文研究一下在CentOS7（IP为192.168.56.104）上安装部署Jenkins，可以管理部署GitLab中的项目（IP为192.168.56.103）到Web服务器（IP为192.168.56.105）。


2. 环境准备1、在Jenkins服务器编辑hostsvim /etc/hosts，添加：
192.168.56.103  gitlab.voidking.com

2、创建密钥ssh-keygen/root/.ssh目录下生成了私钥id_rsa和公钥id_rsa.pub。
3、添加公钥到gitlab浏览器访问 https://gitlab.voidking.com/profile/keys ，把公钥粘贴进去。
4、安装gityum install git
3. 安装JenkinsJenkins依赖Java，因此首先参考《全平台安装JDK》，安装配置好JDK，然后如下步骤安装Jenkins。
1、添加jenkins源：
wget -O /etc/yum.repos.d/jenkins.repo http://jenkins-ci.org/redhat/jenkins.repo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key

2、安装jenkins：
yum install jenkins

3、启动jenkins：
service jenkins start

Jenkins安装目录：/usr/lib/jenkins/。Jenkins配置文件：/etc/sysconfig/jenkins。
4、防火墙设置
firewall-cmd --add-port=8080/tcp --permanentsystemctl reload firewalld

5、基本设置浏览器访问 http://192.168.56.104:8080 ，即可看到jenkins页面。cat /var/lib/jenkins/secrets/initialAdminPassword查看管理员密码，填入页面，继续；安装推荐的插件；创建第一个管理员；设置Jenkins URL；开始使用Jenkins。
4. 使用Jenkins1、新建项目点击新建Item或者创建一个新任务，新建项目。选择Freestyle project，输入名称，确定。
2、设置源码管理点击源码管理Tab页，选择Git，Repository URL填入git@gitlab.voidking.com:root/voidking.git。此时提示没有权限访问，Credentials后面点击添加，Jenkins。Domain全局凭据（unrestricted），类型选择SSH Username with private key，范围选择全局，Username输入root，然后把id_rsa中的内容粘贴进去，添加。然后在Credentials一栏root，页面上的警告就消失了。最后保存即可。
3、构建项目在voidking项目页面，点击立即构建，即可完成项目构建。点击控制台输出，可以看到整个构建过程。从构建过程可以看出，整个过程只拉取了一次代码，其他啥也没做，代码存放在/var/lib/jenkins/workspace/voidking目录。
5. 构建JavaWeb项目构建JavaWeb项目的基本原理：Jenkins从GitLab拉取项目代码，然后在workspace的项目目录中执行mvn install命令生成war包，最后拷贝war包到Web服务器的webapps目录中并启动tomcat。
5.1. GitLab项目准备1、使用本地环境（JDK+Tomcat+Maven），生成JavaWeb项目的war包并放入Tomcat进行测试，本文中使用pandawork-start项目。
2、上传Maven JavaWeb项目的源码到gitlab。如果gitlab不允许git push -f，那么在gitlab中点击项目名，Settings，Repository，修改Protected Branches的配置即可。
5.2. Web服务器准备1、安装JDK，参考参考《全平台安装JDK》。
2、安装Tomcat，参考CentOS7设置tomcat开机自启动。
3、安装Mysql，参考CentOS7设置mysql开机自启动。
4、配置免密登录，使Jenkins服务器可以免密访问Web服务器，使Jenkins可以免密访问Web服务器。ssh-copy-id -i .ssh/id_rsa.pub -p 22 root@192.168.56.105
5.3. Jenkins服务器配置Jenkin服务器配置主要参考Jenkins安装及自动部署Maven项目。
1、安装配置Maven，参考centos7 安装maven。
2、登录Jenkins，系统管理，插件管理，可选插件，下载Maven Integration。
3、返回工作台，系统管理，全局工具配置。配置JDK，配置Maven，保存。
4、Jenkins工作台，新建项目，构建一个Maven项目。源码管理配置gitlab中的Java Web项目。
5、Build选项卡，Goals and Options，输入clean install -Dmaven.test.skip=true。
6、Post Steps选项卡，Add post-build step，执行shell，填入拷贝war包和启动tomcat命令，保存。
scp /var/lib/jenkins/workspace/pandawork-start/target/pandawork-start.war root@192.168.56.105:/opt/apache-tomcat-8.5.43/webapps/ssh root@192.168.56.105 &quot;nohup sh /opt/apache-tomcat-8.5.43/bin/startup.sh &amp;&quot;

7、Jenkins工作台，点击Web项目，立即构建，控制台输出。由上图可以看出，Maven项目构建成功，但是在执行远程拷贝脚本的时候出错，说明之前添加的免密登录没有生效。
8、给Jenkins用户添加免密登录。（1）添加登录权限，vim /etc/passwd，修改jenkins用户权限
#jenkins:x:997:995:Jenkins Automation Server:/var/lib/jenkins:/bin/falsejenkins:x:997:995:Jenkins Automation Server:/var/lib/jenkins:/bin/bash

（2）切换到jenkins用户，su jenkins
（3）生成密钥，ssh-keygen
（4）密钥添加到Web服务器
cd /var/lib/jenkinsssh-copy-id -i .ssh/id_rsa.pub -p 22 root@192.168.56.105

（5）再次构建项目，执行成功！
8、测试服务浏览器访问 http://192.168.56.105:8080/pandawork-start/ ，即可看到hello world。
6. job并发构建需求：已知有10个Java项目，名称分别为test1、test2、…、test10，现在想要使用jenkins对这10个项目进行构建。问题来了，如果使用同一个job，那么每个项目构建时都需要等待前一个项目构建完成，效率很低。如果使用10个job，那么这10个job的配置基本相同，以后如果有修改，维护成本很高。这种情况下该怎么处理？使用job并发构建，参考jenkins 多用户同时触发构建。
1、登录Jenkins，系统管理，插件管理，可选插件，在过滤中输入build name出现了 Build Name and Description Setter 插件，安装它。安装完成重启jenkins。
2、创建一个自由风格的job，名为test
（1）参数化构建过程，添加字符参数，输入projectName（2）勾选 在必要的时候并发构建（3）增加构建步骤，Update build name，Use macro，Build name macro template，输入#$&#123;BUILD_NUMBER&#125;_$&#123;projectName&#125;（4）勾选 Insert macro first（5）增加构建步骤，执行shell，输入
pwdfor i in `seq 0 120`do	echo &quot;Project Name: &quot;$&#123;projectName&#125;    sleep 1sdone

（6）两次点击Build with Parameters，分别输入项目名test1和test2进行构建。可以看到，Build History里的构建名称是两个不同的名称。
3、清除workspace并发构建后，workspace下会生成test和test@2两个目录，如果并发量高了还会更多，此时需要清除workspace。在job配置页面，增加构建后操作步骤，选择 Delete workspace when build is done。
7. 添加slave节点1、新建节点系统管理，新建节点，输入节点名称，选中固定节点，确定。
2、完善节点细节输入描述（主机名），执行器数量（一般和节点CPU核数相同，此处填4），远程工作目录（/home/jenkins），标签（用于构建哪些项目，此处填project1），用法（只允许运行绑定到这台机器的Job），启动方式（Launch agent agents via SSH），主机（节点IP），Host Key Verification Strategy（Non verifying Verification Strategy），可用性（尽量保持代理在线）
3、证书配置以上配置还漏了一个Credentials，这个需要详细说一下。（1）在master节点使用root用户生成密钥对，ssh-keygen（2）在slave节点添加master节点的公钥，vim /root/.ssh/authorized_keys，把master节点的id_rsa.pub内容添加进去。（3）在jenkins节点配置页面，上图红框的地方，添加，Jenkins。（4）添加凭据，Domain（全局凭据），类型（SSH Username with private key），范围（全局），描述（root private key），Username（root），Private Key选中Enter directly，Key（粘贴master节点的id_rsa），添加。
4、启动代理节点列表，点击节点名称，启动代理，建立master节点与slave节点的连接。启动日志显示：
&lt;===[JENKINS REMOTING CAPACITY]===&gt;channel startedRemoting version: 3.36This is a Unix agentEvacuated stdoutAgent successfully connected and online...
表示连接成功，成功加入了一个jenkins slave节点。
8. 指定slave节点构建8.1. 方法一1、在配置job时，勾选限制项目的运行节点，标签表达式输入“project1”（和slave节点的标签对应）。
2、构建项目，因为slave的执行器数量设置为4，所以当同时构建的数量在4以内时，会使用slave1；当同时构建的数量超过4个时，会使用slave2。
8.2. 方法二1、登录Jenkins，系统管理，插件管理，可选插件，在过滤中输入node找到 Node and Label parameter plugin 插件，安装它。
2、参数化构建过程，添加参数，Node。按住ctrl选择使用的机器，并且选中Allow multi node selection for concurrent builds。
9. 使用上次的参数构建如果一个job的参数很多，那么每次构建都要输入很多参数，很麻烦，此时可以使用Rebuilder插件。1、登录Jenkins，系统管理，插件管理，可选插件，在过滤中输入rebuilder找到 Rebuilder 插件（This plugin is for rebuilding a job using the same parameters.），安装它。
2、构建项目页面，可以看到Rebuild Last选项，点击它使用上次参数再次构建。也可以进入某次Build History，可以看到Rebuild选项，点击它，即可使用该次构建的参数再次构建。
10. 任务超时自动停止如果一个构建任务，因为网络或者其他原因阻塞了，卡住了10分钟以上，这时我们希望这个任务能够自动停止。1、登录Jenkins，系统管理，插件管理，可选插件，在过滤中输入timeout。找到 Build Timeout 插件（This plugin allows builds to be automatically terminated after the specified amount of time has elapsed.），安装它。
2、进入需要配置的job，配置，构建环境，勾选 Abort the build if it’s stuck。

Time-out strategy 保持默认 Absolute 
Timeout minutes 调整为 10 
Time-out variable 不用填
Time-out actions，Add action，Abort the build

11. Jenkins API1、创建job
curl -X POST JENKINS_URL/createItem \  --user USER:TOKEN \  --data-urlencode json=&#x27;&#123;&quot;name&quot;:&quot;new_job&quot;,&quot;mode&quot;:&quot;copy&quot;,&quot;from&quot;:&quot;old_job&quot;&#125;&#x27;

2、构建
curl -X POST JENKINS_URL/job/JOB_NAME/buildWithParameters \  --user USER:TOKEN \  --data-urlencode json=&#x27;&#123;&quot;parameter&quot;: [&#123;&quot;name&quot;:&quot;id&quot;, &quot;value&quot;:&quot;123&quot;&#125;, &#123;&quot;name&quot;:&quot;verbosity&quot;, &quot;value&quot;:&quot;high&quot;&#125;]&#125;&#x27;

3、查看buildnum
curl -X POST JENKINS_URL/job/JOB_NAME/lastBuild/buildNumber

如果使用postman测试的话，--user USER:TOKEN 对应 Authorization 中的 Basic Auth，Username和Password。--data-urlencode json 对应 Body 中的 x-www-form-urlencoded。
更多内容参考：

Remote access API
Python Jenkins documentation
jjb/python-jenkins
使用API对Job进行操作
build now option is not coming for job in jenkins
Jenkins api too slow

12. 后记至此，Jenkins安装配置完成，使用Jenkins配置部署了一个Java Web项目。本文中用到了三台服务器，分别是GitLab服务器、Web服务器和Jenkins服务器。Jenkins从GitLab拉取项目代码，然后在workspace的项目目录中执行mvn install命令生成war包，最后拷贝war包到Web服务器的webapps目录中并启动tomcat。
13. 书签
CentOS 安装 Jenkins
Jenkins详细安装与构建部署使用教程
持续集成Jenkins+Gitlab
基于gitlab和jenkins的自动化部署实例
Jenkins常用插件之Deploy Plugin
jenkins plugins
Least Load
使用Jenkins进行持续集成

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>git</tag>
        <tag>java</tag>
        <tag>tomcat</tag>
        <tag>cicd</tag>
        <tag>maven</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学GitLab：CentOS7安装配置GitLab</title>
    <url>/dev-centos7-install-gitlab/</url>
    <content><![CDATA[1. GitLab简介GitLab是GitLab公司开发的基于Rails的开源存储库管理器。它是一个基于Web的git存储库管理器，允许团队协作编写、测试和部署应用程序。GitLab提供了多种功能，包括wiki、问题跟踪、代码审查和活动源。GitLab Inc提供5种产品：

Gitlab CE（社区版）：自托管和免费，社区论坛的支持。
Gitlab EE（企业版）：自托管和付费，附带其他功能。
Gitlab JH（企业级中国发行版）：极狐，自托管和付费，附带其他功能。
GitLab.com：SaaS和免费。
GitLab.io：由GitLab Inc.管理的私有GitLab实例。

在本文中，我们将在具有2GB RAM的CentOS7服务器上逐步安装GitLab CE（Community Edition），服务器IP为192.168.56.103。
参考文档：

How to Install and Configure GitLab CE on CentOS 7
How to Install and Configure GitLab on CentOS 7
CentOS 7 搭建CA认证中心实现https取证



2. 环境准备首先安装GitLab依赖的相关软件。
1、软件安装
yum -y install curl policycoreutils openssh-server openssh-clients postfix

2、postfix设置开机自启动
systemctl start postfixsystemctl enable postfixsystemctl status postfix

3. 安装GitLab1、添加GitLab CE repository
curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash

2、安装gitlab
yum -y install gitlab-ce
软件642M，需要下载一会，耐心等待，安装完成会出现提示。
3、配置gitlab url
vim /etc/gitlab/gitlab.rb

修改external_url为：
external_url &#x27;http://gitlab.voidking.com&#x27;
这个配置的作用，当用户在gitlab项目页面点击Clone时，展示出的URL为 http://gitlab.voidking.com/&lt;group&gt;/&lt;project&gt;.git
4、使gitlab配置生效
gitlab-ctl reconfigure
但是，这个命令执行时间特别久，因此建议在安装的时候就指定external_url：
EXTERNAL_URL=&quot;http://gitlab.voidking.com&quot; yum -y install gitlab-ce

5、开放端口
firewall-cmd --add-port=80/tcp --permanentfirewall-cmd --add-port=443/tcp --permanentsystemctl reload firewalld

6、测试访问已知gitlab主机IP为 192.169.56.103 ，浏览器访问 http://192.169.56.103 ，即可跳转到gitlab登录页面。首次登录会提示重新修改密码。修改密码之后，使用 root 用户登录，即可进入 dashboard 管理页面。
4. 通过域名访问GitLab4.1. 单机配置hosts修改同一局域网内某台设备的 /etc/hosts，添加：
192.168.56.103  gitlab.voidking.com

然后，这台设备就可以通过域名 http://gitlab.voidking.com 访问gitlab服务了。
4.2. 本地DNS配置域名解析本地DNS配置域名解析：gitlab.voidking.com -&gt; 192.168.56.103然后，同一局域网内所有使用本地DNS的设备就可以通过域名 http://gitlab.voidking.com 访问gitlab服务了。
5. 使用GitLab1、创建项目Create a Project，Project name填入voidking，选择Public，勾选Initialize repository with a README。
2、clone项目
git config --global http.sslVerify falsegit clone https://gitlab.voidking.com/root/voidking.git

3、上传代码
# 添加test.txt文件git add .git commit -m &quot;add new file&quot;git push
查看项目，代码已经上传成功。
6. 配置HTTPS访问GitLab6.1. 生成域名证书和DHPARAM证书参考文档《好好学Linux：Linux中安装使用mkcert签发域名证书》，生成 gitlab.voidking.com 域名的域名证书，并生成一个DHPARAM证书。
6.2. 使用域名证书和DHPARAM证书1、创建证书目录并且移动证书
mkdir -p /etc/gitlab/ssl/mv gitlab.voidking.com* /etc/gitlab/ssl/mv dhparams.pem /etc/gitlab/ssl/

2、修改gitlab url
vim /etc/gitlab/gitlab.rb

如下修改：
external_url &#x27;https://gitlab.voidking.com&#x27;nginx[&#x27;redirect_http_to_https&#x27;] = truenginx[&#x27;ssl_certificate&#x27;] = &quot;/etc/gitlab/ssl/gitlab.voidking.com+2.pem&quot;nginx[&#x27;ssl_certificate_key&#x27;] = &quot;/etc/gitlab/ssl/gitlab.voidking.com+2-key.pem&quot;nginx[&#x27;ssl_dhparam&#x27;] = &quot;/etc/gitlab/ssl/dhparams.pem&quot;

3、重新配置gitlab
gitlab-ctl reconfigure

4、测试访问服务器上测试，curl localhost -L，正常。本地浏览器访问 https://gitlab.voidking.com/users/sign_in ，出现安全提示。
这是因为浏览器不信任刚才自建的CA，解决办法是安装rootCA，安装方法参考文档《好好学Linux：Linux中安装使用mkcert签发域名证书》。安装rootCA后，就没有安全警告了。
7. 错误排查关闭服务器之后重启，发现无法访问gitlab服务。1、关闭防火墙
systemctl stop firewalldsystemctl disable firewalld
关闭防火墙之后，依然无法访问。
2、查看服务和端口
ps aux | grep nginxnetstat -nlpt | grep 80

没有找到nginx服务，说明nginx出了问题。
gitlab-ctl restart nginx
无效，80端口依然没有监听。
3、查看gitlab日志
gitlab-ctl tail
这次定位到了问题，缺少pem文件。
2019-08-06_09:26:08.31983 nginx: [emerg] BIO_new_file(&quot;/etc/gitlab/ssl/gitlab.voidking.com+2.pem&quot;) failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen(&#x27;/etc/gitlab/ssl/gitlab.voidking.com+2.pem&#x27;,&#x27;r&#x27;) error:2006D080:BIO routines:BIO_new_file:no such file)

查看/etc/gitlab/ssl/目录，发现gitlab.voidking.com+2.pem变成了gitlab.voidking.com+3.pem！虽然不知道为什么2变成了3，但是改回来应该就可以了。改回原文件名，问题解决。
8. 后记至此，局域网的gitlab安装配置完成，可以正常使用了。
在公网环境下，HTTPS的配置更加简单，gitlab.rb 中配置证书颁发机构颁发的SSL证书即可。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
        <category>troubleshooting</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>git</tag>
        <tag>gitlab</tag>
        <tag>https</tag>
        <tag>加密</tag>
        <tag>好好学GitLab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker容器中安装使用mkcert</title>
    <url>/dev-docker-mkcert/</url>
    <content><![CDATA[1. 前言《好好学Linux：Linux中安装使用mkcert签发域名证书》一文中，我们学习了Linux中安装使用mkcert签发域名证书的方法。
Linux直接安装mkcert存在两个问题：

如果一个新环境需要签发证书，需要找一台主机重新安装nss-tools和mkcert，比较麻烦。
mkcert生成的CA证书和域名证书中，subject和issuer信息依赖主机名。为了提高证书的可读性，个人建议修改主机名，而修改主机名可能会影响主机中的其他服务。

如果把mkcert安装到Docker容器中，就可以解决上面的两个问题。


2. 制作mkcert镜像1、编写 Dockerfile
FROM debian:bookworm-slimRUN sed -i &#x27;s/deb.debian.org/mirrors.ustc.edu.cn/g&#x27; /etc/apt/sources.list.d/debian.sourcesRUN apt update &amp;&amp; apt install -y libnss3-tools wgetRUN wget -O mkcert &quot;https://mirror.ghproxy.com/https://github.com/FiloSottile/mkcert/releases/download/v1.3.0/mkcert-v1.3.0-linux-amd64&quot; &amp;&amp; chmod +x  mkcert &amp;&amp; mv mkcert /usr/local/binWORKDIR /root/.local/share/mkcertCMD [&quot;tail&quot;, &quot;-f&quot;, &quot;/dev/null&quot;]

国内加速小技巧：

替换了Debian软件安装源
替换了github release下载地址

2、打包mkcert镜像并上传
docker build -t voidking/mkcert:v1.3.0-debian-bookworm-slim .docker push voidking/mkcert:v1.3.0-debian-bookworm-slim

3、mkcert镜像地址需要的同学自取：dockerhub - voidking/mkcert
3. 使用mkcert镜像1、启动mkcert容器
docker run --name mkcert -d \  -h localca \  -v /opt/mkcert:/root/.local/share/mkcert \  voidking/mkcert:v1.3.0-debian-bookworm-slim

挂载 /opt/mkcert 到容器的 /root/.local/share/mkcert ，方便下载证书。
2、登录进入容器
docker exec -it mkcert /bin/bash

3、安装CA，颁发证书
mkcert -installmkcert gitlab.voidking.com localhost




]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>https</tag>
        <tag>加密</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux中安装使用mkcert签发域名证书</title>
    <url>/dev-linux-mkcert/</url>
    <content><![CDATA[1. mkcert简介域名证书的签发，有两种方式：一种是权威机构签发，一种是自己签发。
mkcert是一个开源工具，可以帮助我们轻松创建本地开发所需的域名证书，以便进行开发和调试。使用mkcert时，自动生成一个本地CA，并使用这个CA来签发证书。
相关资料：

《浅谈数据加密》
安装及使用mkcert在Linux和macOS中创建本地受信任的SSL证书
github - FiloSottile/mkcert
基于OpenSSL自建CA和颁发SSL证书
使用 OpenSSL 自建 CA 并签发证书



2. 安装mkcert1、安装nss-tools
yum install nss-tools

nss-tools 是 mkcert 的依赖工具。nss-tools 中的 nss 是 Network Security Services 的缩写，它是一组开源软件库，用于支持跨网络的安全和身份验证服务，如安全套接字层（SSL）、传输层安全（TLS）以及其他安全和密钥管理协议。这个工具包可以支持创建、发送和管理网络协议的加密密钥和证书。
2、安装mkcert
wget -O mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.3.0/mkcert-v1.3.0-linux-amd64chmod +x  mkcertmv mkcert /usr/local/bin

3. 使用mkcert自建CA并颁发域名证书本节中，我们会自建CA，并签发 gitlab.voidking.com 域名的域名证书。
1、修改主机名（可选）
echo localca &gt; /etc/hostname &amp;&amp; hostanme localca

主机名决定了生成的CA中的subject和issuer的信息，建议改成localca这种可读性更强的主机名，生成CA证书后可以改回。
2、安装本地CA（生成CA）
mkcert -install

输出：
Using the local CA at &quot;/root/.local/share/mkcert&quot; ✨The local CA is now installed in the system trust store! ⚡ERROR: no Firefox and/or Chrome/Chromium security databases foundThe local CA is now installed in Java&#x27;s trust store! ☕

3、查看CA证书
mkcert -CAROOT # 获取CA根目录cd /root/.local/share/mkcertopenssl x509 -in rootCA.pem -text -noout

4、生成证书（签发证书）
mkcert gitlab.voidking.com localhost 127.0.0.1

输出：
Using the local CA at &quot;/root/.local/share/mkcert&quot; ✨Warning: the local CA is not installed in the Firefox and/or Chrome/Chromium trust store! ⚠Run &quot;mkcert -install&quot; to avoid verification errors ‼ Created a new certificate valid for the following names 📜 - &quot;gitlab.voidking.com&quot; - &quot;localhost&quot; - &quot;127.0.0.1&quot;The certificate is at &quot;./gitlab.voidking.com+2.pem&quot; and the key at &quot;./gitlab.voidking.com+2-key.pem&quot; ✅

5、查看证书
openssl x509 -in gitlab.voidking.com+2.pem -text -noout

4. 配置域名证书信任设备上默认只信任权威机构签发的域名证书，而自签发的域名证书是不被信任的。想要使设备信任自签发的域名证书，需要把用于签发域名证书的CA证书添加为信任机构。
1、下载rootCA.pem从/root/.local/share/mkcert目录中下载 rootCA.pem 到本地。
2、添加CA信任（1）Chrome添加信任设置，高级设置，管理证书，导入，选择rootCA.pem，证书存储选择受信任的根证书颁发机构，完成，出现警告点确定。重启Chrome。
（2）Firefox添加证书信任选项，隐私与安全，证书，查看证书，导入，选择rootCA.pem。重启Firefox。
（3）Linux中添加证书信任拷贝 rootCA.pem 到 /etc/pki/ca-trust/source/anchors/ 目录，然后执行 update-ca-trust
5. 使用openssl生成DHPARAM证书DHPARAM证书是一种用于Diffie-Hellman密钥交换过程的证书。Diffie-Hellman协议是一个密钥交换协议，主要用于在安全通信中产生共享密钥，这个密钥可以用来加密后续的通信过程。使用DHPARAM证书既能够提高通信的安全性，又能够防止潜在的安全威胁。
gitlab 是一个Diffie-Hellman密钥交换的应用，因此需要 DHPARAM证书，这里我们生成一个。
1、使用OpenSSL生成DHPARAM证书pem文件
openssl dhparam -out dhparams.pem 2048

2、将证书文件的权限更改为600
chmod 600 dhparams.pem




]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>gitlab</tag>
        <tag>https</tag>
        <tag>加密</tag>
      </tags>
  </entry>
  <entry>
    <title>ELK配置FileBeat</title>
    <url>/dev-elk-with-filebeat/</url>
    <content><![CDATA[1. 前言《CentOS7安装部署ELK》一文中，已经安装部署了ELK的基本架构，实现了Logstash收集客户节点数据，传递给Elasticsearch，然后显示在Kibana中。本文中，配置使用FileBeat来收集客户节点数据，然后分别传递给Logstash、Elasticsearch，最后配置使用Kibana仪表盘来显示FileBeat的数据。


2. Beats简介Beats 平台集合了多种单一用途数据采集器。它们从成百上千或成千上万台机器和系统向 Logstash 或 Elasticsearch 发送数据。Packetbeat是最先出现的，用来收集网络数据。逐步衍生出了其他5个 Beats，分别是日志文件、系统信息、审计数据、Windows 系统日志和可用性监控，并且另外还有40多个社区维护的 Beats 来记录各种运营数据。
常见的Beats采集器包括：

Filebeat：日志文件。
Metricbeat：指标。
Packetbeat：网络数据。
Winlogbeat：Windows事件日志。
Auditbeat：审计数据。
Heartbeat：运行时间监控。
Functionbeat：无需服务器的采集器。

更多内容参考Beats: Elasticsearch的数据采集器。
3. 配置Logstash首先配置Logstash，使之能够接收Beats发送的数据。
1、拷贝模板到配置目录
cp /etc/logstash/logstash-sample.conf /etc/logstash/conf.d/filebeat.conf

2、编辑filebeat.conf
vim /etc/logstash/conf.d/filebeat.conf

filebeat.conf 修改为（实际上并不需要修改）：
input &#123;  beats &#123;    port =&gt; 5044  &#125;&#125;output &#123;  elasticsearch &#123;    hosts =&gt; [&quot;http://localhost:9200&quot;]    index =&gt; &quot;%&#123;[@metadata][beat]&#125;-%&#123;[@metadata][version]&#125;-%&#123;+YYYY.MM.dd&#125;&quot;    #user =&gt; &quot;elastic&quot;    #password =&gt; &quot;changeme&quot;  &#125;&#125;

3、检查配置
/usr/share/logstash/bin/logstash --path.settings /etc/logstash -t# or/usr/share/logstash/bin/logstash --path.settings /etc/logstash --config.test_and_exit

4、启动logstash并设置开机自启动
systemctl start logstashsystemctl enable logstash

5、查看5044端口服务情况
netstat -nlpt | grep 5044
找不到服务，哪里出了问题？没出啥问题，请等待3分钟以上，然后重新执行命令。
4. 安装FileBeat参考Install Filebeat，安装FileBeat。
1、下载rpm安装包
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.2.0-x86_64.rpm

2、使用安装包安装FileBeat
rpm -vi filebeat-7.2.0-x86_64.rpm

3、查看FileBeat安装信息
rpm -qi filebeat

5. 配置FileBeat到Logstash参考Configure Filebeat，配置FileBeat。
1、编辑filebeat.yml
vim /etc/filebeat/filebeat.yml

filebeat.yml如下修改：
# line 24, change filebeat.inputfilebeat.inputs:- type: log  enabled: true  paths:    - /var/log/*.log# line 148 and 150, comment output.elasticsearch# line 158 and 160, uncomment output.logstashoutput.logstash:  # The Logstash hosts  hosts: [&quot;localhost:5044&quot;]

2、测试启动filebeat
filebeat -e -c /etc/filebeat/filebeat.yml -d &quot;publish&quot;
与logstash建立了连接，启动成功。
3、此时可以在ES中查看到索引浏览器访问 http://192.168.56.101:9200/_cat/indices?v
4、查看filebeat收集的数据浏览器访问 http://192.168.56.101:9200/filebeat-7.2.0-2019.07.30/_search?pretty
5、在kibana中查看日志数据日志数据是/var/log/*.log，实际包括boot.log、yum.log和test.log。
6、查看并且启用filebeat modules
filebeat modules listfilebeat modules enable systemfilebeat modules list

7、启动filebeat并设置开机启动
systemctl start filebeatsystemctl enable filebeat
启用system module后，Kibana的event.dataset会多出system.syslog、system.auth。
PS：重建索引的方法（1）删除索引
curl -XDELETE -u elastic:changeme http://localhost:9200/filebeat-7.2.0-2019.07.30
（2）重建索引删除registry/filebeat，然后重新启动filebeat。
8、更新test.log在test.log中添加一行，FileBeat默认10秒reload一次文件，但是在ES中查询不到更新。神奇的是，在Kibana中可以查询到更新，不知道怎么肥四。留个坑，有缘再填。
6. 配置FileBeat到ES1、编辑filebeat.yml
vim /etc/filebeat/filebeat.yml

filebeat.yml如下修改：
# line 148 and 150, uncomment output.elasticsearchoutput.elasticsearch:  # Array of hosts to connect to.  hosts: [&quot;localhost:9200&quot;# line 158 and 160, comment output.logstash

2、测试启动filebeat
systemctl stop filebeatfilebeat -e -c /etc/filebeat/filebeat.yml -d &quot;publish&quot;`
与ES建立了连接，启动成功。
3、此时可以在ES中查看到索引浏览器访问 http://192.168.56.101:9200/_cat/indices?v
之后在ES和Kibana中查看数据的方法和上面相同。更新test.log，同样在ES中看不到，在Kibana中可以看到。
7. 配置FileBeat仪表盘如果想要使用Filebeat提供的示例Kibana仪表盘，那么需要配置Kibana端点。如果Kibana与ES在同一主机上运行，​​则可以跳过此步骤。
1、编辑filebeat.yml
vim /etc/filebeat/filebeat.yml

filebeat.yml如下修改：
# line 117 and 123, uncomment setup.kibanasetup.kibana:  # Kibana Host  host: &quot;localhost:5601&quot;

2、创建仪表盘
filebeat setup --dashboards

3、测试启动filebeat（不启动也可以）
filebeat -e -c /etc/filebeat/filebeat.yml -d &quot;publish&quot;

4、查看Kibana仪表盘打开dashboard仪表盘，可以选择filebeat相关的仪表盘。
8. 后记至此，安装配置完成了FileBeat，其收集的数据可以传递给Logstash，也可以直接传递给ES。创建了FileBeat的仪表盘，成功显示数据。
9. 书签
How To Install ELK on CentOS 7
Load the index template in Elasticsearch
Filebeat命令参考
filebeat收集日志常见问题

]]></content>
      <categories>
        <category>engineering</category>
        <category>logging</category>
      </categories>
      <tags>
        <tag>elk</tag>
        <tag>日志</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS7安装部署ELK</title>
    <url>/dev-centos7-install-elk/</url>
    <content><![CDATA[1. ELK简介ELK是一个日志分析平台，它主要由Elasticsearch、Logstash和Kibana三个部分组成。Elasticsearch是一个开源的全文搜索和分析引擎，它可以实现数据的实时全文搜索，可以处理大规模日志数据，比如Nginx、Tomcat、系统日志等功能。Logstash负责日志收集和转发，支持日志过滤，支持普通log、自定义json格式的日志解析。Kibana通过接口调用Elasticsearch的数据，并进行前端数据可视化。


2. 部署规划计划在三台CentOS7机器上部署ELK，其中一台机器作为ELK的服务节点，IP为192.168.56.101；另外两台作为客户节点，IP为192.168.56.102/103。其中服务节点部署Elasticsearch、Logstash和Kibana三个组件，客户节点部署Logstash。
3. 服务节点部署3.1. 环境准备默认root用户下操作，其他用户请自觉添加sudo。  
1、安装JDK，参考《全平台安装JDK》。
2、关闭防火墙。
systemctl stop firewalldsystemctl disable firewalld

或者设置防火墙规则：
firewall-cmd --add-port=9200/tcp --permanentfirewall-cmd --add-port=9300/tcp --permanentfirewall-cmd --add-port=5601/tcp --permanentfirewall-cmd --reload

3、添加ELK仓库
cat &lt;&lt;EOF | tee /etc/yum.repos.d/elasticsearch.repo[elasticsearch-7.x]name=Elasticsearch repository for 7.x packagesbaseurl=https://artifacts.elastic.co/packages/7.x/yumgpgcheck=1gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearchenabled=1autorefresh=1type=rpm-mdEOF

4、引入GPG key
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch

5、更新yum包
yum clean allyum makecache

3.2. Elasticsearch1、安装Elasticsearch
yum install -y elasticsearch
安装过程中会下载 https://artifacts.elastic.co/packages/7.x/yum/7.2.0/elasticsearch-7.2.0-x86_64.rpm ，但是，下载速度非常慢。这里建议通过浏览器下载，然后上传到服务节点上。上传完成后，执行安装：
rpm -ivh elasticsearch-7.2.0-x86_64.rpm
提示future versions of Elasticsearch will require Java 11; your Java version does not meet this requirement. 不过没关系，当前版本能用就行。
2、确认Elasticsearch的安装信息
rpm -qi elasticsearch

3、启动Elasticsearch并设置开机启动
systemctl daemon-reloadsystemctl start elasticsearchsystemctl enable elasticsearch

4、查看Elasticsearch运行状态
systemctl status elasticsearchps -ef | grep elasticsearchnetstat -nlpt

5、尝试请求
curl localhost:9200

6、设置允许其他机器访问当前只能响应本机的请求，想要其他机器也能访问的话，那么需要修改elasticsearch的配置。
vim /etc/elasticsearch/elasticsearch.yml

elasticsearch.yml如下修改：
# line 17, uncommentcluster.name: my-application# line 22, uncomment and changenode.name: master# line 55, uncomment and changenetwork.host: 0.0.0.0# line 59, uncommentttp.port: 9200# line 72, uncomment and changecluster.initial_master_nodes: [&quot;master&quot;, &quot;node-2&quot;]

7、重启Elasticsearch
systemctl stop elasticsearchsystemctl start elasticsearch# 启动后稍等一会netstat -nlpt

如果可以看到:::9200，就可以通过外部浏览器访问Elasticsearch服务了，至此Elasticsearch安装配置完成。
3.3. Kibana1、安装Kibana
yum install -y kibana
同样的，如果下载速度缓慢，那么可以通过浏览器下载 https://artifacts.elastic.co/packages/7.x/yum/7.2.0/kibana-7.2.0-x86_64.rpm ，然后上传到服务节点，上传后执行安装：
rpm -ivh kibana-7.2.0-x86_64.rpm

2、确认Kibana的安装信息
rpm -qi kibana

3、启动Kibana并设置开机启动
systemctl start kibanasystemctl enable kibana

4、查看Kibana运行状态
systemctl status kibanaps -ef | grep kibananetstat -nlpt

5、测试访问
curl localhost:5601 -L

6、设置允许其他机器访问
vim /etc/kibana/kibana.yml

kibana.yml如下修改：
# line 2, uncomment and changeserver.port: 5601# line 7, uncomment and changeserver.host: &quot;0.0.0.0&quot;# line 28, uncommentelasticsearch.hosts: [&quot;http://localhost:9200&quot;]

7、重启Kibana
systemctl stop kibanasystemctl start kibana# 启动后稍等一会netstat -nlpt
如果可以看到0.0.0.0:5601，就可以通过外部浏览器访问Kibana服务了，至此Kibana安装配置完成。
3.4. Logstash1、安装Logstash
yum install -y logstash
也可以通过浏览器下载 https://artifacts.elastic.co/packages/7.x/yum/7.2.0/logstash-7.2.0.rpm ，然后上传到服务节点，上传后执行安装：
rpm -ivh logstash-7.2.0.rpm

2、确认Logstash的安装信息rpm -qi logstash
3、启动Logstash
systemctl start logstash

4、查看Logstash运行状态
systemctl status logstashps -ef | grep logstashnetstat -nlpt

启动成功了，然后呢？怎么使用？
5、Logstash hello world
systemctl stop logstash/usr/share/logstash/bin/logstash -e &#x27;input &#123; stdin &#123; &#125; &#125; output &#123; stdout &#123;&#125; &#125;&#x27;
输入启动命令后，耐心等待服务启动，直到出现Successfully started Logstash API endpoint。输入“hello world”回车，即可打印出输出。logstash通过管道来处理数据，标准的管道包含input、filter和output。以上命令，指定了一个管道的参数，没有filter，input是控制台标准输入，output是控制台标准输出。
6、管道配置写入文件新建配置文件std.conf，内容为：
input &#123;     stdin &#123; &#125; &#125; output &#123;     stdout &#123;&#125; &#125;

7、测试配置文件并启动
/usr/share/logstash/bin/logstash -f ~/std.conf --config.test_and_exit/usr/share/logstash/bin/logstash -f ~/std.conf

8、从文件中读取信息（1）新建/usr/local/test.log文件，内容为：
hello logstash!

（2）新建test.conf，内容为：
input &#123;    file &#123;        path =&gt; [&quot;/usr/local/test.log&quot;]        sincedb_path =&gt; &quot;/dev/null&quot;        start_position =&gt; &quot;beginning&quot;    &#125;&#125;filter &#123;&#125;output &#123;    stdout &#123;        codec =&gt; rubydebug    &#125;&#125;

（3）启动Logstash
/usr/share/logstash/bin/logstash -f ~/test.conf

9、写入信息到文件（1）修改test.conf为：
input &#123;    file &#123;        path =&gt; [&quot;/usr/local/test.log&quot;]        sincedb_path =&gt; &quot;/dev/null&quot;        start_position =&gt; &quot;beginning&quot;    &#125;&#125;filter &#123;&#125;output &#123;    file &#123;        path =&gt; [&quot;/usr/local/test.log.out&quot;]    &#125;&#125;

（2）启动Logstash
/usr/share/logstash/bin/logstash -f ~/test.conf
启动后，/usr/local/目录下多了test.log.out文件。
3.5. 数据传递Logstash收集到了数据，怎样传递给Elasticsearch显示？怎样传递给Kibana显示？1、修改test.conf为：
input &#123;    file &#123;        path =&gt; [&quot;/usr/local/test.log&quot;]        sincedb_path =&gt; &quot;/dev/null&quot;        start_position =&gt; &quot;beginning&quot;    &#125;&#125;filter &#123;&#125;output &#123;    elasticsearch &#123;        hosts =&gt; [&quot;http://localhost:9200&quot;]    &#125;&#125;

2、启动Logstash
/usr/share/logstash/bin/logstash -f ~/test.conf

3、查看索引浏览器访问 http://192.168.56.101:9200/_cat/indices?v其中有个索引是logstash的，这就是我们想要查看的数据索引。
4、查看数据浏览器访问 http://192.168.56.101:9200/logstash-2019.07.24-000001/_search看到了hello logstash!，说明数据已经成功传递到了Elasticsearch。链接后添加?pretty参数，可以进行格式化显示。
5、Kibana添加indices浏览器访问Kibana http://192.168.56.101:5601点击页面上的Logs，然后填写indices相关信息。最后点击Update Source，即可在页面上看到Logstash传递的信息。
6、修改test.log修改test.log为：
hello logstash!the log has been updated.
再次查看Kibana的日志显示，可以看到更新。
4. 客户节点部署4.1. 环境准备默认root用户下操作，其他用户请自觉添加sudo。
1、安装JDK。
2、关闭防火墙。
systemctl stop firewalldsystemctl disable firewalld

3、添加ELK仓库
cat &lt;&lt;EOF | tee /etc/yum.repos.d/elasticsearch.repo[elasticsearch-7.x]name=Elasticsearch repository for 7.x packagesbaseurl=https://artifacts.elastic.co/packages/7.x/yumgpgcheck=1gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearchenabled=1autorefresh=1type=rpm-mdEOF

4、引入GPG key
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch

5、更新yum包
yum clean allyum makecache

4.2. Logstash1、安装Logstash
yum install -y logstash
也可以通过浏览器下载 https://artifacts.elastic.co/packages/7.x/yum/7.2.0/logstash-7.2.0.rpm ，然后上传到服务节点，上传后执行安装：
rpm -ivh logstash-7.2.0.rpm

2、确认Logstash的安装信息
rpm -qi logstash

3、创建测试文件新建配置文件test.conf，内容为：
input &#123;    file &#123;        path =&gt; [&quot;/usr/local/test.log&quot;]        sincedb_path =&gt; &quot;/dev/null&quot;        start_position =&gt; &quot;beginning&quot;    &#125;&#125;filter &#123;&#125;output &#123;    elasticsearch &#123;        hosts =&gt; [&quot;http://192.168.56.101:9200&quot;]    &#125;&#125;

新建/usr/local/test.log，内容为：
this is a log recorded by 102

4、启动logstash
/usr/share/logstash/bin/logstash -f ~/test.conf

5、在Kibana查看日志刷新Kibana，即可看到客户节点的日志。
5. 后记以上，完成了ELK的基本安装配置，实现了一个最简单的架构。Logstash收集数据，传递给Elasticsearch，然后Kibana显示Elasticsearch中的数据。更多关于ELK的内容，后续会继续学习。
PS：连接ES，推荐使用VSCode的Database Client for Visual Studio Code插件。
6. 书签
How to Install ELK Stack on CentOS 7
Logstash 实用介绍
Getting Started with Logstash
开源日志实时分析平台ELK
logstash的各个场景应用（配置文件均已实践过）
Logstash介绍
在 CentOS7 安装 ELK
ELK-7.0安装部署收集展示
ELK部署安装以及配置
Learn About the Elastic Stack
Elastic 中文社区
Elastic中文社区运维监控实战之架构篇
从零开始搭建ELK+GPE监控预警系统
《ELK Stack 中文指南》
ELK不权威指南
零代码如何打造自己的实时监控预警系统
Docker下ELK三部曲之一：极速体验

]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>logging</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>elk</tag>
        <tag>日志</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS更换软件安装源</title>
    <url>/dev-centos-change-software-repo/</url>
    <content><![CDATA[1. 前言之前整理过《好好学Linux：Ubuntu/Debian更换软件安装源》，Ubuntu/Debian中更换国内软件安装源，可以加快软件的安装速度。
同样的，在CentOS中更换国内软件安装源，也能加快软件安装速度。


2. 备用国内源
阿里：http://mirrors.aliyun.com/
网易：http://mirrors.163.com/
搜狐：http://mirrors.sohu.com/

其他软件源参考《开源镜像站》
3. CentOS7更换软件安装源（已废弃）1、备份原安装源
cp /etc/yum.repos.d/CentOS-Base.repo&#123;,.bak&#125;

2、下载国内的安装源
wget http://mirrors.aliyun.com/repo/Centos-7.repowget http://mirrors.163.com/.help/CentOS7-Base-163.repo# wget http://mirrors.aliyun.com/repo/Centos-7.repo# wget http://mirrors.cloud.tencent.com/repo/centos7_base.repomv Centos-7.repo /etc/yum.repos.d/Centos-Base.repo

3、更新软件安装源
yum clean allyum makecache

yum makecache可能会报错：
Could not resolve host: mirrorlist.centos.org; Unknown error

解决办法：
curl -fsSL https://autoinstall.plesk.com/PSA_18.0.62/examiners/repository_check.sh | bash -s -- update &gt;/dev/null

参考文档：Plesk or system updates fail on CentOS 7: Could not resolve host: mirrorlist.centos.org
4. CentOS7更新软件安装源（推荐）sed -i.bak  \    -e &#x27;s|^mirrorlist=|#mirrorlist=|g&#x27; \    -e &#x27;s|^#baseurl=http://mirror.centos.org/centos|baseurl=https://mirrors.ustc.edu.cn/centos-vault/centos|g&#x27; \    /etc/yum.repos.d/CentOS-Base.repo

5. CentOS8更换软件安装源CentOS8已于2021年12月31日停止维护，在2022年1月31日，CentOS团队终于从官方镜像中移除CentOS8的所有包。
如果仍然需要运行CentOS8，我们可以在 /etc/yum.repos.d 中更新安装源为阿里云的镜像源。
1、备份原安装源
cp -r /etc/yum.repos.d&#123;,.bak&#125;

2、替换安装源
rm /etc/yum.repos.d/*.repo -rfwget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo#curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo

3、更新软件安装源
yum clean allyum makecache



]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>批量替换文件脚本</title>
    <url>/dev-scripts-for-batch-replace-files/</url>
    <content><![CDATA[1. 任务要求步骤一：【替换文件】log4j.properties文件拷贝覆盖到：/opt/flume/conf/log4j.properties步骤二：【替换文件】run.sh文件拷贝覆盖到：/opt/flume/bin/run.sh步骤三：【删除文件】进入/opt/plugins.d/vipkid/lib目录，如果存在就删除flume-ext-1.0-SNAPSHOT.jar步骤四：【替换文件】flume-ext-1.1-SNAPSHOT.jar文件拷贝覆盖到：/opt/flume/plugins.d/vipkid/lib/flume-ext-1.1-SNAPSHOT.jar其他：替换的文件所属用户为flume用户


2. ansible实现ansible脚本的思路是先备份，然后执行替换删除操作。
---- hosts: commonservers  tasks:  - name: get the time stamp    shell: date +%Y%m%d%H%M%S.%N    register: now_time  - name: backup log4j.properties    shell: cp /opt/flume/conf/log4j.properties&#123;,.&#123;&#123;now_time.stdout&#125;&#125;&#125;   - name: backup run.sh    shell: cp /opt/flume/bin/run.sh&#123;,.&#123;&#123;now_time.stdout&#125;&#125;&#125;  - name: backup flume-ext-1.0-SNAPSHOT.jar    shell: cp /opt/flume/plugins.d/vipkid/lib/flume-ext-1.0-SNAPSHOT.jar&#123;,.&#123;&#123;now_time.stdout&#125;&#125;&#125;  - name: backup flume-ext-1.1-SNAPSHOT.jar    shell: cp /opt/flume/plugins.d/vipkid/lib/flume-ext-1.1-SNAPSHOT.jar&#123;,.&#123;&#123;now_time.stdout&#125;&#125;&#125;  - name: replace log4j.properties    copy:      src: log4j.properties      dest: /opt/flume/conf/log4j.properties      owner: flume      force: yes  - name: replace run.sh    copy:      src: run.sh      dest: /opt/flume/bin/run.sh      owner: flume      force: yes  - name: remove flume-ext-1.0-SNAPSHOT.jar    shell: rm -f /opt/flume/plugins.d/vipkid/lib/flume-ext-1.0-SNAPSHOT.jar  - name: replace flume-ext-1.1-SNAPSHOT.jar    copy:      src: flume-ext-1.1-SNAPSHOT.jar      dest: /opt/flume/plugins.d/vipkid/lib/flume-ext-1.1-SNAPSHOT.jar      owner: flume      force: yes

3. shell脚本实现shell脚本的思路是在A机器上执行batch-main.sh批量处理脚本，读取主机列表，把新文件和replace.sh脚本拷贝到所有B机器上。然后batch-main.sh调用执行所有B机器上的replace.sh脚本，replace.sh脚本负责备份和执行替换删除操作。
batch-main.sh内容为：
#!/bin/bash# zipmkdir newfilecp /opt/flume/conf/log4j.properties newfile/cp /opt/flume/bin/run.sh newfile/cp /opt/flume/plugins.d/vipkid/lib/flume-ext-1.1-SNAPSHOT.jar newfile/tar -czvf newfile.tar newfile &amp;&amp;# scparray_str=`cat hosts`IPS=&#x27; &#x27;array=($array_str)user=`whoami`echo &#x27;scp start&#x27;for data in $&#123;array[@]&#125;do    echo &#x27;host: &#x27;$data    scp newfile.tar $user@$data:~/    scp replace.sh $user@$data:~/    ssh $user@$data &#x27;/bin/tar -xvf ~/newfile.tar&#x27;done &amp;&amp;echo &#x27;scp finished&#x27;# replaceecho &#x27;replace start&#x27;for data in $&#123;array[@]&#125;do    echo &#x27;host: &#x27;$data    ssh $user@$data &#x27;sudo /bin/chown flume -R ~/newfile&#x27;    ssh $user@$data &#x27;/bin/bash ~/replace.sh&#x27;doneecho &#x27;repalce finished&#x27;

replace.sh内容为：
#!/bin/bash# backupnow_time=`date +%Y%m%d%H%M%S.%N`sudo cp /opt/flume/conf/log4j.properties&#123;,.$now_time&#125;sudo cp /opt/flume/bin/run.sh&#123;,.$now_time&#125;if [ ! -f &quot;/opt/flume/plugins.d/vipkid/lib/flume-ext-1.0-SNAPSHOT.jar&quot; ];then  echo &quot;flume-ext-1.0-SNAPSHOT.jar不存在&quot;else  sudo cp /opt/flume/plugins.d/vipkid/lib/flume-ext-1.0-SNAPSHOT.jar&#123;,.$now_time&#125;fisudo cp /opt/flume/plugins.d/vipkid/lib/flume-ext-1.1-SNAPSHOT.jar&#123;,.$now_time&#125;# operationsudo cp -f ~/newfile/log4j.properties /opt/flume/conf/log4j.propertiessudo cp -r ~/newfile/run.sh /opt/flume/bin/run.shsudo rm -f /opt/plugins.d/vipkid/lib/flume-ext-1.0-SNAPSHOT.jarsudo cp -f ~/newfile/flume-ext-1.1-SNAPSHOT.jar /opt/flume/plugins.d/vipkid/lib/flume-ext-1.1-SNAPSHOT.jar

hosts内容为：
l-newman-server1.ops.beta.ali.dml-newman-server2.ops.beta.ali.dml-newman-server3.ops.beta.ali.dml-newman-server4.ops.beta.ali.dm

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>ansible</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>使用ansible进行服务器初始化</title>
    <url>/dev-init-servers-by-ansible/</url>
    <content><![CDATA[1. 前言三台CentOS7机器，一台作为ansible管理机，两台作为ansible客户机。使用ansible管理机完成两台客户机的初始化，主要包括java和tomcat的安装配置。ansible管理机IP为：192.168.56.101。ansible客户机IP为：192.168.56.102/103。 


2. 管理机配置1、安装ansibleyum install ansible
2、生成密钥ssh-keygen
3、把公钥拷贝到客户机ssh-copy-id -i .ssh/id_rsa.pub -p 22 root@192.168.56.102
4、客户机IP写入到hostsvim /etc/ansible/hosts
[commonservers]192.168.56.102192.168.56.103

5、测试连接ansible all -m ping
3. 客户机配置客户机不需要额外配置。
4. java配置1、在oracle官网下载jdk。
2、下载后上传文件到ansible管理机，假设为jdk-8u161-linux-x64.tar.gz。
3、管理机中创建jdk.env，内容为：
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161export JRE_HOME=$JAVA_HOME/jreexport CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATHexport PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH

4、管理机中创建java-install.yml，内容为：
---- hosts: commonservers  tasks:  - name: copy jdk.tar.gz    copy:      src: ./jdk-8u161-linux-x64.tar.gz      dest: ~/jdk.tar.gz  - name: create jvm directory    shell: mkdir -p /usr/lib/jvm  - name: unzip jdk.tar.gz    shell: tar -zxf jdk.tar.gz -C /usr/lib/jvm/  - name: copy env to clients    copy:      src: ./jdk.env      dest: ~/jdk.env  - name: write jdk env to profile    shell: cat jdk.env | tee &gt;&gt; /etc/profile  - name: write jdk env to .bashrc    shell: cat jdk.env | tee &gt;&gt; /root/.bashrc  - name: make the config active    shell: source /root/.bashrc  - name: test jdk    shell: java -version    register: java_out  - name: show java result    debug: var=java_out verbosity=0

5、检查并且执行脚本  
ansible-playbook java-install.yml --syntax-checkansible-playbook java-install.yml

5. tomcat配置1、在apache官网下载tomcat。
2、下载后上传文件上传到ansible管理机，假设为apache-tomcat-8.0.50.tar.gz。
2、管理机创建tomcat-install.yml，内容为：
---- hosts: commonservers  tasks:  - name: copy tomcat    copy:      src: ./apache-tomcat-8.0.50.tar.gz      dest: ~/tomcat.tar.gz  - name: unzip tomcat.tar.gz    shell: tar -zxf tomcat.tar.gz -C /opt/  - name: start tomcat    shell: nohup /opt/apache-tomcat-8.0.50/bin/startup.sh &amp;  - name: test tomcat    shell: ps aux | grep tomcat    register: tomcat_out  - name: show tomcat result    debug: var=tomcat_out verbosity=0

3、检查并且执行脚本  
ansible-playbook tomcat-install.yml --syntax-checkansible-playbook tomcat-install.yml]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>ansible</tag>
      </tags>
  </entry>
  <entry>
    <title>Ubuntu安装配置zabbix</title>
    <url>/dev-ubuntu-install-zabbix/</url>
    <content><![CDATA[1. 前言对于服务器集群的监控，有很多可选的开源技术方案，比如zabbix，nagios，Cacti，Munin，icinga，ganglia ，collectd + graphite，influxed + grafana，prometheus等。以前简单研究过ganglia，本文中，来研究学习一下zabbix。


2. zabbix简介参考zabbix监控的基础概念、工作原理及架构，我们了解到zabbix是一个分布式系统监控和网络监视工具。zabbix支持主机的性能监控、网络设备性能监控、数据库性能监控、多种告警方式、详细的报表图表绘制。zabbix的基本工作原理为：zabbix agent需要安装到被监控的主机上，它负责定期收集各项数据，并发送到zabbix server端，zabbix server将数据存储到数据库中，zabbix web根据数据在前端进行展现和绘图。这里agent收集数据分为主动和被动两种模式。
3. 环境准备在VirtualBox中创建两台Ubuntu16虚拟机，zabbix0和zabbix1。硬件配置都是1核2GB内存，IP分别为192.168.56.200和192.168.56.201。
zabbix0上安装zabbix-server和zabbix-agent，zabbix1上zabbix-agent。
sudo -i切换到root用户，方便安装配置。
4. zabbix0zabbix服务端的安装配置，主要参考在Ubuntu16.04 Server上安装Zabbix。
4.1. 软件安装1、安装zabbix需要的PHP模块apt-get install php7.0-bcmath php7.0-xml php7.0-mbstring
2、下载导入zabbix-server安装源
wget http://repo.zabbix.com/zabbix/3.2/ubuntu/pool/main/z/zabbix-release/zabbix-release_3.2-1+xenial_all.debdpkg -i zabbix-release_3.2-1+xenial_all.debapt-get update
Ubuntu仓库中提供的zabbix是过时的，因此使用官方的安装包。
3、安装zabbix-server（支持mysql的版本）和zabbix-webapt-get install zabbix-server-mysql zabbix-frontend-php
4、安装zabbix-client（可选）apt-get install zabbix-agent
4.2. mysql配置1、登录mysqlmysql -uroot -p默认密码为空。
2、创建zabbix数据库和zabbix用户
create database zabbix character set utf8 collate utf8_bin;grant all privileges on zabbix.* to zabbix@localhost identified by &#x27;zabbix_password&#x27;;exit;

3、导入初始表和数据
zcat /usr/share/doc/zabbix-server-mysql/create.sql.gz | mysql -uzabbix -p zabbix

4.3. zabbix-server配置1、vim /etc/zabbix/zabbix_server.conf，如下修改：
# line 81, uncommentDBHost=localhost# line 91, changeDBName=zabbix# line 107, changeDBUser=zabbix# line 115, uncomment and changeDBPassword=zabbix_password

2、vim /etc/zabbix/apache.conf，如下修改：
# line 19, uncomment and changephp_value date.timezone Asia/Shanghai# line 28, uncomment and changephp_value date.timezone Asia/Shanghai

3、重启apache，启动zabbix-server并设置开机启动
systemctl restart apache2systemctl start zabbix-serversystemctl enable zabbix-serversystemctl status zabbix-server

4.4. zabbix-web配置1、浏览器访问 http://192.168.56.200/zabbix
2、点击“Next step”，按照提示进行安装配置。中间只要填写一次数据库密码，其他的默认即可。安装完成后的配置文件为/usr/share/zabbix/conf/zabbix.conf.php。
3、浏览器访问 http://192.168.56.200/zabbix/index.php使用默认的用户Admin和密码zabbix登录，即可进入管理界面。此时，我们看到Number of hosts enabled为0。
4.5. zabbix-agent配置1、vim /etc/zabbix/zabbix_agentd.conf，如下修改：
# line 95, changeServer=127.0.0.1# line 136, changeServerActive=127.0.0.1# line 147, changeHostname=zabbix0
其中Server和ServerActive指的是Zabbix Server，Hostname指的是client server name。
2、启用zabbix-agent并设置开机启动
systemctl start zabbix-agentsystemctl enable zabbix-agent
此时，在zabbix web管理页面依然看不到主机的信息。
3、在zabbix web管理页面添加zabbix0主机，参考How to Add Host in Zabbix Server to Monitor。
（1）在zabbix web管理页面，Configuration，Hosts，Create host。（2）在Host标签页，填入如下信息。

Hostname: Enter hostname of Remote system
Visible name: Name to be display in zabbix
Group: Select the desired group for you host
Agent interface: Fill the info of Zabbix agent running on host
Enabled: Check for active

（3）在Templates标签页，点击Select选择要监控的指标，点击Add链接确认。（4）点击Add按钮确认添加host。
此时，就可以在zabbix web管理页面看到host的信息了，选择host的Graphs，即可查看host状态相关的绘图。
5. zabbix11、下载导入zabbix-server安装源
wget http://repo.zabbix.com/zabbix/3.2/ubuntu/pool/main/z/zabbix-release/zabbix-release_3.2-1+xenial_all.debdpkg -i zabbix-release_3.2-1+xenial_all.debapt-get update

2、安装zabbix-clientapt-get install zabbix-agent
3、vim /etc/zabbix/zabbix_agentd.conf，如下修改：
# line 95, changeServer=192.168.56.200# line 136, changeServerActive=192.168.56.200# line 147, changeHostname=zabbix1

4、启用zabbix-agent并设置开机启动
systemctl start zabbix-agentsystemctl enable zabbix-agent

5、在zabbix web管理页面添加zabbix1主机，再添加一台不存在的zabbix2主机。添加完成后即可看到三台主机都是Enabled，等待两分钟后界面显示其中一台不可用。
6. 后记至此，zabbix就安装配置完成了，更高级的用法，用到了再去研究学习。
7. 书签Zabbix Documentation 3.2
How to Install Zabbix Server 3.4 on Ubuntu 18.04 &amp; 16.04 LTS
How To Install Zabbix Agent on Ubuntu 18.04 &amp; 16.04 LTS
ubuntu 16.04.1 LTS zabbix-agent安装
Update-rc.d 命令用法详解
zabbix常见指标项梳理
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>监控</tag>
        <tag>ubuntu</tag>
        <tag>zabbix</tag>
      </tags>
  </entry>
  <entry>
    <title>通过远程控制接口批量安装系统</title>
    <url>/dev-batch-install-sys-by-mgmt/</url>
    <content><![CDATA[1. 前言《通过远程控制接口给服务器安装系统》一文中，我们发现通过远程控制接口给服务器安装系统很方便。《Ubuntu系统批量自动安装》一文中，我们学会了使用PXE技术进行系统的批量安装。
这两者结合，能产生怎样的魔力？下面是见证奇迹的时刻。
需求：机房里有40台曙光I620-G20机器需要安装Ubuntu16 Server系统，曙光供应商说需要两天的工作量，报价五千元。那么自己安装的话，需要多长时间？也需要两天吗？


2. 安装步骤1、进入机房，给40台机器配置远程控制接口的IP。参考《通过远程控制接口给服务器安装系统》的远程控制接口配置部分。
2、搭建PXE服务器。参考《Ubuntu系统批量自动安装》，并且进行改进。
3、远程控制批量安装系统。
3. 搭建PXE服务器3.1. 基本准备1、一台服务器，已经安装好了Ubuntu16 Server版系统，IP为172.16.0.162。2、更换源列表并更新，参考《Ubuntu更换源列表》。3、切换到root用户，sudo -i4、开启路由转发，echo 1 &gt; /proc/sys/net/ipv4/ip_forward。务必开启，因为PXE服务器同时作为网关，不开启的话安装会卡住。5、在PXE服务器上安装DNS服务（可选操作，没有什么影响）。
3.2. DHCP和FTP本节主要参考How to Install Ubuntu via PXE Server Using Local DVD Sources。
1、安装DHCP和FTP服务apt-get install dnsmasq
2、指定DHCP网卡ip add，看到正在使用的网卡为ens2f1（172.16.0.162）。
3、配置dnsmasqvim /etc/dnsmasq.conf，在文件末尾添加：
interface=ens2f1dhcp-range=172.16.0.163,172.16.0.254,24hdhcp-option=3,172.16.0.162dhcp-option=6,172.16.0.162dhcp-option=28,172.16.0.255dhcp-option=42,0.0.0.0dhcp-boot=pxelinux.0,172.16.0.162enable-tftptftp-root=/tftpboot# gateway to Internet#dhcp-option=3,172.16.0.135# real dns server#dhcp-option=6,172.20.1.4#server=8.8.4.4
dhcp-option=3表示网关，6表示域名服务器，28表示broadcast，48表示时间服务器。server表示DNS forwarder IPs Address。
需要注意的是，网关一定要配置，否则在使用内网镜像安装时会特别慢。在获取镜像时卡住半小时，查看安装日志如下图所示；在“Updating the list of available packages”时也会卡住半小时。而且，最好不要让服务器连接到外网，否则安装过程中会自动从网络中获取更新，安装就会很慢。就像本文中的配置，网关和域名服务器都设置为PXE服务器的IP，而不是真实的可以访问外网的网关和域名服务器。
4、查看服务systemctl status dnsmasq
3.3. 配置netboot1、下载网络启动文件，只有43M
wget http://ftp.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/netboot.tar.gz
或者从ubuntu-16.04.4-server-amd64.iso镜像中拷贝，netboot.tar.gz相当于install/netboot目录的压缩文件。
2、解压文件
tar -xzvf netboot.tar.gz -C /tftpboot/
解压后的文件包括：
ldlinux.c32 -&gt; ubuntu-installer/amd64/boot-screens/ldlinux.c32pxelinux.0  -&gt; ubuntu-installer/amd64/pxelinux.0pxelinux.cfg -&gt; ubuntu-installer/amd64/pxelinux.cfg/ubuntu-installer/version.info

3.4. 安装系统1、远程连接需要安装系统的服务器，设置BIOS，选择从网络启动。
2、选择Install，正常安装系统即可，不过，系统镜像需要从外网下载。这里没有连接外网，就不再往下演示。
以上，实现了从网络安装源安装Ubuntu系统。但是存在两个问题，一个问题是系统镜像需要从外网下载，浪费带宽。另一个问题是安装过程中需要不断交互，浪费时间。
4. PXE服务器升级4.1. 配置内网镜像1、安装apache2apt-get install apache2 -y
web根目录是 /var/www/html/
2、测试apache2curl 127.0.0.1
3、重启apache2/etc/init.d/apache2 restart
4、上传ubuntu-16.04.4-server-amd64.iso到用户目录。scp ubuntu-16.04.4-server-amd64.iso test@172.16.0.162:~
5、挂载ubuntu-16.04.4-server-amd64.iso到ubuntu目录。在用户目录执行：
mkdir /var/www/html/ubuntumount ubuntu-16.04.4-server-amd64.iso /var/www/html/ubuntu

6、替换netboot为镜像中的netboot，否则内核版本的差异会导致安装失败。
rm -rf /tftpboot/*cp -r /var/www/html/ubuntu/install/netboot/* /tftpboot/

7、再次安装系统，此时就可以从PXE服务器获取系统镜像了。在选择镜像界面，上拉到enter information manually，回车。archive hostname输入172.16.0.162。directory输入/ubuntu，注意，ubuntu后面不要有斜杠。如果系统安装卡住，那么Alt+F2进入控制台，tail /var/log/syslog查看系统安装日志，根据日志进行排错。查看错误后Alt+F1返回图形安装界面。
8、安装完成的系统，启动后发现卡住。这时Alt+F2切换到第二个终端，就可以正常使用系统了。
4.2. 添加硬盘启动系统安装完成，如果依然从网络启动的话，那么每次启动依然会进入到Installer boot menu界面，除非再次修改BIOS首选启动项。不妨在Installer boot menu界面添加一个启动项，该启动项允许从本地硬盘启动。主要参考Install Ubuntu 16.04 via PXE and network boot。
1、编辑pxelinux.cfg文件vim /tftpboot/pxelinux.cfg/default，修改为：
# D-I config version 2.0# search path for the c32 support libraries (libcom32, libutil etc.)path ubuntu-installer/amd64/boot-screens/include ubuntu-installer/amd64/boot-screens/menu.cfgdefault ubuntu-installer/amd64/boot-screens/vesamenu.c32prompt 0# add localboot herelabel localboot    menu label Boot from Disk    localboot 0timeout 0
此外，timeout默认是0，这里可以改为60，那么6秒后会自动选择install选项。
2、再次通过网络启动，即可看到硬盘启动选项。选中它回车，即可启动本地硬盘上的系统。
4.3. 自动选择安装源以上安装，我们需要手动输入archive hostname和directory，能不能在安装过程中自动选择？当然可以。本方法主要参考PXEInstallServer。
1、编辑txt.cfgvim /tftpboot/ubuntu-installer/amd64/boot-screens/txt.cfg，原内容为：
default installlabel install    menu label ^Install    menu default    kernel ubuntu-installer/amd64/linux    append vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet label cli    menu label ^Command-line install    kernel ubuntu-installer/amd64/linux    append tasks=standard pkgsel/language-pack-patterns= pkgsel/install-language-support=false vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet 
修改为：
default installlabel install        menu label ^Install        menu default        kernel ubuntu-installer/amd64/linux        append ks=http://172.16.0.162/ks.cfg vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet  label cli        menu label ^Command-line install        kernel ubuntu-installer/amd64/linux        append tasks=standard pkgsel/language-pack-patterns= pkgsel/install-language-support=false vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet

2、在/var/www/html目录创建ks.cfg文件，内容为：
installurl --url http://172.16.0.162/ubuntu/

3、重新安装系统，即可省去设置archive hostname的步骤。
4.4. 配置无人值守既然可以自动选择安装源，那么其他交互步骤可不可以自动选择？当然可以。控制安装过程的自动交互主要有两个文件：ks.cfg和ubuntu-server.seed。
4.4.1. ks.cfg1、在安装菜单添加ks.cfg和ubuntu-server.seedvim /tftpboot/ubuntu-installer/amd64/boot-screens/txt.cfg，修改为：
default installlabel install    menu label ^Install    menu default    kernel ubuntu-installer/amd64/linux    append ks=http://172.16.0.162/ks.cfg preseed/url=http://172.16.0.162/ubuntu-server.seed vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quietlabel cli    menu label ^Command-line install    kernel ubuntu-installer/amd64/linux    append tasks=standard pkgsel/language-pack-patterns= pkgsel/install-language-support=false vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet 

2、编辑/var/www/html/ks.cfg文件参考《Ubuntu系统批量自动安装》一文中的ks.cfg。编辑后的ks.cfg文件内容如下：
#Generated by Kickstart Configurator#platform=AMD64 or Intel EM64T#System languagelang en_US#Language modules to installlangsupport en_US#System keyboardkeyboard us#System mousemouse#System timezonetimezone Asia/Shanghai#Root passwordrootpw --disabled#Initial useruser test --fullname &quot;test&quot; --iscrypted --password $1$AKq0i3Yu$Tunuha7bYwq5uUV62F2nF0#Reboot after installationreboot#Use text mode installtext#Install OS instead of upgradeinstall#Use Web installationurl --url http://172.16.0.162/ubuntu/#System bootloader configurationbootloader --location=mbr #Clear the Master Boot Recordzerombr yes#Partition clearing informationclearpart --all --initlabel #System authorization infomationauth  --useshadow  --enablemd5 #Network informationnetwork --bootproto=dhcp --device=ens2f0#Firewall configuration#firewall --disabled --trust=ens2f0 --ssh #Do not configure the X Window Systemskipx
需要注意的是网卡的配置，这里的网卡名指的是需要安装系统的网卡名。
3、在/var/www/html目录创建ubuntu-server.seed文件，内容为空（暂时不配置）。
4、再次安装系统，绝大部分步骤都会自动进行选择了。但是，在分区部分卡住。手动分区后，后续会全部自动安装。安装完成后，系统没有安装openssh服务。
4.4.2. ubuntu-server.seed本文中，ubuntu-server.seed主要用来解决分区、选择软件等ks.cfg没有解决的问题。
1、编辑seed文件vim /var/www/html/ubuntu-server.seed，添加：
d-i pkgsel/include string openssh-serverd-i partman/confirm_write_new_label boolean trued-i partman/choose_partition select Finish partitioning and write changes to diskd-i partman/confirm boolean true
其他配置可以参考/var/www/html/ubuntu/preseed/目录中的文件，不过本文中用不到其他配置了。
2、再次安装系统，此时就会自动分区，并且安装openssh服务了。全程不需要手动帮助设置，完美。
4.5. 配置手动安装个别情况下，自动安装会失败，这时就需要手动安装，在Installer boot menu界面添加一个手动安装启动项即可。
1、编辑pxelinux.cfg文件vim /tftpboot/pxelinux.cfg/default，修改为：
# D-I config version 2.0# search path for the c32 support libraries (libcom32, libutil etc.)path ubuntu-installer/amd64/boot-screens/include ubuntu-installer/amd64/boot-screens/menu.cfgdefault ubuntu-installer/amd64/boot-screens/vesamenu.c32prompt 0# add localboot herelabel localboot    menu label Boot from Disk    localboot 0label manual install    menu label Manual Install    kernel ubuntu-installer/amd64/linux    append ks=http://172.16.0.162/manual.cfg vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiettimeout 0

2、在/var/www/html目录创建manual.cfg文件，内容为：
installurl --url http://172.16.0.162/ubuntu/

3、重新安装系统，Installer boot menu界面就会出现手动安装选项。
5. 批量安装1、远程连接需要安装系统的服务器，设置BIOS，选择从网络启动。
2、安装正常系统，有些机器在网络部分卡住。此时，可以手动给网卡配置IP。但是，如果系统只检测到一个网卡，而这个网卡没有连接网线，那么无法完成接下来的安装。因为没有IP，所以无法通过获取ks.cfg等文件。Alt+F2进入控制台，ip add看到IP设置成功，但是无法ping通PXE服务器。Alt+F1返回图形界面。此时，最好的做法是进入机房，把网线接到可以检测到的那个网卡。然后Ctrl+Alt+Del重启服务器。
3、再次安装系统，即可顺利安装。
6. 配置静态IP系统安装完成后，此时不能上网，需要给系统配置静态IP和网关，这时发现了一个小问题：系统中没有安装vim，自带的vi也有毛病。怎么办？使用nano。
1、编辑interfacessudo nano /etc/network/interfaces
2、保存文件编辑完成，Ctrl+X退出，系统提示是否保存，输入Y回车，然后再次回车保存。
3、重启网络服务
sudo /etc/init.d/networking restart# orsudo ifdown ens2f0sudo ifup ens2f0
重启网络服务后发现，此时网卡上有两个IP地址，而且只有DHCP分配的旧的IP地址生效（第一个）。
4、取消IP地址并重启网络服务
sudo ip addr flush dev ens2f0sudo /etc/init.d/networking restart
此时静态IP就配置成功了。
7. 后记给40台机器配置远程控制接口的IP，搭建PXE服务器，安装系统，配置静态IP，总计用时一周左右。从效率上讲不如外包找人安装，从研究上讲还是有很多收获的。还好最近清闲了一些，不然还真没时间把整个安装流程搞清楚。之所以用时很久，主要是因为有一些机器之前使用过，所以安装的时候需要手动确认remove volume data，或者disk detect报错需要手动安装，如下图所示。
8. 书签Netboot Install
PXE-netboot-install
Ubuntu 16.04 LTS (Xenial Xerus) Netboot
How to install PXE Server on Ubuntu 16.04
神奇的fdisk和dd命令
PXE网络克隆图文教程
DHCP tftp PXE实现Ghost网络克隆
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
        <tag>pxe</tag>
      </tags>
  </entry>
  <entry>
    <title>通过远程控制接口给服务器安装系统</title>
    <url>/dev-install-sys-by-remote-controller-interface/</url>
    <content><![CDATA[1. 前言《制作Ubuntu安装盘方法小结》一文中，总结了U盘和光盘两种安装源的制作方法，也提到了硬盘安装和网络安装。我们已经知道，通过网络安装源可以进行批量安装。而如果使用远程控制接口进行安装，则可以实现远程安装，不用进入机房。本文中，就研究一下远程控制接口的配置方法，并且通过该接口进行系统安装。


2. 远程控制接口配置不同的服务器，有着不同的远程控制接口，具体可以参考书签中的内容。本文中使用的服务器是曙光I620-G20，管理接口名为mgmt。主要参考配置管理口管理曙光服务器。
1、开机点击delete进入BIOS设置，切换到ServerManage。
2、BMC Support设置为Enabled。选中BMC network configuration，进行网络配置。
3、选中Configuration Address source，选择Static。
4、根据网络情况设置静态IP，设置完成后Save Configuration，然后点击F4保存退出。
3. 远程管理3.1. 基本管理1、浏览器访问服务器的管理IP，即可看到曙光的登录页面。
2、填入用户名密码（默认都是admin），进入管理页面。此外，这组用户名密码，也可以ssh登录到服务器。
3、在管理页面有很多选项卡，这里我们点击“远程控制”。
4、点击控制台重定向，Java终端，网页会下载jviewer.jnlp文件。
5、双击jviewer.jnlp，会提示Java更新（这里更新或者不更新都可以），启动报错。点开详细看到报错：
JNLPException[category: 安全错误 : Exception: null : LaunchDesc: &lt;jnlp spec=&quot;1.0+&quot; codebase=&quot;http://172.16.101.162:80/Java&quot;&gt;  ......&lt;/jnlp&gt; ]    at com.sun.javaws.security.JNLPSignedResourcesHelper.checkSignedResourcesHelper(Unknown Source)    at com.sun.javaws.security.JNLPSignedResourcesHelper.checkSignedResources(Unknown Source)    at com.sun.javaws.Launcher.prepareResources(Unknown Source)    at com.sun.javaws.Launcher.prepareAllResources(Unknown Source)    at com.sun.javaws.Launcher.prepareToLaunch(Unknown Source)    at com.sun.javaws.Launcher.prepareToLaunch(Unknown Source)    at com.sun.javaws.Launcher.launch(Unknown Source)    at com.sun.javaws.Main.launchApp(Unknown Source)    at com.sun.javaws.Main.continueInSecureThread(Unknown Source)    at com.sun.javaws.Main.access$000(Unknown Source)    at com.sun.javaws.Main$1.run(Unknown Source)    at java.lang.Thread.run(Unknown Source)

6、打开jviewer.jnlp文件，内容为：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;jnlp spec=&quot;1.0+&quot; codebase=&quot;http://172.16.101.162:80/Java&quot;&gt;     &lt;information&gt;        &lt;title&gt;JViewer&lt;/title&gt;        &lt;vendor&gt;American Megatrends, Inc.&lt;/vendor&gt;        &lt;description kind=&quot;one-line&quot;&gt;JViewer Console Redirection Application&lt;/description&gt;        &lt;description kind=&quot;tooltip&quot;&gt;JViewer Console Redirection Application&lt;/description&gt;        &lt;description kind=&quot;short&quot;&gt;            JViewer enables a user to view the video display of managed server via KVM.              It also enables the user to redirect his local keyboard, mouse for managing the server remotely.        &lt;/description&gt;    &lt;/information&gt;    &lt;security&gt;        &lt;all-permissions/&gt;    &lt;/security&gt;    &lt;resources&gt;        &lt;j2se version=&quot;1.5+&quot;/&gt;        &lt;jar href=&quot;release/JViewer.jar&quot;/&gt;    &lt;/resources&gt;    &lt;resources&gt;        &lt;j2se version=&quot;1.5+&quot;/&gt;        &lt;jar href=&quot;release/JViewer-SOC.jar&quot;/&gt;    &lt;/resources&gt;    &lt;resources os=&quot;Windows&quot; arch=&quot;x86&quot;&gt;        &lt;j2se version=&quot;1.5+&quot;/&gt;        &lt;nativelib href=&quot;release/Win32.jar&quot;/&gt;    &lt;/resources&gt;        &lt;resources os=&quot;Windows&quot; arch=&quot;amd64&quot;&gt;           &lt;j2se version=&quot;1.5+&quot;/&gt;           &lt;nativelib href=&quot;release/Win64.jar&quot;/&gt;    &lt;/resources&gt;    &lt;resources os=&quot;Linux&quot; arch=&quot;x86&quot;&gt;        &lt;j2se version=&quot;1.5+&quot;/&gt;        &lt;nativelib href=&quot;release/Linux_x86_32.jar&quot;/&gt;    &lt;/resources&gt;        &lt;resources os=&quot;Linux&quot; arch=&quot;i386&quot;&gt;        &lt;j2se version=&quot;1.5+&quot;/&gt;        &lt;nativelib href=&quot;release/Linux_x86_32.jar&quot;/&gt;    &lt;/resources&gt;        &lt;resources os=&quot;Linux&quot; arch=&quot;x86_64&quot;&gt;        &lt;j2se version=&quot;1.5+&quot;/&gt;        &lt;nativelib href=&quot;release/Linux_x86_64.jar&quot;/&gt;    &lt;/resources&gt;        &lt;resources os=&quot;Linux&quot; arch=&quot;amd64&quot;&gt;        &lt;j2se version=&quot;1.5+&quot;/&gt;        &lt;nativelib href=&quot;release/Linux_x86_64.jar&quot;/&gt;    &lt;/resources&gt;    &lt;resources os=&quot;Mac OS X&quot; arch=&quot;i386&quot;&gt;    &lt;j2se version=&quot;1.5+&quot;/&gt;    &lt;nativelib href=&quot;release/Mac32.jar&quot;/&gt;    &lt;/resources&gt;     &lt;resources os=&quot;Mac OS X&quot; arch=&quot;x86_64&quot;&gt;    &lt;j2se version=&quot;1.5+&quot;/&gt;    &lt;nativelib href=&quot;release/Mac64.jar&quot;/&gt;    &lt;/resources&gt;     &lt;application-desc&gt;        &lt;argument&gt;-apptype&lt;/argument&gt;&lt;argument&gt;JViewer&lt;/argument&gt;&lt;argument&gt;-hostname&lt;/argument&gt;&lt;argument&gt;172.16.101.162&lt;/argument&gt;&lt;argument&gt;-kvmtoken&lt;/argument&gt;&lt;argument&gt;nOLAdVHdp02q3dSQ&lt;/argument&gt;&lt;argument&gt;-kvmsecure&lt;/argument&gt;&lt;argument&gt;0&lt;/argument&gt;&lt;argument&gt;-kvmport&lt;/argument&gt;&lt;argument&gt;80&lt;/argument&gt;&lt;argument&gt;-vmsecure&lt;/argument&gt;&lt;argument&gt;0&lt;/argument&gt;&lt;argument&gt;-cdstate&lt;/argument&gt;&lt;argument&gt;1&lt;/argument&gt;&lt;argument&gt;-fdstate&lt;/argument&gt;&lt;argument&gt;1&lt;/argument&gt;&lt;argument&gt;-hdstate&lt;/argument&gt;&lt;argument&gt;1&lt;/argument&gt;&lt;argument&gt;-cdnum&lt;/argument&gt;&lt;argument&gt;1&lt;/argument&gt;&lt;argument&gt;-fdnum&lt;/argument&gt;&lt;argument&gt;1&lt;/argument&gt;&lt;argument&gt;-hdnum&lt;/argument&gt;&lt;argument&gt;1&lt;/argument&gt;&lt;argument&gt;-extendedpriv&lt;/argument&gt;&lt;argument&gt;259&lt;/argument&gt;&lt;argument&gt;-localization&lt;/argument&gt;&lt;argument&gt;EN&lt;/argument&gt;&lt;argument&gt;-keyboardlayout&lt;/argument&gt;&lt;argument&gt;AD&lt;/argument&gt;&lt;argument&gt;-websecureport&lt;/argument&gt;&lt;argument&gt;443&lt;/argument&gt;&lt;argument&gt;-singleportenabled&lt;/argument&gt;&lt;argument&gt;1&lt;/argument&gt;&lt;argument&gt;-webcookie&lt;/argument&gt;&lt;argument&gt;q7U2w5QbKeOD77EYPgTQMWyjJzAKblir000&lt;/argument&gt;&lt;argument&gt;-oemfeatures&lt;/argument&gt;&lt;argument&gt;9&lt;/argument&gt;    &lt;/application-desc&gt;&lt;/jnlp&gt;

3.2. 报错解决1、参考java8u121 unable open topcoder arena，添加例外站点。（1）Win+S，搜索“Java Control Panel”或者“配置Java”。（2）点击“安全”选项卡，编辑站点列表。添加：
http://172.16.101.162:80

重新启动jviewer.jnlp，依然报错。
2、参考Java Web start未签名的应用程序请求对系统无限制访问，对JDK进行降级。（1）卸载JDK8，必须得卸载，否则多个版本JDK会有冲突。（2）下载JDK6，并安装。（3）参考《IDEA的常用配置》中的JDK配置，修改JAVA_HOME为新的JDK安装目录。
重新启动jviewer.jnlp，果然启动成功。（如果找不到打开的软件，可以手动选择javaws.exe）需要注意的是，要先在浏览器中打开控制台重定向的页面，再启动jviewer.jnlp。否则会报错invalid web session，而且只能看到一个黑色的控制台，没有任何内容。而且，jviewer.jnlp文件有使用期限，每次远程控制都需要重新下载，因为该文件中的kvmtoken是会发生变化的。
3.3. 安装系统1、在Windows系统中创建share目录，并且设置共享。然后把ubuntu-16.04.4-server-amd64.iso镜像拷贝到share目录。
2、在控制台的工具栏上，点击CD/DVD Media，打开选择Virtual Media对话框。
3、选择镜像，然后点击“Connect CD/DVD”，连接成功后close即可。
4、重启服务器，按Del进入BIOS设置，选择从虚拟光驱引导。（如果之前没有安装过系统则不需要此步骤）
5、然后，服务器就进入了正常安装步骤。
4. 后记由本文中的实践可以看出，使用远程控制接口可以帮助机房管理员进行主机的管理，包括系统的安装，非常方便。那么，网络安装源结合远程控制接口，是不是可以实现批量远程安装系统？理论上是完全可行的。
5. 书签华为服务器远程安装系统
DELL iDRAC服务器远程控制设置
BMC ipmitool 对linux服务器进行IPMI管理
曙光天阔服务器远程控制手册
曙光IPMI系统管理平台用户使用指南
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>ubuntu</tag>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>制作Ubuntu安装源方法小结</title>
    <url>/dev-create-ubuntu-installer/</url>
    <content><![CDATA[1. 前言《安装系统之不同平台》和《Ubuntu 14.04.5 Server物理机安装》两篇文章中，都提过Linux安装源的制作方法，但是没有详细的说明。
Linux系统安装源可以分为U盘、光盘、硬盘、网络四个大类，其中最常用的是U盘和光盘，本文会针对这两种安装源进行总结。硬盘安装源的制作方法参考《安装系统之三种方式》和《安装系统之双系统》，网络安装源的制作方法参考《Ubuntu系统批量自动安装》。


2. 制作工具可选的制作工具主要有：

Universal USB Installer，一个专门制作各种Linux的USB系统启动盘的工具。
Win32DiskImager，用于制作Ubuntu的USB系统启动盘，使用教程参考Win32DiskImager makes an Ubuntu family USB boot drive in Windows。
UltraISO，一款强大的光盘映像文件制作、编辑和转换工具。

3. U盘制作3.1. USB Installer1、下载Universal USB Installer，不需要安装就可以启动。
2、选择镜像类型，选择镜像，选择U盘，create即可。不过，正如《Ubuntu 14.04.5 Server物理机安装》一文中所说，usb installer制作的安装源会有些问题，还容易损坏U盘，这里不推荐。
3.2. Win32DiskImager1、下载Win32DiskImager，安装后启动。
2、选择镜像，选择设备，写入即可。写入完成后，U盘空间变小了，变成了2.18MB。而且U盘里只有一个文件夹：efi。不过不用担心，使用这个U盘确实可以安装Ubuntu系统。这里也不推荐，因为U盘现在只能用于安装系统，无法再存储其他文件，除非重新格式化。
3.3. UltraISO1、右键ubuntu-16.04.4-server-amd64.iso镜像，装载。这一步是必须的，因为如果直接使用UltraISO“打开”镜像，只能看到EFI文件夹。只有装载后的镜像，使用UltraISO“打开光盘”，才可以看到全部文件。需要注意的是，只有win8之后的系统才支持装载，更早的系统可以使用虚拟光驱进行镜像挂载。
2、下载UltraISO，安装后右键以管理员身份启动。
3、打开光盘，选择装载的镜像。
4、菜单栏点击启动，写入硬盘映像。
5、在写入硬盘映像对话框中，硬盘驱动器选择U盘，写入方式选择默认的USB-HDD+。
6、（可选操作）依次点击便捷启动，写入新的驱动器引导扇区，Syslinux。
7、点击“写入”，弹出警告U盘中的数据会丢失，确认后等待几分钟即可制作完成。
制作完成后，U盘还可以继续作为移动存储设备使用，这种方法是最推荐的一种方法。
4. 光盘制作1、下载UltraISO，安装后右键以管理员身份启动。
2、光驱中放入空白光盘，容量要大于镜像大小。
3、菜单栏点击工具，刻录光盘映像。
4、映像文件选择ubuntu-16.04.4-server-amd64.iso，然后点击“刻录”，等待几分钟即可制作完成。
5. 后记以上，就是U盘安装源和光盘安装源的制作方法。这两种安装源的使用方法都很常规，启动主机的时候进入BIOS选择启动项，然后正常安装即可。安装过程中会有很多交互设置，个人觉得很麻烦，没有GHOST安装方便。
因此，U盘和光盘无人值守自动安装也是一个很好的研究点，有时间了可以搞一下。主要思路是修改isolinux/txt.cfg文件，参考ubuntu-server.seed的引入方式，创建并引入ks.cfg文件。ks.cfg文件的配置参考ks.cfg参数详解，ubuntu-server.seed的配置参考《Ubuntu系统批量自动安装》。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>ubuntu</tag>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：虚拟机在线迁移实验V2——下</title>
    <url>/dev-openstack-vm-live-migration-experiment-v2-1/</url>
    <content><![CDATA[1. 前言毕业论文送审的日期是3月22日，cloud2019的deadline也是3月22日。大概从2月27日开始写毕业论文，早八点半晚十点，周末不休，3月22日终于写完送审了，算是完成了一件大事。之后，还有两件大事：一个是实现阅卷系统，一个是发表小论文。
本来打算3月20日抽出一天时间做实验写小论文，没想到实验环境崩了！无奈只能放弃cloud。论文送审后一直在忙着修复实验环境，2月24日晚上十点左右，终于修复成功！是时候完成小论文了！


2. 前情回顾《虚拟机在线迁移实验V2——上》一文中，实验遇到瓶颈，所以不得不重新设计实验。经过思考，打算把web服务换成mysql服务，在实例中进行测试。但是，实验表明，如果在迁移过程中对mysql试压，那么会出现和web服务同样的问题，迁移无法成功。所以，本文中去掉了服务降级这个指标，只测量停机时间、迁移时间和迁移数据量。
3. 实例准备1、在horizon控制台，项目，计算，镜像。使用xenial-server-cloudimg-amd64-disk1.img镜像创建ubuntu16的镜像模板。
2、创建实例类型m1.tiny2，CPU个数为1，内存为512M，磁盘为5GB。
3、使用ubuntu16的镜像模板创建实例ubuntu0、ubuntu1、ubuntu2、ubuntu3、ubuntu4，实例类型分别选择m1.tiny2、m1.small、m1.medium、m1.large、m1.xlarge。之所以不使用默认的m1.tiny，是因为m1.tiny的磁盘太小，只有1G，无法安装ubuntu16。

4、创建成功后，分别分配浮动IP。
PS：在进行迁移实验时，关闭其他实例，防止资源占用产生干扰。
4. 脚本和软件准备4.1. 总迁移时间在控制节点，新建mtime.sh脚本：
#!/bin/bashtime1=`grep &#x27;live-migration&#x27; list.log | head -n 1 | awk &#x27;&#123;print $24&#125;&#x27;`time2=`grep &#x27;live-migration&#x27; list.log | head -n 1 | awk &#x27;&#123;print $26&#125;&#x27;`time=$(($(date +%s -d $time2) - $(date +%s -d $time1)));echo &quot;$time s&quot;;

新建allmtime.sh脚本，可以计算所有迁移时间：
#!/bin/bashtime1=`grep &#x27;live-migration&#x27; list.log | awk &#x27;&#123;print $24&#125;&#x27;`time2=`grep &#x27;live-migration&#x27; list.log | awk &#x27;&#123;print $26&#125;&#x27;`IPS=&#x27; &#x27;array1=($time1)array2=($time2)length=$&#123;#array1[@]&#125;COUNTER=0while [ $COUNTER -lt $length ]do    COUNTER=`expr $COUNTER + 1`    t1=$&#123;array1[COUNTER]&#125;    t2=$&#123;array2[COUNTER]&#125;    time=$(($(date +%s -d $t2) - $(date +%s -d $t1)));    echo &quot;$time s&quot;;done

4.2. 停机时间在控制节点，新建downtime.sh脚本：
#!/bin/bashtransmitted=`grep &#x27;packets transmitted&#x27; ping.log | tail -n 1 | awk &#x27;&#123;print $1&#125;&#x27;`received=`grep &#x27;packets transmitted&#x27; ping.log | tail -n 1 | awk &#x27;&#123;print $4&#125;&#x27;`lost=`expr $transmitted - $received`echo &quot;downtime is $lost ms&quot;

4.3. 迁移数据量在两个计算节点，安装iptraf-ng。apt install iptraf-ng
4.4. xshell使用xshell，打开两个controller节点的shell，一个登录实例B后用来ping实例A；一个用来执行迁移命令，迁移之后执行mtime.sh脚本。打开一个compute1节点的shell，用来运行iptraf-ng。打开一个compute2节点的shell，用来运行iptraf-ng。
5. 实验流程假设当前只启动了ubuntu0实例，nova show ubuntu0位于compute1节点。
1、在两个compute节点启动iptraf-ng，配置参考《虚拟机在线迁移的性能统计》。
2、在实例中启动压力测试软件，或者在两个compute节点中启动网络故障模拟软件。（flavor实验中不需要）
3、在实例B中ping实例ubuntu0sudo ping ubuntu0_internal_ip -i 0.001 &gt;&gt; ping.log
4、在controller中执行迁移nova live-migration ubuntu0 compute2
5、观察compute节点的iptraf-ng，等到流量不再增加。关闭压力测试软件或者网络故障模拟软件，然后在实例B中Ctrl+C结束ping命令。
6、在controller节点记录迁移列表nova migration-list &gt; list.log
7、controller节点执行mtime.sh得到迁移时间，在实例B中执行downtime.sh得到停机时间，在两个计算节点观察iptraf-ng得到迁移数据量。
以上流程，适用于接下来的所有实验。
6. flavor实验按照实验流程，分别使用五个flavor，进行五次迁移实验，记录实验结果。
#Flavor    迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MBtiny    24  297   275  277small   26  14    329  332medium  27  69    373  375large   30  50    846  852xlarge  31  57    923  929  

由实验结果可以看出，随着CPU和内存的增加，迁移时间也会随之增加。tiny模板的停机时间最大，其他模板的停机时间在10ms到70ms之间。源主机的迁出数据量和目标主机的迁入数据量也会随之增加，而且迁入数据量要比迁出数据量多2到6MB。
6.1. 系统压力实验6.2. CPU实验接下来的实验，都使用small类型的flavor。1、先给实例安装stress-ngsudo apt install stress-ng
2、在实例中创建一个CPU进程，CPU占用率10%stress-ng -c 1 -l 10
3、创建CPU进程，CPU占用率20%-100%，记录实验结果。
# CPU占用率%   迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MBIdle    26  14    329  33210%     27  15    330  33320%     26  18    331  33330%     24  403   330  33340%     25  3332  330  33450%     25  3175  330  33260%     24  505   327  32970%     26  10372 328  33180%     25  292   329  33190%     26  124   330  333100%    26  573   328  330
由实验结果可以看出，随着CPU占用率的提高，迁移时间没有太大变化。停机时间在CPU使用率大于30%之后，停机时间普遍大于100ms，偶尔会出现停机时间大于3s，没有明显的规律。迁移数据量并没有太大变化，迁入数据一直比迁出数据多2到4MB。
在迁移时，偶尔会出现OpenStack显示实例迁移成功，但是实例停机时间特别长（大于600s）的情况。本文中认为这种情况属于异常，会重做实验，去除这些异常值。
6.3. 内存实验1、在实例中创建一个200M的内存进程stress-ng --vm 1 --vm-bytes 200M --vm-keep
2、创建内存进程，内存占用400M-1600M，记录实验结果。
# 内存占用MB   迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MBIdle    26      14      329     332200     79      3090    6543    6585400     199     144     21344   21481 600     308     440     33368   33585800     422     560     47712   480191000    541     23560   61417   61814    1200    649     250080  74617   751031400    925     27230   107004  1077021600    1049    3140    121600  122390
从实验结果中可以看出，随着内存使用量的增加，总迁移时间急剧增加。停机时间一般大于100ms，有时甚至超过20s。迁移数据量也急剧增加，数量级变为GB。当内存占用1600MB时，总迁移时间超过1000s，迁移数据大于120GB，传输到目标机器的数据与从源机器传输的数据之间的差异为700MB。
6.4. 磁盘实验1、在实例中创建2个写入进程，每个进程写入大小为1M的文件。stress-ng -d 2 --hdd-bytes 1M
2、在实例中创建4-20个写入进程，记录实验结果。
# 写入进程数   迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MB0       26      14      329     3322       42      510     2178    21924       50      100     2954    29746       127     3350    12456   125378       137     490     13519   1360810      172     4570    18179   18296 12      182     6340    18948   19072     14      172     6860    18034   1815016      189     410     19569   1969618      179     600     18790   1891020      246     3160    26687   26859           
从实验结果中可以看出，随着写入进程的增加，迁移时间也在不断增加。停机时间普遍大于400ms，频繁出现大于3s停机时间。迁移数据量呈现阶梯式变化，数量级变成GB。当写入进程数为20时，迁移时间约为250s，迁移数据约为27GB。
7. 网络故障实验网络故障实验之前，需要重启测试实例，否则测出的迁移数据量会偏大一个数量级。
7.1. 网络延迟1、在compute节点（迁出节点）添加网络延迟20mssudo tc qdisc add dev eth1 root netem delay 20ms注：原本的延迟在0.2ms上下。
2、将网络延迟修改为40ms-200ms，记录实验结果。sudo tc qdisc replace dev eth1 root netem delay 40ms
# 网络延迟ms   迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MB0       26      14      329     33220      27      6720    354     35640      29      320     354     35660      32      3200    355     35780      39      160     356     358100     39      3370    360     362120     43      320     357     358140     45      310     358     360160     51      580     356     357180     51      5480    356     357  200     59      620     380     382

由实验结果可以看出，随着网络延迟时间的增加，迁移时间也会随之增加。停机时间普遍大于300ms，经常出现3s到7s的停机时间。迁出数据量和迁入数据量没有明显的变化，差值也保持在稳定的范围。
此外，在网络延迟实验中，实例死机（停机时间大于600s）的频率要高于CPU、内存和磁盘实验。
3、取消模拟：sudo tc qdisc del dev eth1 root
7.2. 丢包1、两个计算节点，都模拟丢包率1%sudo tc qdisc add dev eth1 root netem loss 1%
2、将模拟丢包率修改为1%-10%，记录实验结果。sudo tc qdisc change dev eth1 root netem loss 2%
# 丢包率   迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MB0%      26      14      329     3321%      24      1460    354     3562%      26      4190    355     3563%      30      3990    358     3594%      38      2640    357     3575%      43      7610    359     3606%      67      5960    361     3617%      131     43890   363     3638%      170     20850   368     3689%      283     11430   371     37110%     392     67520   365     365     

从实验结果中可以看出，随着丢包率的增大，迁移时间也会明显增加。丢包率大于7%之后，停机时间普遍大于10s，甚至出现了68s的停机时间。迁移数据量变化不大，迁入迁出数据量的差值稳定在1MB以内。
3、取消模拟：sudo tc qdisc del dev eth1 root
7.3. 包重复1、两个计算节点，都模拟包重复率10%sudo tc qdisc add dev eth1 root netem duplicate 10%随机产生 10% 重复的包。
2、将模拟包重复率修改为20%-100%，记录实验结果sudo tc qdisc change dev eth1 root netem duplicate 20%
# 包重复率   迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MB0%      26      14      329     33210%     32      180     652     65620%     28      180     712     71730%     27      120     769     77440%     28      300     840     84550%     29      3060    887     89360%     29      140     959     96570%     28      540     1016    102380%     32      440     1073    108090%     35      3490    1135    1142100%    32      90      1196    1204     
从实验结果可以看出，随着包重复率的增加，总的迁移时间变化不大。停机时间普遍大于100ms，偶尔出现大于3s的情况，没有明显的变化趋势。迁移数据量随着包重复率的增加而增大，迁入数据量和迁出数据量的差值在4M到8M。
3、取消模拟：sudo tc qdisc del dev eth1 root
7.4. 包损坏1、两个计算节点，都模拟包损坏率1%sudo tc qdisc add dev eth1 root netem corrupt 1%随机产生 1% 损坏的报文（在报文的随机位置造成一个比特的错误）。
在ping的时候会报错：Warning: time of day goes back , taking countermeasures，不过没关系，不影响我们的停机时间统计结果。
2、将模拟包损坏率修改为2%-5%，记录实验结果。sudo tc qdisc change dev eth1 root netem corrupt 2%
# 包损坏率   迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MB0%      26      14      329     3321%      26      660     333     334 2%      27      1120    334     3353%      35      4160    337     3374%      149     2760    342     342 5%      254     15040   352     353 6%      1228    117760  356     356
从实验数据中可以看出，包损坏率大于3%之后，迁移时间会急剧增加。停机时间普遍大于1s，总体呈上升趋势。停机时间在包损坏率等于5%和6%时发生突变。迁移数据量没有明显变化。当包损坏率等于6%时，迁移时间超过20min，停机时间接近120s。
而且，在本文中的实验环境中，包损坏率大于等于7%时，迁移会失败。迁移进行了1600s还没有完成，OpenStack会自动撤销迁移。还有一个有趣的现象是，正常迁移中只要使用49152端口就可以了，在出现包损坏的情况下，会使用49153-49216端口来辅助传输数据。
3、取消模拟：sudo tc qdisc del dev eth1 root
PS：如果迁移无法成功，那么就取消包损坏的模拟，或者撤销迁移。
nova migration-listnova server-migration-show 29a58cf7-646a-43f2-b1d6-89ec421fc9d6 152nova live-migration-abort 29a58cf7-646a-43f2-b1d6-89ec421fc9d6 152

8. 后记从以上结果可以看出，在OpenStack中选择VM进行迁移的时候，为了提高迁移的效率，有以下几个结论：（1）选择更小的flavor，能够减少迁移时间和迁移数据量。（2）基本不用考虑CPU使用率，因为它对迁移影响很小。（3）选择内存占用小的VM，能够减少迁移时间、停机时间和迁移数据量。（4）选择磁盘写入进程较少的VM，能够减少迁移时间、停机时间和迁移数据量。（5）包延迟会稍微增加迁移时间，对停机时间和迁移数据量的影响很小。（6）丢包会急剧增加迁移时间，对停机时间和迁移数据量的影响很小。（7）包重复会增加一些迁移数据量，但是对迁移时间和迁移停机时间的影响很小。（8）包损坏率小于等于3%时，对迁移影响较少，可以忽略。包损坏率大于4%之后，迁移时间和停机时间会急剧上升。包损坏率大于7%时，已经无法迁移成功。
至此，实验完成，数据有了，就差论文了！加油加油！
]]></content>
      <categories>
        <category>engineering</category>
        <category>testing</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>mysql</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>测试</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：OpenStack计算节点的奇葩问题</title>
    <url>/dev-openstack-compute-node-broken/</url>
    <content><![CDATA[1. 问题描述心态爆炸了！火急火燎！一方面马上要交毕业论文，另一方面马上要提交小论文。就当我打算抽出一天先完成小论文时，实验环境崩了！
之前进行迁移实验都好好的，但是今天进行迁移实验的时候，实例经常死机。本着“重启治百病”的思想，重启了两个计算节点，重新挂载了nfs。然后再次迁移时，实例可以从compute2迁移到compute1。但是，实例从compute1迁移到compute2时，compute2就报错！
ERROR oslo_messaging.rpc.server [req-922596a7-f122-45c8-9f91-1446bb855e05 2c04ede78270421da71953f8f07ef115 8de01ef30eed437193725fb759b9992d - default default] Exception during message handling: InvalidCPUInfo: Unacceptable CPU info: CPU doesn&#x27;t have compatibility.
重新部署了compute2计算节点，依然是这个错误。重装了compute2计算节点的操作系统，然后重新部署，依然是这个错误！很绝望！


2. 问题分析重装系统无效，那么大概率是控制节点的锅了！那么问题具体出在哪里？凭什么compute1就好好的，compute2就坏了？是因为控制节点的一些残留信息没有清空吗？数据库中记录说compute2已经没有可用CPU了？数据库中记录说不兼容？还是有缓存或者消息？那么该怎么清空关于compute2的信息？。。。还有一个关键的问题是，为什么突然就无法兼容了？之前不是好好的么？然而，没有人可以告诉我答案，搜索引擎不行，社区群聊不行，身边也没有OpenStack的大牛。现在只想赶紧从OpenStack这个大坑中爬出来，遇到问题根本找不到答案，就问你服不服！
3. 垂死挣扎3.1. 查看数据库1、查看数据库密码less /etc/kolla/passwords.yml | grep password里面有很多密码，我们需要的是nova_database_password。
2、进入mariadb容器docker exec -it mariadb /bin/bash
3、登录mariadb数据库mysql -u nova -p
4、使用nova数据库use nova;
5、查看计算节点的信息select * from compute_nodes\G
3.2. 重命名计算节点如果，给compute2节点更换hostname和IP呢？比如hostname换成compute3，IP也换成其他的。这样的话，计算节点就不会受控制节点中残留信息的影响了。
1、修改compute2的hostname和IP地址
2、修改multinode文件控制节点中，把multinode文件中的compute2节点换成compute3，/etc/hosts中添加compute3对应的IP。
3、重新部署kolla-ansible -i ./multinode deploy
4、查看计算服务
. admin-openrc.shopenstack compute service list

3.3. 设置迁移和挂载参考《OpenStack中虚拟机的在线迁移》，在compute3节点设置OpenStack允许迁移。参考《OpenStack中共享存储的虚拟机在线迁移》，在compute3节点挂载nfs。
1、修改nova.conf的配置vim /etc/kolla/nova-compute/nova.conf，在default中添加：
live_migration_flag=VIR_MIGRATE_UNDEFINE_SOURCE,VIR_MIGRATE_PEER2PEER,VIR_MIGRATE_LIVE

2、修改libvirtd.confvim /etc/kolla/nova-libvirt/libvirtd.conf，修改配置如下：
listen_tcp = 1listen_tls = 0auth_tcp = &quot;none&quot;ca_file = &quot;&quot;log_level = 3log_outputs = &quot;3:file:/var/log/kolla/libvirt/libvirtd.log&quot;listen_addr = &quot;0.0.0.0&quot;

3、挂载nfs
apt-get install nfs-commonmount -t nfs 192.168.56.130:/nfs/share/instances /var/lib/docker/volumes/nova_compute/_data/instances/chmod -R 777 /var/lib/docker/volumes/nova_compute/_data/instances/

4、重启nova_compute和nova_libvirt
docker stop nova_computedocker start nova_computedocker stop nova_libvirtdocker start nova_libvirt

3.4. 再次尝试迁移1、迁移命令nova live-migration demo0 compute3
2、查看状态nova show demo0
3、compute3上查看日志tail -n 20 /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log
虽然迁移失败，但是好歹是不一样的错误了。。。这个错误以前遇到过，重启计算节点就好了。于是重启compute3节点，但是问题没有解决。
4、测试与控制节点的连接curl http://192.168.56.120:35357，结果为：
curl: (7) Failed to connect to 192.168.56.120 port 35357: Connection refused

5、在compute1节点上测试与控制节点的连接curl http://192.168.56.120:35357，结果为：
&#123;&quot;versions&quot;: &#123;&quot;values&quot;: [&#123;&quot;status&quot;: &quot;stable&quot;, &quot;updated&quot;: &quot;2018-02-28T00:00:00Z&quot;, &quot;media-types&quot;: [&#123;&quot;base&quot;: &quot;application/json&quot;, &quot;type&quot;: &quot;application/vnd.openstack.identity-v3+json&quot;&#125;], &quot;id&quot;: &quot;v3.10&quot;, &quot;links&quot;: [&#123;&quot;href&quot;: &quot;http://192.168.56.120:35357/v3/&quot;, &quot;rel&quot;: &quot;self&quot;&#125;]&#125;, &#123;&quot;status&quot;: &quot;deprecated&quot;, &quot;updated&quot;: &quot;2016-08-04T00:00:00Z&quot;, &quot;media-types&quot;: [&#123;&quot;base&quot;: &quot;application/json&quot;, &quot;type&quot;: &quot;application/vnd.openstack.identity-v2.0+json&quot;&#125;], &quot;id&quot;: &quot;v2.0&quot;, &quot;links&quot;: [&#123;&quot;href&quot;: &quot;http://192.168.56.120:35357/v2.0/&quot;, &quot;rel&quot;: &quot;self&quot;&#125;, &#123;&quot;href&quot;: &quot;https://docs.openstack.org/&quot;, &quot;type&quot;: &quot;text/html&quot;, &quot;rel&quot;: &quot;describedby&quot;&#125;]&#125;]&#125;&#125;
这才是正常的啊！
6、重启compute3并且重新挂载nfs
mount -t nfs 192.168.56.130:/nfs/share/instances /var/lib/docker/volumes/nova_compute/_data/instances/docker stop nova_computedocker start nova_computedocker stop nova_libvirtdocker start nova_libvirt

7、再次执行迁移nova live-migration demo0 compute3依然失败，还是同样的错误。
3.5. IP的坑百度谷歌想要找到答案？不存在的。折腾了两天，突然灵光一现！还记得《Kolla安装OpenStack多节点》一文中，控制节点的IP为192.168.56.110。而配置globals.yml时候，kolla_internal_vip_address的值为192.168.56.120。
因此，compute3节点中，nova.conf中的IP有一部分是192.168.56.110，另外一部分是192.168.56.120。
明明所有控制节点的服务都在一个节点上，却有两个IP，看起来就容易闹幺蛾子。实际上，确实闹过幺蛾子！OpenStack刚刚安装成功时，使用192.168.56.120这个IP可以访问到horizon服务，使用192.168.56.110就不可以。后来突然有一天，192.168.56.120这个IP就不能访问horizon了，使用192.168.56.110却可以访问！再后来突然有一天，两个IP都可以访问horizon服务！真的是醉了。。。
这次，会不会也是IP的问题？于是，在compute3上，换一个IP测试访问控制节点。curl http://192.168.56.110:35357，果然可以正常访问！于是，思路就清晰了。
1、修改compute3节点中的nova.conf配置文件vim /etc/kolla/nova-compute/nova.conf，所有的192.168.56.120全部换成192.168.56.110。
2、重启nova_compute
docker stop nova_computedokcer start nova_compute

3、再次迁移，失败，不过报错变了。
依然是IP问题，看来还有其他配置文件中写了192.168.56.120。
4、修改neutron.conf和chrony.conf文件
vim /etc/kolla/neutron-openvswitch-agent/neutron.confvim /etc/kolla/chrony/chrony.conf
所有120替换为110。
5、重启neutron和chrony
docker stop neutron_openvswitch_agentdocker start neutron_openvswitch_agentdocker stop chronydocker start chrony

6、再次迁移，依然失败，报错依然失8780端口。
7、重启所有服务，再次迁移，依然失败。莫非还有什么文件没有注意到？于是放了一个大招，grep -rn &quot;192.168.56.120:8780&quot; *，然而，没有找到包含该配置的文件。
8、重启了compute3，然后，nova_compute无法启动了！启动报错：
/var/lib/docker/volumes/kolla_logs/_data: no such file or directory/var/lib/docker/volumes/libvirtd/_data: no such file or directory/var/lib/docker/volumes/nova_compute/_data: no such file or directory
创建了三个文件夹，依然无法启动nova_compute。
4. 破釜沉舟实在是没办法了，只能使出最后一招：重装整个OpenStack系统！
4.1. 销毁OpenStack环境1、查看镜像glance image-list
或者进入镜像文件夹查看cd /var/lib/docker/volumes/glance/_data/images
2、导出镜像
glance image-download --file ./ubuntu16-env.img 003ae986-4dd9-45a6-9630-1b80fc8dcab3ll -h

3、在horizon中删除所有镜像
4、在horizon中关闭并删除所有实例
5、编辑multinode文件，取消所有节点的注释
6、销毁整个OpenStack环境kolla-ansible destroy -i ./multinode --yes-i-really-really-mean-it
4.2. 安装OpenStack参考《Kolla安装OpenStack多节点》进行OpenStack的安装。
考虑到两个IP的坑，这次安装我仔细阅读了一下globals.yml文件中关于kolla_internal_vip_address的描述：

This should be a VIP, an unused IP on your network that will float betweenthe hosts running keepalived for high-availability. If you want to run anAll-In-One without haproxy and keepalived, you can set enable_haproxy to noin “OpenStack options” section, and set this value to the IP of your‘network_interface’ as set in the Networking section below.

这应该是VIP，网络上未使用的IP将在运行keepalived以获得高可用性的主机之间浮动。如果要在没有haproxy和keepalived的情况下运行多功能一体机，可以在“OpenStack选项”部分中将enable_haproxy设置为no，并将此值设置为“network_interface”的IP，如下面的“网络”部分所述。
看来，kolla_internal_vip_address不应该设置为192.168.56.120，因为我们的环境中不需要haproxy。所以，在配置配置globals.yml的时候，kolla_internal_vip_address应该设置为和控制节点相同的IP：192.168.56.110，同时enable_haproxy设置为no。
安装完成后，导入之前备份的镜像：
. admin-openrc.shglance image-create \--name &quot;ubuntu16-env&quot; \--file ./ubuntu16-env.img  \--disk-format qcow2 \--container-format bare \--visibility public \--progress

4.3. 设置迁移和挂载参考本文中垂死挣扎部分设置迁移和挂载。然后，创建实例，分配浮动IP，测试迁移，成功！nice！
5. 罪魁祸首再次进行迁移，同时启动间隔1ms的ping命令测试停机时间，然后实例迁移后死机了！Ctrl+C关闭ping命令，实例复活。由此猜想是ping命令的锅。
但是，这是一个单向问题！同样是使用间隔1ms的ping命令测试停机时间，实例从compute1迁移到compute2时正常，实例从compute2迁移到compute1时才会死机。就问你神奇不神奇？！
猜测是因为ping的频率太高，给网络或者实例带来了太大的负荷。ping命令降低到10ms一次，双向迁移都正常。
但是，实例从compute1到compute2时，就不会受到ping命令的影响，这次是为什么？两台机器的配置完全相同的啊！诡异，实在是诡异！
更诡异的是，每次迁移的数据居然都通过不同的网卡，也许这次通过eth0（管理网络），下次就通过eth2（外网网络）！还能不能愉快地玩耍了？！
细细回想整个安装流程，突然想到，在安装前设置了三个节点的hosts文件，三个节点通过eth0通信。但是ansible安装OpenStack后，会根据globals.yml中的network_interface（eth1）修改hosts文件，于是hosts中有了两套规则。默认使用hosts文件中靠前的规则，于是我自己设置的规则就把ansible设置的规则覆盖了。eth0既用于节点间通信，又用于实例迁移。由于eth2和eth0属于同一个网段，所以有时会通过eth2网卡进行迁移！
为了验证自己的猜想，修改了三个节点的hosts文件，保留ansible创建的那一份。这样，就指定了迁移时使用的网卡。修改后在控制节点和计算节点重启nova相关服务。
# 计算节点docker stop nova_computedocker start nova_computedocker stop nova_libvirtdocker start nova_libvirt# 控制节点docker stop nova_apidocker start nova_apidocker stop nova_schedulerdocker start nova_schedulerdocker stop placement_apidocker start placement_api

再次迁移实验，迁移数据果然会通过eth1（OpenStack内网网络），而不是eth0或者eth2！而且，双向迁移加1ms的ping命令都可以成功迁移，实例不再死机，问题完美解决！真相大白，罪魁祸首居然是hosts文件！
此外，live_migration_inbound_addr参数也可以指定迁移网络。
6. 风波再起就当我自以为找到了真相，继续实验时，实例再次出现了实例死机的情况。双向迁移都会偶尔出现死机，而且，使用间隔1ms的ping命令时死机概率会更大。如果使用间隔10ms的ping命令来测试停机时间呢？测出来的停机时间是秒级！Are you kidding me ?比如同一个迁移，使用两个ping命令进行测试：
sudo ping instance_float_ip -i 0.001 &gt;&gt; ping.log根据丢包数计算出的downtime为434mssudo ping instance_float_ip -i 0.01 &gt;&gt; ping.log根据丢包数计算出的downtime为3340ms

理论上，应该测出同样的结果才对，然而他们差了一个数量级！诡异，实在是诡异！哪个才是真正的downtime？无论哪个是真正的downtime，ping命令本身绝对存在重大缺陷！
如果downtime是毫秒级，那么就符合理论上的值，实验可以正常进行。但是，使用间隔1ms的ping命令测试停机时间容易实例死机啊大哥！
如果downtime是秒级，那么问题要么是出在OpenStack配置上面，要么是出在网络上面。假设问题出在OpenStack的配置上面，于是参考Configure live migrations，先后设置了最大停机时间，设置了自动收敛，设置了后拷贝。但是，问题依旧，那么应该不是OpenStack的配置问题。假设问题出在网络上面，那么怎么解决呢？给实例的外网（eth3）设置一个单独的网段，不要和管理网络（eth0）在同一个网段？太麻烦。如果，在另外一个实例中对迁移实例进行ping测试呢？这样至少也可以避免了浮动IP的切换问题，是一个好主意。
于是在实例B中运行间隔1ms的ping命令，通过实例间的内网测试实例A，实例A多次迁移，测出的downtime在几十毫秒到几百毫秒不等，而且不会死机。在实例B中使用间隔10ms的ping命令进行测试，测出的downtime在几百毫秒左右，也很合理。所以，不是ping命令的锅？
不管了不管了，总之找到了解决办法！在另外一个实例中对迁移实例使用ping命令通过实例间的内网测试停机时间，而不是在外部机器中使用ping命令通过浮动IP测试停机时间。命令如下：
sudo ping instance_internal_ip -i 0.001 &gt;&gt; ping.log

7. 后记因为实例迁移会死机，所以开始了折腾。折腾了一大圈，重启所有节点，重装计算节点，重装系统，重装OpenStack，最后发现实例迁移依然会死机！这就排除了硬件问题，也排除了软件问题。好在最后灵光一现猜测是hosts的问题，不然这个坑是怎么都爬不出不来了。。。只是没想到刚爬出一个坑，又跌进了一个坑，修改了hosts不死机，只是刚好那几次实验都没死机！在使用了间隔1ms的ping命令之后，死机依然频繁。再次折腾OpenStack配置，失败。再次从网络方面考虑，终于找到了解决方案，太不容易了！
今天（3月24日）终于修复了实验环境，但是，已经错过了cloud2019的deadline。有些遗憾，不过和毕业论文相比，只能先牺牲小论文了。近期还有会议，加把劲，做实验，写论文，投稿！
8. 书签openstack中彻底删除计算节点的操作记录
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>mysql</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>ansible</tag>
        <tag>kolla</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>sysbench的基本用法和结果绘图</title>
    <url>/dev-sysbench-usage-and-plot/</url>
    <content><![CDATA[1. sysbench简介sysbench是一个基于LuaJIT的可编写脚本的多线程基准测试工具。它最常用于数据库基准测试，但也可用于创建不涉及数据库服务器的任意复杂工作负载。
本文来研究一下sysbench的安装使用方法，以及测试结果的绘图方法。


2. 安装部署参考Sysbench环境搭建和sysbench项目，进行sysbench的安装部署。
2.1. 安装mysql1、安装mysql数据库。sudo apt-get install mysql-server mysql-client
接下来三步是可选操作。
2、sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf，修改绑定IP
bind-address            = 0.0.0.0

3、重启mysqlsudo service mysql restart
4、测试登录mysql -u root -h &lt;hostip&gt; -p
2.2. 安装sysbench1、安装sysbench
curl -s https://packagecloud.io/install/repositories/akopytov/sysbench/script.deb.sh | sudo bashsudo apt -y install sysbench
务必先下载脚本并执行，否则直接apt install安装的sysbench版本太低，不支持report-interval参数。
2、mysql -u root -p，创建数据库
create database sbtest;

3、验证安装sysbench --version
3. sysbench指令Usage:  sysbench [options]... [testname] [command]Commands implemented by most tests: prepare run cleanup helpGeneral options:  --threads=N                     number of threads to use [1]  --events=N                      limit for total number of events [0]  --time=N                        limit for total execution time in seconds [10]  --forced-shutdown=STRING        number of seconds to wait after the --time limit before forcing shutdown, or &#x27;off&#x27; to disable [off]  --thread-stack-size=SIZE        size of stack per thread [64K]  --rate=N                        average transactions rate. 0 for unlimited rate [0]  --report-interval=N             periodically report intermediate statistics with a specified interval in seconds. 0 disables intermediate reports [0]  --report-checkpoints=[LIST,...] dump full statistics and reset all counters at specified points in time. The argument is a list of comma-separated values representing the amount of time in seconds elapsed from start of test when report checkpoint(s) must be performed. Report checkpoints are off by default. []  --debug[=on|off]                print more debugging info [off]  --validate[=on|off]             perform validation checks where possible [off]  --help[=on|off]                 print help and exit [off]  --version[=on|off]              print version and exit [off]  --config-file=FILENAME          File containing command line options  --tx-rate=N                     deprecated alias for --rate [0]  --max-requests=N                deprecated alias for --events [0]  --max-time=N                    deprecated alias for --time [0]  --num-threads=N                 deprecated alias for --threads [1]Pseudo-Random Numbers Generator options:  --rand-type=STRING random numbers distribution &#123;uniform,gaussian,special,pareto&#125; [special]  --rand-spec-iter=N number of iterations used for numbers generation [12]  --rand-spec-pct=N  percentage of values to be treated as &#x27;special&#x27; (for special distribution) [1]  --rand-spec-res=N  percentage of &#x27;special&#x27; values to use (for special distribution) [75]  --rand-seed=N      seed for random number generator. When 0, the current time is used as a RNG seed. [0]  --rand-pareto-h=N  parameter h for pareto distribution [0.2]Log options:  --verbosity=N verbosity level &#123;5 - debug, 0 - only critical messages&#125; [3]  --percentile=N       percentile to calculate in latency statistics (1-100). Use the special value of 0 to disable percentile calculations [95]  --histogram[=on|off] print latency histogram in report [off]General database options:  --db-driver=STRING  specifies database driver to use (&#x27;help&#x27; to get list of available drivers) [mysql]  --db-ps-mode=STRING prepared statements usage mode &#123;auto, disable&#125; [auto]  --db-debug[=on|off] print database-specific debug information [off]Compiled-in database drivers:  mysql - MySQL driver  pgsql - PostgreSQL drivermysql options:  --mysql-host=[LIST,...]          MySQL server host [localhost]  --mysql-port=[LIST,...]          MySQL server port [3306]  --mysql-socket=[LIST,...]        MySQL socket  --mysql-user=STRING              MySQL user [sbtest]  --mysql-password=STRING          MySQL password []  --mysql-db=STRING                MySQL database name [sbtest]  --mysql-ssl[=on|off]             use SSL connections, if available in the client library [off]  --mysql-ssl-cipher=STRING        use specific cipher for SSL connections []  --mysql-compression[=on|off]     use compression, if available in the client library [off]  --mysql-debug[=on|off]           trace all client library calls [off]  --mysql-ignore-errors=[LIST,...] list of errors to ignore, or &quot;all&quot; [1213,1020,1205]  --mysql-dry-run[=on|off]         Dry run, pretend that all MySQL client API calls are successful without executing them [off]pgsql options:  --pgsql-host=STRING     PostgreSQL server host [localhost]  --pgsql-port=N          PostgreSQL server port [5432]  --pgsql-user=STRING     PostgreSQL user [sbtest]  --pgsql-password=STRING PostgreSQL password []  --pgsql-db=STRING       PostgreSQL database name [sbtest]Compiled-in tests:  fileio - File I/O test  cpu - CPU performance test  memory - Memory functions speed test  threads - Threads subsystem performance test  mutex - Mutex performance testSee &#x27;sysbench &lt;testname&gt; help&#x27; for a list of options for each test.

4. sysbench实践参考How to Benchmark Your System (CPU, File IO, MySQL) with Sysbench和使用sysbench对MySQL进行压力测试进行性能测试。
4.1. CPU使用以下命令测试CPU性能：sysbench cpu --cpu-max-prime=20000 runtotal time越小，说明CPU性能越好。
4.2. Mysql1、创建测试数据
sysbench /usr/share/sysbench/oltp_read_only.lua \--db-driver=mysql \--mysql-host=127.0.0.1 \--mysql-db=sbtest  \--mysql-user=root \--mysql-password=voidking \--mysql-socket=/var/run/mysqld/mysqld.sock \--tables=10 \--table-size=100000 \--threads=8 \--events=100000  \prepare

2、测试
time sysbench /usr/share/sysbench/oltp_read_only.lua \--db-driver=mysql \--mysql-host=127.0.0.1 \--mysql-db=sbtest  \--mysql-user=root \--mysql-password=voidking \--mysql-socket=/var/run/mysqld/mysqld.sock \--tables=10 \--table-size=100000 \--threads=8 \--events=100000  \run
每秒的transactions越高，说明性能越好。
3、测试命令2
time sysbench /usr/share/sysbench/oltp_read_only.lua \--db-driver=mysql \--mysql-host=127.0.0.1 \--mysql-db=sbtest  \--mysql-user=root \--mysql-password=voidking \--mysql-socket=/var/run/mysqld/mysqld.sock \--tables=10 \--table-size=100000 \--threads=8 \--events=0 \--time=30 \run

4、清理数据（可选）
time sysbench /usr/share/sysbench/oltp_read_only.lua \--db-driver=mysql \--mysql-host=127.0.0.1 \--mysql-db=sbtest  \--mysql-user=root \--mysql-password=voidking \--mysql-socket=/var/run/mysqld/mysqld.sock \--tables=10 \--table-size=100000 \--threads=8 \--events=0 \--time=30 \cleanup

5. 绘图5.1. sysbench统计数据测试搞定了，数据呢？每秒钟的transactions数据呢？关键在于report-interval参数。修改测试命令为：
sysbench /usr/share/sysbench/oltp_read_only.lua \--db-driver=mysql \--mysql-host=127.0.0.1 \--mysql-db=sbtest  \--mysql-user=root \--mysql-password=voidking \--mysql-socket=/var/run/mysqld/mysqld.sock \--tables=10 \--table-size=100000 \--threads=8 \--events=0 \--time=30 \--report-interval=5 \run &gt;&gt; sysbench.log

sysbench.log内容如下：
sysbench 1.0.16 (using bundled LuaJIT 2.1.0-beta2)Running the test with following options:Number of threads: 8Report intermediate results every 5 second(s)Initializing random number generator from current timeInitializing worker threads...Threads started![ 5s ] thds: 8 tps: 792.49 qps: 12698.90 (r/w/o: 11112.31/0.00/1586.59) lat (ms,95%): 11.24 err/s: 0.00 reconn/s: 0.00[ 10s ] thds: 8 tps: 782.20 qps: 12511.60 (r/w/o: 10947.20/0.00/1564.40) lat (ms,95%): 11.04 err/s: 0.00 reconn/s: 0.00[ 15s ] thds: 8 tps: 784.80 qps: 12561.53 (r/w/o: 10991.94/0.00/1569.59) lat (ms,95%): 11.04 err/s: 0.00 reconn/s: 0.00[ 20s ] thds: 8 tps: 808.85 qps: 12935.60 (r/w/o: 11317.90/0.00/1617.70) lat (ms,95%): 10.84 err/s: 0.00 reconn/s: 0.00[ 25s ] thds: 8 tps: 795.95 qps: 12737.76 (r/w/o: 11145.86/0.00/1591.89) lat (ms,95%): 10.84 err/s: 0.00 reconn/s: 0.00[ 30s ] thds: 8 tps: 816.80 qps: 13065.00 (r/w/o: 11431.60/0.00/1633.40) lat (ms,95%): 10.65 err/s: 0.00 reconn/s: 0.00SQL statistics:    queries performed:        read:                            334810        write:                           0        other:                           47830        total:                           382640    transactions:                        23915  (796.79 per sec.)    queries:                             382640 (12748.68 per sec.)    ignored errors:                      0      (0.00 per sec.)    reconnects:                          0      (0.00 per sec.)General statistics:    total time:                          30.0123s    total number of events:              23915Latency (ms):         min:                                    7.65         avg:                                   10.04         max:                                   32.13         95th percentile:                       10.84         sum:                               240031.54Threads fairness:    events (avg/stddev):           2989.3750/3.74    execution time (avg/stddev):   30.0039/0.00

然后把可以绘图的数据单独提取出来：cat sysbench.log | grep tps &gt; sysbench.dat 
5.2. gnuplot绘图# graph titleset title &quot;&quot;set key below box 1# x-axis labelset xlabel &quot;time(ms)&quot;# y-axis labelset ylabel &quot;queries per second&quot;set yrange[10000:15000]set ytics 500plot &quot;sysbench.dat&quot; using 2:9 with linespoints title &quot;sysbench0&quot;,\pause mouse

6. 后记至此，sysbench的使用方法和绘图方法就基本掌握了。更详细和高阶的用法，需要的时候再去学习。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>mysql</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>MediaWiki安装与配置</title>
    <url>/dev-mediawiki-install-and-config/</url>
    <content><![CDATA[1. MediaWiki简介MediaWiki全球最著名的开源wiki程序，运行于PHP+MySQL环境。MediaWiki从2002年2月25日被作为维基百科全书的系统软件，并有大量其他应用实例。MediaWiki的开发得到维基媒体基金会的支持。
本文，就来研究一下MediaWiki的安装和配置，数据库和Web程序位于不同的主机。参考文档：

Ubuntu 16.04 安装 MediaWiki
Manual:Running MediaWiki on Debian or Ubuntu



2. 环境准备2.1. 数据库数据库使用mysql，下面在主机A（IP为10.0.0.19）中进行安装配置。1、安装
sudo apt-get install mysql-server mysql-client
安装过程中设置数据库root密码。
2、配置mysql
sudo mysql -u root -p

登录mysql，执行sql命令：
create database wikidb;create user &#x27;wiki&#x27;@&#x27;%&#x27; identified by &#x27;voidking&#x27;;grant all privileges on wikidb.* to &#x27;wiki&#x27;@&#x27;%&#x27;;flush privileges;quit;

3、修改绑定IP
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

如下修改：
bind-address            = 0.0.0.0

4、重启mysql
sudo service mysql restart

5、测试登录
mysql -u wiki -h 10.0.0.19 -p

2.2. Apache和PHP在主机B中安装Apache和PHP
sudo apt-get install -y apache2 php php-mysql libapache2-mod-php php-xml php-mbstring 

3. 安装部署在主机B（IP为10.0.0.20）中安装mediawiki，步骤如下。
1、下载mediawiki
cd /tmp/wget https://releases.wikimedia.org/mediawiki/1.32/mediawiki-1.32.1.tar.gz

2、解压重命名
tar -xvzf /tmp/mediawiki-*.tar.gzsudo mkdir /var/lib/mediawikisudo mv mediawiki-*/* /var/lib/mediawiki

3、创建链接
sudo ln -s /var/lib/mediawiki /var/www/html/mediawiki

4、重启apache
sudo service apache2 restart

5、端口映射正常情况下，此时mediawiki的访问地址为：
http://10.0.0.20/mediawiki/http://10.0.0.20/mediawiki/mw-config/index.php
但是主机B的IP无法直接访问，所以参考《iptables实现端口映射》进行端口映射，映射后mediawiki的访问地址为：
http://172.16.101.144:2080/mediawiki/http://172.16.101.144:2080/mediawiki/mw-config/index.php

6、测试访问访问映射后的地址，浏览器居然不解析php文件，直接写下载php文件。这是mediawiki的bug吗？测试一下，在/var/www/html中创建index.php，内容为：
&lt;?php    phpinfo();?&gt;
浏览器访问http://172.16.101.144:2080/index.php，正常解析。猜测是mediawiki的锅，换了五个版本的mediawiki，居然存在同样的问题。莫非，是因为设置了端口映射？
参考《Linux下使用VirtualBox》，访问linux图形界面，然后输入firefox启动火狐浏览器，访问地址：
http://10.0.0.20/mediawiki/http://10.0.0.20/mediawiki/mw-config/index.php
正常解析页面，看来是确实是因为端口映射。
7、在浏览器中进行mediawiki的初始化，主要是mysql相关配置，按照主机A中的mysql配置来填写。
8、完成后下载LocalSettings.php，拷贝LocalSettings.php到/var/lib/mediawiki/目录中。
9、测试访问
http://172.16.101.144:2080/mediawiki/index.php/Main_Pagehttp://10.0.0.20/mediawiki/index.php/Main_Page
外部通过端口映射访问，内部通过真实IP访问，都正常。至此，大功告成，nice。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>mysql</tag>
        <tag>ubuntu</tag>
        <tag>apache</tag>
        <tag>wiki</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：虚拟机在线迁移实验V2——上</title>
    <url>/dev-openstack-vm-live-migration-experiment-v2-0/</url>
    <content><![CDATA[1. 前言《虚拟机在线迁移实验》一文中，进行了一些虚拟机迁移实验，并且记录了迁移过程中的指标数据。但是，实验环境是搭建在VirtualBox虚拟机中的，最终的实验结果不理想，会出现停机时间过长，迁移后实例卡住等问题。而且受限于虚拟机的大小，无法创建m1.xlarge这样的大型实例。
最近参照《Ubuntu16使用Kolla安装OpenStack》和《Kolla安装OpenStack多节点》，在物理机环境中重新搭建了OpenStack集群，三个节点分别为controller（network）、compute1、compute2。接下来我们在物理机中重新进行迁移实验，本文记录一下实验的步骤和结果。


2. 实例准备1、在horizon控制台，项目，计算，镜像。使用xenial-server-cloudimg-amd64-disk1.img镜像创建ubuntu16的镜像模板。
2、创建实例类型m1.tiny2，CPU个数为1，内存为512M，磁盘为5GB。
3、使用ubuntu16的镜像模板创建实例ubuntu0、ubuntu1、ubuntu2、ubuntu3、ubuntu4，实例类型分别选择m1.tiny2、m1.small、m1.medium、m1.large、m1.xlarge。之所以不使用默认的m1.tiny，是因为m1.tiny的磁盘太小，只有1G，无法安装ubuntu16。

4、创建成功后，分别分配浮动IP。
PS：在进行迁移实验时，关闭其他实例，防止资源占用产生干扰。
3. 脚本和软件准备3.1. 总迁移时间在两个计算节点，新建total-time.sh脚本：
#!/bin/bashtime1=`grep nova.virt.libvirt.migration /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log | tail -n 1 |awk &#x27;&#123;print $2&#125;&#x27;`;time2=`grep &#x27;VM Stopped&#x27; /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log | tail -n 1 | awk &#x27;&#123;print $2&#125;&#x27;`;# 计算时间差time=$(($(date +%s -d $time2) - $(date +%s -d $time1)));echo &quot;$time s&quot;;

3.2. 停机时间在控制节点，新建downtime.sh脚本：
#!/bin/bashtransmitted=`grep &#x27;packets transmitted&#x27; ping.log | tail -n 1 | awk &#x27;&#123;print $1&#125;&#x27;`received=`grep &#x27;packets transmitted&#x27; ping.log | tail -n 1 | awk &#x27;&#123;print $4&#125;&#x27;`lost=`expr $transmitted - $received`echo &quot;downtime is $lost ms&quot;

3.3. 迁移数据量在两个计算节点，安装iptraf-ng。apt install iptraf-ng
3.4. apache在ubuntu0等实例中安装apache：apt install apache2安装的默认版本为Apache/2.4.18 (Ubuntu)。
3.5. apachebench在控制节点，安装apachebench：apt install apache2-utils
在控制节点，新建format.sh脚本：
#!/bin/bashfilename=$1start_time=`awk &#x27;&#123;print $6&#125;&#x27; $filename | grep -v &#x27;wait&#x27; | sort | uniq -c|head -1|awk &#x27;&#123;print $2&#125;&#x27;`awk &#x27;&#123;print $6&#125;&#x27; $filename | grep -v &#x27;wait&#x27; | sort | uniq -c|awk -v t=$start_time &#x27;&#123;print $2-t,$1&#125;&#x27; &gt; f-$filename# 填充0array_str=`cat f-$filename | awk &#x27;&#123;print $1&#125;&#x27;`array_str2=`cat f-$filename | awk &#x27;&#123;print $2&#125;&#x27;`IPS=&#x27; &#x27;array=($array_str)array2=($array_str2)index=0count=0max=101echo &quot;#Throutput-$filename&quot; &gt; ff-$filenamewhile ( [ $index -lt $max ] &amp;&amp; [ $count -lt $max ] )do    if [[ $&#123;array[index]&#125; == $count ]]    then        echo &quot;$count $&#123;array2[index]&#125;&quot; &gt;&gt; ff-$filename        index=`expr $index + 1`        count=`expr $count + 1`    else        echo &quot;$count 0&quot;  &gt;&gt; ff-$filename        count=`expr $count + 1`    fidone

3.6. xshell使用xshell，打开3个controller节点的shell，一个用来ping实例，一个用来运行apachebench，一个用来执行迁移命令。打开2个compute1节点的shell，一个用来运行iptraf-ng，一个用来执行total-time.sh脚本。打开2个compute2节点的shell，一个用来运行iptraf-ng，一个用来执行total-time.sh脚本。
4. 实验流程假设当前只启动了ubuntu0实例，nova show ubuntu0位于compute1节点。
1、在实例中启动压力测试软件，或者在两个compute节点中启动网络故障模拟软件。（flavor实验中不需要）
2、在两个compute节点启动iptraf-ng，配置参考《虚拟机在线迁移的性能统计》。
3、在三个controller的shell中分别输入好一条命令：
sudo ping 172.16.0.19 -i 0.001 &gt;&gt; ping.logab -g t0.dat -t 100 -n 1000000 http://172.16.0.19/nova live-migration ubuntu0 compute2
然后，依次回车执行这些命令。
4、观察compute节点的iptraf-ng，等到流量不再增加，Ctrl+C结束ping命令。
5、在controller节点执行downtime.sh得到停机时间，在compute1节点执行total-time.sh得到迁移时间，在两个计算节点观察iptraf-ng得到迁移数据量。
6、等到ab命令结束，执行./format.sh t0.dat，得到吞吐量数据ff-t0.dat。
以上流程，适用于接下来的所有实验。
5. flavor实验1、按照实验流程，分别使用五个flavor，进行五次迁移实验，记录实验结果。
#Flavor    迁移时间s    停机时间ms    迁出数据量MB   迁入数据量MBtiny    19  483    456  459small   20  533    443  446medium  21  509    491  494large   22  455    592  596xlarge  24  531    829  834

2、同时得到五个吞吐量数据文件，ff-t0.dat到ff-t4.dat。
6. 吞吐量问题实验结果绘图，发现五种flavor的吞吐量基本没有差别！都是800上下。为了解决apache吞吐量问题，必须得研究一下。
6.1. 修改apache配置思路一：提高apache的最大连接数。参考如何设置Apache中的最大连接数、如何设置Apache中的最大连接数。
1、查看apache moduleapachectl -V可以看到Server MPM为event。如果要换成其他MPM，可以参考Ubuntu配置apache2.4的限速功能进行设置，比如换成prefork，这里我们不进行更换。
cd /etc/apache2/mods-enabledsudo ln -s ../mods-available/mpm_prefork.load .sudo ln -s ../mods-available/mpm_prefork.conf .sudo rm mods_event.loadsudo rm mods_event.confsudo service apache2 restart

2、备份配置
cd /etc/apache2/mods-available/sudo cp mpm_event.conf&#123;,.bak&#125;sudo cp mpm_event.load&#123;,.bak&#125;

3、修改配置编辑/etc/apache2/mods-available/mpm_event.conf，原文件为：
# event MPM# StartServers: initial number of server processes to start# MinSpareThreads: minimum number of worker threads which are kept spare# MaxSpareThreads: maximum number of worker threads which are kept spare# ThreadsPerChild: constant number of worker threads in each server process# MaxRequestWorkers: maximum number of worker threads# MaxConnectionsPerChild: maximum number of requests a server process serves&lt;IfModule mpm_event_module&gt;        StartServers             2        MinSpareThreads          25        MaxSpareThreads          75        ThreadLimit              64        ThreadsPerChild          25        MaxRequestWorkers        150        MaxConnectionsPerChild   0&lt;/IfModule&gt;# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

参考关于apache的mpm-event的参数无法调整问题，修改为：
&lt;IfModule mpm_event_module&gt;    ServerLimit              100    StartServers             20    MinSpareThreads          25    MaxSpareThreads          1200#   ThreadLimit              64    ThreadsPerChild          50    MaxRequestWorkers        5000    MaxConnectionsPerChild   10000&lt;/IfModule&gt;
MaxSpareThreads的参数设置，需要根据StartServers*ThreadsPerChild=1000，因此MaxSpareThreads必须大于1000，在此为1200，否则会有StartServers-MaxSpareThreads/ThreadsPerChild个进程被杀掉。
MaxRequestWorkers的参数设置，需要根据ServerLimit*ThreadsPerChild=5000，否则启动会有警告。
4、重启apache2sudo service apache2 restart
然而，并没有用，吞吐量并没有发生什么变化，依然是800左右，不过波动幅度变大了。MPM更换为prefork，吞吐量降到了700左右。
6.2. 修改ab命令思路二：apachebench的命令有问题。修改ab命令为：ab -g t0.dat -c 1000 -t 100 -n 10000000 http://172.16.0.19/
这次，吞吐量果然上去了，变成8000左右。但是，所有实例的吞吐量都上去了！
6.3. 更换测试工具思路三：测试工具换成webbench。1、安装webbench
wget http://soft.vpser.net/test/webbench/webbench-1.5.tar.gztar zxvf webbench-1.5.tar.gzcd webbench-1.5sudo make &amp;&amp; sudo make install

2、测试webbench -c 10000 -t 10 http://172.16.0.19/10s成功了80000左右的请求，说明ab测出的吞吐量是真实的。这样看来，问题还是出在apache上，吞吐量固定在8000左右。
6.4. 更换为nginx思路四：apache更换为nginx。1、修改apache的端口sudo vim /etc/apache2/ports.conf，端口改为8080。
2、安装nginx
sudo apt install -y nginxsudo apt update --fix-missingsudo apt install -y nginx

3、再次测试ab -g t0.dat -c 1000 -t 100 -n 10000000 http://172.16.0.19/吞吐量有所上升，9000左右，但是最坑的地方在于，所有实例都上升了，都是9000左右！
6.5. 提高ulimit思路五：在实例中提高ulimit。
sudo -iulimit -aulimit -i 257178ulimit -u 257178
然而，吞吐量并没有变化。
6.6. 网络问题莫非，是因为计算机网络问题？于是，在实例ubuntu0、ubuntu2、ubuntu4内部中进行ab测试，吞吐量结果为：
app ubuntu0 ubuntu2 ubuntu4apache 5298 11290 17706nginx  9380 19745 23124
看到这个结果，总算感觉合理了。问题也可以定位了，网络瓶颈。那么新的问题来了，不能在实例内部进行测试的情况下，怎样测试出准确的吞吐量？
6.7. 安装应用如果能够把真实吞吐量降到8000以下，是不是就可以通过内网测出真实吞吐量了？很有可能。那么，就安装一个标准测试应用，来试试效果。找到了两款应用，mediawiki和tpcw，这里我们选择mediawiki，参考《MediaWiki安装与配置》。
安装好mediawiki后进行ab测试：
ab -g t0.dat -c 10 -t 100 -n 1000000 http://172.16.0.19/mediawiki/index.php/Main_Page
ubuntu0吞吐量为30左右，ubuntu2的吞吐量为60左右。
7. 实验流程2假设当前只启动了ubuntu0实例，nova show ubuntu0位于compute1节点。
1、在实例中启动压力测试软件，或者在两个compute节点中启动网络故障模拟软件。（flavor实验中不需要）
2、在两个compute节点启动iptraf-ng，配置参考《虚拟机在线迁移的性能统计》。
3、在三个controller的shell中分别输入好一条命令：
sudo ping 172.16.0.19 -i 0.001 &gt;&gt; ping.logab -g t0.dat -c 10 -t 100 -n 1000000 http://172.16.0.19/mediawiki/index.php/Main_Pagenova live-migration ubuntu0 compute2
然后，依次回车执行这些命令。
4、观察compute节点的iptraf-ng，等到流量不再增加，Ctrl+C结束ping命令。
5、在controller节点执行downtime.sh得到停机时间，在compute1节点执行total-time.sh得到迁移时间，在两个计算节点观察iptraf-ng得到迁移数据量。
6、等到ab命令结束，执行./format.sh t0.dat，得到吞吐量数据ff-t0.dat。
以上流程，适用于接下来的所有实验。
8. flavor实验21、按照实验流程，分别使用安装了mediawiki的五个flavor，进行五次迁移实验，记录实验结果。然而，新的问题出现了，安装了mediawiki的flavor，迁移时间特别久，迁移数据量特别大，等到ab命令结束，迁移才会停止。修改ab命令为：
ab -g t0.dat -t 100 -n 1000000 http://172.16.0.19/mediawiki/index.php/Main_Page
可以成功迁移，但是另一个问题出现了，所有实例的吞吐量都下降为10左右，这日子没发过了！
是mediawiki的问题吗？创建一个index.php文件来测试一下，内容为：
&lt;?php    phpinfo();?&gt;
使用ab命令测试：ab -g t0.dat -c 1000 -t 100 -n 1000000 http://172.16.0.19/index.phpubuntu0吞吐量在800左右，ubuntu2的吞吐量在1200左右，挺不错的结果。然而，如果在ab测试时进行迁移，迁移时间依然会特别久，迁移数据量依然会特别大。ab命令停止，迁移成功停止；ab命令不停止，迁移直到失败。和mediawiki存在同样的问题。
9. 后记实验遇到瓶颈，找叶老师商量后，决定不再适用Apache这种web服务，而是换成系统服务，比如计算任务。在实例内部运行服务记录结果，就不存在网络的影响了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>testing</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>apache</tag>
        <tag>openstack</tag>
        <tag>测试</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>虚拟机在线迁移实验结果绘图</title>
    <url>/dev-vm-live-migration-gnuplot/</url>
    <content><![CDATA[1. 前言《虚拟机在线迁移实验》一文中，模拟了CPU压力、内存压力、磁盘压力还有各种网络故障，得到了这些条件下虚拟机在线迁移的性能数据。《ApacheBenchmark和gnuplot》一文中，找到了测试Web应用性能的方法，学习了gnuplot的基本用法。
本文，就利用gnuplot给迁移过程中得到的性能数据进行绘图，绘图结果用于写论文。


2. 数据处理gnuplot的数据是纯文本的，不带单位的，按列分布的。那么首先就要处理下数据，使之符合gnuplot的要求，以CPU数据为例，处理后的数据如下：
#CPU占用率  迁移时间    停机时间    迁出数据量   迁入数据量0%  18  6.28    283 24810% 20  6.53    353 34120% 19  4.52    323 30030% 20  4.58    354 33840% 21  4.62    336 34750% 20  5.41    355 32360% 19  4.54    303 30970% 21  5.24    356 35580% 19  4.41    351 33890% 17  7.31    328 36299% 19  5.77    351 348

3. 绘图3.1. 设计绘图，最基本的，要知道绘图的目的，想要显示出哪些信息？然后考虑绘制什么图？柱状图还是折线图？x轴是什么？y轴是什么？要不要第二坐标轴？另外，就要考虑在一张图里面放入哪些信息，不能太少，显得空旷；不能太多，显得杂乱。对于迁移过程中的四个指标，我们来分析一下。对于迁移时间和停机时间，可以放到一张图（图1）里面，这里的迁移时间和停机时间都是秒级，所以可以使用同一个坐标轴。对于迁移数据量，也可以放进图1，建立第二坐标轴即可。而对于应用程序性能，这个就麻烦了，等会再讨论。
3.2. CPU与迁移性能1、按照以上设计，新建cpu.plt，内容如下：
# graph titleset title &quot;CPU usage, downtime and migration time&quot;set key below box 1# x-axisset xlabel &quot;CPU usage&quot;set format x &quot;%g%%&quot;# y-axisset ylabel &quot;seconds&quot;set yrange[0:25]set ytics 5 nomirror# y2-axisset y2label &quot;MB&quot;set y2range[0:500]set y2tics 100set gridplot &quot;cpu.dat&quot; using 1:2 with linespoints title &quot;migration time&quot;,\    &quot;cpu.dat&quot; using 1:3 with linespoints title &quot;downtime&quot;,\    &quot;cpu.dat&quot; using 1:4 axis x1y2 with linespoints title &quot;transfered data&quot;#pause 5pause mouse

2、gnuplot cpu.plt，执行脚本绘制图片看上去，挺不错的。但是，实际上的downtime应该是毫秒级，不能和migration time共用y轴，否则看不出downtime的变化。所以，最好把迁移数据量拿出来单独绘图，downtime和migration time绘制在同一个图中，使用不同的坐标系。
3、新建cpu1.plt，绘制迁移时间和停机时间
# graph titleset title &quot;CPU usage, downtime and migration time&quot;set key below box 1# x-axisset xlabel &quot;CPU usage (%)&quot;# y-axisset ylabel &quot;Time (s)&quot;set yrange[0:25]set ytics 5 nomirror# y2-axisset y2label &quot;Time (ms)&quot;set y2range[0:10]set y2tics 2set gridplot &quot;cpu.dat&quot; using 1:2 with linespoints title &quot;migration time (s)&quot;,\    &quot;cpu.dat&quot; using 1:3 axis x1y2 with linespoints title &quot;downtime (ms)&quot;,#pause 5pause mouse

4、新建cpu2.plt，绘制迁移数据量
# graph titleset title &quot;CPU usage and transfered data&quot;set key below box 1# x-axisset xlabel &quot;CPU usage (%)&quot;# y-axisset ylabel &quot;Transfered Data (MB)&quot;set yrange[0:500]set ytics 50plot &quot;cpu.dat&quot; using 1:4 with linespoints title &quot;Source Machine&quot;,\    &quot;cpu.dat&quot; using 1:5 with linespoints title &quot;Target Machine&quot;#pause 5pause mouse

3.3. CPU与吞吐量1、性能测试
ab -g cpu0.dat -t 120 -n 10000000 http://10.0.2.154/ab -g cpu80.dat -t 120 -n 10000000 http://10.0.2.154/
生成cpu0.dat和cpu80.dat两个文件。
2、处理ab数据，计算吞吐量
#!/bin/bashfor var in &#123;0,80&#125;do    start_time=`awk &#x27;&#123;print $6&#125;&#x27; cpu$&#123;var&#125;.dat | grep -v &#x27;wait&#x27; | sort | uniq -c|head -1|awk &#x27;&#123;print $2&#125;&#x27;`    awk &#x27;&#123;print $6&#125;&#x27; cpu$&#123;var&#125;.dat | grep -v &#x27;wait&#x27; | sort | uniq -c|awk -v t=$&#123;start_time&#125; &#x27;&#123;print $2-t,$1&#125;&#x27; &gt; cpu$&#123;var&#125;-t.datdone
生成cpu0-t.dat和cpu80-t.dat两个文件。
3、填充空缺数据为0
#!/bin/bashfor var in &#123;0,80&#125;do    array_str=`cat cpu$&#123;var&#125;-t.dat | awk &#x27;&#123;print $1&#125;&#x27;`    array_str2=`cat cpu$&#123;var&#125;-t.dat | awk &#x27;&#123;print $2&#125;&#x27;`    IPS=&#x27; &#x27;    array=($array_str)    array2=($array_str2)    index=0    count=0    max=121    echo &quot;#cpu$&#123;var&#125;&quot; &gt; cpu$&#123;var&#125;-tf.dat    while ( [ $index -lt $max ] &amp;&amp; [ $count -lt $max ] )    do        if [[ $&#123;array[index]&#125; == $count ]]        then            echo &quot;$count $&#123;array2[index]&#125;&quot; &gt;&gt; cpu$&#123;var&#125;-tf.dat            index=`expr $index + 1`            count=`expr $count + 1`        else            echo &quot;$count 0&quot;  &gt;&gt; cpu$&#123;var&#125;-tf.dat            count=`expr $count + 1`        fi    donedone

4、新建cpu3.plt，绘制吞吐量
# output as png image#set terminal png  size 1000,560#set output &quot;cpu3.png&quot;# graph titleset title &quot;CPU usage and throughput&quot;set key below box 1# x-axis labelset xlabel &quot;Time (s)&quot;# y-axis labelset ylabel &quot;Responses per second&quot;plot &quot;cpu0_tf.dat&quot; using 1:2 with linespoints title &quot;CPU usage 0%&quot;,\    &quot;cpu80_tf.dat&quot; using 1:2 with linespoints title &quot;CPU usage 80%&quot;pause mouse

4. 后记内存压力、磁盘压力、网络故障等条件下的实验结果绘图，和CPU压力类似，详情参考live-migration-plot。
对于吞吐量的绘图，还有很多需要完善。比如说取样，绘制所有CPU占用率下的吞吐量会太乱，取样少了又没有对比，所以2-4条是比较合适的。再比如说时间间隔问题，输入ab测试命令后，输入迁移命令，这中间存在时间间隔。而且通过观察吞吐量曲线，也无法看出从何时开始进行迁移，只能看出停机时间。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>ApacheBenchmark和gnuplot</title>
    <url>/dev-ab-and-gnuplot/</url>
    <content><![CDATA[1. 前言《虚拟机在线迁移的性能统计》一文中，提到了虚拟机迁移的四个性能指标。但是文中只记录了迁移时间、停机时间、迁移数据量的测量方法，对于应用程序的性能，由于没有想到好的测量方法，最终搁置。
又参考了一些论文，发现他们在考虑Web应用程序性能时，主要考虑吞吐量。本文，就来研究一下ApacheBench测量吞吐量的方法，并且使用gnuplot绘制出吞吐量与时间的关系。


2. ApacheBenchApacheBench（ab）是Apache自带的一个Web压力测试工具，也可以单独下载使用。ab非常实用，它不仅可以对apache服务器进行网站访问压力测试，也可以对或其它类型的服务器进行压力测试。比如nginx、tomcat、IIS等。
2.1. 安装1、安装apacheapt install apache2
2、测试
curl localhostab -V

单独下载ab的话，可以使用apt install apache2-utils。
2.2. 使用ab命令使用说明：ab [options] [http[s]://]hostname[:port]/path
可选参数如下：
-n requests     Number of requests to perform-c concurrency  Number of multiple requests to make at a time-t timelimit    Seconds to max. to spend on benchmarking                This implies -n 50000-s timeout      Seconds to max. wait for each response                Default is 30 seconds-b windowsize   Size of TCP send/receive buffer, in bytes-B address      Address to bind to when making outgoing connections-p postfile     File containing data to POST. Remember also to set -T-u putfile      File containing data to PUT. Remember also to set -T-T content-type Content-type header to use for POST/PUT data, eg.                &#x27;application/x-www-form-urlencoded&#x27;                Default is &#x27;text/plain&#x27;-v verbosity    How much troubleshooting info to print-w              Print out results in HTML tables-i              Use HEAD instead of GET-x attributes   String to insert as table attributes-y attributes   String to insert as tr attributes-z attributes   String to insert as td or th attributes-C attribute    Add cookie, eg. &#x27;Apache=1234&#x27;. (repeatable)-H attribute    Add Arbitrary header line, eg. &#x27;Accept-Encoding: gzip&#x27;                Inserted after all normal header lines. (repeatable)-A attribute    Add Basic WWW Authentication, the attributes                are a colon separated username and password.-P attribute    Add Basic Proxy Authentication, the attributes                are a colon separated username and password.-X proxy:port   Proxyserver and port number to use-V              Print version number and exit-k              Use HTTP KeepAlive feature-d              Do not show percentiles served table.-S              Do not show confidence estimators and warnings.-q              Do not show progress when doing more than 150 requests-l              Accept variable document length (use this for dynamic pages)-g filename     Output collected data to gnuplot format file.-e filename     Output CSV file with percentages served-r              Don&#x27;t exit on socket receive errors.-m method       Method name-h              Display usage information (this message)-Z ciphersuite  Specify SSL/TLS cipher suite (See openssl ciphers)-f protocol     Specify SSL/TLS protocol                (TLS1, TLS1.1, TLS1.2 or ALL)

1、模拟10个client，总共发送100个请求（每个client发送10个请求）ab -n 100 -c 10 http://localhost/注意网址后面要加”/“或者明确的path。运行结果如下：
This is ApacheBench, Version 2.3 &lt;$Revision: 1706008 $&gt;Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking localhost (be patient).....doneServer Software:        Apache/2.4.18Server Hostname:        localhostServer Port:            80Document Path:          /Document Length:        11321 bytesConcurrency Level:      10Time taken for tests:   0.013 secondsComplete requests:      100Failed requests:        0Total transferred:      1159500 bytesHTML transferred:       1132100 bytes# 相当于LoadRunner中的每秒事务数Requests per second:    7841.29 [#/sec] (mean)# 相当于LoadRunner中的平均事务响应时间Time per request:       1.275 [ms] (mean)# 每个连接请求实际运行时间的平均值Time per request:       0.128 [ms] (mean, across all concurrent requests)Transfer rate:          88788.85 [Kbytes/sec] receivedConnection Times (ms)              min  mean[+/-sd] median   maxConnect:        0    0   0.1      0       1Processing:     0    1   0.3      1       1Waiting:        0    0   0.2      0       1Total:          1    1   0.3      1       2Percentage of the requests served within a certain time (ms)  50%      1  66%      1  75%      1  80%      1  90%      2  95%      2  98%      2  99%      2 100%      2 (longest request)

性能测试得到的最重要的指标是QPS(Requests per second)，反映了接口的并发承受能力，也就是系统的峰值性能。如果对接口的调用超过了这一限制，就要考虑提升硬件或者做一些优化了。更多结果说明参考Web性能压力测试工具之ApacheBench详解。
2、指定测试的最大时间为10秒ab -t 10 -n 1000000 http://localhost/“-t”可以指定测试的最大时间，如果还不到此数请求已经发完，那么测试也会结束。当使用”-t”参数时，ab内部默认最大的请求数为50000，为了同时使用”-n”指定的参数，可以将”-t”参数放在”-n”参数之前。更多内容参考按时间进行压力测试。
3、测试结果存入gnuplot格式的文件ab -g t10.dat -t 10 -n 1000000 http://localhost/
3. gnuplotgnuplot是一个小巧实用的数据处理工具，主要用来绘制2D/3D的数据或者函数图像，但是也包含数学计算、拟合等功能。虽然它的名字里有个“gnu”，但是它和大名鼎鼎的GNU没什么关系，使用的授权协议也不是GNU GPL，所以这里的“gnu”是小写，全名应该读作“new plot”。
3.1. 安装1、终端输入gnuplot，提示：
The program &#x27;gnuplot&#x27; can be found in the following packages: * gnuplot-nox * gnuplot-qt * gnuplot-x11 * gnuplot5-nox * gnuplot5-qt * gnuplot5-x11Try: sudo apt install &lt;selected package&gt;
可见gnuplot在多个软件包中都包含，这里我们选择安装gnuplot-nox或者gnuplot-qt。
2、安装gnuplot-noxapt install gnuplot-nox
3、测试gnuplot -V
其他平台的安装，参考谈谈gnuplot（一）：安装。
3.2. 使用1、启动输入gnuplot，进入gnuplotshell。这里包含gnuplot的版本、系统、版权等信息。最关键的是最后一条：Terminal type set to ‘unknown’什么是terminal？在gnuplot中，terminal就是说你打算用什么方式输出图片。这里默认的terminal是unknown，因为我们安装了gnuplot-nox。一般需要修改为wxt，也就是直接输出到电脑屏幕上。
2、设置terminal
set terminal dumbplot sin(x)
以上，设置terminal为dumb，也就是直接在shell中显示绘图结果。
3、图形界面显示在shell中绘图，太粗糙，我们想要在图形界面中显示。（1）使用vncserver连接服务器，参考Linux下使用VirtualBox（2）启动gnuplot，设置terminalset terminal wxt或者set terminal x11无法成功，因为没有安装图形界面支持。（3）安装图形界面支持apt install gnuplot-x11之后再启动gnuplot，默认terminal为x11。（4）尝试绘图更多绘制方法，参考程序员的绘图利器 — Gnuplot。
4、gnuplot脚本（1）新建sin.plt脚本，内容为：
# 绘制sinset xlabel &#x27;x&#x27;set ylabel &#x27;y&#x27;plot sin(x)
（2）运行脚本gnuplot sin.plt或者在gnushell中执行load &#39;sin.plt&#39;，值得一提的是，前一种方法绘制的图像会一闪而过。（3）保存脚本如果在gnushell执行了一些set命令，然后plot进行绘图，想要保存这些操作，方便下次使用，那么可以使用save &#39;filename.plt&#39;命令保存脚本。
4. 绘制ab数据在ab使用一节，已经保存两个文件，n10000.dat和t10.dat，接下来我们把该文件中的数据绘制一下。主要参考使用Apache Bench 和 Gnuplot产生性能测试图。
4.1. 响应时间1、查看t10.dat文件tail t10.dat

starttime：人类可读的开始时间
seconds：开始时间的unix时间戳值
ctime：对应的ab输出中的Connection Times(ms)中的Connect
dtime：对应的ab输出中的Connection Times(ms)中的Processing
ttime：对应的ab输出中的Connection Times(ms)中的Total
wait：对应的ab输出中的Connection Times(ms)中的Waiting

2、新建t10.plt脚本，绘制request与response time的关系图
# output as png imageset terminal png# save file to &quot;t10.png&quot;set output &quot;t10.png&quot;# graph titleset title &quot;ab -g t10.dat -t 10 -n 1000000 http://localhost/&quot;# nicer aspect ratio for image sizeset size 1,0.7# y-axis gridset grid y#x-axis labelset xlabel &quot;request&quot;#y-axis labelset ylabel &quot;response time (ms)&quot;#plot data from &quot;t10.dat&quot; using column 9 with smooth sbezier linesplot &quot;t10.dat&quot; using 9 smooth sbezier with lines title &quot;t10&quot;

3、执行t10.plt脚本gnuplot t10.plt生成的t10.png图像如下：这张图，和教程中给的结果差别很大，看不出逐渐增长的趋势。从上图可以看出，10s内发送了接近6万个请求，response time最大才12ms，应该是因为本机性能太好导致的。所以，郝同学决定换一个主机测试。
4、测试个人博客ab -g t10.dat -t 10 -n 1000000 http://www.voidking.com/
5、再次执行t10.plt脚本生成的图像如下：由上图可以看出，10s内发送了接近120个请求，response time逐渐增长，最大为99ms。
4.2. 吞吐量上面的效果并不是我们最终想要的，我们需要的，是response per second与time的关系，也就是吞吐量。
1、新建pt10.sh脚本，处理t10.dat
#!/bin/bashstart_time=`awk &#x27;&#123;print $6&#125;&#x27; t10.dat | grep -v &#x27;wait&#x27; | sort | uniq -c|head -1|awk &#x27;&#123;print $2&#125;&#x27;`awk &#x27;&#123;print $6&#125;&#x27; t10.dat | grep -v &#x27;wait&#x27; | sort | uniq -c|awk -v t=$start_time &#x27;&#123;print $2-t,$1&#125;&#x27; &gt; epochtime.dat

2、执行pt10.sh
chmod a+x pt10.sh./pt10.sh

3、新建throughput.plt脚本
# output as png imageset terminal png  size 1000,560set output &quot;throughput.png&quot;#graph titleset title &quot;Throughput&quot;set key invert reverse Left outside# nicer aspect ratio for image size#set size 1,0.6# y-axis gridset grid y# x-axis labelset xlabel &quot;time&quot;# y-axis labelset ylabel &quot;responses per second&quot;plot &quot;epochtime.dat&quot; using 1:2 with lines title &quot;t10&quot;

生成的图像如下：
5. 后记以上，完成了ApacheBenchmark和gnuplot的基础学习。gnuplot确实是一个很棒的绘图工具，绘出的图看起来就专业。以后就不用excel绘图了，安装一个windows版gnuplot，完美。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>apache</tag>
      </tags>
  </entry>
  <entry>
    <title>Win10安装Ubuntu</title>
    <url>/hobby-ubuntu-on-windows/</url>
    <content><![CDATA[1. 前言有几十个文件需要批量重命名，按照以前的做法，郝同学都是把文件上传到linux服务器，然后使用rename命令进行重命名，然后再拷贝回来。具体参考《批量重命名和批量替换字符串》。
但是，这次需要重命名的文件太大了，上传不方便，所以想到了另外一个思路：在Win10中使用Linux命令。要使用Linux命令，最简单的是使用git bash，但是只有ls、vim等简单命令。cygwin也可以模拟Linux环境，但是命令也有限。记得微软说Win10集成了Linux，所以决定试试，下面进行记录。
参考文档：

开启 Windows 10 的 Linux 子系统
使用xshell登录ubuntu on windows



2. 安装Ubuntu1、Win+S，打开搜索，输入“启用或关闭Windows功能”。
2、勾选“适用于 Linux 的 Windows 子系统”，点击确定，然后重启电脑。
3、Shift+右键，打开Powershell，输入bash，会提示访问Microsoft Store安装Linux。
4、访问Microsoft Store，选择Ubuntu，点击进行安装。
5、安装完成后，在Powershell中再次输入bash，即可进入ubuntu的shell，第一次进入会提示设置用户名和密码，这里设置用户名为voidking。
至此，ubuntu on windows就安装完成了。使用方法和正常的ubuntu一模一样，nice。当然也有不同的地方，比如c盘对应的目录为/mnt/c/，d盘对应的目录为/mnt/d/。
3. 配置SSH从Powershell进入bash，很难用，比如无法复制粘贴。所以，我们需要配置SSH，以便使用xshell访问。
1、切换root用户并设置密码
sudo -ipasswd

2、安装ssh服务apt-get install openssh-server
3、vim /etc/ssh/sshd_config，如下修改：
# line 13, uncomment and changePort 3422# line 15, uncommentListenAddress 0.0.0.0# line 32, uncomment and changePermitRootLogin yes# line 56, changePasswordAuthentication yes

4、重启sshservice ssh restart
如果报错：Could not load host key: /etc/ssh/ssh_host_rsa_keyCould not load host key: /etc/ssh/ssh_host_ecdsa_keyCould not load host key: /etc/ssh/ssh_host_ed25519_key
处理方法为：
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_keyssh-keygen -t rsa -f /etc/ssh/ssh_host_ecdsa_keyssh-keygen -t rsa -f /etc/ssh/ssh_host_ed25519_key

5、ubuntu中本地测试ssh voidking@localhost -p 3422
6、windows中xshell连接新建会话，主机填写127.0.0.1，端口填写3422，用户名密码是之前安装ubuntu时设置的。
4. 开机自启动SSH重启计算机后，发现无法通过ssh连接ubuntu，原来是因为ubuntu on windows默认不会开机自启动ssh服务。所以需要把ssh服务添加到开启自启动。直接使用systemctl enable ssh命令，无效，所以需要把ssh服务添加到开机自启动脚本，参考ubuntu18.04配置rc.local。
1、创建rc-local.service链接ln -fs /lib/systemd/system/rc-local.service /etc/systemd/system/rc-local.service
2、vim /etc/systemd/system/rc-local.service，添加：
[Install]WantedBy=multi-user.targetAlias=rc-local.service

3、创建rc.localvim /etc/rc.local，添加：
#!/bin/bash/etc/init.d/ssh startexit 0

4、添加执行权限chmod 755 /etc/rc.local
5、重启ubuntu无法使用reboot等命令重启ubuntu on windows，所以需要重启win10。
然而，重启后依然无效！看来是ubuntu on windows与纯净的ubuntu还是有些差别，参考在Windows子系统（WSL）中配置开机自启动服务进行处理。
1、win10中进入启动目录Win+R输入shell:startup，回车
2、创建wls_rclocal.vbs，内容为：
set ws=wscript.createobject(&quot;wscript.shell&quot;)ws.run &quot;C:\Windows\System32\bash.exe -c &#x27;sudo /etc/rc.local&#x27;&quot;,0

3、登录ubuntu，创建sudoers文件vim /etc/sudoers.d/rc-local，内容如下：
voidking * = (root) NOPASSWD: /etc/rc.local

4、重启win10，使用xshell登录ubuntu，成功。
5. 后记最终的效果，比预想的还要好。两个系统可以同时使用，这样就集成了windows和linux两者的优点，完美。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>ubuntu</tag>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo添加百度统计</title>
    <url>/dev-hexo-baidustatistic/</url>
    <content><![CDATA[1. 前言一直不关心网站的访问量，所以也一直没有统计过访问量数据。2019年刚刚开始，突然想到，如果在2019年终的时候，统计一下2019年的访问量，应该会很有意思。
虽然在《hexo生成sitemap》一文中的方法也可以在百度搜索资源平台看到一些站点统计信息，但是并不详细。所以，本文就来研究一下怎样在Hexo中添加百度统计。


2. 账号准备1、访问百度统计，注册账号（好好学习的郝）。
2、注册完成后登录百度统计，管理，新增站点。
3、点击获取代码，即可拿到站点的统计代码。
&lt;script&gt;var _hmt = _hmt || [];(function() &#123;  var hm = document.createElement(&quot;script&quot;);  hm.src = &quot;https://hm.baidu.com/hm.js?b759ac2a7fa45129e3ef060bf68259f0&quot;;  var s = document.getElementsByTagName(&quot;script&quot;)[0];   s.parentNode.insertBefore(hm, s);&#125;)();&lt;/script&gt;

3. 使用配置1、在yilia/layout/_partial/post目录下，新建baidustatistic.ejs，内容为：
&lt;!--百度统计的代码--&gt;&lt;script&gt;var _hmt = _hmt || [];(function() &#123;  var hm = document.createElement(&quot;script&quot;);  hm.src = &quot;https://hm.baidu.com/hm.js?&lt;%=theme.baidustatistic.siteid%&gt;&quot;;  var s = document.getElementsByTagName(&quot;script&quot;)[0];   s.parentNode.insertBefore(hm, s);&#125;)();&lt;/script&gt;

2、编辑yilia/layout/_partial/after-footer.ejs，添加：
&lt;% if (theme.baidustatistic.enable)&#123; %&gt;&lt;%- partial(&#x27;post/baidustatistic&#x27;) %&gt;&lt;% &#125; %&gt;

3、编辑yilia/_config.yml，添加：
# 百度统计baidustatistic:  enable: true  siteid: b759ac2a7fa45129e3ef060bf68259f0

至此，百度统计配置完成。
4、查看统计结果hexo s上传新的页面，20分钟后，查看百度统计，即可看到统计信息。

点击查看报告，可以看到更加详细的统计信息。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>图像倾斜矫正</title>
    <url>/dev-gp-image-tilt-correction/</url>
    <content><![CDATA[1. 理论没有找到关于图像倾斜矫正的综述性文献，那就自己整理一下吧。
图像倾斜可以分为两种情况，一种是平面倾斜，这种情况下拍照设备与试卷平行，拍出来的图像只需要进行旋转即可完成矫正；另一种是Z轴倾斜，这种情况下拍照设备与试卷存在一定的角度，拍出来的图像要先进行透视变换，然后再进行旋转等操作才可以完成矫正。
图像倾斜矫正关键在于根据图像特征自动检测出图像倾斜方向和倾斜角度。
对于平面倾斜，先利用边缘（轮廓）检测算法算法找到图像的边界，然后利用Radon变换法（基于投影的方法）、Hough变换法、线性回归法等找到倾斜角度，然后再利用仿射变换进行旋转。
对于Z轴倾斜，先利用边缘（轮廓）检测算法找到图像的边界，然后利用透视变换把视平面上的点投影到现实平面，然后再利用仿射变换进行旋转。


2. 实践2.1. 边缘检测# -*- coding: utf-8 -*-import numpy as npimport cv2img = cv2.imread(&#x27;../image/tilt.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)CannyImage = cv2.Canny(GrayImage,50, 150, apertureSize=3)cv2.imshow(&#x27;gray&#x27;,GrayImage)cv2.imshow(&#x27;canny&#x27;,CannyImage)cv2.waitKey(0)

edges=cv.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])参数说明：

image：输入图像。
threshold1：最小阈值。
threshold2：最大阈值。
apertureSize：Sobel算子的孔径大小。

更多内容参考：

OpenCV Canny
Canny边缘检测
基于Sobel和Canny边缘检测
Python-OpenCV 处理图像（五）：图像中边界和轮廓检测

以上代码，调用了OpenCV中的canny方法找到了边缘，但是，文字的边缘也被显示出来了。那么，怎么去掉文字边缘，只要长方形框的边缘呢？OpenCV也提供了方法，使用findContours来查找轮廓，使用drawContours来绘制轮廓。
# -*- coding: utf-8 -*-import numpy as npimport cv2img = cv2.imread(&#x27;../image/tilt.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)CannyImage = cv2.Canny(GrayImage,50, 150, apertureSize=3)ret,BinImage=cv2.threshold(CannyImage,127,255,cv2.THRESH_BINARY)_, contours, _= cv2.findContours(BinImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)cv2.imshow(&#x27;bin&#x27;,BinImage)cv2.drawContours(img, contours, -1, (0,255, 0), 1)cv2.imshow(&#x27;edge&#x27;,img)cv2.waitKey(0)


contours, hierarchy = cv.findContours(image, mode, method[, contours[, hierarchy[, offset]]])参数说明：

image：输入图像，二值化图像。
mode：轮廓检索模式，请参阅RetrievalModes。
method：轮廓近似方法，请参见ContourApproximationModes。

如果按照官方文档调用，会报错，参考ValueError: too many values to unpack解决。实际上，这是因为opencv3之后该函数的返回值有三个，而官方文档有多个版本，比如这一版中就说明了有三个参数。
2.2. 计算倾斜角以上，已经找到了图片的边缘，接下来计算倾斜角度。
# -*- coding: utf-8 -*-import numpy as npimport cv2img = cv2.imread(&#x27;../image/tilt.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)CannyImage = cv2.Canny(GrayImage,50, 150, apertureSize=3)ret,BinImage=cv2.threshold(CannyImage,127,255,cv2.THRESH_BINARY)lines = cv2.HoughLinesP(BinImage, 1, np.pi / 180, 160, minLineLength=200, maxLineGap=180)# 寻找长度最长的线distance = []for line in lines:    x1, y1, x2, y2 = line[0]    dis = np.sqrt(pow((x2 - x1), 2) + pow((y2 - y1), 2))    distance.append(dis)max_dis_index = distance.index(max(distance))max_line = lines[max_dis_index]x1, y1, x2, y2 = max_line[0]# 获取旋转角度angle = cv2.fastAtan2((y2 - y1), (x2 - x1))print(angle)

以上代码，很尴尬，并不是利用findContours的结果进行计算的，而是一个新的思路。为什么没有使用contours？因为不会写代码。。。如果要继续findContours思路，那么可以参考图像矫正技术深入探讨进行改写。
2.3. 仿射变换求出倾斜角度之后，利用仿射变换进行旋转。
# 计算图片中心centerpoint = (img.shape[1]/2,img.shape[0]/2)# 获取旋转矩阵rotate_mat = cv2.getRotationMatrix2D(centerpoint,angle,1.0)correct_image = cv2.warpAffine(img,rotate_mat,(img.shape[1],img.shape[0]),borderValue =(255,255,255) )cv2.imshow(&#x27;right&#x27;,correct_image)cv2.waitKey(0)


2.4. Z轴倾斜import cv2import numpy as npdef gray_and_bin(init_img):    gray_img = cv2.cvtColor(init_img, cv2.COLOR_BGR2GRAY)    blur_img = cv2.GaussianBlur(gray_img, (3, 3), 0)                     # 高斯模糊去噪（设定卷积核大小影响效果）    canny_img = cv2.Canny(blur_img, 35, 189, apertureSize=3)    _, bin_img = cv2.threshold(canny_img, 127, 255, cv2.THRESH_BINARY)  # 设定阈值165（阈值影响开闭运算效果）    return gray_img, bin_imgdef points_and_box(init_img, bin_img):    image, contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)    c = sorted(contours, key=cv2.contourArea, reverse=True)[0]   # 计算最大轮廓的旋转包围盒    rect = cv2.minAreaRect(c)                                    # 获取包围盒（中心点，宽高，旋转角度）    box = np.int0(cv2.boxPoints(rect))                           # box    # box_img = cv2.drawContours(init_img.copy(), [box], -1, (0, 0, 255), 2)    # cv2.imshow(&#x27;box&#x27;,box_img)    empty_img = np.zeros(init_img.shape, np.uint8) # 创建空白图像    empty_img[...] = 255 # 设置白底    # cv2.imshow(&#x27;test&#x27;,empty_img)    cv2.drawContours(empty_img, contours, -1, (0, 0, 0), 1) # 在空白图像上绘制试卷轮廓    # cv2.imshow(&#x27;edge&#x27;, empty_img)    _, bin_img2 = gray_and_bin(empty_img)    # cv2.imshow(&#x27;bin&#x27;,bin_img2)    lines = cv2.HoughLinesP(bin_img2, 1, np.pi / 180, 100, minLineLength=200, maxLineGap=10)    for i in range(int(np.size(lines) / 4)):        for x1, y1, x2, y2 in lines[i]:            cv2.line(empty_img, (x1, y1), (x2, y2), (255, 255, 0), 1)    # cv2.imshow(&#x27;line&#x27;,empty_img)    points = None    if len(contours) &gt; 0:        contours = sorted(contours, key=cv2.contourArea, reverse=True)        for c in contours:            peri = cv2.arcLength(c, True)  # 轮廓按大小降序排序            approx = cv2.approxPolyDP(c, 0.02 * peri, True)  # 获取近似的轮廓            if len(approx) == 4:  # 近似轮廓有四个顶点                points = approx                break    print(&#x27;piont[0]&#x27;, points[0]) # 左下    print(&#x27;piont[1]&#x27;, points[1]) # 左上    print(&#x27;piont[2]&#x27;, points[2]) # 右上    print(&#x27;piont[3]&#x27;, points[3]) # 右下    print(&#x27;box[0]:&#x27;, box[0]) # 右下    print(&#x27;box[1]:&#x27;, box[1]) # 右上    print(&#x27;box[2]:&#x27;, box[2]) # 左上    print(&#x27;box[3]:&#x27;, box[3]) # 左下    return points,boxdef perspective_transform(box,points,init_img):    # 原图中试卷的四个顶点    pts1 = np.float32([points[0], points[1], points[2], points[3]])    # box中的四个顶点    pts2 = np.float32([box[3], box[2], box[1], box[0]])    # 生成透视变换矩阵；进行透视变换    M = cv2.getPerspectiveTransform(pts1,pts2)    result_img = cv2.warpPerspective(init_img, M, (1200, 1300))    return result_imgif __name__==&#x27;__main__&#x27;:    init_img = cv2.imread(&#x27;../image/init.jpg&#x27;)    gray_img, bin_img = gray_and_bin(init_img)    points, box = points_and_box(init_img,bin_img)    result_img = perspective_transform(box,points,init_img)    # cv2.imshow(&#x27;init&#x27;,init_img)    cv2.namedWindow(&#x27;result&#x27;, 0)    cv2.resizeWindow(&#x27;result&#x27;, 640, 1200)    cv2.imshow(&#x27;result&#x27;, result_img)    # cv2.imwrite(&#x27;result.jpg&#x27;,result_img)    cv2.waitKey(0)

以上结果，并不是很理想。透视矫正完成了，但是存在黑边白边，试卷长宽比例也有问题，也没有进行平面倾斜矫正。就先这样吧，作为一个小白，做成这样已经很不容易了。
更多内容参考利用opencv库，实现校正图片中的A4纸、用numpy+OpenCV快速实现矫正图像的功能、OpenCV—python图像矫正和对倾斜的图像进行修正——基于opencv透视变换。
3. 后记图像倾斜矫正这部分，是图像预处理中的一个难点。以上，虽然实现了基本的图像倾斜矫正算法，但是算法原理还需要进一步学习。而且，上面的代码中使用的图片都是理想的，干扰很小，而实际拍摄的图片，除了目标物体（试卷），还会出现其他物体，比如签字笔文具盒等等。所以，后续还需要考虑其他物体的干扰。
另外，发现OpenCV成为了开发过程中的瓶颈，想要实现一些功能，每次都要求助于百度谷歌。因此，必须找时间系统学习一下OpenCV。
4. 书签图像矫正原理说明
图像矫正-基于opencv实现
利用包围轮廓和仿射变换进行图像倾斜校正
对Z轴倾斜的图像进行校正–基于OpenCV透视变换
OpenCV实现基于傅里叶变换的旋转文本校正
Python+OpenCV实现旋转文本校正
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>毕设</tag>
        <tag>opencv</tag>
      </tags>
  </entry>
  <entry>
    <title>图像去噪</title>
    <url>/dev-gp-image-denoise/</url>
    <content><![CDATA[1. 理论1.1. 噪声噪声在图像上常表现为一引起较强视觉效果的孤立像素点或像素块。一般，噪声信号与要研究的对象不相关，它以无用的信息形式出现，扰乱图像的可观测信息。通俗的说就是噪声让图像不清楚。
图像常见噪声基本上有四种：高斯噪声，泊松噪声，乘性噪声，椒盐噪声。
令f(x,y)表示原始图象，g(x,y)表示图象信号，n(x,y)表示噪声。
根据噪声和信号的关系可将其分为三种形式：1、加性噪声，此类噪声与输入图象信号无关，含噪图象可表示为f(x,y)=g(x,y)+n(x,y)，信道噪声及光导摄像管的摄像机扫描图象时产生的噪声就属这类噪声；2、乘性噪声，此类噪声与图象信号有关，含噪图象可表示为f(x,y)=g(x,y)+n(x,y)g(x,y)，飞点扫描器扫描图象时的噪声，电视图象中的相关噪声，胶片中的颗粒噪声就属于此类噪声；3、量化噪声，此类噪声与输入图象信号无关，是量化过程存在量化误差，再反映到接收端而产生。
更多内容参考图像噪声简介和浅析“高斯白噪声”，“泊松噪声”，“椒盐噪声”的区别。


1.2. 去噪图像的去噪处理方法基本上可分为空间域法和变换域法两大类。
空间域去噪方法的思想就是在原图像上对图像灰度值进行处理，通常采取“平均”或“平滑”的方法，将突变的噪声分量分散到周围像素中去，使图像变得较为平滑，降低噪声的影响。常用的空间域去噪方法有：均值去噪法，中值去噪法，高斯去噪法、维纳滤波去噪法等。
变换域去噪方法的思想是将原图像进行相关的变换，将图像信息变换到变换域中,再通过一定的方法来对图像信息进行处理，之后再通过反变换恢复图像信息，以达到图像去噪的目的。常用的变换域去噪方法有：傅里叶变换去噪方法，小波变换去噪方法等。
更多内容参考图像降噪和一文道尽传统图像降噪方法。
去噪方法又可以分为线性滤波和非线性滤波，更多内容参考什么是线性滤波、非线性滤波。
2. 实践主要参考Image Denoising、Smoothing Images和OpenCV图像噪声与去噪函数方法对比使用介绍。
OpenCV中有多个可以降低图像噪声、对图像实现平滑滤波的函数，最常见的就是均值模糊与高斯模糊，它们都可以在一定程度上减低上述几种噪声，另外还有中值模糊、双边模糊、非局部去噪等函数方法可以使用，针对特定种类的噪声，使用有针对性函数与合理的参数可以取得较好的效果。

blur：对各种噪声都有一定的抑制作用。
GaussianBlur：对随机噪声比较好，对椒盐噪声效果不好。
medianBlur：对椒盐噪声效果比较好。
fastNlMeansDenoising：只支持灰度图像，非局部去噪，速度很慢，可以调参。
fastNlMeansDenoisingColored：同上，去噪针对彩色图像。

2.1. 添加噪声参考Python+opencv3对图像添加高斯噪声和椒盐噪声，给图像添加椒盐噪声。
# -*- coding: utf-8 -*-import cv2import numpy as npimport matplotlib.pyplot as plt# 定义添加椒盐噪声的函数def SaltAndPepper(src,percetage):    SP_NoiseImg=np.copy(src)    SP_NoiseNum=int(percetage*src.shape[0]*src.shape[1])    for i in range(SP_NoiseNum):        randX=np.random.random_integers(0,src.shape[0]-1)        randY=np.random.random_integers(0,src.shape[1]-1)        if np.random.random_integers(0,1)==0:            SP_NoiseImg[randX,randY]=0        else:            SP_NoiseImg[randX,randY]=255    return SP_NoiseImgimg = cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)NoiseImage = SaltAndPepper(GrayImage,0.01)plt.subplot(121),plt.imshow(GrayImage,&#x27;gray&#x27;)plt.subplot(122),plt.imshow(NoiseImage,&#x27;gray&#x27;)plt.show()


2.2. 简单去噪# -*- coding: utf-8 -*-import numpy as npimport cv2import matplotlib.pyplot as plt# 定义添加椒盐噪声的函数def SaltAndPepper(src,percetage):    SP_NoiseImg=np.copy(src)    SP_NoiseNum=int(percetage*src.shape[0]*src.shape[1])    for i in range(SP_NoiseNum):        randX=np.random.random_integers(0,src.shape[0]-1)        randY=np.random.random_integers(0,src.shape[1]-1)        if np.random.random_integers(0,1)==0:            SP_NoiseImg[randX,randY]=0        else:            SP_NoiseImg[randX,randY]=255    return SP_NoiseImg# 加载图片，灰度化，添加噪声img = cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)NoiseImage = SaltAndPepper(GrayImage,0.01)# 直接调用opencv函数dst = cv2.fastNlMeansDenoising(NoiseImage,10,7,21)plt.subplot(121),plt.imshow(NoiseImage,&#x27;gray&#x27;)plt.subplot(122),plt.imshow(dst,&#x27;gray&#x27;)plt.show()

dst=cv.fastNlMeansDenoising(src, h[, dst[, templateWindowSize[, searchWindowSize[, normType]]]])参数说明：

src：输入8位1通道，2通道，3通道或4通道图像。
dst：输出与src具有相同大小和类型的图像。
h：参数调节滤波器强度。大h值可以完美地消除噪点，但也可以消除图像细节，较小的h值可以保留细节，但也会保留一些噪点。
templateWindowSize：用于计算权重的模板修补程序的窗口大小，奇数，推荐值7像素。
searchWindowSize：窗口的像素大小，用于计算给定像素的加权平均值，奇数，推荐21像素。

2.3. 均值滤波去噪法# -*- coding: utf-8 -*-import numpy as npimport cv2import matplotlib.pyplot as plt# 定义添加椒盐噪声的函数def SaltAndPepper(src,percetage):    SP_NoiseImg=np.copy(src)    SP_NoiseNum=int(percetage*src.shape[0]*src.shape[1])    for i in range(SP_NoiseNum):        randX=np.random.random_integers(0,src.shape[0]-1)        randY=np.random.random_integers(0,src.shape[1]-1)        if np.random.random_integers(0,1)==0:            SP_NoiseImg[randX,randY]=0        else:            SP_NoiseImg[randX,randY]=255    return SP_NoiseImgimg = cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)NoiseImage = SaltAndPepper(GrayImage,0.01)dst = cv2.blur(NoiseImage, (3,3))plt.subplot(121),plt.imshow(NoiseImage,&#x27;gray&#x27;)plt.subplot(122),plt.imshow(dst,&#x27;gray&#x27;)plt.show()


dst=cv.blur(src, ksize[, dst[, anchor[, borderType]]])参数说明：

src：输入的图片。
ksize：模糊内核大小。
anchor：锚点，默认值Point(-1，-1)表示锚点位于内核中心。
borderType：边界模式用于外推图像外的像素。

均值滤波是一种典型的线性滤波算法，主要是利用像素点邻域的像素值来计算像素点的值。其具体方法是首先给出一个滤波模板kernel，该模板将覆盖像素点周围的其他邻域像素点，去掉像素本身，将其邻域像素点相加然后取平均值即为该像素点的新的像素值，这就是均值滤波的本质。官方给出的kernel模板如下：
2.4. 中值滤波去噪法# -*- coding: utf-8 -*-import numpy as npimport cv2import matplotlib.pyplot as plt# 定义添加椒盐噪声的函数def SaltAndPepper(src,percetage):    SP_NoiseImg=np.copy(src)    SP_NoiseNum=int(percetage*src.shape[0]*src.shape[1])    for i in range(SP_NoiseNum):        randX=np.random.random_integers(0,src.shape[0]-1)        randY=np.random.random_integers(0,src.shape[1]-1)        if np.random.random_integers(0,1)==0:            SP_NoiseImg[randX,randY]=0        else:            SP_NoiseImg[randX,randY]=255    return SP_NoiseImgimg = cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)NoiseImage = SaltAndPepper(GrayImage,0.01)dst = cv2.medianBlur(NoiseImage, 3)plt.subplot(121),plt.imshow(NoiseImage,&#x27;gray&#x27;)plt.subplot(122),plt.imshow(dst,&#x27;gray&#x27;)plt.show()

dst=cv.medianBlur(src, ksize[, dst])参数说明：

src：输入的图片。
ksize：窗口的尺寸。它必须是奇数且大于1，例如：3、5、7。

中值滤波法和均值滤波法类似，不同的是，它将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值。
2.5. 高斯滤波去噪法# -*- coding: utf-8 -*-import numpy as npimport cv2import matplotlib.pyplot as plt# 定义添加椒盐噪声的函数def SaltAndPepper(src,percetage):    SP_NoiseImg=np.copy(src)    SP_NoiseNum=int(percetage*src.shape[0]*src.shape[1])    for i in range(SP_NoiseNum):        randX=np.random.random_integers(0,src.shape[0]-1)        randY=np.random.random_integers(0,src.shape[1]-1)        if np.random.random_integers(0,1)==0:            SP_NoiseImg[randX,randY]=0        else:            SP_NoiseImg[randX,randY]=255    return SP_NoiseImgimg = cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)NoiseImage = SaltAndPepper(GrayImage,0.01)dst = cv2.GaussianBlur(NoiseImage, (3,3), 0.8, 0.8)plt.subplot(121),plt.imshow(NoiseImage,&#x27;gray&#x27;)plt.subplot(122),plt.imshow(dst,&#x27;gray&#x27;)plt.show()

dst=cv.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])参数说明：

src：输入的图片。
ksize：高斯核大小。ksize.width和ksize.height可以不同，但​​它们都必须是正数和奇数。或者，它们可以为零，然后根据sigma计算。
sigmaX：X方向的高斯核标准偏差。
sigmaY：Y方向的高斯核标准偏差。如果sigmaY为零，则将其设置为等于sigmaX，如果两个sigma均为零，则分别从ksize.width和ksize.height计算。

高斯滤波的原理参考高斯滤波器详解。
2.6. 傅里叶变换去噪# -*- coding: utf-8 -*-import numpy as npimport cv2import matplotlib.pyplot as plt# 定义添加椒盐噪声的函数def SaltAndPepper(src,percetage):    SP_NoiseImg=np.copy(src)    SP_NoiseNum=int(percetage*src.shape[0]*src.shape[1])    for i in range(SP_NoiseNum):        randX=np.random.random_integers(0,src.shape[0]-1)        randY=np.random.random_integers(0,src.shape[1]-1)        if np.random.random_integers(0,1)==0:            SP_NoiseImg[randX,randY]=0        else:            SP_NoiseImg[randX,randY]=255    return SP_NoiseImg# 加载图片，灰度化，添加噪声img = cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)NoiseImage = SaltAndPepper(GrayImage,0.01)# dftdft = cv2.dft(np.float32(NoiseImage),flags = cv2.DFT_COMPLEX_OUTPUT)dft_shift = np.fft.fftshift(dft)rows, cols = NoiseImage.shapecrow, ccol = int(rows/2) , int(cols/2)# create a mask first, center square is 1, remaining all zerosmask = np.zeros((rows,cols,2),np.uint8)mask[crow-30:crow+30, ccol-30:ccol+30] = 1# apply mask and inverse DFTfshift = dft_shift*maskf_ishift = np.fft.ifftshift(fshift)img_back = cv2.idft(f_ishift)dst = cv2.magnitude(img_back[:,:,0],img_back[:,:,1])plt.subplot(121),plt.imshow(NoiseImage,&#x27;gray&#x27;)plt.subplot(122),plt.imshow(dst,&#x27;gray&#x27;)plt.show()

dst=cv.dft(src[, dst[, flags[, nonzeroRows]]])参数说明：

src：输入数组可能是实数或复数。
flag：转换标志，代表DftFlags的组合。
nonezeroRows：当参数不为零时，该函数假定只有输入数组的第一个非零行或仅输出数组的第一个非零行包含非零。因此，函数可以更有效地处理其余行并节省一些时间，这种技术对于使用DFT计算阵列互相关或卷积非常有用。

更多内容参考：

傅里叶分析之掐死教程
opencv python 傅里叶变换
OpenCV+Python离散傅里叶变换DFT
opencv dft官方文档
小波和傅里叶变换之间关系详细讲解。

3. 后记以上，实现了部分滤波去噪法。对于维纳滤波去噪法和小波变换去噪法，数学原理比较复杂，就先放一边，等有需要了再来进行实现。对于傅里叶变换去噪法，一知半解，还需要继续学习。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>毕设</tag>
        <tag>opencv</tag>
      </tags>
  </entry>
  <entry>
    <title>图像二值化</title>
    <url>/dev-gp-image-binary/</url>
    <content><![CDATA[1. 理论图像的二值化，就是将图像上的像素点的灰度值设置为0或255，也就是将整个图像呈现出明显的只有黑和白的视觉效果。
一幅图像包括目标物体、背景还有噪声，要想从多值的数字图像中直接提取出目标物体，常用的方法就是设定一个阈值T，用T将图像的数据分成两部分：大于T的像素群和小于T的像素群。这是研究灰度变换的最特殊的方法，称为图像的二值化（Binarization）。
常见的二值化方法有三种，分别是固定阈值法、平均值法、自适应阈值法和直方图法。
固定阈值法就是设定一个固定阈值K，小于等于K的像素值设为0(黑色)，大于K的像素值设为255(白色)。
平均值法计算像素的平均值K，然后扫描图像的每个像素值，小于等于K像素值设为0(黑色)，大于K的像素值设为255(白色)。
自适应阈值法对平均值法进行改进，规定一个区域大小，求区域平均值作为阈值K，然后区域中的像素值与K进行比较。
直方图方法主要是发现图像的两个最高的峰，然后阈值K取值在两个峰之间的峰谷最低处。图像的直方图用来表征该图像像素值的分布情况。用一定数目的小区间(bin)来指定表征像素值的范围，每个小区间会得到落入该小区间表示范围的像素数目。
更多内容参考图像的二值化之python+opencv和opencv python图像二值化。


2. 实践2.1. 固定阈值法import cv2from matplotlib import pyplot as pltimg=cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)ret,thresh1=cv2.threshold(GrayImage,127,255,cv2.THRESH_BINARY)ret,thresh2=cv2.threshold(GrayImage,127,255,cv2.THRESH_BINARY_INV)ret,thresh3=cv2.threshold(GrayImage,127,255,cv2.THRESH_TRUNC)ret,thresh4=cv2.threshold(GrayImage,127,255,cv2.THRESH_TOZERO)ret,thresh5=cv2.threshold(GrayImage,127,255,cv2.THRESH_TOZERO_INV)titles = [&#x27;Gray Image&#x27;,&#x27;BINARY&#x27;,&#x27;BINARY_INV&#x27;,&#x27;TRUNC&#x27;,&#x27;TOZERO&#x27;,&#x27;TOZERO_INV&#x27;]images = [GrayImage, thresh1, thresh2, thresh3, thresh4, thresh5]for i in range(6):   plt.subplot(2,3,i+1),plt.imshow(images[i],&#x27;gray&#x27;)   plt.title(titles[i])   plt.xticks([]),plt.yticks([])plt.show()


如果报错没有matplotlib，那么先执行pip install matplotlib进行安装。
retval,dst = cv.threshold(src, thresh, maxval, type[, dst] )参数解释：

src：原图像，原图像应该是灰度图。
thresh：用来对像素值进行分类的阈值。
maxval：大于阈值置为maxval。
type：不同的阈值方法。

2.2. 平均阈值法import cv2import numpy as npfrom matplotlib import pyplot as pltimg=cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)k=np.mean(GrayImage)ret,thresh1=cv2.threshold(GrayImage,k,255,cv2.THRESH_BINARY)ret,thresh2=cv2.threshold(GrayImage,k,255,cv2.THRESH_BINARY_INV)ret,thresh3=cv2.threshold(GrayImage,k,255,cv2.THRESH_TRUNC)ret,thresh4=cv2.threshold(GrayImage,k,255,cv2.THRESH_TOZERO)ret,thresh5=cv2.threshold(GrayImage,k,255,cv2.THRESH_TOZERO_INV)titles = [&#x27;Gray Image&#x27;,&#x27;BINARY&#x27;,&#x27;BINARY_INV&#x27;,&#x27;TRUNC&#x27;,&#x27;TOZERO&#x27;,&#x27;TOZERO_INV&#x27;]images = [GrayImage, thresh1, thresh2, thresh3, thresh4, thresh5]for i in range(6):   plt.subplot(2,3,i+1),plt.imshow(images[i],&#x27;gray&#x27;)   plt.title(titles[i])   plt.xticks([]),plt.yticks([])plt.show()

2.3. 自适应阈值法import cv2from matplotlib import pyplot as pltimg=cv2.imread(&#x27;../image/test.jpg&#x27;)GrayImage=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)ret,th1 = cv2.threshold(GrayImage,127,255,cv2.THRESH_BINARY)th2 = cv2.adaptiveThreshold(GrayImage,255,cv2.ADAPTIVE_THRESH_MEAN_C,\                    cv2.THRESH_BINARY,3,4)th3 = cv2.adaptiveThreshold(GrayImage,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\                    cv2.THRESH_BINARY,3,4)titles = [&#x27;Gray Image&#x27;, &#x27;Global Thresholding (v = 127)&#x27;,&#x27;Adaptive Mean Thresholding&#x27;, &#x27;Adaptive Gaussian Thresholding&#x27;]images = [GrayImage, th1, th2, th3]for i in range(4):   plt.subplot(2,2,i+1),plt.imshow(images[i],&#x27;gray&#x27;)   plt.title(titles[i])   plt.xticks([]),plt.yticks([])plt.show()


dst = cv.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst] )参数解释：

src：指原图像，原图像应该是灰度图。
maxValue：大于阈值置为maxValue。
adaptiveMethod：要使用的自适应阈值算法。
thresholdType：阈值类型必须是THRESH_BINARY或THRESH_BINARY_INV。
blockSize：用于计算像素的阈值的像素邻域的大小：3,5,7等。
C：从平均值或加权平均值中减去常数。通常情况下，它是正数，但也可能为零或负数。

2.4. 直方图法import cv2import numpy as npfrom matplotlib import pyplot as pltimg=cv2.imread(&#x27;../image/test.jpg&#x27;)img=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)# global thresholdingret1,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)# Otsu&#x27;s thresholdingret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)# Otsu&#x27;s thresholding after Gaussian filteringblur = cv2.GaussianBlur(img,(5,5),0)ret3,th3 = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)# plot all the images and their histogramsimages = [img, 0, th1,          img, 0, th2,          blur, 0, th3]titles = [&#x27;Original Noisy Image&#x27;,&#x27;Histogram&#x27;,&#x27;Global Thresholding (v=127)&#x27;,          &#x27;Original Noisy Image&#x27;,&#x27;Histogram&#x27;,&quot;Otsu&#x27;s Thresholding&quot;,          &#x27;Gaussian filtered Image&#x27;,&#x27;Histogram&#x27;,&quot;Otsu&#x27;s Thresholding&quot;]for i in range(3):    plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],&#x27;gray&#x27;)    plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])    plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)    plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])    plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],&#x27;gray&#x27;)    plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])plt.show()

3. 后记至此，实现了常用的四种图像二值化算法。根据不同的需要，选择不同的算法。比如对于这幅图，如果要最佳的二值化显示效果，那么平均值法最好；如果要提取轮廓，那么自适应阈值法最好；如果要获取兔斯基，那么直方图法最好。
4. 书签OpenCV3.4官方文档
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>毕设</tag>
        <tag>opencv</tag>
      </tags>
  </entry>
  <entry>
    <title>图像灰度化</title>
    <url>/dev-gp-image-graying/</url>
    <content><![CDATA[1. 前言离研究生毕业只有六个月了，毕业设计也该动手了。去年开题，选择了《基于图像处理的客观题自动阅卷系统的设计与实现》这个题目。今年中期，考虑了很久，最终还是没有换题目。想要做AIOps，但是自己基础薄弱，也没有特定的方向，到了六月份也不一定能完成。保险起见，还是做阅卷系统吧！
阅卷系统主要有五个方面的研究内容，分别是图像预处理、目标检测、图像分割、手写识别和移动Web。其中图像预处理包括试卷图像的灰度化、二值化、去噪、倾斜矫正等；目标检测是为了定位学号和答案的位置，并且提取出这些信息；图像分割是为了分割学号和多选题的字符；手写识别是为了识别手写字符，包括数字和字母；移动Web是为了方便老师随时随地使用系统。
本文，就来研究一下图像的灰度化。


2. 理论在RGB模型中，如果R=G=B时，则彩色表示一种灰度颜色，其中R=G=B的值叫灰度值，因此，灰度图像每个像素只需一个字节存放灰度值（又称强度值、亮度值），灰度范围为0-255。一般有以下四种方法对彩色图像进行灰度化：
1、分量法将彩色图像中的三分量的亮度作为三个灰度图像的灰度值，可根据应用需要选取一种灰度图像。f1(i,j)=R(i,j) f2(i,j)=G(i,j) f3(i,j)=B(i,j) 其中fk(i,j)(k=1,2,3)为转换后的灰度图像在(i,j)处的灰度值。
2、最大值法 　　将彩色图像中的三分量亮度的最大值作为灰度图的灰度值。f(i,j)=max(R(i,j),G(i,j),B(i,j))
3、平均值法 　　将彩色图像中的三分量亮度求平均得到一个灰度图。f(i,j)=(R(i,j)+G(i,j)+B(i,j))/3
4、加权平均法 　　根据重要性及其它指标，将三个分量以不同的权值进行加权平均。由于人眼对绿色的敏感最高，对蓝色敏感最低，因此，按下式对RGB三分量进行加权平均能得到较合理的灰度图像。f(i,j)=0.30R(i,j)+0.59G(i,j)+0.11B(i,j))
更多内容参考图像分析。
3. 实践算法的实现，主要参考Python下OpenCV的使用 – 图像灰度化。
3.1. 安装opencv这个毕业设计，打算使用python来实现，编辑器使用pycharm。图像处理方面，主要使用opencv。首先安装opencv，很简单，一条命令即可：pip install opencv-python
如果离线安装的话，不妨参考使用OpenCV实现实时视频目标检测。
安装完成后，命令行中输入python，进入python shell。输入import cv2回车，如果没有报错，说明安装成功。
3.2. 简单灰度化在加载图片的时候，直接调用opencv的灰度化方法。
import cv2image = cv2.imread(&#x27;image/test.jpg&#x27;)grayimage = cv2.imread(&#x27;image/test.jpg&#x27;,cv2.IMREAD_GRAYSCALE)cv2.imshow(&#x27;image&#x27;,image)cv2.imshow(&#x27;grayimage&#x27;,grayimage)cv2.waitKey(0)

3.3. 分量法import cv2image = cv2.imread(&#x27;image/test.jpg&#x27;)b,g,r = cv2.split(image)   # the order is not r,g,bcv2.namedWindow(&quot;Image&quot;)cv2.imshow(&quot;Image&quot;,image)cv2.namedWindow(&quot;ImageR&quot;)cv2.imshow(&quot;ImageR&quot;,r)cv2.namedWindow(&quot;ImageG&quot;)cv2.imshow(&quot;ImageG&quot;,g)cv2.namedWindow(&quot;ImageB&quot;)cv2.imshow(&quot;ImageB&quot;,b)cv2.waitKey(0)cv2.destroyAllWindows()

3.4. 最大值法import cv2import numpyimage = cv2.imread(&#x27;image/test.jpg&#x27;)shape = (image.shape[0],image.shape[1])newImage = numpy.ndarray(shape,image.dtype)for i in range(image.shape[0]):    for j in range(image.shape[1]):       newImage[i,j] = max(image[i,j][0],image[i,j][1],image[i,j][2])cv2.namedWindow(&quot;NewImageMax&quot;)cv2.imshow(&quot;NewImageMax&quot;,newImage)cv2.waitKey(0)cv2.destroyAllWindows()

3.5. 平均值法import cv2import numpyimage = cv2.imread(&#x27;image/test.jpg&#x27;)shape = (image.shape[0],image.shape[1])newImage = numpy.ndarray(shape,image.dtype)for i in range(image.shape[0]):    for j in range(image.shape[1]):       newImage[i,j] = (int(image[i,j][0]) + int(image[i,j][1]) + int(image[i,j][2])) / 3cv2.namedWindow(&quot;NewImageAver&quot;)cv2.imshow(&quot;NewImageAver&quot;,newImage)cv2.waitKey(0)cv2.destroyAllWindows()

3.6. 加权平均法import cv2import numpyimage = cv2.imread(&#x27;image/test.jpg&#x27;)shape = (image.shape[0],image.shape[1])newImage = numpy.ndarray(shape,image.dtype)for i in range(image.shape[0]):    for j in range(image.shape[1]):        newImage[i,j] = 0.11 * image[i,j][0] + 0.59 * image[i,j][1] + 0.30 * image[i,j][2]cv2.namedWindow(&quot;NewImageWeightAver&quot;)cv2.imshow(&quot;NewImageWeightAver&quot;,newImage)cv2.waitKey(0)cv2.destroyAllWindows()

4. 后记关于图像灰度化，就整理到这里，之后会继续研究其他知识点。
5. 书签opencv-python快速入门篇
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>毕设</tag>
        <tag>opencv</tag>
      </tags>
  </entry>
  <entry>
    <title>使用Libvirt进行虚拟机在线迁移</title>
    <url>/dev-libvirt-vm-live-migration/</url>
    <content><![CDATA[1. 前言《虚拟机在线迁移实验》一文中提到，迁移过程中的downtime不合理，但是没有找到原因。
本文，我们来使用Libvirt进行虚拟机在线迁移，如果downtime在毫秒级，那么说明问题出在OpenStack上。参考An analysis of the performance of live migration in Openstack的评论部分，可以发现有人遇到的同样的问题，downtime远大于1s。作者的猜测是libvirt的设置引起的，也可能是由于迁移后网络适应速度较慢造成的。
参考文档：KVM 虚拟机在物理主机之间迁移的实现


2. 实验环境参考《使用Libvirt创建虚拟机》一文，在node0和node1两个节点上安装libvirt和qemu，IP分别为192.168.56.200和192.168.56.201。
2.1. NFS要想进行虚拟机的迁移，首先迁出主机（node0）和迁入主机（node1）存放虚拟机磁盘的目录结构要相同。
参考文档《Linux中安装NFS》，在node0安装nfs-server，node1安装nfs-client。
配置node0的nfs服务路径为：
192.168.56.200:/opt/qemu

配置node1挂载nfs目录：
mkdir /opt/qemumount -t nfs 192.168.56.200:/opt/qemu /opt/qemuchmod -R 777 /opt/qemu

2.2. libvirt端口配置1、测试连接virsh -c qemu+tcp://node1/system
提示：
error: failed to connect to the hypervisorerror: unable to connect to server at &#x27;node1:16509&#x27;: Connection refused

2、在node0和node1调整libvirt的配置vim /etc/libvirt/libvirtd.conf，如下修改：
# line 22, uncommentlisten_tls = 0# line 33, uncommentlisten_tcp = 1# line 45, uncommenttcp_port = &quot;16509&quot;# line 55, uncomment and changelisten_addr = &quot;0.0.0.0&quot;# line 163, uncomment and changeauth_tcp = &quot;none&quot;

3、重启libvirtservice libvirtd restart
4、查看libvirt服务netstat -anpt | grep libvirt理论上应该看到监听16509，但是没有。
5、修改libvirt-bin.conf配置（可选）vim /etc/init/libvirt-bin.conf，如下修改：
# line 11, add -lenv libvirtd_opts=&quot;-d -l&quot;

6、重启libvirt-bin（可选）service libvirt-bin restart依然没有监听16509端口。
vim /etc/init/libvirt-bin.conf，如下修改：
# line 58, add -lexec /usr/sbin/libvirtd $libvirtd_opts -l
重启libvirt-bin依然无效。
7、还原libvirt-bin.conf，修改libvirt-binvim /etc/default/libvirt-bin，如下修改：
# line 8, uncomment and changelibvirtd_opts=&quot;-d -l&quot;
重启libvirt-bin，发现端口监听成功。
8、再次测试连接virsh -c qemu+tcp://node1/system连接成功，nice。
2.3. 免密登录虚拟机迁移，可以使用普通用户，也可以使用root用户。如果是普通用户进行迁移，那么在输入迁移命令后，需要输入node1中普通用户的密码。如果是root用户进行迁移，那么在输入迁移命令后，同样需要输入node1中root用户的密码。但是，root用户并没有启用密码登录，所以，需要给root用户设置免密登录。
1、在node0上创建密钥对ssh-keygen所有的提示按enter键即可，完成后在home目录执行ll .ssh，即可看到创建好的id_rsa和id_rsa.pub文件。
2、把公钥拷贝到所有客户机中ssh-copy-id -i .ssh/id_rsa.pub -p 22 voidking@node1
3、在node1中使用root用户执行cp -r /home/voidking/.ssh ~
同样的，也给node1添加免密登录node0。
3. 虚拟机迁移参考kvm虚拟机迁移，进行虚拟机动态迁移。
1、在node0查看虚拟机virsh list
2、虚拟机迁移virsh migrate --live --verbose cirros qemu+ssh://node1/system tcp://node1 --unsafe其中node1可以换成root@node1。
4. 后记本来打算在《Libvirt虚拟机网络配置》一文中配置好网络，使得node0可以访问到node1中的实例，node1可以访问到node0中的实例。但是，配置失败了。所以，就无法测试虚拟机迁移的停机时间了。如果之后解决了桥接的问题，就再来更新。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>storage</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>存储</tag>
        <tag>ubuntu</tag>
        <tag>libvirt</tag>
        <tag>virsh</tag>
      </tags>
  </entry>
  <entry>
    <title>IPv6入门篇</title>
    <url>/dev-ipv6-start/</url>
    <content><![CDATA[1. IPv6简介IPv6，全称Internet Protocol version 6，是用于网络层的一种新的网络协议，被设计用来取代目前主流的IPv4。由于IPv4的地址资源有限，且无法满足日益增长的互联网设备数量，因此引入了IPv6这种新的网络协议。
在互联网世界里，每一台连接到网络的设备都需要一个独特的标识，就如同每一栋房子都需要一个独特的地址一样。这个唯一的标识我们称之为“IP地址”。IPv4是现在最常见的IP地址类型，然而，由于IPv4的设计限制，其只能支持约42亿个地址。虽然当初设计IPv4的时候，42亿个地址看起来已经相当大了，然而在今天，由于互联网的飞速发展，这些地址已经分配的差不多了。而IPv6则可以提供远超于IPv4的地址数量，理论上可达到340多万亿亿亿个。
IPv6的一个关键特性是其128位的地址长度，质的飞跃让IPv6的地址空间从IPv4的42亿增加到了可供每个地球表面的杆菌都拥有自己的网络的程度，适应了互联网物联网时代的发展。
参考文档：

WikiPedia - IPv6
Internet Protocol Version 6 Address Space
GRH DFP pages
IPv6 工具箱使用文档
IPv6 DNS 地址列表



2. IPv6地址的组成IPv6地址长度为128位，由8组16位的二进制数表示（每4位二进制数可以表示为一个十六进制数），每组之间以冒号(:)分隔。例如：2031:0000:130F:0000:0000:09C0:876A:130B 这样的数值。
与IPv4类似，IPv6也有不同种类的地址，包括单播、多播和任播地址。单播地址就是普通的网络地址，一般用于标识单一的网络接口。多播地址则用于一次传输信息给多个目的地址。而任播地址是IPv6中的一个新特性，用于将数据发送到任何一个接口。
此外，IPv6还引入了一个新概念——区域标识(zone index)，用于在同一台设备上区分多个相同地址的接口。
3. IPv6的特性和功能IPv6相比IPv4有很多显著的优势，主要体现在以下几个方面：

更大的地址空间：IPv6的地址长度为128位，比32位的IPv4提供了更大的地址空间。
寻址方式更灵活：IPv6的寻址方式更加灵活和统一，支持多种寻址方式，如全球单播地址、全球组播地址、本地链接地址等。
更高的传输效率：由于IPv6头部的结构更加简洁，可以提高路由的转发效率和性能。
更强的安全性：IPv6原生支持IPSec，可以提供数据的保密性、完整性和认证性。
更好的质量服务(QoS)：IPv6可以根据流的优先级、可用带宽等信息提供更好的服务质量。
更好的移动性和可扩展性：IPv6自带的移动性和自我配置功能，使得设备可以在不改变IP地址的情况下轻松移动。这一特性使得IPv6非常适合于无线和移动网络。

4. IPv6地址分类IPv6地址可分为三种：单播（unicast）地址、多播（multicast）地址 和 任播（anycast）地址。
4.1. 单播（unicast）地址单播地址标示一个网络接口。协议会把送往地址的数据包送往给其接口。IPv6的单播地址可以有一个代表特殊地址名字的范畴，如链路本地地址（link local address）和唯一区域地址（ULA，unique local address）。单播地址包括可聚类的全球单播地址、链路本地地址等。
全球单播地址：全球单播地址类似于IPv4中的公网地址，可以在整个互联网上进行路由。这种地址的前缀通常为2000::/3，也就是地址的第三位为2或3。中间部分（40-64位）通常是网络运营商分配的前缀，后面部分（65-128位）是用户自定义的接口标识符。
链路本地地址：链路本地地址类似于IPv4中的私有地址，主要用于单个网络中设备间的通信。这种地址的前缀通常为FE80::/10。
4.2. 多播（multicast）地址多播地址也称组播地址。多播地址被指定到一群不同的接口，送到多播地址的数据包会被发送到所有的地址。多播地址由皆为一的字节起始，亦即：它们的前置为FF00::/8。其第二个字节的最后四个比特用以标明”范畴”。一般有node-local(0x1)、link-local(0x2)、site-local(0x5)、organization-local(0x8)和global(0xE)。多播地址中的最低112位会组成多播组群标识符，不过因为传统方法是从MAC地址产生，故只有组群标识符中的最低32位有使用。定义过的组群标识符有用于所有节点的多播地址0x1和用于所有路由器的0x2。另一个多播组群的地址为”solicited-node多播地址”，是由前置FF02::1:FF00:0/104和剩余的组群标识符（最低24位）所组成。这些地址允许经由邻居发现协议（NDP，Neighbor Discovery Protocol）来解译链接层地址，因而不用干扰到在区网内的所有节点。
值得注意的是，IPv6中没有所谓的“广播地址”。在IPv4中，广播地址用于将数据包发送到网络中的所有设备，而在IPv6中，这个功能由组播地址代替。
4.3. 任播（anycast）地址任播像是Unicast（单点传播）与Broadcast（多点广播）的综合。单点广播在来源和目的地间直接进行通信；多点广播存在于单一来源和多个目的地进行通信。而Anycast则在以上两者之间，它像多点广播（Broadcast）一样，会有一组接收节点的地址列表，但指定为Anycast的数据包，只会发送给距离最近或发送成本最低（根据路由表来判断）的其中一个接收地址，当该接收地址收到数据包并进行回应，且加入后续的传输。该接收列表的其他节点，会知道某个节点地址已经回应了，它们就不再加入后续的传输作业。以目前的应用为例，Anycast地址只能分配给中间设备（如路由器、三层交换机等），不能分配给终端设备（手机、电脑等），而且不能作为发送端的地址。
5. 特殊IPv6地址5.1. 未指定地址::/128：所有比特皆为零的地址称作未指定地址，完整地址为0:0:0:0:0:0:0:0/128。类似于IPv4中的0.0.0.0，被用来表示一个未知的、未指定的或无效的IP地址。这个地址不可指定给某个网络接口，并且只有在主机尚未知道其来源IP时，才会用于软件中。路由器不可转送包含未指定地址的数据包。这个地址一般在一个设备获取新的IP地址时被使用，例如，在启动、重启或在没有其他静态或动态IPv6配置时。
5.2. 链路本地地址::1/128：是一种单播绕回地址（循环地址），完整地址为0:0:0:0:0:0:0:1/128。如果一个应用程序将数据包送到此地址，IPv6堆栈会转送这些数据包绕回到同样的虚拟接口（相当于IPv4中的127.0.0.1/8），用于设备自我测试和配置。
fe80::/10：链路本地地址是在单个网络段中使用的，它们在路由器之外不可路由。这些地址主要用于在同一网络链路内的通信，例如邻居发现邻居协议。这有点类似于IPv4中的169.254.0.0/16。
5.3. 唯一区域地址fc00::/7：唯一区域地址（ULA，unique local address）只可用于本地通信，类似于IPv4的1918专用网络地址10.0.0.0/8、172.16.0.0/12和192.168.0.0/16。这定义在RFC 4193中，是用来取代站点本地位域。这地址包含一个40比特的伪随机数，以减少当网站合并或数据包误传到网络时碰撞的风险。这些地址除了只能用于区域外，还具备全局性的范畴，这点违反了唯一区域位域所取代的站点本地地址的定义。
如果不使用唯一区域地址，其实也可以使用全球单播地址（2000::/3）来实现本地通信，因为IPv6地址空间非常大，因此对于内网设备可以直接分配全球单播地址，而不再需要进行地址转换。
5.4. 多播地址ff00::/8：这个前置表明定义在”IP Version 6 Addressing Architecture”（RFC 4291）中的多播地址。其中，有些地址已用于指定特殊协议，如ff0X::101对应所有区域的NTP服务器（RFC 2375）。
本地多播地址（FF01::/16）：本地多播地址只在同一节点的应用之间使用，例如本机的所有网络接口。
5.5. 请求节点多播地址（Solicited-node multicast address）ff02::1:FFXX:XXXX：XX:XXXX 为相对应的单播或任播地址中的三个最低的字节。
5.6. IPv4转译地址::ffff:x.x.x.x/96：用于IPv4映射地址。2001::/32：用于Teredo隧道。2002::/16：用于6to4。
5.7. ORCHID2001:10::/28：ORCHID (Overlay Routable Cryptographic Hash Identifiers)（RFC 4843）。这些是不可绕送的IPv6地址，用于加密散列识别。
5.8. 文件2001:db8::/32：这前置用于文件（RFC 3849）。这些地址应用于IPV6地址的示例中，或描述网络架构。
5.9. 遭舍弃或删除的用法::/96：这个前置曾用于IPv4兼容地址，现已删除。fec0::/10：这个站点本地前置指明这地址只在组织内合法。它已在2004年9月的RFC3879中舍弃，并且新系统不应该支持这类型的地址。
6. IPv6配置方法与IPv4相比，IPv6的配置方法有一些不同。下面是IPv6的三种常见配置方法：

自动配置（Automatic Configuration）：IPv6提供了一种称为SLAAC（Stateless Address Autoconfiguration）的自动配置机制。在SLAAC中，设备可以从接入网络的IPv6路由器上接收到一个前缀，然后使用自动产生的接口标识符生成一个全局唯一的IPv6地址。此外，SLAAC还通过邻居发现协议（Neighbor Discovery Protocol）自动发现设备周围的其他IPv6地址。
DHCPv6（Dynamic Host Configuration Protocol for IPv6）：IPv6支持使用DHCPv6来分配IPv6地址。类似于IPv4中的DHCP，DHCPv6服务器可以为设备分配IPv6地址、网关、DNS服务器和其他配置信息。这种方式适用于需要更大灵活性和管理控制的情况。
手动配置（Manual Configuration）：与IPv4手动配置类似，可以手动为设备分配IPv6地址。管理员可以在设备上手动设置IPv6地址、子网掩码、网关和DNS服务器等。这种方式适用于小型网络或需要精确控制的情况。

需要注意的是，IPv6的配置方法可能因操作系统、网络设备或网络规模的不同而有所差异。此外，在实际网络中，常常使用组合配置的方式，例如结合SLAAC和DHCPv6来为设备分配IPv6地址和其他配置信息。
PS：IPv6引入了临时地址（Temporary Address）的概念，用于提高设备的隐私和安全性。设备可以在生成全球唯一的IPv6地址之外，生成一个临时的IPv6地址。临时地址在一段时间后会自动更换，防止设备的长期跟踪和定位。
7. 测试IPv67.1. 测试公网出口IPv6浏览器访问 IPv6 连接测试 或者 IPADDRESS.COM，即可看到有没有公网出口IPv6。
7.2. 测试本机是否配置了IPv67.2.1. 本地查看本机IPv6 IPifconfig
查看inet6地址。
7.2.2. ping测试ping -6 2001:0db8:85a3:0000:0000:8a2e:0370:7334 # windowsping6 2001:0db8:85a3:0000:0000:8a2e:0370:7334 # linux/macos

如果是ping链路本地地址，那么需要加上网卡名称。
ping6 fe80::1c57:b137:5d25:f5ec%en0

fe80:: 开头的地址是链路本地地址。这种类型的IPv6地址只能用来在同一个网络链路上（同一个交换机）的设备之间进行通信。如果要ping链路本地地址，除了ip地址外，还需要指定网络接口。
7.2.3. 外部看到的本机IPv6# 查询本机外网IPv4地址curl 4.ipw.cn# 查询本机外网IPv6地址curl 6.ipw.cn# 测试网络是IPv4还是IPv6访问优先(访问IPv4/IPv6双栈站点，如果返回IPv6地址，则IPv6访问优先)curl test.ipw.cn

7.3. 测试本机是否可以访问公网IPv6ping6 2402:4e00::ping6 2400:3200::1

7.4. 测试本机IPv6是否可以被访问1、找另外一台主机ping自己的本机IPv6
2、浏览器访问IPv6 Ping 测试，在这个页面 ping 自己的本机IPv6。
8. 后记尽管IPv6在技术上具有很多优势，但实际上仍存在一些挑战。其中一个主要的挑战是IPv6的部署和适配问题，因为在很多网络设备和应用程序中仍然广泛使用IPv4。因此，推动IPv6的广泛采用需要各方的合作和努力。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
      </tags>
  </entry>
  <entry>
    <title>Libvirt虚拟机网络配置</title>
    <url>/dev-libvirt-vm-network/</url>
    <content><![CDATA[1. 前言《使用Libvirt创建虚拟机》一文中，我们学会了使用Libvirt创建虚拟机的方法。但是，文中创建的虚拟机只能在宿主机node0上访问，node1就无法访问，也就是说虚拟机和宿主机外部的网络还没有打通。
本文，就来研究一下虚拟机网络配置的方法，使得node1也可以访问到node0中的虚拟机。


2. 网络配置1最简单的实现方法，就是VM要有一个对外提供服务的IP，这个IP不依赖于宿主机，而是和宿主机的IP同级。
参考KVM 以及桥接网络配置和libvirt kvm 虚拟机上网 – Bridge桥接进行配置。
1、在node0的网络配置中进行修改：
#auto eth0#iface eth0 inet static#address 192.168.56.200#netmask 255.255.255.0auto eth0iface eth0 inet manualup ifconfig $IFACE 0.0.0.0 upup ifconfig $IFACE promiscauto br0iface br0 inet staticaddress 192.168.56.200netmask 255.255.255.0gateway 192.168.56.1bridge_ports eth0bridge_stp offbridge_fd 0bridge_maxwait 0

使用br0替代eth0，eth0做流量转发。
2、重启网络
/etc/init.d/networking restart

3、修改路由（可选）
route -nroute del -net 192.168.56.0/24 dev eth0route add -net 192.168.56.0/24 dev br0

同样的，在node1中也配置br0。
下面参考虚拟机在线添加网卡，添加网卡。
4、添加网卡
virsh domiflist cirrosvirsh attach-interface cirros --type bridge --source br0 --configvirsh domiflist cirros
如果在实例开机状态下添加网卡，需要重启实例后才能看到。
5、网卡信息保存到配置文件
virsh dumpxml cirros &gt;/etc/libvirt/qemu/cirros.xml virsh define /etc/libvirt/qemu/cirros.xml 

PS：删除网卡
virsh detach-interface cirros --type bridge --mac 52:54:00:09:36:ad

6、登录实例后，配置新网卡的IP
sudo vi /etc/network/interfaces

添加eth1的配置：
auto eth1iface eth1 inet staticaddress 192.168.56.202netmask 255.255.255.0

7、重启实例
sudo reboot
至此，实例桥接网络配置完成。
8、测试网络在node0上ping 192.168.56.202 -c2，正常。在node1上ping 192.168.56.202 -c2，不通。在virtualbox宿主机上ping 192.168.56.202 -c2，不通。
以上，我们的配置失败了，因为除了node0，其他机器无法访问实例。下面我们来分析一下当前网络，如下图：经过测试，node0到switch0是通的，node0到vm0是通的，但是vm0到switch0不通。诡异，理论上应该是通的啊！
3. 网络配置2那么，怎么打通vm0到switch0呢？最容易想到的，是把vm0直接连接到eth0上，而br0单纯作为node0的管理网络。修改后的方案如下图：
1、修改cirros配置
virsh edit cirros

找到：
&lt;interface type=&#x27;bridge&#x27;&gt;  &lt;mac address=&#x27;52:54:00:09:36:ad&#x27;/&gt;  &lt;source bridge=&#x27;br0&#x27;/&gt;  &lt;model type=&#x27;rtl8139&#x27;/&gt;  &lt;address type=&#x27;pci&#x27; domain=&#x27;0x0000&#x27; bus=&#x27;0x00&#x27; slot=&#x27;0x06&#x27; function=&#x27;0x0&#x27;/&gt;&lt;/interface&gt;
把source中的br0改为eth0。
2、重启实例
virsh shutdown cirrosvirsh start cirros

报错：
error: Failed to start domain cirroserror: Unable to add bridge eth0 port vnet1: Operation not supported
想法是美好的，但是行不通啊！
参考Attaching a Virtual NIC Directly to a Physical Interface进行修改，但是又报出了其他错误：
error: Failed to start domain cirroserror: error creating macvtap interface macvtap0@eth0 (52:54:00:c8:33:b3): Device or resource busy
参考Using KVM with Libvirt and macvtap Interfaces也许可以解决，但是感觉好麻烦，所以决定放弃这个方案。
4. 网络配置3如果，再创建一个网桥，这个网桥不指定IP呢？如下图：
1、创建新网桥br1
vim /etc/network/interfaces

添加：
auto br1iface br1 inet dhcpbridge_ports eth0bridge_stp offbridge_fd 0bridge_maxwait 0

2、启动br1
ifup br1

报错：
device eth0 is already a member of a bridge; can&#x27;t enslave it to bridge br1.Internet Systems Consortium DHCP Client 4.3.3......
由上可知，一个物理网卡，只能绑定到一个网桥上。所以，这个想法也是行不通的。
5. 网络配置4那么，把br1绑定到eth2（eth1是上网网卡）上面呢？如下图：
1、修改br1配置
vim /etc/network/interfaces

如下修改：
auto eth2iface eth2 inet manualup ifconfig $IFACE 0.0.0.0 upup ifconfig $IFACE promiscauto br1iface br1 inet dhcpbridge_ports eth2bridge_stp offbridge_fd 0bridge_maxwait 0

2、启动br1
ifup eth2ifup br1

3、编辑cirros配置，修改br0为br1，重启cirros。结果，vm0到switch0依然不通。证明这个方案也不行。
6. 网络配置5网络配置2、3、4都不行，看来拍脑门想出来的方法不太靠谱。实际上网络配置1应该是可以的，但是，却无法连通，问题出在哪里呢？莫非，是因为virtualbox？virtualbox有多种网络模式，node1和node2使用的是Host-only Adapter，如果换成Bridged Adapter呢？那就试试吧！结果，依然失败。
7. 后记还能不能愉快地玩耍了？明明很简单的桥接，为啥就不通呢？诡异，实在诡异。郝同学表示已经无能为力，留个坑吧！
8. 书签
KVM/Networking
Networking
VirtualNetworking
KVM and OVS on Ubuntu 16.04
ubuntu16.04 配置kvm openvswitch 桥接环境
Open vSwitch + libvirt 搭建vlan网络
How to Put a KVM Guest Domain on a Bridged Network

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>ubuntu</tag>
        <tag>libvirt</tag>
        <tag>virsh</tag>
      </tags>
  </entry>
  <entry>
    <title>组网技术概述</title>
    <url>/dev-network-building/</url>
    <content><![CDATA[1. 前言组网技术是指网络组建技术，分为以太网组网技术和局域网组网技术。而在工作和生活中，我们最常使用到的是局域网组网技术：Mesh组网、中继组网 和 AC+AP组网。本文中，我们就来学习一下这些局域网组网技术。
参考文档：

组网技术与配置



2. Mesh组网2.1. Mesh组网概述Mesh组网可以实现WiFi信号的全屋覆盖，并且在WiFi信号的切换过程中实现无缝漫游，拥有无感式切换的高速上网体验。
Mesh组网有两种方式：无线组网和有线组网。无线组网是主路由和子路由之间通过WiFi连接，路由位置的摆放不受布线的限制。有线组网是主路由和子路由之间通过网线连接，有线Mesh相比对无线Mesh更稳定，但无线Mesh相比对有线Mesh布置更自由和美观。
参考文档：

Mesh组网教程（有线+无线）

2.2. 有线Mesh组网1、主路由配置能正常上网
2、主路由开启Mesh组网功能
3、子路由通电，恢复出厂状态
4、子路由WAN口，连接主路由LAN口
5、子路由等待WAN口灯亮和网络灯常亮，表示组网成功
2.3. 无线Mesh组网1、主路由配置能正常上网
2、主路由开启Mesh组网功能
3、子路由通电，恢复出厂状态
4、同时按下主路由和子路由的WPS键，等待主路由和子路由的网络灯开始闪烁
5、继续等待，子路由网络灯常亮，表示组网成功
3. 单线复用Mesh组网3.1. 单线复用Mesh组网概述如果装修时弱电箱和客厅之间预留有两根网线，那么完全可以正常进行Mesh组网，主路由放在客厅。如果弱电箱和客厅之间只有一根网线，那么我们可以把主路由放到弱电箱里，其他子路由发射WIFI信号。但是这样就浪费了主路由的WIFI能力，而且弱电箱不方便散热，主路由性能会收到影响。那么，如果弱电箱和客厅之间只有一根网线，主路由还不想放到弱电箱，该怎么办？这时候就需要单线复用Mesh组网技术。单线复用Mesh组网，和正常Mesh组网，速度方面没什么差别，只不过需要多准备两台vlan交换机（管理交换机）。
3.2. 单线复用Mesh组网架构常用的单线复用Mesh组网架构有两种：iptv独占端口 和 iptv不占端口。至于单臂路由，稍微复杂，本文不做讨论。
参考资料：

小红书旋律果子 - 网工级硬核组网科普 - Vlan，单臂路由，网管交换机，单线复用
小红书旋律果子 - 网工级硬核组网科普 - 交换机Vlan配置
小红书阿列 - 全屋wifi第二弹，一根网线实现全屋mesh组网

3.3. 组网相关概念
VLAN（Virtual Local Area Network）：虚拟机局域网
VID（VLAN ID）：VLAN编号，编号相同的表示同一个虚拟局域网。交换机中的每一个虚拟局域网，都会对端口进行逻辑划分，包括untagged端口（成员端口）、tagged端口和非成员端口。
untagged端口：untagged端口表示在某个端口上时数据包状态为没有标志。终端设备的数据包（二层帧）里是没有tag的，交换机不知道怎么转发，终端设备数据经过untagged端口进入交换机会加上tag，交换机就知道怎样进行转发了。
tagged端口：tagged端口表示在某个端口上时数据包状态为有标志。交换机中的数据包，都是tagged数据包，通过tagged端口转发给其他交换机。
非成员端口：虚拟局域网中不包含的端口。 
PVID（Port-base VLAN ID）：交换机物理端口的唯一标志。经过端口的数据包会被打上tag，tag的值就是PVID（默认为1），和VID对应，以便交换机识别处理。
Access端口：Access端口是物理端口，和untagged端口对应，是交换机上连接终端设备的端口。
Trunk端口：Trunk端口是物理端口，和tagged端口对应，是交换机上用来和其他交换机连接的端口。Trunk端口允许多个VID通过。
Hybrid端口：Hybrid是Access与Trunk的混合模式，它允许VID=PVID。Hybrid与Trunk一样，在该端口上可以传送多个VLAN的包，一般用于交换机与交换机之间，或者交换机与服务器之间的连接。如果收到的数据包不带VLAN，则加上PVID进行转发；如果收到的数据包带VLAN，则判断该端口是否允许该VLAN进入，如果可以则进行转发，否则丢弃。

3.4. 自用Mesh组网架构个人更倾向于 iptv独占端口 组网架构，配置思路简单清晰。

vlan交换机配置：

交换机A 端口1-8 默认VID为1，全部untagged
交换机A 端口1 配置VID为101
交换机A 端口2 配置VID为102
交换机A 端口4-8 配置VID为103
交换机A 端口3 配置为trunk，允许vlan1、vlan2、vlan3通过
交换机B 端口1-8 默认VID为1
交换机B 端口1 配置VID为101
交换机B 端口2 配置VID为102
交换机B 端口4-8 配置VID为103
交换机B 端口3 配置为trunk，允许vlan1、vlan2、vlan3通过

端口连接：

光猫任意网络端口连接交换机A 端口1
光猫iptv端口连接交换机A 端口2
交换机A 端口3 连接 交换机B 端口3
交换机B 端口1 连接 主路由WAN
交换机B 端口2 连接 IPTV设备
主路由LAN任意端口 连接 交换机B 端口4
交换机A 端口4-8 连接 子路由，交换机B 端口5-8 连接 子路由

附上架构图原图以便看官们修改：mesh.drawio
交换机配置方法参考文档：MERCURY全千兆智能网管交换机用户手册
注意1：如果是光猫拨号，那么主路由器开启ipv6后，可能依然无法使用ipv6。此时需要把IPv6信息获取模式改为手动，IPv6配置信息照抄光猫。其中，网关保持默认 fe80::1 即可。
注意2：如果是光猫拨号，主路由器开启了ipv6，但是设备获取的ipv6经常会失效，那么建议使用路由器拨号。
4. 拨号说明4.1. 光猫拨号光猫：负责光电转换、拨号、路由、NAT转换、DHCP、防火墙路由器：负责路由、NAT转换、DHCP、防火墙
上网链路：终端设备 -&gt; 路由器NAT -&gt; 光猫NAT -&gt; 光猫光电转换 -&gt; 宽带供应商NAT -&gt; 公网IP
4.2. 路由器拨号光猫：负责光电转换路由器：负责拨号、路由、NAT转换、DHCP、防火墙
上网链路：终端设备 -&gt; 路由器NAT -&gt; 光猫光电转换 -&gt;宽带供应商NAT -&gt; 公网IP
4.3. 拨号建议建议路由器拨号，让光猫只负责光电转换。

光猫性能有限，只做光电转换，光电转换能力会有些提升，上网速度会更快。
终端设备到家庭宽带出口只会经过一次NAT地址转换，上网速度会更快。

5. 中继组网5.1. 中继组网概述有些老的路由器，是不支持Mesh组网的。不同品牌的路由器，是不能Mesh组网的。因为Mesh组网一般使用的是路由器厂商的私有技术，不同厂商之间不兼容。
在无法使用Mesh组网的情况下，我们可以使用中继组网，最好是有线中继组网。中继组网的原理其实很简单。当我们的无线设备（比如手机或电脑）距离无线路由器过远，信号弱，无法正常连接时，我们可以在中间放置一个无线中继器。我们的设备连接无线中继器，而无线中继器再连接无线路由器，完成一种“中继”的流程，这就是中继组网。中继器会接收到路由器的信号，然后放大并传递，这样就可以扩大无线网络的覆盖范围。
5.2. 无线中继组网 VS 无线Mesh组网无线中继组网有以下缺点：

由于中继器需要接收并重新发送信号，这会导致网络的带宽减半，影响网络速度。
如果网络中添加了过多的中继器，可能会导致网络延时增大，影响网络体验。
中继器的覆盖范围有限，如果距离中继器过远，信号依然会衰弱，不能解决大范围的信号覆盖问题。

Mesh网络可以解决以上问题：

在Mesh网络中，每个节点都可以直接与其他节点通信，不需要经过中继器的“中继”。这就解决了网络带宽减半的问题，能够提供更好的网络速度。
Mesh网络具有自我修复和自我优化的功能。它可以自动选择最佳的通信路径，如果某个节点出现问题，网络可以自动绕过这个节点，保证信息的顺利传递，这就解决了网络延迟的问题。
Mesh网络的覆盖范围大。只要在需要覆盖的区域内添加足够的Mesh节点，都可以提供稳定的无线信号，解决了大范围信号覆盖的问题。

5.3. 有线中继组网有线中继组网，也被称为以太网回程或有线回程，可以解决部分无线中继组网的缺点。

网络带宽：与无线中继组网相比，有线中继组网不会导致网络带宽减半，因为它通过有线（一般是以太网线）连接主路由器与中继器，并不占用无线的带宽，所以无线网络速度不会受到影响。
网络延迟：因为有线连接的稳定性比无线连接更高，所以有线中继组网的网络延迟也会相对较低。
信号覆盖：有线中继可以有效扩大无线网的覆盖范围，但如果需要覆盖的区域较大或有复杂的建筑结构，还是需要布设较多的中继器，且每增加一个中继，网络的复杂性也会增加。

总的来说，有线中继组网对于应对带宽减半和网络延迟的问题确实有较好的效果，但也存在一定的局限，尤其是需要布线，安装比较复杂，且想要实现大范围的稳定覆盖，可能还需配合使用其他技术或方案。
6. AC+AP组网6.1. AC+AP组网概述AC+AP组网是一种常见的WiFi无线覆盖方案。
在这个组网方案中，AC（Access Controller）是访问控制器的缩写，有时也被称为无线控制器。AC是一个中心化的设备或系统，用于管理、控制和监督访问点（AP）的网络活动。AC可以为网络管理员提供一种集中管理和控制无线网络的方式。例如，当需要更改网络设置时，管理员只需要在AC上进行更改，所有的AP都会自动更新。
AP（Access Point）是网络的访问点。AP接收有线网络的信号，并将其转换为无线信号。网络用户通过AP连接到网络。
在AC+AP的组网模式中，AC负责对AP进行集中管理和控制。例如，当一个新的AP添加到网络时，AC会自动检测到它，并将所需的配置信息发送给这个新的AP。同样，当网络的配置需要更改时，只需在AC上进行更改，所有的AP都会自动更新。
总的来说，AC+AP组网方案可以为企业和大型机构提供一个易于管理和控制的无线网络解决方案。
6.2. UniFi设备组网（本节内容来自chatgpt）
UniFi WiFi是一种由Ubiquiti Networks提供的企业级无线局域网（WLAN）解决方案。UniFi WiFi通过集中管理和控制的方式，提供高性能、可扩展和易于部署的无线网络。
UniFi WiFi的主要特点和功能：

集中式管理：UniFi WiFi通过UniFi控制器软件实现集中管理。管理员可以使用控制器软件来配置和监控整个网络，包括无线接入点（AP）、路由器和交换机等设备。这简化了网络管理过程，同时提供了全面的网络可视化和统计信息。
高性能无线接入点：UniFi WiFi提供了一系列高性能的无线接入点，适用于不同规模和需求的网络环境。这些接入点支持最新的无线标准，如802.11ac和802.11ax（Wi-Fi 6），提供更快的数据传输速率和更大的网络容量。
灵活的网络拓扑：UniFi WiFi支持灵活的网络拓扑结构。管理员可以根据需要进行无线接入点的部署，包括单一AP、多个AP的无线覆盖范围扩展，以及网桥模式和无线分离模式等。这样可以根据具体需求实现最佳的无线覆盖和网络性能。
客户端管理和身份验证：UniFi WiFi提供了客户端管理功能，允许管理员对连接到网络的设备进行监控和控制。此外，UniFi WiFi还支持多种身份验证方法，包括预共享密钥（PSK）、WPA Enterprise和RADIUS认证等，以确保网络的安全性和可控性。
强大的网络分析和报告：UniFi控制器软件提供了丰富的网络分析和报告功能。管理员可以查看实时的网络状态、流量分布和用户连接情况，以及历史数据和性能趋势。这些数据和报告有助于网络优化和故障排除。

参考文档：

UniFi AP 介绍
UniFi - 设备采用
UniFi Network - 优化无线客户端连接

7. 修改 Wi-Fi 信道
信道、频道或波道，是信号在通信系统中传输的通道，由信号从发射端传输到接收端所经过的传输媒质所构成。广义的信道定义除了包括传输媒质，还包括传输信号的相关设备。

根据国际电信联盟（ITU）划分的频段，蓝牙、Wi-Fi、WAPI等无线技术都在2.4G频段工作。蓝牙传输技术中，工作频率是2.402～2.480GHz，在蓝牙3.0技术有79个信道，而4.0则降低到40个（由于带宽变大导致，并非坏事）。Wi-Fi 2.4G技术中，工作频率为2.400～2.483GHz，中国采用欧洲/ETSI，有13个信道。在这83.5MHz频带中，每个信道的中心频率相差5MHz，向上向下分别拓展11MHz，带宽22MHz。
Wi-Fi中，每个信道的带宽是22MHz。但是，实际使用中，有效的带宽是20MHz，其中有2MHz是隔离频带，起保护作用。
20MHz信道带宽对应的是65M带宽 ，它的特性是穿透性好 传输的距离远（100米左右）。
40MHz信道带宽对应的是150M带宽 ，它的穿透性差 传输的距离较近 （50米左右)。
因为一个信道的带宽是有限的，所以如果一个信道被附近的很多Wi-Fi使用，那么这些可能会相互干扰，导致网络质量变差。
修改信道，可以尽量避免Wi-Fi互相干扰导致的网络质量变差。
1、检测信道状态对于macos用户，打开wireless Diagnostics（无线诊断），忽略打开的窗口。菜单栏点击窗口，扫描，看到网络信息中，频段就是信道。当前使用的wifi信息会被加粗。按频段排序，可以看到哪些频段繁忙，哪些频段空闲。
PS：按住option，单击桌面wifi图标，可以直接显示当前使用wifi信息（包括信道）。
2、修改信道登录路由器设置页面，修改信道到空闲的信道。
参考文档：

信道
信道是什么
无线局域网信道列表
WIFI中频段、信道、信道带宽、传输速率无线2.4GHz和5GHz的区别？
Wi-Fi 路由器和接入点的推荐设置
如何修改信道？
无线路由器怎么修改WiFi信道

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>chatgpt</tag>
      </tags>
  </entry>
  <entry>
    <title>使用Libvirt创建虚拟机</title>
    <url>/dev-libvirt-create-vm/</url>
    <content><![CDATA[1. 前言本文中，我们学习一下qemu和libvirt，分别使用它们创建一个cirros虚拟机，并配置好网络。
宿主机node0的系统为ubuntu16，IP为192.168.56.200。


2. qemu和libvirt简介QEMU是一套由Fabrice Bellard编写的模拟处理器的自由软件，它是一个完整的可以单独运行的软件，可以独立模拟出整台计算机，包括CPU，内存，IO设备，通过一个特殊的“重编译器”对特定的处理器的二进制代码进行翻译，从而具有了跨平台的通用性。
总的来说，QEMU是一个独立的虚拟化解决方案，并不依赖KVM，它本身自己可以做CPU和内存的模拟，只不过效率较低。
KVM（Kernel-based Virtual Machine）是另一套虚拟化解决方案，实现对CPU的底层虚拟化和内存的虚拟化，使Linux内核成为虚拟化层，需要x86架构的、支持虚拟化功能的硬件支持（比如Intel VT，AMD-V），所以虚拟效率较高，是一种全虚拟化架构。
但是KVM本身不提供其他设备的虚拟化，借用了QEMU的代码进行了定制，所以KVM方案一定要依赖QEMU。
而Libvirt是用于管理虚拟化平台的开源的API，后台程序和管理工具。它可以用于管理KVM、Xen、VMware ESX、QEMU和其他虚拟化技术。
Libvirt主要由三个部分组成：API库，一个守护进程libvirtd和一个默认命令行管理工具 virsh。
参考文档：

QEMU和QEMU-KVM的关系
libvirt 介绍

3. 安装qemu和libvirt参考Ubuntu16.04安装QEMU与libvirt，安装QEMU和libvirt。
1、切换到root用户
sudo -i#orsudo su

2、查看主机是否支持硬件虚拟化
egrep -c &#x27;(vmx|svm)&#x27; /proc/cpuinfo

因为我们的实验本身是在虚拟机中进行的，所以输出0，表示不支持硬件虚拟化。
3、安装QEMU和libvirt
apt install -y qemu libvirt-bin bridge-utils virt-manager virt-viewer# 如果是支持硬件虚拟化，那么执行安装apt install -y kvm qemu-kvm libvirt-bin bridge-utils virt-manager virt-viewer


bridge-utils是网桥管理工具。
virt-manager是一个通用的桌面管理工具。
virt-viewer是一个用于显示虚拟机的图形控制台的最小工具。

4、检查安装
virsh --versionvirt-manager --versionvirt-viewer --version

5、查看网络
ip abrctl showvirsh net-list
安装完成以后，默认是启用了桥接网络virbr0，IP为192.168.122.1/24。如果没有启用，那么参考ubuntu16.04环境安装KVM进行启用。
4. 安装vncserver安装vncserver，方便远程查看虚拟机的安装过程
apt-get install vnc4server

5. qemu创建虚拟机参考文档：

嵌套虚拟化—VMware+QEMU/KVM
ubuntu下使用qemu安装虚拟机并配置桥接网络
使用qemu安装虚拟机

5.1. 下载系统镜像下载cirros系统镜像
wget http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img

5.2. 创建磁盘mkdir /opt/qemucd /opt/qemuqemu-img create -f qcow2 cirros0.img 5g

参数说明：

qcow2是qemu最常使用的磁盘格式，该格式下可以采用写时复制技术来优化性能
cirros0.img是磁盘名称
5g是指磁盘文件大小

5.3. 安装系统到磁盘qemu-system-x86_64 -hda cirros0.img -boot d -cdrom ./cirros-0.4.0-x86_64-disk.img -m 512 -vnc :1# 如果是支持硬件虚拟化，那么执行安装qemu-system-x86_64 -enable-kvm -hda cirros0.img -boot d -cdrom ./cirros-0.4.0-x86_64-disk.img -m 512 -vnc :1

参数说明：

enable-kvm使用KVM进行加速；
hda指定要使用的虚拟磁盘；
boot d指定启动位置，d表示从光盘启动；
cdrom指定镜像文件；
m指定虚拟机内存大小，默认单位是MB；
vnc:1通过vnc创建虚拟桌面。

出现警告：warning: TCG doesn’t support requested feature: CPUID.01H:ECX.vmx [bit 5]没啥影响，不去管它。
5.4. 查看安装过程怎样查看安装过程？怎样在安装时进行配置？答：使用VNC。本地打开vncviewer client，输入ip:5901进行连接，即可看到安装过程。
安装卡在了安装界面：
Boot failed: could not read the boot diskNo more network devicesNo bootable device

参考Play with Libvirt/KVM发现，cirros-0.4.0-x86_64-disk.img本身就是一个磁盘，尴尬。。。
5.5. 启动虚拟机qemu-system-x86_64 -m 512 -smp 4 \-drive file=cirros-0.4.0-x86_64-disk.img \-vnc :1
本地打开vncviewer client，输入ip:5901进行连接，即可看到启动过程。因为没有enable-kvm，所以启动很慢，请耐心等待。
5.6. IP配置1、查看虚拟机IP登录cirros（默认用户名cirros，密码gocubsgo），然后查看IP、内存和硬盘
ip addfree -mdf -h
IP默认为10.0.2.15；内存512M，是指定的；硬盘默认256M。
当ctrl+c结束qemu-system-x86_64命令后，虚拟机就被关闭了。再次使用qemu-system-x86_64命令启动虚拟机，登录虚拟机后使用history命令，发现之前的命令记录都还在，说明两次使用的都是同一块磁盘，也就是cirros-0.4.0-x86_64-disk.img。
2、设置IP
vim /etc/network/interfaces

eth0的配置修改为：
auto eth0iface eth0 inet staticaddress 192.168.122.100netmask 255.255.255.0

3、重启网卡
ifdown eth0ifup eth0

但是，无法ping通192.168.122.1，说明虚拟机的网卡并没有和virbr0进行连接。
6. 创建网络虚拟机有了，但是还不能连接外部网络。我们需要创建网络，重新启动虚拟机，详情参考访问qemu虚拟机的五种姿势。
1、创建网桥
brctl add virbr0ifconfig virbr0 192.168.122.1 net mask 255.255.255.0 up
网桥已经存在，所以这一步不需要了。
2、创建tap接口，并添加到网桥
apt install uml-utilitiestunctl -t tap0ifconfig tap0 0.0.0.0 upbrctl addif virbr0 tap0

3、让虚拟机使用tap0作为网络设备启动
qemu-system-x86_64 -m 512 -smp 4 \-drive file=cirros-0.4.0-x86_64-disk.img \-netdev tap,id=tapnet,ifname=tap0,script=no \-device rtl8139,netdev=tapnet \-vnc :1

4、登录虚拟机，测试网络
ping 192.168.122.1
此时网络已经通了。
7. libvirt创建虚拟机以上创建虚拟机的过程，其实并没有用到libvirt，下面使用下面使用libvirt进行虚拟机的创建。参考文档Play with Libvirt/KVM
1、创建虚拟机
virt-install --connect=qemu:///system --name=cirros --ram=512 --vcpus=1 --disk path=cirros-0.4.0-x86_64-disk.img,format=qcow2 --import --network network:default --vnc

无法使用vncviwer进行连接，然后卡在上图的界面，最后ctrl+c强制结束。配置文件位于/etc/libvirt/qemu/cirros.xml，如果要编辑它，使用virsh edit cirros命令。
2、显示vnc port
virsh vncdisplay cirros

3、显示虚拟机列表
virsh list

4、查看虚拟机详情 
virsh dominfo cirrosvirsh domstate cirros

问题来了，怎么访问这个虚拟机呢？最简单的方法是使用virsh console cirros命令打开控制台，使用ctrl+]关闭控制台，另外就是通过ssh或者vnc。
5、查看虚拟机网络（mac地址和ip地址） 
virsh dumpxml frontend1 | grep macarp -an
本次找到的IP为192.168.122.49
6、访问虚拟机
ssh cirros@192.168.122.49
输入默认密码gocubsgo，即可登录虚拟机。
7、测试网络
ping 8.8.8.8 -c3
至此，大功告成，创建虚拟机成功。
8. libvirt配置网络以上我们知道，可以使用arp -an命令查看虚拟机IP，如果查看到的IP都不能访问呢？这时就需要我们来手动配置。参考文档：

给libvirt创建的虚拟机指定固定IP
linux初学者-网络桥接篇
Documentation/Networking

1、查看libvirt网络
virsh net-list --all

2、查看虚拟机的网络
virsh domiflist cirros
查看到mac地址为52:54:00:7f:0e:77。
3、修改libvirt网络配置
virsh net-edit default

如下修改：
&lt;network&gt;  &lt;name&gt;default&lt;/name&gt;  &lt;uuid&gt;cc522162-7487-41dc-82ee-63df0df4003e&lt;/uuid&gt;  &lt;forward mode=&#x27;nat&#x27;/&gt;  &lt;bridge name=&#x27;virbr0&#x27; stp=&#x27;on&#x27; delay=&#x27;0&#x27;/&gt;  &lt;mac address=&#x27;52:54:00:d6:65:fb&#x27;/&gt;  &lt;ip address=&#x27;192.168.122.1&#x27; netmask=&#x27;255.255.255.0&#x27;&gt;    &lt;dhcp&gt;      &lt;range start=&#x27;192.168.122.2&#x27; end=&#x27;192.168.122.254&#x27;/&gt;      &lt;!--line 10, add--&gt;      &lt;host mac=&#x27;52:54:00:7f:0e:77&#x27; name=&#x27;cirros&#x27; ip=&#x27;192.168.122.100&#x27;/&gt;    &lt;/dhcp&gt;  &lt;/ip&gt;&lt;/network&gt;

以上，给cirros虚拟机配置了静态IP为192.168.122.100。
4、重启网络
virsh net-destroy defaultvirsh net-start default#virsh --connect qemu:///system net-destroy default#virsh --connect qemu:///system net-start default 

5、重启虚拟机
virsh shutdown cirrosvirsh start cirros

9. libvirt配置VNC某些情况下，无法通过IP访问虚拟机，这时就需要配置vnc，详情参考kvm虚拟机VNC的配置。
1、配置vnc
virsh edit cirros

找到
&lt;graphics type=&#x27;vnc&#x27; port=&#x27;-1&#x27; autoport=&#x27;yes&#x27;/&gt;

修改为：
&lt;graphics type=&#x27;vnc&#x27; port=&#x27;5901&#x27; autoport=&#x27;no&#x27; listen=&#x27;0.0.0.0&#x27;&gt;         &lt;listen type=&#x27;address&#x27; address=&#x27;0.0.0.0&#x27;/&gt;        &lt;/graphics&gt;

2、重启虚拟机
virsh shutdown cirrosvirsh start cirros

3、vnc访问本地打开vncviewer client，输入ip:5901进行连接。登录虚拟机后，也可以设置静态IP，这样就不用通过virsh net-edit default修改了。
10. 后记其实，虚拟机中安装的虚拟机，也可以支持KVM了，开启嵌套虚拟化即可。详情参考：

KVM嵌套虚拟化 – 在虚拟机中创建虚拟机
Nested KVM

1、查看宿主机是否开启嵌套虚拟化
cat /sys/module/kvm_intel/parameters/nested
N代表没有开启。
2、开启嵌套虚拟化
echo &#x27;options kvm_intel nested=1&#x27; &gt;&gt; /etc/modprobe.d/qemu-system-x86.conf
然后重启系统。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>libvirt</tag>
        <tag>virsh</tag>
      </tags>
  </entry>
  <entry>
    <title>使用Libvirt创建Win10虚拟机</title>
    <url>/dev-libvirt-create-win10/</url>
    <content><![CDATA[1. 前言本文中，我们使用Libvirt相关命令，在Linux系统中创建一个Win10虚拟机。
参考文档：

《使用Libvirt创建虚拟机》
《virsh常用命令》
KVM–虚拟机Windows10系统安装
linux + virsh + kvm虚拟机安装windows



2. 下载系统镜像1、下载Win10镜像参考文档下载Windows 10 光盘映像(ISO 文件)Win10正式版官方原版镜像下载，下载镜像 cn_windows_10_multiple_editions_x64_dvd_6848463.iso ，并重命名为 cn_windows.iso 。
2、上传Win10镜像上传Win10镜像到Linux系统的 /opt/vm 目录
3. 下载virtio驱动1、下载 virtio-win 驱动镜像访问 virtio-win下载页，下载镜像 virtio-win-0.1.215.iso
该镜像包含磁盘驱动和网卡驱动。
2、上传驱动镜像上传驱动镜像到Linux系统的 /opt/vm 目录
4. 创建磁盘创建一个100G的磁盘，用于安装Win10系统。
qemu-img create -f qcow2 /opt/vm/win10.qcow2 100G

5. 创建Win10虚拟机创建Win10虚拟机，指定磁盘，指定系统镜像，指定驱动镜像，启动vnc以便远程。
virt-install --name win10 --os-type=windows \    --ram 8192 --vcpus=4 \    --disk path=/opt/vm/win10.qcow2,format=qcow2,size=100,bus=virtio \    --disk device=cdrom,path=/opt/vm/cn_windows.iso \    --disk device=cdrom,path=/opt/vm/virtio-win-0.1.215.iso \    --accelerate --network bridge=br0 \    --graphics vnc,listen=0.0.0.0,port=5922  --noautoconsole

6. 安装Win10系统1、使用VNC Client远程连接Linux输入Linux的IP加5922端口，连接VNC

2、选择安装专业版
3、加载驱动程序，浏览选择驱动镜像中的w10
4、出现驱动器后，新建分区，并对主分区格式化，然后点击下一步
注意：一定要新建分区并格式化，否则安装完成后在启动时可能会一直卡在“请稍等”页面。
安装完成后，系统将会重启。但是无法自行重启。
5、手动启动Win10虚拟机
virsh start win10

6、配置Win10配置时，建议选择自定义设置。然后，一步一步根据提示配置即可。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>libvirt</tag>
        <tag>virsh</tag>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：virsh命令</title>
    <url>/dev-linux-virsh/</url>
    <content><![CDATA[1. 前言本文中会整理一些工作学习中常用的virsh命令，备忘。
参考文档：

libvirt
Domain XML format
libguestfs
virt-builder
KVM : Create a Virtual Machine
KVM : GPU Passthrough
virsh常用命令篇
KVM 虚拟机 XML 配置文件详解



2. 创建虚拟机本节以创建ubuntu18.04.6的虚拟机为例，ubuntu镜像下载地址为Ubuntu Release
2.1. 常规创建1、下载镜像
cd /opt/iso/wget https://releases.ubuntu.com/18.04.6/ubuntu-18.04.6-live-server-amd64.iso

2、创建虚拟机
virt-install \    --virt-type=kvm \    --name=ubuntu18.04 \    --vcpus=2 \    --ram=4096 \    --cdrom=/opt/iso/ubuntu-18.04.6-live-server-amd64.iso \    --disk path=/opt/kvm/ubuntu18.04.qcow2,device=disk,format=qcow2,bus=virtio,cache=writeback,size=100 \    --network default,model=virtio \    --os-type=linux \    --os-variant=ubuntu18.04 \    --graphics vnc,listen=0.0.0.0 \    --noautoconsole \    --accelerate

model=virtio 可以换成 model=e1000或者model=rtl8139。但是这里建议使用virtio，因为使用virtio要比使用e1000或者rtl8139网络吞吐性能要高出2倍左右。
详情参考：

QEMU新的-nic选项
KVM总结-KVM性能优化之网络性能优化

3、查看虚拟机
virsh listvirsh list --all

4、查看vnc端口
virsh vncdisplay ubuntu18.04

使用vnc client连接虚拟机完成安装。
2.2. 单独创建磁盘qemu-img create -f qcow2 /opt/kvm/ubuntu18.04.qcow2  100Gvirt-install --disk path=/opt/kvm/ubuntu18.04.qcow2 ...

2.3. 使用桥接ifconfigip avirt-install --network bridge=br0,model=virtio ...

3. 查看虚拟机IP方法一：通过VNC登录虚拟机后查看
方法二：通过mac地址查看
nmap -sP 192.168.56.0/24virsh dumpxml ubuntu18.04 | grep &quot;mac a&quot;arp -a | grep grep &quot;52:54:00:46:98:a7&quot;cat /proc/net/arp | grep &quot;52:54:00:46:98:a7&quot;

4. 网络优化vhost-net是对virtio的优化，在内核中加入了vhost-net.ko模块，使得对网络数据可以在内核态得到处理。数据包从虚拟网卡出来后，直接跳到内核那一层中。这里要注意的是，如果要使用vhost-net那么，虚拟机的网卡类型必须是virtio。
virsh destroy ubuntu18.04virsh edit ubuntu18.04

添加dirver配置：
&lt;interface type=&#x27;bridge&#x27;&gt;  &lt;mac address=&#x27;52:54:00:46:98:a7&#x27;/&gt;  &lt;source bridge=&#x27;br0&#x27;/&gt;  &lt;model type=&#x27;virtio&#x27;/&gt;  &lt;driver name=&#x27;vhost&#x27;/&gt;  &lt;address type=&#x27;pci&#x27; domain=&#x27;0x0000&#x27; bus=&#x27;0x00&#x27; slot=&#x27;0x03&#x27; function=&#x27;0x0&#x27;/&gt;&lt;/interface&gt;

5. 修改虚拟机配置5.1. 安装virt-customizeyum -y install libguestfs-tools

5.2. 关闭虚拟机virsh shutdown ubuntu18.04

在使用virt-customize修改虚拟机配置之前，需要先关闭虚拟机，否则修改不会生效，还可能发生非预期问题。
5.3. 修改主机名virt-customize -a /opt/kvm/ubuntu18.04.qcow2 --hostname node01

5.4. 启用root ssh登录virt-customize -a /opt/kvm/ubuntu18.04.qcow2 --run-command &#x27;echo &quot;PermitRootLogin yes&quot; &gt;&gt; /etc/ssh/sshd_config&#x27;

5.5. 修改root密码virt-customize -a /opt/kvm/ubuntu18.04.qcow2 --root-password password:voidking# orvirt-customize -a /opt/kvm/ubuntu18.04.qcow2 --password root:voidking

5.6. 添加authorized_keysvirt-customize -a /opt/kvm/ubuntu18.04.qcow2 --ssh-inject root:string:&quot;ssh-rsa xxx vk@macpro.lan&quot;

5.7. 安装软件包virt-customize -a /opt/kvm/ubuntu18.04.qcow2 --install vim,wget,curl,unzipvirt-customize -v -x -a /opt/kvm/ubuntu18.04.qcow2 --install vim,wget,curl,unzip

5.8. 手动配置静态IPubuntu18之前在/etc/network/interfaces进行配置，ubuntu18及之后版本在/etc/netplan/*.yaml中进行配置。
提前手动配置好一台虚拟机的静态IP，然后，其他的克隆出来的主机可以使用virt-customize调用sed命令修改IP。
1、查看当前网关
route -nip routenetstat -rn

2、配置静态IP配置 /etc/netplan/00-installer-config.yaml 内容为：
# This is the network config written by &#x27;subiquity&#x27;network:  version: 2  ethernets:    ens3:      dhcp4: false                         # 禁止动态IP      addresses: [192.168.56.200/24]       # IP地址和掩码      gateway4: 192.168.56.1               # 网关      nameservers:        addresses: [192.168.56.1,114.114.114.114]   #DNS服务器

3、使配置生效
netplan applyip a

5.9. 手动配置静态IP2.01、提前准备 00-installer-config.yaml 内容为：
# This is the network config written by &#x27;subiquity&#x27;network:  version: 2  ethernets:    ens3:      dhcp4: false                         # 禁止动态IP      addresses: [192.168.56.200/24]       # IP地址和掩码      gateway4: 192.168.56.1               # 网关      nameservers:        addresses: [192.168.56.1,114.114.114.114]   #DNS服务器

2、拷贝到宿主机
virt-customize -a /opt/kvm/ubuntu18.04.qcow2 --copy-in 00-installer-config.yaml:/etc/netplan/00-installer-config.yaml

5.10. 自动配置静态IPvirt-customize -a /opt/kvm/ubuntu18.04.qcow2 --run-command &quot;sed -i \&quot;s#addresses: \[.*/24\]#addresses: \[192.168.56.201/24\]#\&quot; /etc/netplan/00-installer-config.yaml&quot;

6. 启停虚拟机1、停止虚拟机
# smooth shutdownvirsh shutdown ubuntu18.04# forced shutdownvirsh destroy ubuntu18.04

2、开启虚拟机
virsh start ubuntu18.04

7. 销毁虚拟机virsh list --allvirsh destroy ubuntu18.04virsh snapshot-list ubuntu18.04virsh snapshot-delete ubuntu18.04 1627446728virsh undefine ubuntu18.04#rm /etc/libvirt/qemu/ubuntu18.04.xmlrm /opt/kvm/ubuntu18.04.qcow2

8. 开机自动启动virsh autostart ubuntu18.04virsh autostart --disable ubuntu18.04

9. 调整虚拟机CPU和内存virsh destroy ubuntu18.04virsh edit ubuntu18.04

调整相关配置：
&lt;memory unit=&#x27;KiB&#x27;&gt;4194304&lt;/memory&gt;&lt;currentMemory unit=&#x27;KiB&#x27;&gt;4194304&lt;/currentMemory&gt;&lt;vcpu placement=&#x27;static&#x27;&gt;2&lt;/vcpu&gt;

这里需要注意的是，vcpu的最小值是1，详情参考CPU Allocation
10. 根据xml导出创建虚拟机的命令virsh list --allvirsh dumpxml ubuntu18.04 &gt; ubuntu18.04.xml

11. 克隆虚拟机virt-clone -o ubuntu18.04 -n ubuntu18.04-tmp -f /opt/kvm/ubuntu18.04-tmp.qcow2virsh dominfo ubuntu18.04-tmpvirsh domstate ubuntu18.04-tmp

12. 拷贝虚拟机到其他宿主机1、拷贝虚拟机磁盘文件和配置文件
scp /opt/kvm/ubuntu18.04.qcow2 192.168.56.102:/opt/kvm/ubuntu18.04.qcow2scp /etc/libvirt/qemu/ubuntu18.04.xml 192.168.56.102:/etc/libvirt/qemu/ubuntu18.04.xml

2、拉起虚拟机
ssh 192.168.56.102virsh define /etc/libvirt/qemu/ubuntu18.04.xmlvirsh list --allvirsh start ubuntu18.04

PS：临时拉起虚拟机
virsh create /etc/libvirt/qemu/ubuntu18.04.xml

拷贝虚拟机到其他宿主机之后，在其他宿主机上启动虚拟机时可能会报错：
错误：开始域 ubuntu18.04 失败错误：the CPU is incompatible with host CPU: 主机 CPU 不提供所需功能: hle, rtm
这是因为不同宿主机的CPU特性不同，解决办法为修改cpu相关配置。
virsh edit ubuntu18.04

修改cpu部分为：
&lt;cpu mode=&#x27;host-model&#x27; check=&#x27;partial&#x27;/&gt;

13. 虚拟机迁移参考文档《使用Libvirt进行虚拟机在线迁移》
14. 虚拟机扩容磁盘参考文档：

裸数据盘扩展文件系统
在线扩展数据盘分区及文件系统

14.1. 给虚拟机增加磁盘virsh shutdown ubuntu18.04-tmpvirsh domblklist ubuntu18.04-tmpqemu-img info /opt/kvm/ubuntu18.04-tmp.qcow2qemu-img resize /opt/kvm/ubuntu18.04-tmp.qcow2 +20G # 增加20Gqemu-img resize /opt/kvm/ubuntu18.04-tmp.qcow2 200G # 最终大小变为200G

PS：qcow2镜像不支持缩容。
如果给虚拟机增加磁盘后，虚拟机启动时报错：qcow2: Image is corrupt; cannot be opened read/write
那么说明磁盘出现问题，修复方法如下
qemu-img check /opt/kvm/ubuntu18.04-tmp.qcow2qemu-img check -r all /opt/kvm/ubuntu18.04-tmp.qcow2

14.2. 扩容分区和文件系统14.2.1. 扩容分区和文件系统概述首先，检查磁盘分区是否需要扩展。可以使用 lsblk 或 fdisk -l 查看磁盘分区情况。
如果磁盘分区已经使用了所有可用空间（例如 LVM），那么可以直接扩展逻辑卷。
如果分区未使用所有空间，你需要先扩展分区。
14.2.2. 扩容逻辑卷（LVM）参考文档：ubuntu的ubuntu–vg-ubuntu–lv磁盘扩容
如果 lsblk 内容为：
# lsblkNAME                      MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTSvda                       252:0    0  500G  0 disk├─vda1                    252:1    0    1M  0 part├─vda2                    252:2    0    2G  0 part /boot└─vda3                    252:3    0   98G  0 part  └─ubuntu--vg-ubuntu--lv 253:0    0   49G  0 lvm  /

那么执行扩容逻辑卷即可。具体步骤为：
1、查看物理卷、卷组和逻辑卷的状态
pvdisplayvgdisplaylvdisplay

2、新建分区
fdisk /dev/vda

新建分区过程为：
```textCommand (m for help): pDisk /dev/vda: 500 GiB, 536870912000 bytes, 1048576000 sectorsUnits: sectors of 1 * 512 = 512 bytesSector size (logical/physical): 512 bytes / 512 bytesI/O size (minimum/optimal): 512 bytes / 512 bytesDisklabel type: gptDisk identifier: BEC7F83B-0F82-4AF6-8AA8-A022495D4EB6Device       Start       End   Sectors Size Type/dev/vda1     2048      4095      2048   1M BIOS boot/dev/vda2     4096   4198399   4194304   2G Linux filesystem/dev/vda3  4198400 209713151 205514752  98G Linux filesystemCommand (m for help): nPartition number (4-128, default 4): 4First sector (209713152-1048575966, default 209713152):Last sector, +/-sectors or +/-size&#123;K,M,G,T,P&#125; (209713152-1048575966, default 1048575966):Created a new partition 4 of type &#x27;Linux filesystem&#x27; and of size 400 GiB.Command (m for help): tPartition number (1-4, default 4): 4Partition type or alias (type L to list all): lvmChanged type of partition &#x27;Linux filesystem&#x27; to &#x27;Linux LVM&#x27;.Command (m for help): pDisk /dev/vda: 500 GiB, 536870912000 bytes, 1048576000 sectorsUnits: sectors of 1 * 512 = 512 bytesSector size (logical/physical): 512 bytes / 512 bytesI/O size (minimum/optimal): 512 bytes / 512 bytesDisklabel type: gptDisk identifier: BEC7F83B-0F82-4AF6-8AA8-A022495D4EB6Device         Start        End   Sectors  Size Type/dev/vda1       2048       4095      2048    1M BIOS boot/dev/vda2       4096    4198399   4194304    2G Linux filesystem/dev/vda3    4198400  209713151 205514752   98G Linux filesystem/dev/vda4  209713152 1048575966 838862815  400G Linux LVMCommand (m for help): wThe partition table has been altered.Syncing disks.

3、创建物理卷组
pvcreate /dev/vda4

4、扩容卷组
vgextend ubuntu-vg /dev/vda4

5、扩容逻辑卷
lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv

6、扩容文件系统
resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv

14.2.3. 扩容分区1、安装扩容分区工具
# centosyum install -y cloud-utils-growpart# ubuntuapt-get install -y cloud-guest-utils

2、虚拟机内部执行扩容分区
fdisk -lLANG=en_US.UTF-8growpart /dev/vda 2fdisk -l

14.2.4. 扩容文件系统df -hdf -ihT# ext2/ext3/ext4resize2fs /dev/vda2# xfsxfs_growfs /dev/vda2df -h

14.3. 裸数据盘扩容文件系统所谓裸数据盘，就是没有分区的数据盘。
df -hdf -ihT# ext2/ext3/ext4resize2fs /dev/vdb# xfsxfs_growfs /dev/vdcdf -h

14.4. 磁盘扩容不符合预期问题问题描述：virsh创建的vm，使用qemu-img resize把磁盘从40G扩容到了200G，然后在vm中执行 xfs_growfs /dev/vda2 。fdisk -l 发现磁盘变成了200G，df -h 发现 /dev/vda2 变成了100G，期望应该是200G才对，为什么丢失了100G？
原因分析：一般来说，当使用qemu-img resize对虚拟机的磁盘进行扩容之后，再通过xfs_growfs命令扩展文件系统时，文件系统大小应该和磁盘大小一致。然而，如果在执行xfs_growfs后，发现文件系统大小不等于磁盘大小，可能是因为分区表没有更新。
解决办法：分区表必须更新以反映磁盘大小的增加。具体操作步骤如下：
1、进入VM，查看当前磁盘分区状态
fdisk -l /dev/vda

2、删除不正确的分区vda2。（请注意，此操作不会删除分区上的文件，只是删除分区表中的记录）
fdisk /dev/vda&gt; d         &gt; 2
这里，d是删除分区的命令，2是选择你想要删除的分区号。
3、创建一个新的分区，该分区使用磁盘上所有可用的空间。
&gt; n&gt; p&gt; 2&gt; &lt;start value&gt;&gt; &lt;default end value&gt;&gt; w
在这里，n是新建分区，p是选择主分区，2是分区号，使用默认设置，最后使用w写入新的分区表。
4、重新读取分区表
partprobe /dev/vda 

5、再次尝试使用xfs_growfs扩展文件系统
xfs_growfs /dev/vda2

6、最后再确认一下是否已经扩展至200G
df -h


15. 虚拟机增加磁盘virsh destroy ubuntu18.04-tmpqemu-img create -f qcow2 /opt/kvm/ubuntu18.04-tmp_ext.qcow2 100Gvirsh edit ubuntu18.04-tmp

disk部分添加：
&lt;disk type=&#x27;file&#x27; device=&#x27;disk&#x27;&gt;  &lt;driver name=&#x27;qemu&#x27; type=&#x27;qcow2&#x27;/&gt;  &lt;source file=&#x27;/opt/kvm/ubuntu18.04-tmp_ext.qcow2&#x27;/&gt;  &lt;target dev=&#x27;vdb&#x27; bus=&#x27;virtio&#x27;/&gt;&lt;/disk&gt;

磁盘格式化和挂载方法参考《好好学Linux：Linux挂载文件系统》
16. 维修虚拟机当虚拟机出现问题时，不方便通过U盘启动的方式进行维修，此时可以使用一些虚拟机管理命令。
拷贝文件到虚拟机：
virt-customize -a /path/to/image.qcow2 --copy-in example.txt:/root/

挂载虚拟机磁盘到宿主机：
# 查看可用文件系统virt-filesystems --long --all -a /path/to/image.qcow2 # 挂载文件系统guestmount -a /path/to/image.qcow2 -m /dev/sda1 /mnt# 卸载挂载guestunmount /mnt

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>ubuntu</tag>
        <tag>问题排查</tag>
        <tag>libvirt</tag>
        <tag>virsh</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Vim进阶</title>
    <url>/dev-vim-advance/</url>
    <content><![CDATA[1. 前言Vim是Linux和Unix上的编辑器，由Vi升级而来。
已经使用了Vim好多年，但是，水平仅仅停留在修改配置文件的水平。不会把它当做主要的编辑器，更不会用它进行编码开发。最近，童钢老师提醒了我，作为运维人员，Vim需要用得更加熟练一些，最好能把Vim作为默认编辑器使用。
所以，本文决定研究一下更高阶的Vim使用技巧，主要参考优雅玩转Vim。


2. VimrcVimrc指的是Vim的配置文件，rc=run command，Vim 的全局配置一般在/etc/vim/vimrc或者/etc/vimrc，对所有用户生效。用户个人的配置在~/.vimrc。
如果只对单次编辑启用某个配置项，可以在命令模式下，先输入一个冒号，再输入配置。举例来说，set number这个配置可以写在.vimrc里面，也可以在命令模式输入。
配置项一般都有”打开”和”关闭”两个设置。”关闭”就是在”打开”前面加上前缀”no”。
&quot; 打开set number&quot; 关闭set nonumber
上面代码中，双引号开始的行表示注释。
查询某个配置项是打开还是关闭，可以在命令模式下，输入该配置，并在后面加上问号。
:set number?
上面的命令会返回number或者nonumber。
如果想查看帮助，可以使用help命令。
:help number

更多内容参考Vim 配置入门。
有一些大神把Vim配置得非常炫酷，比如amix和humiaozuzu。
附上一份常用的vim配置：
&quot; 语法高亮syntax on&quot; 显示行号set number&quot; 回车自动缩进set autoindent&quot; 设置tab宽度set tabstop=4&quot; shift缩进宽度set shiftwidth=4&quot; tab自动转空格set expandtab&quot; tab转空格宽度set softtabstop=4&quot; 粘贴格式化set paste

如果cat查看文件可以正常看到中文，vim编辑文件出现乱码，那么可以指定编码方式为 utf8 来解决这个问题。
&quot; 解决中文乱码问题set termencoding=utf-8set encoding=utf8set fileencodings=utf8,ucs-bom,gbk,cp936,gb2312,gb18030

设置显示换行等特殊字符。
set list

3. 四种模式3.1. 普通模式进入vim后默认普通模式，可以进行移动、复制、粘贴、删除、修改等。其他模式点击Esc返回普通模式。
3.2. 可视模式对一块区域进行操作，就像Windows中的鼠标选中。普通模式下点击v进入可视模式。
3.3. 插入模式插入模式和普通文本编辑器相同，可以进行输入和删除。普通模式下点击i进入插入模式。
3.4. 命令模式在命令模式中可以执行一些指令，就像在shell里一样。普通模式下点击Shift + :进入命令模式。
4. 快捷键4.1. 保存退出Shift + zz，退出或者保存退出。
4.2. 移动跳转为了减少右手移动距离，h可代替左，j可代替下，k可代替上，l可代替右。
0移动到行首，Shift + 6也是移动到行首（不包含空格）。Shift + 4移动到行尾。
w光标正向移动到下一个单词，3w正向移动3个单词，b反向移动。e正向移动到下一个单词词尾，ge反向移动。W（Shift+w）表示忽略特殊字符，比如逗号句号等。
gg跳回第一行，10gg跳到第10行，Shift+g跳到最后一行。g、Ctrl+g显示文件信息。
f、空格，跳到下一个空格。
Shift+&gt;&gt;右缩进，Shift+&lt;&lt;左缩进。
多行缩进，进入命令模式，输入：
75,80&gt;75&gt;6

4.3. 删除复制yy复制一行，内容存入无名寄存器和0号寄存器。
准确地来说，Vim中没有删除，只有剪切。dd剪切一行，内容存入无名寄存器和1号寄存器。
p粘贴到光标下一行，P粘贴到光标前一行。u撤销上一步的操作。ctrl+r重做。
命令模式下输入reg可以查看寄存器。
yw复制当前单词，y2w复制正向两个单词。yx复制当前字符。
dw删除当前单词，dj删除下一行，dk删除上一行，dh删除左边一个字符，dl删除当前字符。
d、Shift+6删除到行首，d、Shift+4删除到行尾。
3dd向下删除3行，5dw正向删除5个单词。
4.4. 修改查找shift+i，跳到行首并进入插入模式；shift+a，跳到行尾并进入插入模式。
o进入下一行插入模式，O进入上一行插入模式。
3i进入插入模式，输入内容，Esc，输入的内容会被复制3次。
5o进入下一行插入模式，输入内容，Esc，输入的内容会插入5行。
~单个字符大小写转换，g~w整个单词大小写转换，g~$整行大小写转换，.对上一个操作重复。
fa在行内查找a，Fa在行内反向查找a。
/word查找word，n查找下一个，N查找上一个。在命令模式下设置set hlsearch可以高亮显示。
进入命令模式，进行替换：
# 替换当前行所有的oldword:s/oldword/newword/g# 替换文中所有的oldword:%s/oldword/newword/g

r替换单个字符，shift+r替换当前行，cw替换当前单词，c$修改光标后的内容。
5. 多文件编辑假设有buffer1.txt、buffer2.txt、buffer3.txt三个文件，那么可以使用vim buffer*打开三个文件。当前显示buffer1.txt，进入命令行模式，查看缓冲区列表：
:files:buffers:ls

badd buffer4.txt打开一个缓冲区，bn切换下一个缓冲区，bp切换上一个缓冲区，bf切换到第一个缓冲区，bl切换到最后一个缓冲区，b3切换到第三个缓冲区，b buffer1.txt切换到buffer1.txt，ball编辑所有缓冲区，1,3bd删除1-3缓冲区列表，%bd删除所有缓冲区列表，qall退出所有。
bufdo set number所有缓冲区设置number。
6. 多窗口与标签分组vim -o buffer*，分屏打开多个文件，垂直方向分屏。vim -O buffer*，分屏打开多个文件，竖直方向分屏。
ctrl+w、s垂直复制分屏，ctrl+w、v水平复制分屏，ctrl+w、q删除分屏。
sp buffer4.txt打开buffer4.txt并且上下分屏，vsp buffer4.txt打开buffer4.txt并且左右分屏。
ctrl+w、h向左移动，ctrl+w、j向下移动，ctrl+w、k向上移动，ctrl+w、l向右移动。ctrl+w、+增加高度，ctrl+w、-减少高度，ctrl+w、=设置高度相等。
标签里面可以包含多个窗口，使用类似于buffers。命令模式下，tabnew创建标签，tabfind查找并在新标签中打开文件，tabs查看打开的标签列表，tabclose关闭当前标签页，tabonly只保留当前标签页，tabn/p/first/last切换标签页，tabm 0将标签页放到第一个位置。普通模式下，gt/gT切换标签页。
7. 文本对象和宏文本对象：w代表word，s代表sentence，p代表paragraph，此外还有textblock块对象。
操作：
&#123;operator&#125;&#123;i&#125;&#123;object&#125;&#123;operator&#125;&#123;a&#125;&#123;object&#125;daw = delete a word      ==a==n object: include the tail spaceciw = change inner word      ==i==nner object: not include the tail space

viw选中单词，v3iw选中三个单词，vis选中句子，vip选中段落，vi(选中圆括号中的内容，vi[选中中括号中的内容，vit选中标签中的内容。
使用宏输入1到99：普通模式下，o进入下一行插入，输入1，Esc返回普通模式，qa开始录制，yyp复制粘贴一行，ctrl+a当前数字加一，q完成录制，97@a执行97次。
8. Visual模式在可视模式下，可以对一个文本块的整体进行操作。可视模式有三种子模式，v激活面向字符的可视模式，V激活面向行的可视模式，ctrl+v激活面向列块的可视模式。
gv重选刚才选择的选区，o在选择区两端跳动。ctrl+v、3j垂直下拉三列，r进行替换。ctrl+v、3j垂直下拉三列，e选择单词直到词尾，c进行单词替换，输入完成，Esc。ctrl+v、3j垂直下拉三列，Shift+$选择到行尾，A进行插入，输入完成，Esc。
9. 后记至此，已经系统学习了Vim的高阶知识点，包括Vim配置、四种模式、快捷键、多文本编辑、多窗口标签、文本对象和宏、可视模式等。接下来，就是在学习工作中多多使用Vim，最终熟能生巧。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>编辑器</tag>
      </tags>
  </entry>
  <entry>
    <title>阿里云技术体系概览</title>
    <url>/dev-ali-tech-system-summary/</url>
    <content><![CDATA[1. 阿里云据Gartner发布的2017年度全球公有云市场份额分析报告，全球范围内的公有云市场已经形成了3A格局：亚马逊AWS、微软Azure、阿里云AliYun。
今年，天猫双11以2135亿的成交额再破记录。这背后，是阿里云的技术在支撑，阿里云的大数据计算平台已经拥有全世界最牛的大数据并发处理能力。
本文，就来研究整理一下阿里云的技术栈。


2. 历史2009年之前IOE（IBM小型机，Oracle数据库，EMC存储设备），Oracle之巅。
2009年9月阿里云成立，愿景是要做一套大数据分布式计算/存储的云平台，自主研发飞天体系，包括三大部分：底层的分布式存储系统——盘古、分布式调度系统——伏羲、开放数据处理服务——ODPS（Open Data Processing Service）。后来又添加了集群诊断系统——华佗、网络连接模块——夸父、监控系统——神农、集群部署——大禹。
2009-2011年期间云梯1（Hadoop）、云梯2（飞天），两套系统，云梯1承载初期集团淘宝的数据业务，云梯2助力阿里金融的第一个产品“牧羊犬”应用上线。
2011年，集团主要业务全部登上云梯1，阿里构建了国内领先的hadoop集群。两座云梯同时发展，开始进行博弈，规则很明确：要想成功肩负起阿里巴巴的底层计算系统，就必须有能力独自调度5000台服务器，“赢者通吃”，继承家业。
2011年，大数据实时计算发展也随业务而生。数据魔方研发了第一个版本的galaxy，b2b搜索团队也研发了一个叫iprocess的流计算系统。在12年7月份，数据平台事业部（CDO）成立，两个团队走到了一起组建了实时计算团队。
2012年冰火鸟：数据业务层统一。云梯1、云梯2运维团队合并。
2013年5K之战：突破集群存储的瓶颈。云梯2突破5K。
2014年-2015年登月计划：打造集团统一大数据平台。云梯1停止，云梯2成为集团统一的大数据平台。ODPS更名为“MaxCompute”。
2015年，支持公有云，阿里大数据计算能力，走出集团服务全球。
2016年，建设主备双链路容灾、实时全链路监控、Tesla运维诊断工具实时全链路保障方案。大力发展实时计算，Galaxy、JStorm和Search dump三支实时计算团队资源整合。
2017年，MaxCompute架构升级，在NewSQL、富结构化、联合计算平台、AliORC多个方向上发力，继续构建高可用、高性能、高自适性的一站式的大数据解决方案开放平台。
更详细历史，参考首次探秘！双11奇迹背后的大数据力量，十年发展五部曲。
阿里大数据技术如何进化？资深技术专家带你回顾一文中作者观滔提到：

如果把阿里巴巴整体数据体系比作这个战队，那么MaxCompute就是中间的那艘航空母舰，几乎阿里巴巴99%的数据存储以及95%的计算能力都在这个平台上产生。

3. 计算平台阿里的计算平台服务主要有MaxCompute、流计算Blink、图计算FLASH、机器学习PAI。
3.1. MaxCompute由历史可以看出，现在的阿里技术核心围绕MaxCompute。上图是MaxCompute的技术架构。最下面一层是物理机器，MaxCompute有自己的物理集群，在集群之上有非常重要的能力：它把一个集群组织成了1万台计算机，MaxCompute 2.0很大的特性是集群能力得到了扩展，从5千变成了1万。黑色部分是飞天操作系统，提供整个分布式系统任务协同、资源管理、集群调度等功能，为上层云产品提供统一的操作系统服务。其上是MaxCompute统一的计算引擎，支持SQL、MR、迭代计算、图计算、流计算。
详情参考阿里巴巴大数据计算平台MaxCompute（原名ODPS）全套攻略和性能追求之路——MaxCompute2.0（原ODPS）的前世今生。
3.2. BlinkApache Flink是面向数据流处理和批处理的分布式开源计算框架，2016年阿里巴巴引入Flink框架，改造为Blink。2017年，阿里整合了所有流计算产品，决定以Blink引擎为基础，打造一款全球领先的实时计算引擎。当年双11，Blink支持了二十多个事业部／群，同时运行了上千个实时计算job，每秒处理的日志数峰值达到惊人的4.7亿。
详情参考阿里新一代实时计算引擎 Blink，每秒支持数十亿次计算。
3.3. FLASHFLASH是阿里的图计算平台。简单的说图计算就是研究在大规模图数据下，如何高效计算，存储和管理图数据等相关问题的领域。
应用：搜索推荐、关联分析、实时预测等。
详情参考FLASH:大规模分布式图计算引擎及其应用和大规模图计算系统综述。
3.4. PAIPAI是阿里自己的智能平台，该平台基于阿里云的云计算平台，具有处理超大规模数据的能力和分布式的存储能力，同时整个模型支持超大规模的建模以及GPU计算。此外，该平台还具有社区的特点：实验结果可共享、社区团队相互协作。该智能平台主要分为三层，第一层是Web UI界面，第二层是IDST算法层，最后一层是ODPS平台层。
详情参考阿里云机器学习平台——PAI平台。
4. 技术架构除了MaxCompute，阿里还有很多其他的技术，如下图：1、基础设施新技术大规模文件分发系统、X-Cluster、时间序列异常检测算法、Changefre系统、阿里智能流量调度系统、StarAgent、调度混部技术、网络自愈技术、“无人值守”的全链路压测
2、新零售技术创新时尚大脑、门店发货/自提、智慧门店天梭、随身购物袋、数据银行
3、菜鸟菜鸟方舟
4、支付OceanBase、离在线混部、GeaBase、支付工具精准推荐、海淘正品溯源
5、机器智能“新物种”战队云小蜜、蚂蚁安安、首席拣货官、小G二代：智能配送员、天巡、尖兵、达灵、鲁班、蚂蚁佐罗、无人汽车售卖机
6、云计算阿里云弹性计算ECS、ApsaraCache、阿里云数据集RDS、阿里云内容分发网络CDN、日志服务、移动云、阿里云OSS、阿里云DDoS防御技术、混合云架构、专有网络VPC、负载均衡SLB、高速通道、NAT网关、视频直播、云导播
7、安全霸下——七层流量清洗、钱盾、MTEE3、御城河、CTU
5. 技术点5.1. 调度混部技术Sigma通过和离线任务的伏羲调度系统深度集成，突破了若干CPU、内存和网络资源隔离的关键技术，实现了在线和离线任务的混合部署。
Sigma调度可以跨多个不同平台来分配双11所需的资源，使双11 IT成本下降50%；混部技术把服务器资源利用率提升30%，复用计算任务集群为双11节约大量服务器。
在线服务的容器就像砖块，而计算任务就像沙子和水。当在线服务压力小的时候，计算任务就占住那些空隙，把空闲的资源都使用起来，而当在线服务忙的时候，计算任务便立即退出空隙，把资源还给在线服务。
详情参考想了解阿里巴巴的云化架构 看这篇就够了。
5.2. Pouch容器阿里的容器技术经历了一个从集中式到分布式架构上的演化，最开始是直接跑在物理机上，之后引入了虚拟化技术，但整体的资源使用率不高，因而过渡到了容器技术。2015 年以后，阿里巴巴引入Docker标准，形成一套新的容器技术——Pouch，并集成到整个运维体系。
全网Pouch化实现阿里在线服务全部容器化，内部达到百万级容器部署规模，Pouch容器已对外开源。
5.3. 全链路压测阿里巴巴双11备战期间，保障系统稳定性最大的难题在于容量规划，而容量规划最大的难题在于准确评估从用户登录到完成购买的整个链条中，核心页面和交易支付的实际承载能力。
最初采用的方式是在线上单机的生产环境的压力测试和容量规划，基于单台服务能力和预估即将到来的业务流量进行容量规划，确定所需服务器的数目。
2010-2012年双11暴露了不少问题，单个系统ready不代表全局ready，究其根本原因在于系统之间相互关联和依赖调用之间相互影响。在做单个系统的容量规划时，所有的依赖环节能力是无限的，进而使得获取的单机能力值是偏乐观的；同时，采用单系统规划时，无法保证所有系统均一步到位，大多数精力都集中核心少数核心系统；此外，部分问题只有在真正大流量下才会暴露，比如网络带宽等等。
阿里从13年起着手进行全链路压测。全链路压测的本质是让双11零点这一刻提前在系统预演（用户无感知），模拟“双11”同样的线上环境、用户规模、业务场景、业务量级，之后再针对性地进行系统调优，是站点的一次高仿真模拟考试。
全链路压测核心要素主要包括四点：

压测环境，它是指具备数据与流量隔离能力的生产环境，不能影响到原有的用户体验和用户流程、BI报表以及推荐算法等；
压测基础数据，它主要包括压测用户、店铺、商品等基础数据；
压测场景模型，它主要是指压测哪些业务场景，每个场景下压测多大量等；
压测流量，它主要由压测请求的协议来决定压测流量的输出

详情参考系统稳定性保障核武器——全链路压测。
5.4. OceanBaseOceanBase是阿里完全自主研发的金融级分布式关系数据库，从架构上可以通过扩展机器来解决集群服务能力的扩展需求。OceanBase采用多副本复制的方案解决了可靠性和可用性的需求，而且构建在普通PC服务器上，不依赖于高端引擎。
OceanBase从诞生、到电商数据库、再到金融数据库及最终的云数据库，经历了四个阶段。
详情参考阿里蚂蚁金服的关系型数据库：OceanBase架构详解和OceanBase 2.0 到底如何做到 50% 的性能提升？。
5.5. DubboDubbo是阿里巴巴于 2012 年开源的分布式服务治理框架，目前已是国内影响力最大、使用最广泛的开源服务框架之一，在Github上的 fork、star数均已破万。
Dubbo 致力于提供高性能和透明化的RPC远程服务调用方案，以及SOA服务治理方案，使得应用可通过高性能RPC 实现服务的输出和输入功能， 和 Spring 框架无缝集成。Dubbo包含远程通讯、集群容错和自动发现三个核心部分。提供透明化的远程方法调用，实现像调用本地方法一样调用远程方法，只需简单配置，没有任何API侵入。 
同时具备软负载均衡及容错机制，可在内网替代F5等硬件负载均衡器，降低成本，减少单点。可以实现服务自动注册与发现，不再需要写死服务提供方地址，注册中心基于接口名查询服务提供者的 IP 地址，并且能够平滑添加或删除服务提供者。
详情参考分布式服务框架Dubbo疯狂更新！阿里开源要搞大事情？。
5.6. StarAgentStarAgent 是一个生态平台。阿里所有的物理机、虚拟机以及容器都会装StarAgent，基本上要和机器交互都需要通过这个平台。它实际上不会做具体的业务，具体的业务还是通过各个业务平台去实现的。
StarAgent 核心功能就是一个命令的通道，它既可以同步执行任务又可以异步执行任务，还可以查询任务状态和插件管理。插件分为两种，一种是静态的，静态的实际上就是脚本、命令之类的。另一种动态的是一个常驻进程，必须常驻在系统里面。我们会守护这个进程，如果它挂了会重新拉起来，如果其占用内存、CPU超过设定的范围会删掉它。整个协议是比较简单的，使用起来耦合度也是比较低的。

详情参考双11黑科技，阿里百万级服务器自动化运维系统StarAgent揭秘、阿里巴巴运维中台的演进与建设、阿里巴巴运维中台的演进与建设ppt。
5.7. 蜻蜓蜻蜓系统是纯碎的 P2P 的文件分发系统。
详情参考阿里巴巴运维中台的演进与建设。
5.8. NormandyNormandy 是运维整个阿里巴巴业务的PaaS平台。这个平台实际上提供三大功能，分别是基础设施即代码（Infrastructure as Code）、部署和应用运维支撑。
详情参考阿里巴巴运维中台的演进与建设。
6. 书签
阿里云的这群疯子
《阿里技术参考图册》
2017杭州云栖大会100位大咖视频+讲义全分享
阿里研究员毕玄谈应用运维体系的变迁，DevOPS是大势所趋
阿里巴巴毕玄解密AIOps：一文读懂阿里巴巴运维体系的前世今生

]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>aiops</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：虚拟机在线迁移实验</title>
    <url>/dev-openstack-vm-live-migration-experiment/</url>
    <content><![CDATA[1. 前言《虚拟机在线迁移的性能统计》解决了性能统计的问题，《虚拟机在线迁移过程中的故障注入》解决了环境模拟的问题。接下来我们进行一些虚拟机迁移实验，收集不同环境下迁移过程中产生的性能统计数据。
共享存储和非共享存储的虚拟机迁移，性能差别很大。本次实验，在共享存储的条件下进行。对于非共享存储的虚拟机迁移实验，放在其他文章中记录。


2. 实例准备《OpenStack中虚拟机的在线迁移》和《OpenStack中共享存储的虚拟机在线迁移》两篇文章中，使用cirros作为迁移对象；本次实验中，使用ubuntu16作为迁移对象，因为这样可以更方便地注入故障，实例创建参考《OpenStack添加镜像》和《Ubuntu16手动安装OpenStack——创建实例》。
1、在horizon控制台，项目，计算，镜像。使用xenial-server-cloudimg-amd64-disk1.img镜像创建ubuntu16的镜像模板。
2、使用ubuntu16的镜像模板创建实例ubuntu0，实例类型选择m1.small，配置为1核2G内存20G存储。
3、创建成功后，分配浮动IP。本次分配的浮动IP为10.0.2.154。
4、登录ubuntu0实例，安装stress-ng，方便接下来的实验。
3. 正常虚拟机迁移1、controller节点ping实例并记录sudo ping 10.0.2.154 -i 0.01 &gt;&gt; ping.log
2、在compute和compute2节点，都启动iptraf，监听49152 to 49261。
3、在控制节点执行迁移命令nova live-migration ubuntu0 compute2
4、查看是否迁移完成nova show ubuntu0
5、迁移完成后进行统计。（1）在compute节点执行time.sh，得到总的迁移时间，22s。（2）在controller执行downtime.sh，计算downtime，6.28s。（3）根据iptraf的监听结果，得到迁出和迁入的数据量，283M，248M。
downtime不合理！为什么这么久？理论上是毫秒级的哇！参考《详解openstack下热迁移机制》，发现OpenStack可以进行迁移的参数设置，downtime可以进行调节。查看迁移日志：grep downtime /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log发现日志中显示的downtime都是固定的50ms，每次迁移只有一次停机。感觉挺复杂的样子，先不管它，按照默认设置来实验吧。
4. 取消迁移如果虚拟机迁移失败，可以取消迁移。
1、查看迁移任务nova server-migration-list ubuntu0该命令查看到迁移任务的ID为12。
2、查看迁移过程nova server-migration-show ubuntu0 12
3、取消迁移nova live-migration-abort ubuntu0 12
5. CPU故障1、在ubuntu0实例中创建一个CPU进程，CPU占用率10%stress-ng -c 1 -l 10使用top可以看到，CPU占用率不稳定，stress-ng进程偶尔跳到最高，占用率10%到90%不等。
2、实例迁移，得到总的迁移时间，34s。
3、设置CPU占用率分别为20%、30%到100%，统计结果如下：
| CPU占用率 | 迁移时间 | 停机时间 | 迁出数据量 | 迁入数据量 || —– | —– | —– | —– | —– | —– || 0%  | 18s | 6.28s | 283M | 248M || 10% | 20s | 6.53s | 353M | 341M || 20% | 19s | 4.52s | 323M | 300M || 30% | 20s | 4.58s | 354M | 338M || 40% | 21s | 4.62s | 336M | 347M || 50% | 20s | 5.41s | 355M | 323M || 60% | 19s | 4.54s | 303M | 309M || 70% | 21s | 5.24s | 356M | 355M || 80% | 19s | 4.41s | 351M | 338M || 90% | 17s | 7.31s | 328M | 362M || 99% | 19s | 5.77s | 351M | 348M |
6. 内存故障1、在实例中创建一个100M的内存进程stress-ng --vm 1 --vm-bytes 100M --vm-keep使用free -h，可以看到内存被使用掉了100M。
2、虚拟机迁移，得到总的迁移时间，22s。
3、设置内存进程分别占用300M、600M到1.5G，统计结果如下：
| 内存占用 | 迁移时间 | 停机时间 | 迁出数据量 | 迁入数据量 || —– | —– | —– | —– | —– | —– || 100M | 22s | 7.63s | 501M | 653M || 300M | 29s | 7.88s | 1238M | 1159M || 600M | 26s | 7.72s | 1341M | 1279M || 900M | 78s | 8.31s | 5572M | 6155M || 1200M | 92s | 8.14s | 8208M | 8070M || 1500M | 102s | 8.47s | 8396M | 9088M |
7. IO故障1、在实例中创建一个IO进程stress-ng --io 1使用iostat -d -k 2查看IO状态，发现tps稳定在200以上，而磁盘读写都为0。使用top命令可以看到，CPU占用率很高，在90%上下。
2、在实例中创建1-10个IO进程，统计结果如下：
| IO进程数 | 迁移时间 | 停机时间 | 迁出数据量 | 迁入数据量 || —– | —– | —– | —– | —– | —– || 1 | 37s | 7.59s | 2067M | 1954M || 2 | 40s | 6.83s | 2032M | 2007M || 3 | 36s | 7.04s | 2071M | 1788M || 4 | 36s | 8.08s | 1846M | 1969M || 5 | 36s | 6.02s | 2074M | 2012M || 6 | 35s | 7.44s | 1805M | 2032M || 7 | 36s | 5.64s | 2066M | 2010M || 8 | 36s | 8.60s | 1972M | 1987M || 9 | 34s | 10.95s | 2064M | 1880M || 10 | 36s | 7.02s | 1865M | 1981M |
8. 磁盘故障1、在虚拟机中创建一个写入进程stress-ng -d 1使用iostat -d -k 2查看IO状态，发现tps在30上下，kB_wrtn/s在30000上下。使用top命令可以看到，CPU占用率在25%到99%浮动。
2、在实例中创建1-10个写入进程，统计结果如下：
| 写入进程数 | 迁移时间 | 停机时间 | 迁出数据量 | 迁入数据量 || —– | —– | —– | —– | —– | —– || 1 | 35s | 4.83s | 2059M | 2023M || 2 | 37s | 8.70s | 1874M | 1957M || 3 | 34s | 8.92s | 2055M | 1980M || 4 | 36s | 4.97s | 2024M | 1959M || 5 | 36s | 5.50s | 2055M | 1960M || 6 | 35s | 4.64s | 2025M | 1957M || 7 | 34s | 6.02s | 2055M | 2041M || 8 | 43s | 9.11s | 2009M | 1978M || 9 | 35s | 6.60s | 2069M | 1997M || 10 | 34s | 8.89s | 1931M | 1942M | 
9. 网络延迟1、在compute节点（迁出节点）添加网络延迟10mstc qdisc add dev eth0 root netem delay 10ms注：原本的延迟在0.5ms上下。
2、将网络延迟修改为20ms-100mstc qdisc replace dev eth0 root netem delay 20ms
统计结果如下：
| 延迟时间 | 迁移时间 | 停机时间 | 迁出数据量 | 迁入数据量 || —– | —– | —– | —– | —– | —– || 10ms | 28s | 10.43s | 325M | 307M || 20ms | 36s | 8.80s | 305M | 287M || 30ms | 38s | 5.73s | 344M | 305M || 40ms | 43s | 11.28s | 305M | 290M || 50ms | 44s | 6.81s | 240M | 292M || 60ms | 47s | 7.09s | 256M | 295M || 70ms | 52s | 14.48s | 303M | 292M || 80ms | 50s | 12.17s | 242M | 297M || 90ms | 51s | 11.37s | 249M | 297M || 100ms | 63s | 18.22s | 298M | 344M |
在延迟时间大于100ms后，迁移显示成功，实例状态active，但是无法ping通实例。
取消模拟：tc qdisc del dev eth0 root
10. 丢包1、两个计算节点，都模拟丢包率1%tc qdisc add dev eth0 root netem loss 1%
2、将模拟丢包率修改为2%-10%tc qdisc change dev eth0 root netem loss 2%
统计结果如下：
| 丢包率 | 迁移时间 | 停机时间 | 迁出数据量 | 迁入数据量 || —– | —– | —– | —– | —– | —– || 0.5% | 20s | 8.55s | 295M | 330M || 1% | 31s | 11.24s | 301M | 249M || 1.5% | 29s | 7.44s | 335M | 313M || 2% | 28s | 11.62s | 261M | 299M || 2.5% | 40s | 10.34s | 300M | 325M || 3% | 43s | 15.19s | 306M | 268M |
在丢包率大于3%后，迁移显示成功，实例状态active，但是无法ping通实例。
取消模拟：tc qdisc del dev eth0 root
11. 包重复1、两个计算节点，都模拟包重复率1%tc qdisc add dev eth0 root netem duplicate 1%随机产生 1% 重复的包。
2、将模拟包重复率修改为2%-10%-100%tc qdisc change dev eth0 root netem duplicate 2%
统计结果如下：
| 包重复率 | 迁移时间 | 停机时间 | 迁出数据量 | 迁入数据量 || —– | —– | —– | —– | —– | —– || 1% | 18s | 4.82s | 288M | 283M || 2% | 21s | 4.06s | 308M | 288M || 3% | 18s | 8.02s | 249M | 309M || 4% | 21s | 6.37s | 316M | 300M || 5% | 19s | 4.70s | 314M | 307M || 6% | 18s | 4.18s | 285M | 316M || 7% | 21s | 3.81s | 324M | 315M || 8% | 18s | 7.60s | 305M | 234M || 9% | 21s | 6.87s | 328M | 316M || 10% | 18s | 7.71s | 310M | 317M || 20% | 21s | 11.24s | 360M | 347M || 30% | 19s | 9.90s | 381M | 388M || 40% | 22s | 6.67s | 429M | 376M || 50% | 18s | 4.43s | 445M | 434M || 60% | 24s | 8.04s | 499M | 438M || 70% | 19s | 11.30s | 488M | 518M || 80% | 25s | 5.61s | 581M | 485M || 90% | 29s | 5.08s | 745M | 646M || 100% | 21s | 7.89s | 706M | 727M |
取消模拟：tc qdisc del dev eth0 root
12. 包损坏1、两个计算节点，都模拟包损坏率1%tc qdisc add dev eth0 root netem corrupt 1%随机产生 1% 损坏的报文（在报文的随机位置造成一个比特的错误）。
在ping的时候会报错：Warning: time of day goes back , taking countermeasures，不过没关系，不影响我们的停机时间统计结果。
2、将模拟丢包率修改为2%-10%tc qdisc change dev eth0 root netem corrupt 2%
| 包损坏率 | 迁移时间 | 停机时间 | 迁出数据量 | 迁入数据量 || —– | —– | —– | —– | —– | —– || 0.5% | 20s | 4.43s | 359M | 369M || 1% | 24s | 5.90s | 371M | 351M || 1.5% | 30s | 12.13s | 374M | 357M || 2% | 32s | 9.09s | 331M | 362M || 2.5% | 42s | 7.49s | 345M | 337M || 3% | 48s | 13.46s | 378M | 374M |
迁移成功率随着包损坏率的增加而降低，包损坏率大于3%之后，基本不会成功。会停留在迁移状态，每秒几K传输。
取消模拟：tc qdisc del dev eth0 root
13. 后记至此，完成了系统故障对于OpenStack虚拟机在线迁移过程中的性能影响的实验。实验数据和预期差别很大，比如停机时间量级不合理，比如停机时间基本上不受系统故障影响，比如迁移成功后却出现无法ping通的现象等等。
后续任务有两个，一是需要进行共享存储的虚拟机在线迁移实验，用于对比。二是尝试进行调参，把停机时间调整到毫秒级。
再进一步的话，可以研究下虚拟机迁移成功后无法ping通的原因，还可以研究下同样故障下虚拟机迁移时而成功时而失败的原因。
]]></content>
      <categories>
        <category>engineering</category>
        <category>testing</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>测试</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：虚拟机在线迁移过程中的故障注入</title>
    <url>/dev-openstack-vm-live-migration-fault-injection/</url>
    <content><![CDATA[1. 前言《虚拟机在线迁移的性能统计》一文中，已经找到了虚拟机迁移过程中性能统计的方法，可以统计出迁移时间、停机时间、迁移数据量等指标。
而实际生产环境是复杂的，所以我们想模拟一些环境，看看这些环境（故障）对于虚拟机迁移性能的影响。本文就来研究一下CPU故障、内存故障、磁盘故障、网络故障等的模拟方法。


2. 故障注入故障注入有两个大的思路：一个是注入到被迁移的虚拟机，另一个是注入到宿主机。
本次故障注入主要有CPU故障、内存故障、磁盘故障、网络故障。
故障模拟方法如下（感谢卢澄志同学提供模拟方法）：

对于CPU故障、内存故障、磁盘故障的模拟，使用stress或者stress-ng
对于网络故障的模拟，使用netem

参考文档：

Linux 压力测试软件 Stress 使用指南
stress-ng：模拟特定的cpu百分比
使用 tc netem 模拟网络异常
netem wiki

3. 安装使用stress-ng3.1. stress-ng简介stress-ng是一个压力测试工具，用于对计算机系统实施较大的工作负荷，以验证其健壮性和稳定性，或者用于检测可能存在的软硬件问题。
stress-ng 特点：

提供了超过 240 种压力测试。
各种压力测试可以单独运行，也可以以随机的方式混合运行，以便在真实场景下对系统进行压力测试。
还可以实施各种操作系统调用的压力测试。例如，可以选择进行 CPU 忙测试、硬盘测试、内存测试等。
通过调整测试时间、测试并发量等参数，可以根据需求定制不同的压力测试环境。

3.2. 安装stress-ngapt-get install stress-ng

3.3. stress-ng使用示例3.3.1. 模拟CPU压力产生3个CPU进程1分钟后关闭
stress-ng --cpu 3 --verbose --timeout 1m

3.3.2. 模拟内存压力产生2个10MB的内存进程1分钟后关闭
stress-ng --vm 2 --vm-bytes 10M --vm-keep --timeout 1m

3.3.3. 模拟IO进程压力产生3个IO进程1分钟后关闭
stress-ng --io 3 --timeout 1m

4. 安装使用tc netem4.1. tc netem简介TC(Network Control) 是 Linux 系统中的 “流量控制” 工具，TC netem（network emulator）则是 TC 的一个扩展，用于模拟网络的不同条件，以便在各种环境下测试网络应用。
TC netem 可以模拟的网络条件包括但不限于：

延迟：模拟网络传输数据包之间的延迟，包括固定的延迟和随机的延迟。
丢包：模拟网络数据包在传输过程中的丢失。
乱序：模拟网络数据包接收顺序的混乱。
带宽限制：模拟某种预设的网络带宽条件。
包重复：模拟在网络中发送重复数据包的情况。

所有这些条件都可以单独或一起使用，以模拟几乎任何可能的网络环境。使用tc netem可以让开发者更有效地进行网络应用开发和测试，从而提高应用在实际网络环境下的性能和稳定性。
4.2. 安装tc netem在Ubuntu16中，tc netem不需要安装，系统自带。
4.3. tc netem使用示例4.3.1. 模拟延迟传输报文延迟 100ms 发送
tc qdisc add dev eth0 root netem delay 100ms

报文延迟的时间在 100ms ± 20ms 之间，具体值随机选择
tc qdisc replace dev eth0 root netem delay 100ms 20ms

4.3.2. 模拟丢包率丢包率是 0.3%，并且当前报文丢弃的可能性和前一个报文 25% 相关。
tc qdisc change dev eth0 root netem loss 0.3% 25%

4.3.3. 模拟包重复随机产生 50% 重复的包
tc qdisc change dev eth0 root netem duplicate 50%

4.3.4. 模拟包损坏随机产生 2% 损坏的报文（在报文的随机位置造成一个比特的错误）
tc qdisc add dev eth0 root netem corrupt 2%

4.3.5. 模拟包乱序固定的每隔一定数量的报文就乱序一次：每5个报文会正常发送，其他的报文延迟 100ms
tc qdisc change dev eth0 root netem reorder 50% gap 3 delay 100ms

注意：要想看到 ping 报文的乱序，我们要保证发送报文的间隔小于报文的延迟时间 100ms，这里可以用 -i 0.05 把发送间隔设置为 50ms。
ping -i 0.05 192.168.56.101

使用概率来选择乱序的报文：50% 的报文会正常发送，其他报文（1-50%）延迟 300ms 发送。
tc qdisc change dev eth0 root netem reorder 50% 15% delay 300ms

4.3.6. 取消模拟tc qdisc del dev eth0 root







]]></content>
      <categories>
        <category>engineering</category>
        <category>testing</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>测试</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：虚拟机在线迁移的性能统计</title>
    <url>/dev-openstack-vm-live-migration-performance/</url>
    <content><![CDATA[1. 前言OpenStack中共享存储和非共享存储的虚拟机迁移环境都已经搭建完成，接下来可以进行实验了。对于不同的宿主机、不同的实例、不同的负载，虚拟机迁移的性能也会有所不同。那么，虚拟机迁移的性能指标有哪些？又该怎样统计呢？本文就来研究一下。


2. 性能指标参考论文《Virt-LM: a benchmark for live migration of virtual machine》，可以得知，虚拟机迁移过程中的主要性能指标有四个：

整体迁移时间：从迁移开始到迁移结束的时间。
虚拟机停机时间：迁移过程中虚拟机停机（停止服务）的时间。
迁移数据量：在虚拟机迁移期间传输的数据总量。
应用程序的性能：迁移过程中对虚拟机中应用程序产生的影响。

3. 测量方法3.1. 监控既然是实验，那么肯定要收集实验过程中的数据。郝同学给这个四个节点的OpenStack集群安装了ganglia，用来监控各种指标。但是，ganglia更适合监控实时数据和观察变化，不适合进行统计。
3.2. 迁移时间整体迁移时间怎么测出来？通过观察图表？不靠谱。叶可江老师提供了思路，整体迁移时间的话，可以通过日志来获取：tail -n 40 /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log
通过查看迁出宿主机的日志，然后对比ganglia图表，可以找到两个关键节点：
2018-11-22 09:59:42.442 5 INFO nova.virt.libvirt.migration [req-d9849256-eaa8-4446-9cec-9545b74df126 202e7852a6074540a70638eed185538e eb83bf3b057f45e1b386cbc8c0702ae1 - default default] [instance: eebe068a-ba85-4905-a39b-f003a7480b14] Increasing downtime to 50 ms after 0 sec elapsed time......2018-11-22 10:00:02.984 5 INFO nova.compute.manager [req-4b409237-6c82-4b56-ae4c-9f905a4f932e b2edc935f89d4d2684ec4039c02a21cc ab7e3eb8c00d4299afd8572e1ec437bf - default default] [instance: eebe068a-ba85-4905-a39b-f003a7480b14] VM Stopped (Lifecycle Event)
但是，依然需要手动来计算迁移时间，很麻烦，那就写一个脚本来搞定吧！参考linux shell 时间运算以及时间差计算方法。
#!/bin/bashtime1=`grep nova.virt.libvirt.migration /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log | tail -n 1 |awk &#x27;&#123;print $2&#125;&#x27;`;time2=`grep &#x27;VM Stopped&#x27; /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log | tail -n 1 | awk &#x27;&#123;print $2&#125;&#x27;`;# 计算时间差time=$(($(date +%s -d $time2) - $(date +%s -d $time1)));echo &quot;$time s&quot;;
以上脚本，保存为time.sh，并添加执行权限。
另外，还可以通过nova migration-list &gt; list.log查看到迁移的开始时间和结束时间。然后通过脚本计算迁移时间：
#!/bin/bashtime1=`grep &#x27;live-migration&#x27; list.log | head -n 1 | awk &#x27;&#123;print $24&#125;&#x27;`time2=`grep &#x27;live-migration&#x27; list.log | head -n 1 | awk &#x27;&#123;print $26&#125;&#x27;`time=$(($(date +%s -d $time2) - $(date +%s -d $time1)));echo &quot;$time s&quot;;
不过，这种方法测出的迁移时间，比我们从nova-compute.log日志中测出的迁移时间，要多出7秒左右。这种方法应该更准确一些，毕竟是OpenStack自己记录的。
3.3. 停机时间参考How to measure Migration time and Down time，使用ping命令来测量停机时间。
我们的虚拟机有两个IP，一个是内网IP，一个是浮动IP。外部机器可以直连的，一般是浮动IP，所以在迁移过程中，建议ping浮动IP，比如sudo ping 10.0.2.159 -i 0.01 &gt;&gt; ping.log。ping.log中的信息为：
PING 10.0.2.159 (10.0.2.159) 56(84) bytes of data.64 bytes from 10.0.2.159: icmp_seq=1 ttl=63 time=3.40 ms64 bytes from 10.0.2.159: icmp_seq=2 ttl=63 time=1.15 ms64 bytes from 10.0.2.159: icmp_seq=3 ttl=63 time=1.17 ms64 bytes from 10.0.2.159: icmp_seq=4 ttl=63 time=1.33 ms......64 bytes from 10.0.2.159: icmp_seq=4139 ttl=63 time=1.17 ms64 bytes from 10.0.2.159: icmp_seq=4140 ttl=63 time=1.05 ms64 bytes from 10.0.2.159: icmp_seq=4141 ttl=63 time=1.12 ms64 bytes from 10.0.2.159: icmp_seq=4142 ttl=63 time=1.06 ms--- 10.0.2.159 ping statistics ---4142 packets transmitted, 3668 received, 11% packet loss, time 44127msrtt min/avg/max/mdev = 0.580/1.412/36.648/1.407 ms, pipe 3

由统计信息可以求出停机时间：
# 方法一：(缺失的icmp_seq总数)*(时间间隔)(4142-3668)*0.01=4.74s# 方法二：(总时间)*(icmp_seq缺失百分比)(44127/1000)*0.11 = 4.85s

两种方法相差0.1s，方法一更精确一些。如果想要更加精确，还可以把ping命令的时间间隔调整为1ms。
方法一写成脚本为：
#!/bin/bashtransmitted=`grep &#x27;packets transmitted&#x27; ping.log | tail -n 1 | awk &#x27;&#123;print $1&#125;&#x27;`received=`grep &#x27;packets transmitted&#x27; ping.log | tail -n 1 | awk &#x27;&#123;print $4&#125;&#x27;`lost=`expr $transmitted - $received`# echo &quot;downtime is $lost ms&quot;downtime=$(echo &quot;scale=3; $lost / 1000&quot; | bc)echo &quot;downtime is $downtime s&quot;

方法二写成脚本为：
#!/bin/bashtime_str=`grep &#x27;packets transmitted&#x27; ping.log | tail -n 1 | awk &#x27;&#123;print $10&#125;&#x27;`loss_str=`grep &#x27;packets transmitted&#x27; ping.log | tail -n 1 | awk &#x27;&#123;print $6&#125;&#x27;`time_num=`echo $&#123;time_str%ms&#125;`loss_percent=`echo $&#123;loss_str%\%&#125;`# downtime = (time_num)*(loss_percent/100)/1000num=`expr $time_num \* $loss_percent`downtime=$(echo &quot;scale=3; $num / 100000&quot; | bc)echo &quot;downtime is $downtime s&quot;

以上方法求出的停机时间，是总的停机时间，无论迁移过程中有几次停机，该方法都是适用的。我们会好奇，实际迁移过程中，停机是否只有一次？我们写一个脚本来寻求答案：
#!/bin/bash# 去掉头部和尾部，保留第五列cat ping.log | sed &#x27;1d&#x27; | tac | sed &#x27;1,4d&#x27; | tac | awk &#x27;&#123;print $5&#125;&#x27; &gt; seq.log &amp;&amp;# 得到最后一行的序列号max=`tail -n 1 seq.log | awk -F &#x27;=&#x27; &#x27;&#123;print $2&#125;&#x27;`# 所有序列号赋值给数组array_str=`cat seq.log | awk -F &#x27;=&#x27; &#x27;&#123;print $2&#125;&#x27;`IPS=&#x27; &#x27;array=($array_str)# 打印没有响应的序列号COUNTER=0while [ $COUNTER -lt $max ]do    COUNTER=`expr $COUNTER + 1`    if [[ &quot;$&#123;array[@]&#125;&quot; =~ $COUNTER ]]    then        continue    else        echo $COUNTER  &gt;&gt; result.log    fidone &amp;&amp;# 对比seq数量start=`head -n 1 result.log`end=`tail -n 1 result.log`num=`expr $end - $start + 1`echo &quot;if continuous, num should be $num&quot;result_str=`cat result.log`IPS=&#x27;\n&#x27;result=($result_str)echo &quot;real num is $&#123;#result[@]&#125;&quot;

脚本输出两个474，说明实例没有响应的序列号是连续的，证明了停机只有一次。比如result.log中为{3,4,5,6,7}，那么7-3+1=5，集合中元素个数也为5，证明连续。比如result.log中为{3,4,5,10,11}，那么11-3+1=9，集合中元素个数为5，证明不连续。
3.4. 带宽测量出的downtime为4.74s，很不合理，因为虚拟机迁移理论上的停机时间是毫秒级的！是因为带宽太低导致停机时间过长吗？可以用如下方法进行测试：1、compute1和compute2上都安装iperfsudo apt install iperf
2、compute1上启动iperf服务iperf -s
3、compute2上启动iperf客户端iperf -c 192.168.56.112 -i 1
3.5. 迁移数据量对于迁移数据量，可以使用iptables对端口进行监控，参考Linux下对端口流量进行统计和Linux 使用iptables进行shadowsocks流量统计。但是，计算节点网络非常复杂，郝同学不想对其进行修改，所以采用另外一种方案，iptraf，参考使用iptraf查看TCP/UDP某个特定端口的带宽与流量。
1、两个计算节点安装iptrafsudo apt install iptraf
需要注意的是，iptraf只支持名为ethX的网卡。想要显示enoX、enp0sX网卡，要么网卡重命名，要么安装使用iptraf-ng。sudo apt install iptraf-ng
2、启动iptrafsudo iptraf启动后，进入到一个字符界面。
3、设置Configure… -&gt; Additional ports…-&gt; 22 -&gt; Exit Configuration -&gt; Statistical breakdowns… -&gt; By TCP/UDP port -&gt; eth0 -&gt; statistic view
参考Configure live migrations，可以得知：

By default, libvirt uses the TCP port range from 49152 to 49261 for copying memory and disk contents. Compute hosts must accept connections in this range.

所以在实际使用的时候，端口设置为49152 to 49261。重新进入统计页面，之前的统计数据会清零。
3.6. 应用性能Web应用的性能，主要包括执行时间、传输速度、吞吐量、资源占用率。
基准测试（benchmarking）是一种测量和评估软件性能指标的活动。你可以在某个时候通过基准测试建立一个已知的性能水平（称为基准线），当系统的软硬件环境发生变化之后再进行一次基准测试以确定那些变化对性能的影响。这是基准测试最常见的用途。其他用途包括测定某种负载水平下的性能极限、管理系统或环境的变化、发现可能导致性能问题的条件，等等。详情参考什么是基准测试？。
对于性能的监控，最简单的思路是Apache Benchmark。使用ab来监控Web服务的性能，检查迁移过程中会发生哪些变化。但是，ab什么时候运行？什么时候停止？请求多少次？怎么计算迁移过程中的平均性能？这些都不能确定。如果使用cAdvisor+prometheus呢？参考linux工匠之docker和kubernetes的监控(cadvisor+prometheus)。不行，因为这种方法监控的是docker容器，收集的是CPU、内存等数据，而不是Web性能数据。使用SPEC？也不好，收费，安装使用复杂。
很绝望，前辈们都是怎么进行测试的？不管了，先不考虑应用性能指标。
4. 书签
An analysis of the performance of live migration in Openstack
An analysis of the performance of block live migration in Openstack
Performance analysis of “post-copy” live migration in Openstack

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>testing</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>测试</tag>
        <tag>好好学OpenStack</tag>
        <tag>iperf</tag>
      </tags>
  </entry>
  <entry>
    <title>VirtualBox中Ubuntu扩容</title>
    <url>/dev-virtualbox-ubuntu-extend-storage/</url>
    <content><![CDATA[1. 前言VirtualBox中，安装了Ubuntu16，磁盘40G。现在，因为某些原因，要对磁盘进行扩容。有两个思路，一个是添加磁盘，另一个是对原磁盘进行扩容。
本文就来研究一下这两种思路的具体做法。


2. 添加磁盘2.1. VirtualBox配置1、Settings，Storage，Controller: SATA，Adds hard disk。
2、Create new disk，VDI，Dynamically allocated，命名为sdb，大小选择512GB，Create。
2.2. 分区挂载参考《Linux挂载文件系统》一文。
2.3. 扩展卷组在硬盘分区、格式化之后，不要进行挂载。参考VirtualBox下Linux虚机扩容，进行如下操作：
1、创建物理卷pvcreate /dev/sdb1
2、查看物理卷pvdisplay
3、查看卷组vgdisplay可以看到一个卷组ubuntu-vg。
4、扩展卷组vgextend ubuntu-vg /dev/sdb1
5、查看逻辑卷lvdisplay可以看到root卷的LV Path为/dev/ubuntu-vg/root。
6、扩展逻辑卷
lvextend -L +512G -n /dev/ubuntu-vg/rootlvdisplay

7、调整root逻辑卷大小
resize2fs /dev/ubuntu-vg/rootdf -h

3. 原磁盘扩容3.1. VirtualBox配置1、切换到root用户sudo -i
2、查看磁盘列表vboxmanage list hdds
3、找到需要扩容的磁盘，记下UUID
4、扩容到512Gvboxmanage modifyhd --resize 512000 ba3e897a-742e-45dc-a99a-3302c07be984
3.2. 分区挂载1、查看磁盘
df -hfdisk -l

2、增加分区fdisk /dev/sda输入n，报错：No free sectors available.没有找到好的解决办法，只能搁置。
3.3. 扩展卷组无法分区，同样无法扩展卷组。
4. 后记以上两种方案，殊途同归。增加容量后，都要经过分区，格式化，然后分区挂载或者扩展卷组。鉴于第二种方案中分区会出错，所以使用第一个方案更好些。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>ubuntu</tag>
        <tag>virtualbox</tag>
      </tags>
  </entry>
  <entry>
    <title>Linux中安装NFS</title>
    <url>/dev-install-nfs-on-linux/</url>
    <content><![CDATA[1. NFS简介
Network File System (NFS) is a distributed file system protocol originally developed by Sun Microsystems (Sun) in 1984, allowing a user on a client computer to access files over a computer network much like local storage is accessed. NFS, like many other protocols, builds on the Open Network Computing Remote Procedure Call (ONC RPC) system. NFS is an open IETF standard defined in a Request for Comments (RFC), allowing anyone to implement the protocol.

参考文档：

Wikipedia - Network File System



2. CentOS中安装NFS2.1. 安装nfs-server作为存储节点的主机安装nfs-server，本节中的存储节点主机IP为192.168.56.101。
参考文档：

搭建NFS Server

1、关闭防火墙
systemctl stop firewalld.servicesystemctl disable firewalld.service

2、安装nfs（包括nfs-server和nfs-client）
yum install -y nfs-utils rpcbind

3、创建共享目录，设置共享目录权限
mkdir -p /data/nfschmod -R 777 /data/nfs

4、配置共享目录
echo &#x27;/data/nfs *(insecure,rw,sync,no_root_squash)&#x27; &gt;&gt; /etc/exports

5、启动nfs-server
systemctl enable rpcbindsystemctl enable nfs-serversystemctl start rpcbindsystemctl start nfs-serversystemctl status rpcbindsystemctl status nfs-serverexportfs -r # 重新挂载

6、检查nfs状态
exportfsrpcinfo -p | grep nfs

7、查看目录挂载权限
cat /var/lib/nfs/etab

2.2. 安装nfs-client在需要挂载nfs的主机上安装nfs-client，本节中主机IP为192.168.56.102。
1、关闭防火墙
systemctl stop firewalld.servicesystemctl disable firewalld.service

2、安装nfs（包括nfs-server和nfs-client）
yum install -y nfs-utils rpcbind

3、启动nfs-client
systemctl enable rpcbindsystemctl enable nfssystemctl start rpcbindsystemctl start nfssystemctl status rpcbindsystemctl status nfs

2.3. 测试nfs挂载192.168.56.102机器上，测试nfs挂载。
1、检查101是否有nfs共享目录
showmount -e 192.168.56.101

2、挂载nfs共享目录到本机
mkdir /data/mntnfsmount -t nfs 192.168.56.101:/data/nfs /data/mntnfs

3、测试读写文件102机器读写文件
echo &quot;test&quot; &gt; /data/mntnfs/test.txtcat /data/mntnfs/test.txt

101机器读文件
cat /data/nfs/test.txt

4、配置自动挂载
cat &lt;&lt;EOF | tee -a /etc/fstab192.168.56.101:/data/nfs /data/mntnfs nfs default 0 0EOFmount -a

3. Ubuntu中安装NFS3.1. 安装nfs-server作为存储节点的主机安装nfs-server，本节中的存储节点主机IP为192.168.56.103。
1、安装nfs-server
apt-get -y install nfs-kernel-server

2、创建共享目录，设置共享目录权限
mkdir -p /data/nfschmod -R 777 /data/nfs

3、配置共享目录
# echo &#x27;/data/nfs *(rw,sync,no_root_squash)&#x27; &gt;&gt; /etc/exportsecho &#x27;/data/nfs *(insecure,rw,sync,no_root_squash)&#x27; &gt;&gt; /etc/exports

4、启动nfs-server，配置开机自启动
# service nfs-kernel-server restartsystemctl start nfs-kernel-serversystemctl status nfs-kernel-serversystemctl enable nfs-kernel-server

5、检查nfs状态
exportfsrpcinfo -p | grep nfs

3.2. 安装nfs-client在需要挂载nfs的主机上安装nfs-client，本节中主机IP为192.168.56.104。
安装nfs-client
apt-get install nfs-common

3.3. 测试nfs挂载192.168.56.104机器上，测试nfs挂载。
1、检查103是否有nfs共享目录
showmount -e 192.168.56.103

2、挂载nfs共享目录到本机
mkdir /data/mntnfsmount -t nfs 192.168.56.103:/data/nfs /data/mntnfs

3、测试读写文件104机器读写文件
echo &quot;test&quot; &gt; /data/mntnfs/test.txtcat /data/mntnfs/test.txt

103机器读文件
cat /data/nfs/test.txt

4、配置自动挂载
cat &lt;&lt;EOF | tee -a /etc/fstab192.168.56.103:/data/nfs /data/mntnfs nfs default 0 0EOFmount -a

4. NFS高可用上文中，我们搭建的nfs-server都是单节点的，如果节点挂掉，会造成很多依赖它的软件不可用。如果节点数据丢失，更是一场灾难。因此，生产环境最好搭建nfs高可用环境。
参考文档：

NFS双机热备高可用环境 - 运维笔记
NFS高可用(NFS+keepalive+Sersync)

5. NFS重叠挂载已知NFS的共享和挂载情况如下：

主机1通过NFS对外共享了其本地的 /data 目录。
主机2通过NFS对外共享了其本地的 /data 目录。
主机1的 /data/subdata 目录通过NFS挂载了主机2共享的 /data 目录。
主机3的 /data 目录挂载了主机1共享的 /data 目录。

当主机3 /data 目录时，如果其访问的路径深入到了/data/subdata，在这种特殊的设置下，主机3实际上通过主机1中的重叠挂载，间接地访问了主机2的/data目录。
这种场景是通过NFS的转发和重叠挂载功能实现的：

当主机1收到针对 /data/subdata 目录的NFS请求时，它会将这些请求转发给主机2。
主机2接收到请求后，会像处理任何NFS请求一样处理这些转发过来的请求，返回相应的数据。

注意：不是所有的NFS配置都默认支持这种重叠挂载转发行为。某些NFS版本和实现（特别是较早的版本）可能不支持这样的设置或需要特殊的配置参数来启用重叠挂载的转发。例如，NFSv4协议引入了重叠挂载和委托功能的改进，但是在使用时仍然需要注意初始NFS共享和客户端挂载的正确配置。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>storage</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>存储</tag>
        <tag>ubuntu</tag>
        <tag>nfs</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：OpenStack中共享存储的虚拟机在线迁移</title>
    <url>/dev-openstack-vm-live-migration/</url>
    <content><![CDATA[1. 前言《OpenStack中虚拟机的在线迁移》一文中，虚拟机迁移时，把block一起进行了迁移。生产环境中，这些虚拟机往往是共享存储设备，不需要对block进行迁移。所以，本文就来重新配置一下环境，实现共享存储的虚拟机在线迁移。


2. 原理常规安装的OpenStack中，实例存放在 /var/lib/nova/instances/ 目录；使用kolla安装的OpenStack中，实例存放在 /var/lib/docker/volumes/nova_compute/_data/instances/ 目录。
共享存储，需要两个步骤：1、有一台NFS（Network File System）服务器，共享某个目录，比如/nfs/share/instances。2、所有计算节点的instances目录，都挂载上NFS服务器的/nfs/share/instances目录。
3. 主机准备使用virtualbox创建一个新的ubuntu16虚拟机，或者clone其他节点并且通过快照恢复初始状态，作为nfs存储节点。这里通过clone的方法得到一个新的主机，设置IP为192.168.56.130。参考《VirtualBox中Ubuntu扩容》，对虚拟机进行扩容。
4. NFS准备参考文档《Linux中安装NFS》，在存储节点安装nfs-server，计算节点安装nfs-client。
4.1. 存储节点nfs配置1、创建共享目录并修改权限
mkdir -p /nfs/share/instanceschmod o+x /nfs/share/instances

2、写入nfs配置
echo &#x27;/nfs/share/instances *(rw,sync,no_root_squash)&#x27; &gt;&gt; /etc/exports

4.2. 计算节点nfs配置1、挂载nfs目录
mount -t nfs 192.168.56.130:/nfs/share/instances /var/lib/docker/volumes/nova_compute/_data/instances/

instances目录中还有文件和文件夹，直接挂载覆盖确定没有影响吗？没有。
两个计算节点都执行同样的操作，此时它们的instances目录是同一个目录。
2、修改instances目录权限
chmod -R 777 /var/lib/docker/volumes/nova_compute/_data/instances/
一定要修改，否则创建实例时会报错。
3、重启compute服务和libvirt服务
docker stop nova_computedocker start nova_computedocker stop nova_libvirtdocker start nova_libvirt

如果不重启，那么创建的实例会存放在计算节点本地磁盘上，卸载instances目录后可以看到。
5. 迁移5.1. 创建实例参考《Kolla安装OpenStack多节点》的初始化配置部分，使用cirros镜像创建demo2实例。
1、使管理员环境生效
source /etc/kolla/admin-openrc.sh

2、创建实例
openstack server create \    --image cirros \    --flavor m1.tiny \    --key-name mykey \    --nic net-id=25e6c0ef-6a0a-481c-a08a-46f7ef67ad3e \    demo2

3、分配浮动IP
openstack network listopenstack floating listopenstack floating ip create public1openstack server add floating ip demo2 10.0.2.156

3、查看实例计算节点上，在/var/lib/docker/volumes/nova_compute/_data/instances目录中，可以看到新创建的实例。
5.2. 迁移实验1、查看实例列表
nova list

2、查看实例信息
nova show demo2
可以看到demo2位于compute节点。
3、实例迁移
nova live-migration demo2 compute2

4、再次查看实例信息
nova show demo2
可以看到demo2迁移到了compute2节点，迁移成功，nice。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>storage</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>存储</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>nfs</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：OpenStack中虚拟机的在线迁移</title>
    <url>/dev-openstack-vm-block-live-migration/</url>
    <content><![CDATA[1. 前言虚拟机迁移主要有三种方式：P2V、V2V和V2P。P2V指迁移物理服务器上的操作系统及其上的应用软件和数据到VMM（Virtual Machine Monitor）管理的虚拟服务器中。V2V迁移是在虚拟机之间移动操作系统和数据。V2P 指把一个操作系统、应用程序和数据从一个虚拟机中迁移到物理机的主硬盘上，是 P2V 的逆操作。
V2V迁移又分为离线迁移和在线迁移。离线迁移也叫做常规迁移、静态迁移，在迁移之前需要将虚拟机关闭。在线迁移又称为实时迁移，是指保持虚拟机正常运行的同时进行迁移。本文要研究的，就是虚拟机的在线迁移。更多详细内容，请参考虚拟机迁移技术漫谈，第 1 部分。


2. 迁移相关命令本文研究的是OpenStack集群中的虚拟机迁移，那么先研究一下OpenStack的虚拟机迁移命令。主要参考Live-migrate instances、Block Live Migration in OpenStack environment。
1、查看实例IDopenstack server list
PS：openstack server可以换成nova。
2、查看实例详情openstack server show 180617ec-1348-4144-a496-8751d12e84bf
PS：实例ID可以换成demo1。
3、查看可以迁移的节点openstack compute service list
4、查看其它节点资源使用情况openstack host list
openstack host show compute2
5、迁移nova live-migration 180617ec-1348-4144-a496-8751d12e84bf compute2啊嘞，没有反应是闹哪样？查看实例详情，依然位于compute节点。
6、调试迁移nova live-migration 180617ec-1348-4144-a496-8751d12e84bf compute2 --debug信息很多，找不到重点。在openstack热迁移和冷迁移一文中发现，在线迁移又可以分为两种：live migration和block migration。live migration需要实例保存在NFS共享存储中，这种迁移主要是实例的内存状态的迁移，速度应该会很快。block migration除了实例内存状态要迁移外，还得迁移磁盘文件，速度会慢些，但是它不要求实例存储在共享文件系统中。
而我们的这个集群，没有共享存储，所以应该用block migration？
7、再次迁移nova live-migration --block-migrate 180617ec-1348-4144-a496-8751d12e84bf compute2修改后的命令，在shell中同样没有更多提示，但是查看horizon，发现了惊喜，正在迁移。等到迁移完成，查看实例详情，却发现，实例所在主机依然没有发生变化！是因为nova没有配置好吗？很有可能，那么修改一下nova的配置试试。
3. nova配置因为是使用kolla部署的OpenStack，所以修改配置重启服务的方法，和传统方法有所不同。参考kolla(Ocata)部署nova组件关键配置和volume映射和Openstack 之 调整nova相关参数。可以知道，主机中的/etc/kolla/nova-compute/nova.conf，对应容器中的/etc/nova/nova.conf。修改后重启容器，nova-compute服务配置就会发生改变。
1、在两个计算节点，修改nova.confvim /etc/kolla/nova-compute/nova.conf，添加:
live_migration_flag=VIR_MIGRATE_UNDEFINE_SOURCE,VIR_MIGRATE_PEER2PEER,VIR_MIGRATE_LIVE

2、重启nova-compute
docker stop nova_computedocker start nova_compute

3、进入容器docker exec -it nova_compute /bin/bash
4、查看配置是否生效vi /etc/nova/nova.conf
4. 再次迁移1、在控制节点执行迁移命令nova live-migration --block-migrate 180617ec-1348-4144-a496-8751d12e84bf compute2满怀期望，但是依然失败。
2、在计算节点查看日志tail -n 20 /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log日志报错部分如下：
2018-11-13 19:12:04.614 6 ERROR nova.virt.libvirt.driver [req-e6654355-fcec-4b93-a662-c758a03766fa b2edc935f89d4d2684ec4039c02a21cc ab7e3eb8c00d4299afd8572e1ec437bf - default default] [instance: 180617ec-1348-4144-a496-8751d12e84bf] Live Migration failure: operation failed: Failed to connect to remote libvirt URI qemu+tcp://compute2/system: Unable to resolve address &#x27;compute2&#x27; service &#x27;16509&#x27;: Name or service not known: libvirtError: operation failed: Failed to connect to remote libvirt URI qemu+tcp://compute2/system: Unable to resolve address &#x27;compute2&#x27; service &#x27;16509&#x27;: Name or service not known
由日志可以看出，问题出现在libvirt上，那么修改一下libvirt的配置。
5. libvirt配置参考OpenStack虚拟机冷迁移与热迁移、虚拟机在 OpenStack 里没有共享存储条件下的在线迁移、虚拟化之KVM virsh常用命令篇，对libvirt的配置进行修改。
1、查看libvirtd.confvim /etc/kolla/nova-libvirt/libvirtd.conf修改配置如下：
listen_tcp = 1listen_tls = 0auth_tcp = &quot;none&quot;ca_file = &quot;&quot;log_level = 3log_outputs = &quot;3:file:/var/log/kolla/libvirt/libvirtd.log&quot;listen_addr = &quot;0.0.0.0&quot;

2、重启libvirt服务
docker stop nova_libvirtdocker start nova_libvirt

3、查看libvirt服务netstat -anpt | grep libvirt看到libvirtd监听在0.0.0.0:16509端口即为正常。
PS：可以在其他节点使用virsh命令测试连通性virsh -c qemu+tcp://compute2/system
6. 第三次迁移1、在控制节点执行迁移命令nova live-migration --block-migrate 180617ec-1348-4144-a496-8751d12e84bf compute2
2、查看实例详情openstack server show demo1迁移成功了，nice。
7. 其他命令1、查看迁移任务nova server-migration-list 180617ec-1348-4144-a496-8751d12e84bf该命令查看到迁移任务的ID，然后根据这个ID就可以看到更详细的迁移过程。
2、查看迁移过程nova server-migration-show 180617ec-1348-4144-a496-8751d12e84bf 2
3、查看警告或者出错日志grep WARNING.*180617ec-1348-4144-a496-8751d12e84bf /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log
4、取消迁移nova live-migration-abort 180617ec-1348-4144-a496-8751d12e84bf 2
8. 后记以上，实现了OpenStack虚拟机的block live migration。后续实验，不妨再研究下共享存储的live migration。
9. 书签OpenStack命令行操作之虚机管理实现
虚拟机迁移之热迁移(live_migrate)源码解读
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Kolla安装OpenStack添加新节点</title>
    <url>/dev-kolla-openstack-add-new-node/</url>
    <content><![CDATA[1. 前言《Kolla安装OpenStack多节点》一文中，使用kolla安装了三个节点的openstack。
因为实验需要，现在需要再添加一个计算节点。那么问题来了，怎样在不破坏现有环境的基础上，添加一个新的计算节点呢？本文就来研究一下。



2. 新节点配置2.1. 网络设置在新添加的计算节点上，执行以下操作：
1、切换到root用户sudo -i
2、vim /etc/network/interfaces，设置网卡为：
# This file describes the network interfaces available on your system# and how to activate them. For more information, see interfaces(5).source /etc/network/interfaces.d/*# The loopback network interfaceauto loiface lo inet loopback# The primary network interfaceauto eth0iface eth0 inet staticaddress 192.168.56.113netmask 255.255.255.0auto eth1iface eth1 inet dhcpauto eth2iface eth2 inet manualup ifconfig $IFACE 0.0.0.0 upup ifconfig $IFACE promisc

3、启用网卡ifup eth2
4、修改/etc/hosts，添加：
192.168.56.110  controller192.168.56.111  network192.168.56.112  compute192.168.56.113  compute2

2.2. python安装pythonapt -y install python-simplejson不安装的话，在使用ansible的时候会报错/bin/sh: 1: /usr/bin/python: not found。
2.3. 安装docker1、安装dockerapt -y install docker.io
2、为docker和kolla创建配置文件
mkdir -p /etc/systemd/system/docker.service.dvim /etc/systemd/system/docker.service.d/kolla.conf

修改为：
[Service]MountFlags=shared

3、重启docker
systemctl daemon-reloadsystemctl restart docker

4、查看docker信息docker info
2.4. 密钥登录1、在控制节点将公钥写入新的计算节点
ssh-copy-id -i .ssh/id_rsa.pub -p 22 voidking@192.168.56.11

2、在新的计算节点把voidking用户添加进sudo免密，方便以后的操作sudo vim /etc/sudoers，添加
voidking ALL = NOPASSWD: ALL

3. 控制节点配置1、配置multinode文件vim multinode，如下修改：
# line 3, change[control]#192.168.56.110 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true# line 12, change[network]#192.168.56.111 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true# line 17, change[inner-compute]#192.168.56.112 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true192.168.56.113 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true# line 21, change[external-compute]#192.168.56.112 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true192.168.56.113 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true# line 28, comment[monitoring]#monitoring01# line 36, comment[storage]#storage01

2、测试连通ansible -i multinode -m ping all
3、拷贝kolla.tar到新计算节点scp kolla.tar voidking@192.168.56.113:~
4、在新计算节点导入所有imagesdocker load -i kolla.tar
5、在控制节点执行初始化kolla-ansible -i ./multinode bootstrap-servers报错如图：参考《Ubuntu16使用Kolla安装OpenStack》解决。
6、预检查kolla-ansible -i ./multinode prechecks
7、部署kolla-ansible -i ./multinode deploy报错，创建数据库失败。解决办法：取消multinode文件中control节点的注释，然后重新执行部署。kolla-ansible -i ./multinode deploy --limit @/usr/local/share/kolla-ansible/ansible/site.retry实际上，site.retry文件里面放的是一个单纯的IP地址192.168.56.113。如上图，等待几分钟后，出现部署成功的提示。
4. 测试使用新的问题来了，上面的操作显示新的计算节点部署成功，那么，安装过程中有没有对原系统产生影响？我们来测试查看一下。1、使admin环境生效source /etc/kolla/admin-openrc.sh
2、查看计算服务openstack compute service list如上图，此时已经有了两个计算节点。
3、查看以前的实例openstack server list之前的实例还在，没有受到影响，nice。
5. 后记上面的步骤，完美添加了一个新的计算节点，前提是《Kolla安装OpenStack多节点》一文中的globals.yml等配置文件保持不变。
我很好奇，如果主机清单multinode中的110、111、112三个节点没有添加注释，会不会对它们进行重新安装。于是，我把注释取消，执行了一次kolla-ansible -i ./multinode deploy。实践证明，没啥影响。那么，如果从bootstrap-servers开始执行呢？依次执行bootstrap-servers、prechecks、deploy三条命令会不会重装？于是，先执行bootstrap-servers。然后，卡在了时间同步，卡了一个钟头，遂Ctrl+C放弃。算了算了，不探究了，如无必要勿增实体，还是把精力放在更重要的事情上。
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>ansible</tag>
        <tag>kolla</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>AIOps概览</title>
    <url>/dev-aiops-summary/</url>
    <content><![CDATA[1. AIOps是什么？运维的发展，经历了手工运维、自动化运维、DevOps、AIOps。
Gartner公司2013提出ITOA（IT Operations Analytics），2016年将ITOA概念升级为AIOps（Algorithmic IT Operations），2017年发现AI越来越火，于是把AIOps重新定义为Artificial Intelligence for IT Operations，也就是智能运维。AIOps将人工智能应用于运维领域，基于已有的运维数据（日志、监控信息、应用信息等），通过机器学习的方式来进一步解决自动化运维没办法解决的问题。
AIOps的目标是，利用大数据、机器学习和其他分析技术，通过预防预测、个性化和动态分析，直接和间接增强IT业务的相关技术能力，实现所维护产品或服务的更高质量、合理成本及高效支撑。


2. AIOps做什么？2.1. 知识领域AIOps涉及到行业领域知识、运维场景领域知识和机器学习领域知识，如下图：
2.2. 团队构成一个AIOps团队，要有运维工程师、运维开发工程师、运维AI工程师，如下图：
2.3. 研究方向研究方向按照时间来分，可以分为针对历史事件、针对当前事件、针对未来事件，如下图：
研究方向按照应用场景来分，可以分为效率提升、质量保障、成本管理，如下图：
3. AIOps平台3.1. 能力分级AIOps 的建设可以先由无到局部单点探索、再到单点能力完善，形成解决某个局部问题的运维AI“学件” ，再由多个具有 AI 能力的单运维能力点组合成一个智能运维流程。AIOps 能力分级可具体可᧿述为5级，如下图：

开始尝试应用AI能力，还无较成熟单点应用
具备单场景的AI运维能力，可以初步形成供内部使用的学件
有由多个单场景AI运维模块串联起来的流程化AI运维能力，可以对外提供可靠的运维AI学件
主要运维场景均已实现流程化免干预AI运维能力，可以对外提供可靠的AIOps服务。
有核心中枢AI，可以在成本、质量、效率间从容调整，达到业务不同生命周期对三个方面不同的指标要求，可实现多目标下的最优或按需最优。

“学件”（Learnware）一词由南京大学周志华老师原创，学件=模型+规约。
学件，亦称AI运维组件，类似程序中的API或公共库，但API及公共库不含具体业务数据，只是某种算法，而 AI 运维组件（或称学件），则是在类似API的基础上，兼具对某个运维场景智能化解决的“记忆”能力，将处理这个场景的智能规则保存在了这个组件中。
这个智能规则是在一定量的数据下学习而来的，且具有“可重用” 、“可演进”、“可了解”的特性，既可共享由专家利用数据训练的算法，又可保护数据和隐私。
3.2. 能力框架基于上述 AIOps 能力分级， 对应的 AIOps 能力框架如下。
3.3. 能力体系AIOps工作平台能力体系主要功能是为AIOps的实际场景建设落地而提供功能的工具或者产品平台，其主要目的是降低AIOps的开发人员成本，提升开发效率，规范工作交付质量。 AIOps平台功能与一般的机器学习(或者数据挖掘)平台极为类似，此类产品国外的比如Google的AutoML。
3.4. 平台要素AIOps平台从底层到上层应该包含如下要素：

数据源：大量并且种类繁多的 IT 基础设施
大数据平台：用于处理历史和实时的数据
计算与分析：通过已有的 IT 数据产生新的数据，例如数据清洗、去除噪声等
算法：用于计算和分析，以产生 IT 运维场景所需要的结果
机器学习：这里一般指无监督学习，可根据基于算法的分析结果来产生新的算法

4. AIOps关键技术4.1. 数据采集数据采集负责将智能运维所需要的数据接入至AIOps平台，所接入的运维数据类型一般包括（但不限于）日志数据，性能指标数据，网络抓包数据，用户行为数据，告警数据，配置管理数据，运维流程类数据等。
数据采集方式可分为无代理采集以及有代理采集两种。其中无代理采集为服务端采集，支持SNMP、数据库JDBC、TCP/UDP监听、SYSLOG、Web Service，消息队列采集等主流采集方式。有代理采集则用于本地文件或目录采集，容器编排环境采集，以及脚本采集等。
4.2. 数据处理
数据字段提取：通过正则解析，KV解析，分隔符解析等解析方式提取字段
规范化数据格式：对字段值类型重定义和格式转换
数据字段内容替换：基于业务规则替换数据字段内容，比如必要的数据脱敏过程，同时可实现无效数据、缺失数据的替换处理
时间规范化：对各类运维数据中的时间字段进行格式统一转换
预聚合计算：对数值型字段或指标类数据基于滑动时间窗口进行聚合统计计算，如取1分钟CPU平均值

4.3. 数据存储
数据需要进行实时全文检索，分词搜索。可选用主流的 ElasticSearch 引擎
时间序列数据（性能指标），主要以时间维度进行查询分析的数据，可选用主流的rrdtool、graphite、influxdb等时序数据库
关系类数据，以及会聚集在基于关系进行递归查询的数据可选择图数据库
数据的长期存储和离线挖掘以及数据仓库构建，可选用主流的 Hadoop、Spark 等大数据平台

4.4. 离线和在线计算离线计算：针对存储的历史数据进行挖掘和批量计算的分析场景，用于大数据量的离线模型训练和计算，如挖掘告警关联关系，趋势预测/容量预测模型计算，错误词频分析等场景。
在线计算：对流处理中的实时数据进行在线计算，包括但不限于数据的查询、预处理和统计分析，数据的实时异常检测，以及部分支持实时更新模型的机器学习算法运用等。主流的流处理框架包括：Spark Streaming、Kafka Streaming、Flink、Storm等。
4.5. 算法技术运维场景通常无法直接基于通用的机器学习算法以黑盒的方式解决，因此需要一些面向AIOps 的算法技术，作为解决具体运维场景的基础。

指标趋势预测：通过分析指标历史数据，判断未来一段时间指标趋势及预测值，常见有Holt-Winters、时序数据分解、ARIMA等算法。该算法技术可用于异常检测、容量预测、容量规划等场景。
指标聚类: 根据曲线的相似度把多个 KPI 聚成多个类别。该算法技术可以应用于大规模的指标异常检测：在同一指标类别里采用同样的异常检测算法及参数，大幅降低训练和检测开销。常见的算法有DBSCAN、K-medoids、CLARANS等，应用的挑战是数据量大，曲线模式复杂。
多指标联动关联挖掘: 多指标联动分析判断多个指标是否经常一起波动或增长。该算法技术可用于构建故障传播关系，从而应用于故障诊断。常见的算法有Pearson correlation、Spearman correlation、Kendall correlation等，应用的挑战为KPI种类繁多，关联关系复杂。
指标与事件关联挖掘: 自动挖掘文本数据中的事件与指标之间的关联关系（ 比如在程序 A 每次启动的时候 CPU 利用率就上一个台阶）。该算法技术可用于构建故障传播关系，从而应用于故障诊断。常见的算法有 Pearson correlation、J-measure、Two-sample test等，应用的挑战为事件和KPI种类繁多，KPI测量时间粒度过粗会导致判断相关、先后、单调关系困难。
事件与事件关联挖掘: 分析异常事件之间的关联关系，把历史上经常一起发生的事件关联在一起。该算法技术可用于构建故障传播关系，从而应用于故障诊断。常见的算法有 FP-Growth、Apriori、随机森林等，但前提是异常检测需要准确可靠。
故障传播关系挖掘：融合文本数据与指标数据，基于上述多指标联动关联挖掘、指标与事件关联挖掘、事件与事件关联挖掘等技术、由 tracing 推导出的模块调用关系图、辅以服务器与网络拓扑，构建组件之间的故障传播关系。该算法技术可以应用于故障诊断，其有效性主要取决于其基于的其它技术。

5. 顶会推荐ACM SIGCOMMACM IMCACM/USENIX NSDIACM MobiSysACM CoNEXTACM MobiComACM SIGMETRICSIEEE INFOCOMACM KDDUSENIX SecurityIEEE Security &amp; PrivacyACM CCSNDSS
6. 后记总而言之，AIOps就是利用机器学习来做一些运维工作。涉及到行业知识、运维知识、机器学习知识，是一个很大的研究方向，可以细分为很多小的研究方向。
AIOps围绕质量保障、成本管理和效率提升的基本运维场景，逐步构建智能化运维场景。在质量保障方面，保障现网稳定运行细分为异常检测、故障诊断、故障预测、故障自愈等基本场景；在成本管理方面，细分为指标监控，异常检测，资源优化，容量规划，性能优化等基本场景；在效率方面，分为智能预测，智能变更、智能问答，智能决策等基本场景（注：三者之间不是完全独立的，是相互影响的，场景的划分侧重于主影响维度）。
每一个小方向，都是一个研究点，最终成果是一个学件。通过学件的组合，就构成了AIOps平台。
更多内容请参考书签中给出的链接。
7. 书签基于机器学习的智能运维基于机器学习的智能运维-ppt《企业级 AIOps 实施建议》白皮书-V0.6 版本
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>aiops</tag>
      </tags>
  </entry>
  <entry>
    <title>Win10开热点</title>
    <url>/hobby-win10-wifi/</url>
    <content><![CDATA[1. 问题描述magicbook，使用wifi共享大师、猎豹免费wifi开热点，出现了一个十分诡异的问题：启动热点后，使用手机连接热点，可以正常上网，可以访问电脑上的ftp等服务。但是，如果手机息屏了，大概30秒后，打开手机依然显示wifi连接正常，但是无法再上网，无法再访问电脑的服务。必须先断开连接，然后重新连接wifi，才能恢复正常，非常麻烦。


2. 尝试修改网卡电源管理，去掉“允许计算机关闭此设备以节约电源”前的勾，不行。回退网卡驱动版本，不行。换成360wifi、wifi共享精灵，不行。尝试了国外的connectfy、mhotspot，质量还不如国内的软件。换了几个版本，收费就算了，可以破解，但是有的窗口无法拖动，有的广告很多，有的读不出网卡，有的干脆启动后没反应。。。服！！！
3. 解决办法在尝试各种解决办法的过程中，发现win10其实自带移动热点的功能。但是，使用自带的移动热点，有时连接不上，有时连上没多久就掉线，还不如用第三方软件。
不过，峰回路转，在某次尝试的过程中，把网络频带固定为2.4GHz，居然，好了！再没有任何毛病！不会连接不上，也不会自动断开！
整个设置过程如下：
1、右键右下角网络图标，选择打开“网络和Internet”设置。2、点击“移动热点”，“编辑”。3、设置网络名称和密码，重点是网络频带选择2.4GHz，切记！4、然后，就能开开心心上网了，再也没有出现过自动断开的情况，比所有的wifi共享软件都好用！
如果出现其他问题，参考知乎：win10系统开启移动热点一段时间后就自动关闭了怎么办？
4. 注意假设电脑IP为172.20.110.235，需要特别注意的是：1、在使用第三方软件（比如wifi共享大师）开热点时，如果电脑要启动ftp服务给手机访问，那么需要绑定ftp服务的地址为172.20.110.235。ftp下载速度很快，大概20MB/s。2、在使用win10自带的“移动热点”开热点时，如果电脑要启动ftp服务给手机访问，那么需要绑定ftp服务的地址为192.168.137.1。ftp下载速度很慢，大概2MB/s。如果想要加速到20MB/s，那么网络频带选择5GHz，这时有些设备无法连接。
很神奇，如果不这么设置，就访问不到ftp服务。由上面的经验可以总结如下：如果使用win10自带的移动热点，那么此时的虚拟网卡相当于插在电脑上的一块网卡，可以通过访问它的IP而访问到电脑里的服务。如果使用第三方软件，那么此时的虚拟网卡相当于路由器，所以访问它的IP只是访问到了路由器，需要访问电脑的IP才能访问到电脑的服务。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
        <tag>wifi</tag>
      </tags>
  </entry>
  <entry>
    <title>左神排序算法文字总结</title>
    <url>/dev-zuo-sort/</url>
    <content><![CDATA[1. 前言相见恨晚！看了左神的算法视频，才知道自己到底浪费了多少时间。算法，居然能讲的这么有意思，居然能讲的这么通俗易懂！左神，不愧是左“神”！
《leetcode刷题记录-排序算法总结》一文中，也总结过排序算法，但是和左神的讲解一比，就是渣渣。所以，本文按照左神的讲解，再次总结一下排序算法。


2. 排序算法常见的排序算法有9个，分别是冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序、希尔排序、计数排序和基数排序。它们的时间空间复杂度如下：



算法
时间复杂度
空间复杂度
稳定性



冒泡排序
$O(N^2)$
$O(1)$
稳定


选择排序
$O(N^2)$
$O(1)$
不稳定


插入排序
$O(N^2)$
$O(1)$
稳定


归并排序
$O(N*logN)$
$O(N)$
稳定


快速排序
$O(N*logN)$
$O(logN)到O(N)$
不稳定


堆排序
$O(N*logN)$
$O(1)$
不稳定


希尔排序
$O(N*logN)$
$O(1)$
不稳定


计数排序
$O(N)$
$O(M)$
稳定


基数排序
$O(N)$
$O(M)$
稳定


注：M代表桶数量。
如上图，按照时间复杂度的不同，分为三大类。以下算法描述，实现的都是升序排序，N代表元素个数，a[i]代表i位置的元素。
3. 笨蛋算法3.1. 冒泡排序1、依次比较，a[i] &gt; a[i+1] 则交换元素，结果最大元素位于N-1位置2、范围缩小1
3.2. 选择排序1、0到N-1选出最小元素放在0位置（交换元素即可）2、1到N-1选出最小元素放在1位置
3.3. 插入排序1、1位置和0位置元素比较，小则交换2、2位置和1位置元素比较，小则交换；1位置和0位置元素比较，小则交换
4. 聪明算法4.1. 归并排序1、数组分割成长度为1的有序区间2、有序区间两两合并，得到长度为2的有序区间
4.2. 快速排序1、数组中随机选择一个数，小于等于这个数的元素放左，大于这个数的元素放右2、左右两边递归
快速划分过程：（1）左区间初始值为0，划分值放在N-1位置（2）遍历数组，元素大于划分值则跳过，小于等于划分值则交换该元素与左区间加一的元素，到N-1位置则交换划分值与左区间加一的元素。
4.3. 堆排序1、建立大根堆（数组表示），把堆顶元素与最后一个元素交换，交换后最后一个元素有序2、调整后，把堆顶元素与倒第二个元素交换，交换后最后两个元素有序
完全二叉树当前节点索引为i，那么父节点为(i-1)/2，子节点为2i+1和2i+2调整大根堆：调整当前节点大于左右子节点，若发生调整则递归建立大根堆：从下往上，从第一个父节点开始视频讲解：堆排序
4.4. 希尔排序1、设定初始步长，按照步长比较元素大小并交换2、步长减一，比较元素大小并交换
注：希尔排序是改良版的插入排序
5. 变态算法5.1. 计数排序1、建立0-M号桶2、把元素按大小放入对应桶3、依次把0-M号桶中的元素倒出
5.2. 基数排序1、准备0-9号桶2、元素按个位数放入对应桶3、依次把0-9号桶中的元素倒出（先进先出），成为序列4、按序列把元素按十位数放入对应桶
注：计数排序和基数排序都属于桶排序的实现，桶排序是一种排序思想。
6. 后记代码实现：排序算法  
好记性不如烂笔头，记下来，多看几遍，多想几遍，多写几遍，熟能生巧。
7. 书签直通BAT — 求职算法精品课
牛客算法基础班
牛客算法进阶班
]]></content>
      <categories>
        <category>engineering</category>
        <category>algorithm</category>
      </categories>
      <tags>
        <tag>算法</tag>
      </tags>
  </entry>
  <entry>
    <title>使用frp进行内网穿透</title>
    <url>/dev-frp/</url>
    <content><![CDATA[1. 前言《使用lanproxy进行内网穿透》一文中，郝同学使用lanproxy搭建了一个内网穿透服务，并且介绍了使用方法。
但是，lanproxy不太稳定，最近无法使用它正常进行内网穿透，访问内网主机的22端口。因此，本文学习另外一个内网穿透工具frp的安装配置方法，用做lanproxy的替代。
参考文档：

frp源码地址
frp中文文档
使用frp实现内网穿透
frp内网穿透搭建，超级简单实用



2. 机器资源1、一台公网服务器（centos7，运行frp-server），IP为 120.77.36.182。2、一台内网pc或服务器（运行frp-client）。
3. 安装配置frps3.1. 安装并测试启动frps1、下载frp程序访问frp releases，下载frp程序，本文中下载frp_0.21.0_linux_amd64.tar.gz（包含服务端和客户端）。
cd /optwget https://github.com/fatedier/frp/releases/download/v0.21.0/frp_0.21.0_linux_amd64.tar.gz# orcurl -C - -O -L https://github.com/fatedier/frp/releases/download/v0.21.0/frp_0.21.0_linux_amd64.tar.gz

2、解压并重命名
tar -xzvf frp_0.21.0_linux_amd64.tar.gzmv frp_0.21.0_linux_amd64 frp

3、修改服务端配置 frps.ini
cd frpvim frps.ini

可以参考frps_full.ini，修改后内容如下：
[common]bind_addr = 0.0.0.0bind_port = 7000dashboard_addr = 0.0.0.0dashboard_port = 7500dashboard_user = voidkingdashboard_pwd = voidkingtoken = 12345678

4、测试启动frps
./frps -c ./frps.ini

后台启动参考《Linux后台运行脚本或程序》
5、测试访问在浏览器访问 http://120.77.36.182:7500 ，输入用户名和密码，即可看到frp dashboard。
3.2. 开机自启动frps3.2.1. 基于systemd（推荐）1、创建systemd服务配置文件
cd /etc/systemd/system/vim frps.service

frps.service内容为：
[Unit]Description=frpsAfter=network.target[Service]ExecStart=/opt/frp/frps -c /opt/frp/frps.ini[Install]WantedBy=multi-user.target

2、启动frps并设置开机自启动
systemctl start frpssystemctl status frpssystemctl enable frps

3.2.2. 基于SysVinit编辑 rc.local 文件：
vim /etc/rc.local

在exit 0之前添加：
nohup /opt/frp/frps -c /opt/frp/frps.ini &amp;
其中nohup可以省略。
3.3. 探活重启frps*/5 * * * * systemctl is-active --quiet frps.service || systemctl start frps.service

4. nginx反向代理frps1、添加域名解析frp到公网ip。
2、在/etc/nginx/conf.d/目录中，添加frp.voidking.com.conf，内容如下：
server &#123;    listen 80;    server_name frp.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header Host $host;        proxy_set_header X-Forward-For $remote_addr;        proxy_set_header X-Real-IP $remote_addr;        proxy_pass http://127.0.0.1:7500;    &#125;&#125;

3、重启nginx
/usr/sbin/nginx -t/usr/sbin/nginx -s reload

4、测试访问访问 http://frp.voidking.com/ ，同样可以看到dashboard界面。
至此，服务端配置完成。
5. 安装配置frpc5.1. linux中安装配置frpc1、下载frp程序访问frp releases，下载frp程序，本文中下载frp_0.21.0_linux_amd64.tar.gz（包含服务端和客户端）。
cd /optwget https://github.com/fatedier/frp/releases/download/v0.21.0/frp_0.21.0_linux_amd64.tar.gz# orcurl -C - -O -L https://github.com/fatedier/frp/releases/download/v0.21.0/frp_0.21.0_linux_amd64.tar.gz

2、解压并重命名
tar -xzvf frp_0.21.0_linux_amd64.tar.gzmv frp_0.21.0_linux_amd64 frp

4、修改客户端配置 frps.ini
cd frpvim frpc.ini

修改如下：
[common]server_addr = 120.77.36.182server_port = 7000token = 12345678[ssh_3422]type = tcplocal_ip = 127.0.0.1local_port = 22remote_port = 3422

5、启动客户端
./frpc -c ./frpc.ini

6、测试登录使用xshell连接本地服务器，IP填入120.77.36.182，端口填入3422，顺利连接。
7、设置开机自启动配置方法参考【frps设置开机自启动】一节，frps改成frpc即可。
8、探活重启frpc
*/5 * * * * systemctl is-active --quiet frpc.service || systemctl start frpc.service

5.2. windows中安装配置frpc1、下载frp程序访问frp releases，下载frp程序，这里下载frp_0.21.0_windows_amd64.zip（包含服务端和客户端）。
2、解压frp_0.21.0_windows_amd64.zip，并重命名为frp。
3、修改客户端配置编辑frp/frpc.ini文件，修改为：
[common]server_addr = 120.77.36.182server_port = 7000token = 12345678[web_3480]type = tcplocal_ip = 127.0.0.1local_port = 80remote_port = 3480

server_addr是服务器端的公网IP地址，server_port是frp服务端口号，auto_token是连接服务器的口令，必须和服务器保持一致。
web_3480是客户端和服务端之间的通道名，每个客户端必须不一样，remote_port是服务器端对外提供本机服务的端口号。
4、启动客户端shift+右键，在此处打开powershell窗口，然后执行：
.\frpc.exe -c .\frpc.ini

5、本地启动web服务，在80端口提供服务。
6、测试访问在浏览器访问 http://120.77.36.182:3480 ，即可看到本地80服务。
6. 结语至此，大功告成，nice。frp和lanproxy相比，安装更加简便（不需要java环境），配置更加方便（客户端自己控制外网端口）。最重要的是，服务端主机是centos系统，也可以正常代理内网的22端口。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>nginx</tag>
        <tag>内网穿透</tag>
        <tag>代理</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Kolla安装OpenStack多节点</title>
    <url>/dev-ubuntu16-kolla-openstack-multinode/</url>
    <content><![CDATA[1. 前言《Ubuntu16使用Kolla安装OpenStack》一文中，使用kolla安装了单节点的openstack。
在实际的部署中，当然要安装多节点的openstack，本文就来研究一下在多个节点上使用kolla安装部署openstack的方法。


2. 环境VirtualBox虚拟机三台，系统为ubuntu-16.04.4-server-amd64，分别作为控制节点、网络节点和计算节点，用户名为voidking/root，密码为voidking。控制节点4核8G内存40G存储，主机名为controller，eth0的IP为192.168.56.110，eth1为nat上网网卡，eth2为neutron服务网络。网络节点2核4G内存40G存储，主机名为network，eth0的IP为192.168.56.111，eth1为nat上网网卡，eth2为neutron服务网络。计算节点2核4G内存40G存储，主机名为compute，eth0的IP为192.168.56.112，eth1为nat上网网卡，eth2为neutron服务网络。
3. 准备3.1. 网络设置在控制节点上，执行以下操作：
1、切换到root用户sudo -i
2、vim /etc/network/interfaces，设置网卡为：
# This file describes the network interfaces available on your system# and how to activate them. For more information, see interfaces(5).source /etc/network/interfaces.d/*# The loopback network interfaceauto loiface lo inet loopback# The primary network interfaceauto eth0iface eth0 inet staticaddress 192.168.56.110netmask 255.255.255.0auto eth1iface eth1 inet dhcpauto eth2iface eth2 inet manualup ifconfig $IFACE 0.0.0.0 upup ifconfig $IFACE promisc

3、启用网卡ifup eth2
4、修改/etc/hosts，添加：
192.168.56.110  controller192.168.56.111  network192.168.56.112  compute

网络节点和计算节点参考控制节点配置即可。需要注意的是，hosts文件在OpenStack安装完成后会被ansible修改，建议保留ansible创建的那一份，原因参考《OpenStack计算节点的奇葩问题》。
3.2. python在三台机器上安装pythonsudo apt -y install python-simplejson不安装的话，在使用ansible的时候会报错/bin/sh: 1: /usr/bin/python: not found。
3.3. 安装docker1、在三台机器上安装dockercurl -SSL https://get.docker.io | bash
或者：apt install docker.io
2、为docker和kolla创建配置文件
mkdir -p /etc/systemd/system/docker.service.dvim /etc/systemd/system/docker.service.d/kolla.conf

修改为：
[Service]MountFlags=shared

3、重启docker
systemctl daemon-reloadsystemctl restart docker

4、查看docker信息docker info
如果报错的话，参考《Ubuntu16使用Kolla安装OpenStack》解决。
3.4. 加速镜像拉取使用 Docker 的时候，需要经常从官方获取镜像，但是由于显而易见的网络原因，拉取镜像的过程非常耗时，严重影响使用 Docker 的体验。因此DaoCloud推出了加速器工具解决这个难题，通过智能路由和缓存机制，极大提升了国内网络访问 Docker Hub 的速度，目前已经拥有了广泛的用户群体，并得到了 Docker 官方的大力推荐。
1、访问DaoCloud官网，注册一个账号。
2、访问配置 Docker 加速器，可以获得一个专属加速地址（每次访问都会获得不同的地址）。
3、参考Docker 加速器，创建或修改 /etc/docker/daemon.json，内容为：
&#123;    &quot;registry-mirrors&quot;: [        &quot;http://2fd0f33c.m.daocloud.io&quot;    ],    &quot;insecure-registries&quot;: []&#125;

4、重启docker
systemctl daemon-reloadsystemctl restart docker

PS：也可以使用阿里云的镜像加速器。
3.5. 密钥登录1、在控制节点生成密钥ssh-keygen，连续回车即可。
2、将公钥写入三台机器
ssh-copy-id -i .ssh/id_rsa.pub -p 22 voidking@192.168.56.110ssh-copy-id -i .ssh/id_rsa.pub -p 22 voidking@192.168.56.111ssh-copy-id -i .ssh/id_rsa.pub -p 22 voidking@192.168.56.112

3、在三台机器把voidking用户添加进sudo免密，方便以后的操作sudo vim /etc/sudoers，添加
voidking ALL = NOPASSWD: ALL


4. 控制节点主要参考OpenStack, Ansible, and Kolla on Ubuntu 16.04、kolla queens on centos7.4和Kolla-Ansible’s documentation!。
4.1. 安装依赖1、安装并升级pip
apt-get updateapt-get install python-pippip install --upgrade pip

2、安装依赖apt-get -y install python-dev libffi-dev gcc libssl-dev python-selinux
3、安装ansible
apt-get install software-properties-commonapt-add-repository ppa:ansible/ansibleapt-get updateapt-get install ansible

4、vim /etc/ansible/ansible.cfg，添加如下：
line 10, add[defaults]host_key_checking=Falsepipelining=Trueforks=100

4.2. 安装Kolla-ansible1、安装kolla-ansiblepip install kolla-ansible
报错：ImportError: cannot import name main，参考升级pip后出现ImportError: cannot import name main，编辑/usr/bin/pip文件，如下修改：
# line 9, changefrom pip import __main__if __name__ == &#x27;__main__&#x27;:    sys.exit(__main__._main())

报红：oslo-config 6.4.0 has requirement PyYAML&gt;=3.12, but you’ll have pyyaml 3.11 which is incompatible.忽略。
2、拷贝globals.yml和passwords.yml到/etc/kolla目录cp -r /usr/local/share/kolla-ansible/etc_examples/kolla /etc/
3、拷贝all-in-one和multinode清单文件到当前目录cp /usr/local/share/kolla-ansible/ansible/inventory/* .
4、生成kolla密码kolla-genpwd我们部署中使用的密码存储在/etc/kolla/passwords.yml文件中。此文件中的所有密码都是空白的，必须手动填写或运行随机密码生成器。
5、配置multinode文件vim multinode，如下修改：
# line 3, change[control]192.168.56.110 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true# line 12, change[network]192.168.56.111 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true# line 17, change[inner-compute]192.168.56.112 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true# line 21, change[external-compute]192.168.56.112 ansible_ssh_user=voidking ansible_ssh_pass=voidking ansible_sudo=voidking ansible_sudo_pass=voidking ansible_become_user=root ansible_become_pass=voidking ansible_become=true# line 28, comment[monitoring]#monitoring01# line 36, comment[storage]#storage01

6、测试连通ansible -i multinode -m ping all报错：ERROR! to use the ‘ssh’ connection type with passwords, you must install the sshpass program安装sshpass：apt install sshpass然后重新测试连通。
4.3. 配置globals.yml1、查看globals.yml配置grep -vE &#39;^$|^#&#39; /etc/kolla/globals.yml
2、vim /etc/kolla/globals.yml，如下修改：
# line 15,uncommentkolla_base_distro: &quot;centos&quot;# line 18,uncommentkolla_install_type: &quot;binary&quot;# line 21,uncomment and changeopenstack_release: &quot;queens&quot;# line 31,changekolla_internal_vip_address: &quot;192.168.56.120&quot;# line 85,uncomment and changenetwork_interface: &quot;eth0&quot;# line 100,uncomment and changeneutron_external_interface: &quot;eth2&quot;# line 331,uncommentdesignate_backend: &quot;bind9&quot;designate_ns_record: &quot;sample.openstack.org&quot;# line 340,uncomment and changenova_compute_virt_type: &quot;qemu&quot;# othertempest_image_id:tempest_flavor_ref_id:tempest_public_network_id:tempest_floating_network_name:

kolla_internal_vip_address为192.168.56.120，它是一个和OpenStack宿主机内网连接网络（eth0 192.168.56.110）同一个网段的未使用IP。network_interface为eth0，意思是openstack内部网络使用eth0网卡。neutron_external_interface为eth2，意思是openstack的虚拟机外部网络使用eth2网卡。因为是在虚拟机中安装openstack，所以nova_compute_virt_type设置为qemu。
PS：实际上，kolla_internal_vip_address最好设置为192.168.56.110，原因参考《OpenStack计算节点的奇葩问题》。
4.4. 部署1、初始化kolla-ansible -i ./multinode bootstrap-servers时间很久，请耐心等待。
2、预检查kolla-ansible -i ./multinode prechecks

3、拉取镜像kolla-ansible -i ./multinode pull这一步的时间特别久，挺耐心等待。如果拉取失败，就多尝试几次。这里郝同学取巧一下，直接把《Ubuntu16使用Kolla安装OpenStack》一文中的images拷贝到控制节点、网络节点和计算节点。
（1）打包所有imagesdocker save $(docker images | grep -v REPOSITORY | awk &#39;BEGIN&#123;OFS=&quot;:&quot;;ORS=&quot; &quot;&#125;&#123;print $1,$2&#125;&#39;) -o kolla.tar
（2）导入所有imagesdocker load -i kolla.tar
4、查看镜像docker images
5、部署kolla-ansible -i ./multinode deploy
5. 测试使用openstack1、在控制节点安装openstack客户端pip install python-openstackclient python-glanceclient python-neutronclient --ignore-installed
2、生成admin-openrc.sh等kolla-ansible post-deploy
3、使admin环境生效source /etc/kolla/admin-openrc.sh
4、查看计算服务openstack compute service list
5、查看网络服务openstack network agent list
6. 初始化配置1、执行init-runonce脚本. /usr/local/share/kolla-ansible/init-runonce
执行初始化之前，可以参考openstack 之 Kolla部署指南，设置一下外部网络。这样，就可以从路由器给虚拟机分配IP，安装完虚拟机就可以宿主机互相ping通。比如：
EXT_NET_CIDR=&#x27;192.168.56.0/24&#x27;EXT_NET_RANGE=&#x27;start=192.168.56.200,end=192.168.56.240&#x27;EXT_NET_GATEWAY=&#x27;192.168.56.1&#x27;
这里配置的网络范围，对应globals.yml中配置的neutron_external_interface，也就是eth2网卡。
2、根据提示，创建实例
openstack server create \    --image cirros \    --flavor m1.tiny \    --key-name mykey \    --nic net-id=25e6c0ef-6a0a-481c-a08a-46f7ef67ad3e \    demo1

3、给demo1实例分配浮动IP
source /etc/kolla/admin-openrc.shopenstack network listopenstack floating ip create public1openstack server add floating ip demo1 10.0.2.157

4、查看demo1openstack server list
5、访问horizon服务curl 192.168.56.120 -L
在浏览器访问 http://192.168.56.120 ，使用admin账号登录，密码在passwords.yml文件中查看。less /etc/kolla/passwords.yml | grep keystone_admin_password
至此，使用kolla安装部署多节点openstack成功。
7. 网络配置7.1. 网络节点关于网络的配置，主要参考Kolla配置实例网络。
1、在网络节点编辑/etc/network/interfaces，添加：
auto br-exiface br-ex inet staticaddress 10.0.2.1netmask 255.255.255.0

2、启用br-exifup br-ex
3、添加路由
ip netnsip netns exec qrouter-9dbabbdd-cc45-42c3-b9e0-9dbbc03e66f1 ip addroute add -net 10.0.0.0/24 gw 10.0.2.153 dev br-ex

重启后路由失效，如果想要永久生效，那就在/etc/network/interfaces文件的尾部添加：
up route add -net 10.0.0.0/24 gw 10.0.2.153 dev br-ex

4、在网络节点连通demo1
ping 10.0.2.157 -c3ping 10.0.0.3 -c3

5、设置demo1上网
iptables -I INPUT -i eth2 -j ACCEPTiptables -I INPUT -i br-ex -j ACCEPTiptables -t nat -A POSTROUTING -s 10.0.2.0/24 -o eth1 -j MASQUERADEiptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth1 -j MASQUERADE

设置永久生效参考《Linux配置SNAT上网》。
7.2. 控制节点1、添加路由route add -net 10.0.2.0/24 gw 192.168.56.110 dev eth0同样的，想要路由永久生效，也要添加到interfaces文件的尾部。
2、在控制节点连通demo1ping 10.0.2.157 -c3
3、连接demo1ssh cirros@10.0.2.157
4、测试外网连通ping 8.8.8.8 -c3
8. 清除OpenStack安装OpenStack，会对宿主机的网络有很大的影响。安装完成后，如果网络瘫痪，不要怕，还有补救措施：清除已经安装部署的OpenStack环境，然后重新配置，重新部署。清除OpenStack的命令如下：kolla-ansible destroy -i ./multinode  --yes-i-really-really-mean-it
9. 后记本文的网络设置不合理，但是也能正常使用。因为已经安装部署完成，所以就不再更改了，在此做一下补充说明。本文中eth0作为管理网卡，eth1作为上网网卡，eth0作为OpenStack宿主机间的内网网卡，eth2作为OpenStack的虚拟机外网网卡。这里eth0复用了，既作为管理网卡，又作为OpenStack内网网卡。更好的做法是，再添加一块网卡作为OpenStack的内网网卡，使每个网卡只有一个职责。
在物理机中安装OpenStack的话，可以参考《Ubuntu16手动安装OpenStack——neutron篇》。控制节点一个网卡，用来上网和管理；网络节点三个网卡，一张用来上网和管理，一张用来OpenStack宿主机内网连接，一张用来OpenStack虚拟机外网连接；计算节点两个网卡，一张用来上网和管理，一张用来OpenStack宿主机内网连接。
还有，在物理机安装OpenStack时，要关闭其他虚拟化工具，比如VirtualBox或者VMware，否则会无法创建实例，查看日志tail -n 20 /var/lib/docker/volumes/kolla_logs/_data/nova/nova-compute.log，出现KVM Error – ioctl(KVM_CREATE_VM) failed: 16 Device or Resource busy问题。
10. 书签Kolla’s documentation!
Kolla让OpenStack部署更贴心
Kolla安装Ocata单节点
kolla部署openstack ocata版
Install and configure OpenStack Ocata with Kolla as a standalone
Kolla OpenStack系统视频课程
利用kolla快速搭建openstack-pike多节点
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>ansible</tag>
        <tag>kolla</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>Ansible实战</title>
    <url>/dev-ansible-in-action/</url>
    <content><![CDATA[1. 目标本文，目标是使用ansible安装lnmp+haproxy。共四台主机，都是ubuntu14 server版，一台作为ansible管理机，另外三台作为ansible客户机用来部署服务。客户机A安装nginx+php+mysql，客户机B安装nginx+php，客户机C安装haproxy用来负载均衡。如下图：


2. 设计项目结构参考最佳实践-Content Organization，如下图：
注意点：

角色分配简单明确。
在hosts中分组添加变量，指定安装软件。
二次执行脚本时不能影响已经安装的服务。

3. 安装流程1、添加密钥2、设置sudo免密3、更新安装源4、时间同步5、安装nginx+php+mysql+haproxy6、检查安装
4. 实践4.1. 文件准备1、创建项目lnmpmkdir lnmp &amp;&amp; cd lnmp
2、创建角色目录mkdir rolesmkdir -p roles/&#123;1_key,2_sudo,3_sources,4_chrony,5_software,6_check&#125;/&#123;tasks,handlers,vars,files,templates&#125;
3、创建变量目录mkdir group_vars host_vars
4、创建hosts文件，分配安装组，内容如下：
[base]192.168.56.102192.168.56.103192.168.56.104[nginx_php_mysql]192.168.56.102[nginx_php]192.168.56.103[haproxy]192.168.56.104[base:vars]install_nginx=falseinstall_php=falseinstall_mysql=falseinstall_haproxy=false[nginx_php_mysql:vars]install_nginx=trueinstall_php=trueinstall_mysql=trueinstall_haproxy=false[nginx_php:vars]install_nginx=trueinstall_php=trueinstall_mysql=falseinstall_haproxy=false[haproxy:vars]install_nginx=falseinstall_php=falseinstall_mysql=falseinstall_haproxy=true

5、创建site.yml文件，内容为：
---- hosts: all  gather_facts: false  roles:    - 1_key    - 2_sudo    - 3_sources    - 4_chrony    - 5_software    - 6_check

6、关闭第一次使用ansible连接客户端时命令提示sudo vim /etc/ansible/ansible.cfg，如下修改
# line 62, uncommenthost_key_checking = False

4.2. 添加密钥1、设置用户和密码vim group_vars/base.yml，内容为：
---ansible_ssh_user: voidkingansible_ssh_pass: voidkingansible_sudo: voidkingansible_sudo_pass: voidking

2、vim test.yml，内容为：
---- hosts: all  vars_files:    - group_vars/base.yml  roles:    - 1_key

3、vim roles/1_key/tasks/main.yml，内容为：
---- name: copy public key  copy:    src: /home/voidking/.ssh/id_rsa.pub    dest: /home/voidking/.ssh/id_rsa.pub.tmp    owner: voidking    group: voidking    mode: 0600    force: yes- name: add public key  shell: cd /home/voidking/.ssh &amp;&amp; cat id_rsa.pub.tmp | tee &gt;&gt; authorized_keys

4、vim test_hosts，内容为：
[test]192.168.56.102

5、执行脚本
ansible-playbook test.yml -i test_hosts --syntax-checkansible-playbook test.yml -i test_hosts

5、在客户端查看结果ll .ssh
6、测试登录ssh voidking@192.168.56.102
4.3. sudo免密1、vim test.yml，内容为：
---- hosts: all  vars_files:    - group_vars/base.yml  roles:    #- 1_key    - 2_sudo

2、vim roles/2_sudo/tasks/main.yml，内容为：
---- name: add sudo user  shell: &#x27;sudo sh -c &quot;echo voidking ALL = NOPASSWD: ALL &gt;&gt; /etc/sudoers&quot;&#x27;  #shell: &#x27;echo &quot;voidking ALL = NOPASSWD: ALL&quot; | sudo tee &gt;&gt; /etc/sudoers&#x27;

3、执行脚本
ansible-playbook test.yml -i test_hosts --syntax-checkansible-playbook test.yml -i test_hosts -s


4、在客户端查看结果sudo ls
4.4. sudo免密2上面的方法，虽然成功添加了sudo，但是从提示我们看出，在shell模块中sudo马上就不能使用了。
1、vim roles/2_sudo/tasks/main.yml，修改如下：
---- name: add sudo user  become_user: root  become: true  shell: &quot;echo voidking ALL = NOPASSWD: ALL &gt;&gt; /etc/sudoers&quot;

2、执行脚本
ansible-playbook test.yml -i test_hosts --syntax-checkansible-playbook test.yml -i test_hosts


4.5. 更新安装源1、vim test.yml，内容为：
---- hosts: all  vars_files:    - group_vars/base.yml  roles:    #- 1_key    #- 2_sudo    - 3_sources

2、vim roles/3_sources/files/sources.list，内容为：
deb http://cn.archive.ubuntu.com/ubuntu/ trusty main restricted universe multiversedeb http://cn.archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiversedeb http://cn.archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe multiversedeb http://cn.archive.ubuntu.com/ubuntu/ trusty-backports main restricted universe multiverse##測試版源deb http://cn.archive.ubuntu.com/ubuntu/ trusty-proposed main restricted universe multiverse# 源碼deb-src http://cn.archive.ubuntu.com/ubuntu/ trusty main restricted universe multiversedeb-src http://cn.archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiversedeb-src http://cn.archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe multiversedeb-src http://cn.archive.ubuntu.com/ubuntu/ trusty-backports main restricted universe multiverse##測試版源deb-src http://cn.archive.ubuntu.com/ubuntu/ trusty-proposed main restricted universe multiverse# Canonical 合作夥伴和附加# deb http://archive.canonical.com/ubuntu/ trusty partner# deb http://extras.ubuntu.com/ubuntu/ trusty main

3、vim roles/3_sources/tasks/main.yml，内容为：
---- name: replace sources.list  copy:    src: ../files/sources.list    dest: /etc/apt/sources.list    force: yes- name: update  become_user: root  become: true  shell: apt update- name: upgrade  become_user: root  become: true  apt:    upgrade: yes

4、执行脚本
ansible-playbook test.yml -i test_hosts --syntax-checkansible-playbook test.yml -i test_hosts

4.6. 时间同步4.6.1. 管理机准备1、管理机安装chronyapt -y install chrony
2、重启chronyservice chrony restart
3、拷贝chrony.confcp /etc/chrony/chrony.conf roles/4_chrony/files/
4、vim roles/4_chrony/files/chrony.conf，如下修改：
# line 20,comment#server 0.debian.pool.ntp.org offline minpoll 8#server 1.debian.pool.ntp.org offline minpoll 8#server 2.debian.pool.ntp.org offline minpoll 8#server 3.debian.pool.ntp.org offline minpoll 8# line 24,addserver 192.168.56.101 iburst

4.6.2. playbook配置1、vim test.yml，内容为：
---- hosts: all  vars_files:    - group_vars/base.yml  roles:    #- 1_key    #- 2_sudo    #- 3_sources    - 4_chrony

2、vim group_vars/base.yml，内容为：
---ansible_ssh_user: voidkingansible_ssh_pass: voidkingansible_sudo: voidkingansible_sudo_pass: voidkingansible_become_user: rootansible_become_pass: voidkingansible_become: true

3、vim roles/4_chrony/tasks/main.yml，内容为：
---- name: install chrony  apt:    name: chrony    state: latest- name: change config  copy:     src: ../files/chrony.conf     dest: /etc/chrony/chrony.conf    owner: root    group: root    mode: 0644    force: yes- name: restart chrony  service:    name: chrony    state: restarted

4、执行脚本
ansible-playbook test.yml -i test_hosts --syntax-checkansible-playbook test.yml -i test_hosts

5、在客户机验证chronychronyc sources
4.7. 安装核心软件1、vim test_hosts，内容为：
[test]192.168.56.102[test:vars]install_nginx=trueinstall_php=trueinstall_mysql=trueinstall_haproxy=true

2、vim test.yml，内容为：
---- hosts: all  vars_files:    - group_vars/base.yml  roles:    #- 1_key    #- 2_sudo    #- 3_sources    #- 4_chrony    - 5_software

3、vim roles/5_software/tasks/main.yml，内容为：
---- name: install nginx  apt:    name: nginx    state: latest  when: install_nginx- name: install php  apt:    name: &quot;&#123;&#123;item&#125;&#125;&quot;    state: latest    update_cache: yes  with_items:    - php5    - libapache2-mod-php5    - php5-mcrypt    - php5-curl    - php5-imagick    - php5-cli    - php5-json    - php5-fpm    - php5-mysql  when: install_php- name: install mysql  apt:    name: &quot;&#123;&#123;item&#125;&#125;&quot;    state: latest  with_items:    - mysql-common    - mysql-server    - mysql-client    - python-mysqldb  when: install_mysql- name: config mysql passwd  mysql_user:    login_user: root    login_password: &quot;\n&quot;    name: root    password: &quot;voidking&quot;    host: &quot;&#123;&#123;item&#125;&#125;&quot;    priv: &#x27;*.*:ALL,GRANT&#x27;    state: present    check_implicit_admin: yes  with_items:    - &quot;localhost&quot;    - &quot;%&quot;  when: install_mysql- name: comment bind-address  shell: sed -i &#x27;s/^bind-address/#bind-address/g&#x27; /etc/mysql/my.cnf  when: install_mysql- name: restart mysql service  service:     name: mysql     state: restarted    enabled: true  when: install_mysql- name: install haproxy  apt:    name: haproxy    state: latest  environment:    RUNLEVEL: 1  when: install_haproxy- name: config haproxy  shell: sed -i &#x27;s/ENABLED=0/ENABLED=1/g&#x27; /etc/default/haproxy  when: install_haproxy- name: config haproxy port  copy:     src: ../files/haproxy.cfg    dest: /etc/haproxy/haproxy.cfg    force: yes  when: install_haproxy- name: restart haproxy  service:    name: haproxy    state: restarted  when: install_haproxy

4、vim roles/5_software/files/haproxy.cfg，内容为
global  log /dev/log    local0  log /dev/log    local1 notice  chroot /var/lib/haproxy  user haproxy  group haproxy  daemondefaults  log     global  mode    http  option  httplog  option  dontlognull  contimeout 5000  clitimeout 50000  srvtimeout 50000  errorfile 400 /etc/haproxy/errors/400.http  errorfile 403 /etc/haproxy/errors/403.http  errorfile 408 /etc/haproxy/errors/408.http  errorfile 500 /etc/haproxy/errors/500.http  errorfile 502 /etc/haproxy/errors/502.http  errorfile 503 /etc/haproxy/errors/503.http  errorfile 504 /etc/haproxy/errors/504.httpfrontend http_front  bind *:8080  stats uri /haproxy?stats  default_backend http_backbackend http_back  balance roundrobin  option httpchk GET /index.html  option forwardfor header X-Forwarded-For  server node1 192.168.56.102:80 check inter 2000 rise 3 fall 3 weight 30  server node2 192.168.56.103:80 check inter 2000 rise 3 fall 3 weight 30

5、执行脚本
ansible-playbook test.yml -i test_hosts --syntax-checkansible-playbook test.yml -i test_hosts

6、在客户机测试mysql -uroot -p -h 192.168.56.102
curl localhost
curl localhost:8080
4.8. 整合所有步骤1、执行脚本
ansible-playbook site.yml -i hosts --syntax-checkansible-playbook site.yml -i hosts

2、查看安装
curl 192.168.56.102curl 192.168.56.103curl 192.168.56.104:8080

4.9. 检查安装1、vim site.yml，修改为：
---- hosts: all  gather_facts: false  roles:    #- 1_key    #- 2_sudo    #- 3_sources    #- 4_chrony    #- 5_software    - 6_check

2、vim roles/6_check/tasks/main.yml，内容为：
---- name: copy index.html  template:    src: ../templates/index.j2    dest: /usr/share/nginx/html/index.html    force: yes  when: install_nginx


3、vim roles/6_check/templates/index.j2，内容为：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Home Page&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &#123;&#123; ansible_eth0.ipv4.address &#125;&#125;&lt;/body&gt;&lt;/html&gt;

PS：查看变量ansible 192.168.56.102 -m setup &gt; var.txt
4、执行脚本
ansible-playbook site.yml -i hosts --syntax-checkansible-playbook site.yml -i hosts

5、测试访问curl 192.168.56.104:8080多次执行，可以看到两个不同的IP会来回切换。
5. 源码分享项目地址：https://github.com/voidking/lnmp.git
6. 书签Ansible Documentation
Ansible中文权威指南
Ansible精讲
烂泥：高负载均衡学习haproxy之安装与配置
HAproxy 配置参数详解
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>ansible</tag>
      </tags>
  </entry>
  <entry>
    <title>Ansible Playbooks</title>
    <url>/dev-ansible-playbooks/</url>
    <content><![CDATA[1. Playbooks简介与ad-hoc任务执行模式相比，Playbooks使用ansible是一种完全不同的方式，并且功能特别强大。
简而言之，playbooks是真正简单的配置管理和多机器部署系统的基础，与已有的系统不同，并且非常适合部署复杂的应用程序。
Playbooks可以声明配置，但它们也可以协调任何手动有序流程的步骤，即使不同的步骤必须按照特定顺序在机器组之间来回跳转。它们可以同步或异步启动任务。更多playbooks的介绍参考官方文档。


Playbooks以YAML格式表示（请参阅YAML语法），具有最少的语法，它不是编程语言或脚本，而是配置或进程的模型。
每个剧本由列表中的一个或多个“戏剧”组成。戏剧的目标是将一组主机映射到一些定义明确的角色，由ansible调用任务表示。在基本级别，任务只不过是对ansible模块的调用（请参阅使用模块）。
通过编写多个“戏剧”的剧本，可以编排多机部署，在Web服务器组中的所有计算机上运行某些步骤，然后在数据库服务器组上执行某些步骤，然后在Web服务器组上执行更多命令，等等。。
你可以有很多戏剧影响你的系统做不同的事情。这并不是说你只是定义了一个特定的状态或模型，而是可以在不同的时间运行不同的戏剧。
2. Playbooks Demo以下是只包含一个戏剧的剧本：
---- hosts: webservers  vars:    http_port: 80    max_clients: 200  remote_user: root  tasks:  - name: ensure apache is at the latest version    yum:      name: httpd      state: latest  - name: write the apache config file    template:      src: /srv/httpd.j2      dest: /etc/httpd.conf    notify:    - restart apache  - name: ensure apache is running    service:      name: httpd      state: started  handlers:    - name: restart apache      service:        name: httpd        state: restarted


文件开头三个横杠代表yaml文件。
hosts表示一个主机组。
tasks表示动作集合。
name是一个注释说明。
yum和下面两行，表示使用yum安装最新版的httpd。
template和下面两行，表示使用ansible的template模块传输/srv/httpd.j2作为客户机的/etc/httpd.conf。和copy相比，template支持jinja语法。
notify和下面一行，表示传输成功后触发重启apache命令。它需要和handlers组合使用。
handlers中的name和notify中的信息要完全相同。service和下面两行，表示重启httpd。

Playbooks可以包含多个戏剧。您可能有一个首先针对Web服务器，然后是数据库服务器的playbook。例如：
---- hosts: webservers  remote_user: root  tasks:  - name: ensure apache is at the latest version    yum:      name: httpd      state: latest  - name: write the apache config file    template:      src: /srv/httpd.j2      dest: /etc/httpd.conf- hosts: databases  remote_user: root  tasks:  - name: ensure postgresql is at the latest version    yum:      name: postgresql      state: latest  - name: ensure that postgresql is started    service:      name: postgresql      state: started

3. 入门实例目标：编写一个playbook，在客户机上安装chrony，然后检查启动情况。
1、新建chrony.yml，内容为：
---- hosts: commonservers  tasks:  - name: install chrony    apt:      name: chrony      state: latest    notify:    - restart chrony  - name: ensure chrony is running    service:      name: chrony      state: started  handlers:    - name: restart chrony      service:        name: chrony        state: restarted

上面的playbook中用到了apt模块，如果有疑问可以查看帮助，ansible-doc apt。实际上，有一种更加简单的写法，就是把apt模块换成command模块，然后直接写命令。
2、检查playbookansible-playbook chrony.yml --syntax-check
3、以sudo权限执行playbookansible-playbook chrony.yml --user=voidking --private-key=/home/voidking/.ssh/id_rsa -s
4、在客户机测试chronyc sources
4. 角色如果需要将一个大文件拆分为各个小文件，我们经常使用的就是include，这也是原先ansible拆分文件的做法。如今ansible使用roles来拆分文件，将nginx、mysql等分为各个角色，在各个角色内定义具体的小任务，方便管理。另一方面，类似于php类的自动加载，roles基于一个已知的文件结构，可以自动去加载某些vars_files、tasks、handlers等。
目标：使用角色的方式，编写配置playbook，在客户机上安装chrony，然后检查启动情况。
1、创建chrony角色目录
mkdir -p roles/chrony/tasksmkdir -p roles/chrony/handlers

2、创建总的入口文件site.yml，内容为：
---- hosts: commonservers  roles:    - chrony

3、创建安装剧本vim roles/chrony/tasks/main.yml，内容为：
---- name: install chrony  apt:    name: chrony    state: latest  notify:  - restart chrony- name: ensure chrony is running  service:    name: chrony    state: started


vim roles/chrony/handlers/main.yml，内容为：
---- name: restart chrony  service:    name: chrony    state: restarted

4、以sudo权限执行playbookansible-playbook site.yml --user=voidking --private-key=/home/voidking/.ssh/id_rsa -s

5、在客户机测试chronyc sources
5. 简单编程5.1. 变量---- hosts: commonservers  vars:    pre1: all info is     pre2: hostname is  tasks:  - name: register vars    shell: hostname    register: info  - name: display info    debug: msg=&quot;&#123;&#123;pre1&#125;&#125; &#123;&#123;info&#125;&#125;&quot;  - name: display hostname    debug: msg=&quot;&#123;&#123;pre2&#125;&#125; &#123;&#123;info.stdout&#125;&#125;&quot;

这里涉及到一个知识点：使用ansible-playbook执行命令（使用shell/command模块），默认不显示标准输出/标准失败，只能看到执行成功还是失败。如果想要显示标准输出/标准失败，必须要使用变量，就像上面的这个例子一样。
5.2. 基本循环---- hosts: commonservers  tasks:  - name: loops demo    debug: msg=&quot;&#123;&#123;item&#125;&#125;&quot;    with_items:      - one      - two      - three


5.3. 循环字典---- hosts: commonservers  tasks:  - name: loops dict    debug: msg=&quot;key -&gt; &#123;&#123;item.key&#125;&#125;,value -&gt; &#123;&#123;item.value&#125;&#125;&quot;    with_items:      - &#123;key: 1, value: &quot;one&quot;&#125;      - &#123;key: 2, value: &quot;two&quot;&#125;      - &#123;key: 3, value: &quot;three&quot;&#125;


5.4. 嵌套循环---- hosts: commonservers  tasks:  - name: loops2     debug: msg=&quot;item0 -&gt; &#123;&#123;item[0]&#125;&#125;,item1 -&gt; &#123;&#123;item[1]&#125;&#125;&quot;    with_nested:      - [&#x27;1&#x27;,&#x27;2&#x27;]      - [&#x27;one&#x27;,&#x27;two&#x27;,&#x27;three&#x27;]


5.5. 散列循环---- hosts: commonservers  vars:    user:       voidking:        name: voidking        tel: 17600000000      haojin:        name: haojin        tel: 15100000000  tasks:  - name: loops3     debug: msg=&quot;key -&gt; &#123;&#123;item.key&#125;&#125;,value -&gt; &#123;&#123;item.value&#125;&#125;&quot;    with_dict:      - &quot;&#123;&#123;user&#125;&#125;&quot;


5.6. 文件循环---- hosts: commonservers  tasks:  - name: loop file    debug: msg=&quot;&#123;&#123;item&#125;&#125;&quot;    with_fileglob:      - /home/voidking/*.yml

5.7. 命令循环---- hosts: commonservers  tasks:    - name: exec command      shell: &quot;&#123;&#123;item&#125;&#125;&quot;      with_items:        - hostname        - uname      register: ret    - name: display result      debug: msg=&quot;&#123;% for i in ret.results %&#125; &#123;&#123;i.stdout&#125;&#125; &#123;% endfor %&#125;&quot; 


5.8. 条件判断ansible的条件判断使用关键字when，有两种方式：

python语法支持的原生态格式 conditions &gt; 1 or conditions == “ss”，in，not等等
ansible Jinja2 filters

---- hosts: commonservers  vars:    pre: hostname is  tasks:  - name: register vars    shell: hostname    register: info  - name: display hostname    debug: msg=&quot;&#123;&#123;pre&#125;&#125; &#123;&#123;info.stdout&#125;&#125;&quot;  - name: print true    debug: msg=&quot;result is ubuntu14&quot;    when: info.stdout == &quot;ubuntu14&quot;  - name: print false    debug: msg=&quot;result is not ubuntu14&quot;    when: info.stdout != &quot;ubuntu14&quot;  - name: print warning    debug: msg=&quot;client OS is ubuntu&quot;    when: info[&#x27;stdout&#x27;].startswith(&#x27;u&#x27;)


6. 后记至此，playbooks最基础的东西学习完毕。更高级的用法，在使用中再慢慢学习。
7. 书签
Ansible Documentation
Ansible中文权威指南
Ansible精讲
Ansible Tutorial for Beginners: Ultimate Playbook &amp; Examples
Working with Ansible Playbooks – Tips &amp; Tricks with Examples

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>ansible</tag>
      </tags>
  </entry>
  <entry>
    <title>YAML语言</title>
    <url>/dev-yaml/</url>
    <content><![CDATA[1. YAML语言简介
YAML (/ˈjæməl/ and YAH-ml) is a human-readable data-serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted. YAML targets many of the same communications applications as Extensible Markup Language (XML) but has a minimal syntax which intentionally differs from SGML. It uses both Python-style indentation to indicate nesting, and a more compact format that uses […] for lists and {…} for maps thus JSON files are valid YAML.

参考文档：

Wikipedia - YAML
YAML 语言教程
Helm - YAML技术
YAML | 竖线 &gt; 大于号等特殊符号的作用
YAML Multiline
JSONPath

下文主要转载自阮一峰大佬的YAML 语言教程。


2. YAML基本语法YAML基本语法规则：

大小写敏感
使用缩进表示层级关系
缩进时不允许使用Tab键，只允许使用空格。
缩进的空格数目不重要，只要相同层级的元素左侧对齐即可
# 表示注释，从这个字符一直到行尾，都会被解析器忽略。

YAML数据结构：

对象：键值对的集合，又称为映射（mapping）/ 哈希（hashes） / 字典（dictionary）
数组：一组按次序排列的值，又称为序列（sequence） / 列表（list）
纯量（scalars）：单个的、不可再分的值

3. YAML和JSONYAML和JSON可以互相转换。网上有很多工具：

JSON to YAML
YAML JavaScript parser
json工具
yaml工具
YAML 转 JSON
JSON-YAML互转

如果有python3环境，还可以使用python-json2yaml工具。
pip install PyYAML==5.1pip install python-json2yamlcat a.json | json2yaml &gt; a.yamlcat a.yaml | yaml2json

4. 对象对象的一组键值对，使用冒号结构表示。
animal: pets

YAML 也允许另一种写法，将所有键值对写成一个行内对象。
hash: &#123; name: Steve, foo: bar &#125; 

5. 数组一组连词线开头的行，构成一个数组。
animals:- Cat- Dog- Goldfish

数组也可以采用行内表示法。
animals: [Cat, Dog, Goldfish]

数据结构的子成员是一个数组，则可以在该项下面缩进一个空格。
animals:- - Cat - Dog - Goldfish

6. 复合结构对象和数组可以结合使用，形成复合结构。
languages: - Ruby - Perl - Python websites: YAML: yaml.org  Ruby: ruby-lang.org  Python: python.org  Perl: use.perl.org 

7. 纯量纯量是最基本的、不可再分的值。以下数据类型都属于 JavaScript 的纯量。

字符串
布尔值
整数
浮点数
Null
时间
日期

数值直接以字面量的形式表示。
number: 12.30

布尔值用true和false表示。
isSet: true

null用~表示。
parent: ~ 

时间采用 ISO8601 格式。
iso8601: 2001-12-14t21:59:43.10-05:00

日期采用复合 iso8601 格式的年、月、日表示。
date: 1976-07-31

YAML 允许使用两个感叹号，强制转换数据类型。
e: !!str 123f: !!str true

8. 字符串字符串是最常见，也是最复杂的一种数据类型。
字符串默认不使用引号表示。
str: 这是一行字符串

如果字符串之中包含空格或特殊字符，需要放在引号之中。
str: &#x27;内容： 字符串&#x27;

单引号和双引号都可以使用，双引号不会对特殊字符转义。
s1: &#x27;内容\n字符串&#x27;s2: &quot;内容\n字符串&quot;

单引号之中如果还有单引号，必须连续使用两个单引号转义。
str: &#x27;labor&#x27;&#x27;s day&#x27; 

字符串可以写成多行，从第二行开始，必须有一个单空格缩进。换行符会被转为空格。
str: 这是一段  多行  字符串

多行字符串可以使用|保留换行符，也可以使用&gt;折叠换行。
this: |  Foo  Barthat: &gt;  Foo  Bar

+表示保留文字块末尾的换行，-表示删除字符串末尾的换行。
s1: |  Foos2: |+  Foos3: |-  Foo

字符串之中可以插入 HTML 标记。
message: |  &lt;p style=&quot;color: red&quot;&gt;    段落  &lt;/p&gt;

9. 引用锚点&amp;和别名*，可以用来引用。
defaults: &amp;defaults  adapter:  postgres  host:     localhostdevelopment:  database: myapp_development  &lt;&lt;: *defaultstest:  database: myapp_test  &lt;&lt;: *defaults

&amp;用来建立锚点（defaults），&lt;&lt;表示合并到当前数据，*用来引用锚点。等同于下面的代码。
defaults:  adapter:  postgres  host:     localhostdevelopment:  database: myapp_development  adapter:  postgres  host:     localhosttest:  database: myapp_test  adapter:  postgres  host:     localhost

下面是另一个例子。
- &amp;showell Steve - Clark - Brian - Oren - *showell 

10. YAML特殊符号说明
| ：管道符/竖线，保留文本块中的换行符。
|+ ：保留文本块中的换行符。等价于|。
|- ：保留文本块中的换行符。删除最后一行的换行符。
&gt; ：大于号，文本块中的换行替换为空格。保留最后一行的换行符。
&gt;+ ：文本块中的换行替换为空格。等价于&gt;。
&gt;- ：文本块中的换行替换为空格。删除最后一行的换行符。
|2 ：缩进指示符。通常，用于缩进块的空格数将从第一行自动猜测。如果块的第一行以空格开头，可能需要块缩进指示器。在这种情况下，需要指定用于缩进的空格数（1 到 9 之间）。

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
        <category>k8s</category>
        <category>cloudnative</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>k8s</tag>
        <tag>helm</tag>
        <tag>ansible</tag>
      </tags>
  </entry>
  <entry>
    <title>Ansible入门篇</title>
    <url>/dev-ansible-start/</url>
    <content><![CDATA[1. Ansible简介ansible是一个用于自动化运维的配置管理和应用部署工具。基于Python开发，集合了众多运维工具（puppet、cfengine、chef、func、fabric.SaltStack ）的优点，实现了批量系统配置、批量程序部署、批量运行命令等功能。ansible是基于模块工作的，本身没有批量部署的能力，真正具有批量部署能力的是ansible所运行的模块。
ansible的基本架构：

连接插件（connectior plugins）用于连接管理端和被管理端。
核心模块（core modules）连接主机实现操作，它依赖于具体的模块来做具体的事情。
自定义模块（custom modules）根据自己的需求编写具体的模块。
插件（plugins）完成模块功能的补充。
剧本（playbooks）ansible的配置文件，将多个任务定义在剧本中，由ansible自动执行。
主机清单（host inventory）定义ansible需要操作主机的范围。

上述简介摘自ansible基础。
参考文档：

Ansible Documentation
Ansible中文权威指南
Ansible精讲
运维自动化-Ansible ( 一 )
Ansible 运维自动化 ( 配置管理工具 )



2. 实验环境准备两台主机，都是ubuntu14.04.4 server版，用户都是voidking。一台作为管理机，IP为192.168.56.101，一台作为被管理机（以下称为客户机），IP为192.168.56.102。
在管理机上安装ansible，一条命令搞定：sudo apt install ansible。不过，安装的ansible版本很低，默认1.5.4。这里我们通过添加源来安装最新版ansible：
sudo apt remove ansiblesudo apt-get install software-properties-commonsudo apt-add-repository ppa:ansible/ansiblesudo apt-get updatesudo apt-get install ansible

或者，使用pip安装指定版本的ansible，本文选择安装 2.6.11
sudo pip install ansible==2.6.11 -i https://pypi.tuna.tsinghua.edu.cn/simple
想要知道都有哪些版本，可以查看ansible版本列表
PS：如果是centos系统，还可以下载ansible rpm包，通过rmp命令安装 rpm -ivh ansible-2.6.11-1.el7.ans.noarch.rpm
3. 连接准备1、在管理机生成密钥
ssh-keygen
连续回车即可。
2、将公钥写入客户机
ssh-copy-id -i .ssh/id_rsa.pub -p 22 voidking@192.168.56.102

3、在管理机添加客户机到hosts
sudo vim /etc/ansible/hosts

在文档最后添加：
[commonservers]192.168.56.102

4、在管理机执行第一条命令
ansible all -m ping

5、在客户机把voidking用户添加进sudo免密，方便以后的操作
sudo vim /etc/sudoers

添加
voidking ALL = NOPASSWD: ALL

4. Ansible程序说明4.1. 程序/命令列表
ansible：主程序，临时命令执行工具
ansible-doc：查看配置文档，模块功能查看工具
ansible-galaxy：下载/上传优秀代码或Roles模块的官网平台
ansible-lint：对playbook的语法进行检查的一个工具
ansible-playbook：定制自动化任务，编排剧本工具
ansible-pull：远程执行命令的工具
ansible-vault：文件加密工具
ansible-console：基于console界面与用户交互的执行工具

其中，最重要和最常用的是ansible。
4.2. ansible命令详解ansible命令是ansible套装的核心部分，其主要用于执行ad-hoc命令，即单条命令。默认后面需要跟主机和选项部分，默认不指定模块时，使用的是command模块。command模块不是调用的shell的指令，所以没有bash的环境变量，也不能使用shell的一些操作方式，其他和shell模块没有区别。
格式：ansible &lt;host-pattern&gt; [-m module_name] [options]
参数列表：
ansible [-h] [--version] [-v] [-b] [--become-method BECOME_METHOD]        [--become-user BECOME_USER]        [-K | --become-password-file BECOME_PASSWORD_FILE]        [-i INVENTORY] [--list-hosts] [-l SUBSET] [-P POLL_INTERVAL]        [-B SECONDS] [-o] [-t TREE] [--private-key PRIVATE_KEY_FILE]        [-u REMOTE_USER] [-c CONNECTION] [-T TIMEOUT]        [--ssh-common-args SSH_COMMON_ARGS]        [--sftp-extra-args SFTP_EXTRA_ARGS]        [--scp-extra-args SCP_EXTRA_ARGS]        [--ssh-extra-args SSH_EXTRA_ARGS]        [-k | --connection-password-file CONNECTION_PASSWORD_FILE] [-C]        [--syntax-check] [-D] [-e EXTRA_VARS] [--vault-id VAULT_IDS]        [--ask-vault-password | --vault-password-file VAULT_PASSWORD_FILES]        [-f FORKS] [-M MODULE_PATH] [--playbook-dir BASEDIR]        [--task-timeout TASK_TIMEOUT] [-a MODULE_ARGS] [-m MODULE_NAME]        pattern

常用参数：

-m：指定模块
-a：指定参数
-i：指定主机列表

例子：
# 查看主机清单中的所有主机ansible all --list-hosts# 指定主机清单，查看主机清单中的所有主机ansible all -i ./hosts --list-hosts# 查看commonservers组的主机ansible commonservers --list-hosts# 测试连通ansible all -m ping# 单台主机执行命令ansible all -i &quot;192.168.56.102,&quot; -m ping# 查看客户机的内存ansible all -m command -a &quot;free -h&quot;# 在客户机执行管理机脚本cp /bin/echo ~/echo.shansible all -m script -a &quot;~/echo.sh hello&quot; -vvv# 在客户机执行客户机脚本ansible all -m command -a &quot;cp /bin/echo /home/voidking/echo.sh&quot;ansible all -m shell -a &quot;/home/voidking/echo.sh hello&quot;# 向客户机拷贝文件ansible all -m copy -a &quot;src=/home/voidking/echo.sh dest=/tmp/ owner=voidking group=voidking mode=0755&quot;# 后台执行命令ansible all -m ping -B 3600 -P 0ansible all -m async_status -a &quot;jid=&#x27;182829279182.2094&#x27;&quot;# 批量添加sudo权限ansible all -i hosts -s -m shell -a &quot;echo &#x27;haojin ALL=(ALL) NOPASSWD: ALL&#x27; &gt;&gt; /etc/sudoers&quot;# 批量移除sudo权限ansible all -i hosts -s -m shell -a &quot;sed -i &#x27;/haojin ALL=(ALL) NOPASSWD: ALL/d&#x27; /etc/sudoers&quot;# 使用root用户备份和拷贝文件ansible all -i hosts -s -m shell -a &quot;mv /tmp/run.sh /tmp/run.sh.bak&quot;ansible all -i hosts -b --become-user=root -m shell -a &quot;mv /tmp/run.sh /tmp/run.sh.bak&quot;ansible all -i hosts -s -m copy -a &quot;src=/root/run.sh dest=/tmp/run.sh owner=voidking group=root mode=0755&quot;# 重启服务ansible all -i hosts -s -m shell -a &quot;/tmp/stop.sh&quot;ansible all -i hosts -s -m shell -a &quot;/tmp/run.sh&quot; -u voidking

4.3. 配置文件
/etc/ansible/ansible.cfg，主配置文件，配置ansible工作特性
/etc/ansible/hosts，主机清单
/etc/ansible/roles/，存放角色的目录

1、ansible.cfg的配置参考Configuring Ansible，主要关注inventory、forks、poll_interval、remote_port、log_path等参数。
2、hosts文件有一个特性很棒，可以指定变量，例如：
[commonservers]192.168.56.102[commonservers:vars]ansible_ssh_pass=&#x27;123456&#x27;ansible_ssh_port=22[commonservers1]192.168.56.103[commonservers:children]commonservers1

如果在hosts文件中指定了用户名和密码，使用时报错 to use the ‘ssh’ connection type with passwords, you must install the sshpass program那么需要先安装sshpass，安装命令为：
# ubuntuapt-get install sshpass# centosyum install epel-release -yyum install sshpass -y

sshpass可以实现免交互指定密码登录机器，使用方法为：
sshpass -p xxx ssh ubuntu@192.168.56.102

4.4. ansible内置变量小结# 主机名ansible_ssh_host # 端口号ansible_ssh_port# 用户名 ansible_ssh_user # 密码ansible_ssh_pass # 使用sudo连接用户时的密码ansible_sudo_pass # 秘钥文件如果不想使用ssh-agent管理时可以使用此选项ansible_ssh_private_key_file # shell的类型默认shansible_shell_type # SSH 连接的类型ansible_connection # 指定python解释器的路径ansible_python_interpreter 

4.5. ansible-doc格式：ansible-doc [options] [module...]
例子：
ansible-doc pingansible-doc -s pingansible-doc -lansible-doc -a

4.6. 直接指定主机ansible命令中直接指定主机，而不是主机清单
ansible all -i &#x27;192.168.56.101,&#x27; -m ping

4.7. 主机通配符# 匹配所有主机，相当于allansible &#x27;*&#x27; -m ping# 匹配含有common的主机组ansible &#x27;*common*&#x27; -m ping# 匹配IP为100-109的主机ansible &#x27;192.168.56.10?&#x27; -m ping# 匹配IP段ansible &#x27;192.168.56.101:192.168.56.200&#x27; -m ping# 逻辑与ansible &#x27;commonservers:&amp;dbservers&#x27; --list# 逻辑非ansible &#x27;commonservers:!dbservers&#x27; --list# 正则表达式ansible &#x27;~192\.168\.56\.[1-254]&#x27; --list

注意：在不指定主机清单的情况下，使用主机通配符，会匹配 /etc/ansible/hosts 中符合条件的主机。
4.8. 常用模块
command：在客户机运行命令
shell：在客户机shell进程下运行命令，支持shell特性，如管道等
copy：从管理机拷贝文件到客户机
fetch：从客户机获取文件
file：设置文件属性
yum：centos软件包管理
service：服务管理
script：执行脚本
user：用户管理
cron：crontab管理

shell比command更强大，command比shell更安全。因为command不支持重定向、管道等，避免了shell注入的风险。
5. 指定ansible.cfg如果ansible没有指定ansible.cfg，或者想要临时使用自己定义的ansible.cfg，该怎么处理？
1、参考ansible.cfg，创建ansible.cfg文件，假设路径为 /home/voidking/ansible.cfg
2、设置环境变量
export ANSIBLE_CONFIG=/home/voidking/ansible.cfg

3、查看配置
ansible --version

6. 批量推送公钥6.1. 需求描述在本文的一开始，我们通过ssh-copy-id命令把管理机的公钥推送给了客户机。如果客户机有几百个，肯定不能再手动搞了，这时可以使用ansible的authorized_key模块来实现推送。
6.2. 准备hosts1、创建hosts文件，把所有客户机都添加进去
[common]192.168.56.102192.168.56.103192.168.56.104[common:vars]ansible_ssh_user=&quot;voidking&quot;ansible_ssh_pass=&quot;123456&quot;

2、配置忽略host_key_checking
vim /etc/ansible/ansible.cfg

去掉 host_key_checking = False 的注释
3、测试连通性
ansible all -i hosts -m ping

6.3. 通过命令推送公钥方法一：通过命令推送公钥
1、推送公钥
SSH_PUB_KEY=$(cat /home/voidking/.ssh/id_rsa.pub)ansible common -i hosts -u voidking -m authorized_key -a &quot;user=voidking key=\&quot;$SSH_PUB_KEY\&quot;&quot;

2、验证
ssh voidking@192.168.56.102

6.4. 通过剧本推送公钥方法二：通过剧本推送公钥
1、编写剧本 ssh-copy.yaml
---- hosts: common      remote_user: voidking  tasks:  - authorized_key:     user: voidking     key: &quot;&#123;&#123; lookup(&#x27;file&#x27;, &#x27;/home/voidking/.ssh/id_rsa.pub&#x27;) &#125;&#125;&quot;

2、推送公钥
ansible-playbook -i hosts ssh-copy.yaml

3、验证
ssh voidking@192.168.56.102

7. ssh-agentansible可能使用到ssh-agent，那就顺便学习一下，参考了解ssh代理：ssh-agent。
ssh-agent是ssh代理程序，ssh-agent可以帮助我们管理私钥。
那么什么时候需要ssh代理帮助我们管理私钥呢？当遇到如下情况时，我们会需要ssh代理。

使用不同的密钥连接到不同的主机时，需要手动指定对应的密钥，ssh代理可以帮助我们选择对应的密钥进行认证，不用手动指定密钥即可进行连接。
当私钥设置了密码，我们又需要频繁的使用私钥进行认证时，ssh代理可以帮助我们免去重复的输入密码的操作。
管理机可以通过密钥连接到客户机A和客户机B，但是客户机A无法通过密钥连接到客户机B，ssh代理可以帮助我们在客户机A也能通过密钥连接到客户机B。

我们以添加.ssh/id_rsa私钥给ssh代理为例，查看一下使用流程。
1、启用ssh-agentssh-agent bash
2、把.ssh/id_rsa添加到ssh代理ssh-add ~/.ssh/id_rsa
3、测试登录ssh voidking@192.168.56.102登录成功。切换到root用户，ssh-agent失效。重新使用voidking登录，ssh-agent失效。想要重新登录依然生效，那就创建个脚本登录时自动执行。
常用命令： 

ssh-agent $SHELL，启动
ssh-agent -k，关闭
ssh-add ~/.ssh/key_name，添加指定私钥
ssh-add -l，查看私钥
ssh-add -L，查看公钥
ssh-add -d /path/of/key/key_name，删除指定私钥
ssh-add -D，删除所有私钥
ssh-add -x，设置密码，锁定ssh代理
ssh-add -X，输入密码，解锁ssh代理

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>ansible</tag>
        <tag>ssh</tag>
        <tag>sshpass</tag>
      </tags>
  </entry>
  <entry>
    <title>Kolla配置实例网络</title>
    <url>/dev-kolla-instance-network/</url>
    <content><![CDATA[1. 前言紧接着《Ubuntu16使用Kolla安装OpenStack》，假设已经使用kolla安装好了openstack，本文我们来研究一下怎样访问实例和怎样配置网络。


2. 访问实例1、在dashboard查看网络拓扑如下：上图中网关的ip为10.0.2.158，ping不通。上图中实例demo1的ip为10.0.0.3，ping不通。这是因为，当前网络和实例的网络属于不同的命名空间。
2、查看命名空间ip netns
3、查看qrouter的网络ip netns exec qrouter-6e5d8c97-3f9c-4ce9-a9b4-580f2f38e0f3 ip add
4、登录实例demo1ip netns exec qrouter-6e5d8c97-3f9c-4ce9-a9b4-580f2f38e0f3 ssh cirros@10.0.0.3
3. 直接访问实例上面的登录方式，比较麻烦，因为要指定命名空间。下面我们来配置一下，使得不指定命名空间也可以访问实例。
1、在/etc/network/interfaces中添加配置：
auto br-exiface br-ex inet staticaddress 10.0.2.1netmask 255.255.255.0

2、启用br-exifup br-ex
3、测试连接此时，可以ping通网关10.0.2.158，ping不通实例10.0.0.3。
4、解决方法一，添加路由
route add -net 10.0.0.0/24 gw 10.0.2.158 dev br-exping 10.0.0.3 -c3

5、解决方法二，给demo1实例分配浮动IP
source /etc/kolla/admin-openrc.shopenstack network listopenstack floating ip create public1openstack server add floating ip demo1 10.0.2.153ping 10.0.2.153 -c3

4. 外网配置在实例中，ping不通8.8.8.8。参考Openstack实践（1）部署使用实例及neutron网络，可以通过修改init-runonce脚本解决，EXT_NET_CIDR、EXT_NET_RANGE和EXT_NET_GATEWAY需要根据物理网络配置。
这里，郝同学使用snat来解决。
1、参考Linux配置SNAT上网，设置snat
iptables -I INPUT -i eth2 -j ACCEPTiptables -I INPUT -i br-ex -j ACCEPTiptables -t nat -A POSTROUTING -s 10.0.2.0/24 -o eth1 -j MASQUERADEiptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth1 -j MASQUERADE
这里的设置，使10.0.2.0/24网段和10.0.0.0/24网段都可以上网了。实际上，只要配置10.0.2.0/24网段可以上网就可以了，因为这个网段用于配置浮动IP。
2、测试网络ssh cirros@10.0.0.3
ping 8.8.8.8 -c3如上图，实例已经可以ping通外网。
5. 书签Openstack 之 kolla部署外部网络配置
Openstack 之 kolla部署外部网络配置
kolla queens on centos7.4
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>kolla</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16使用Kolla安装OpenStack</title>
    <url>/dev-ubuntu16-kolla-openstack-all-in-one/</url>
    <content><![CDATA[1. 前言完成了《Ubuntu16手动安装OpenStack——XXX》系列，看上去，我们已经总结出了一套不错的文档。遵照文档，理论上就能成功安装更多的机器。但是，设想是美好的，而在实际安装过程中，又出现很多莫名其妙的报错。有些错误可以解决，很好；有些错误解决不了，那么整个环境就废了，需要重装系统。而且，整个安装流程非常复杂，难以保证每一步都不出错。
于是，郝同学决定寻找更加通用、更加方便的方法。树添同学给出建议：能不能使用docker来进行部署？经过讨论和搜索资料，发现这条道路确实可行，而且有OpenStack官方文档。
本文，就来研究一下使用Kolla安装OpenStack Queens版本的方法，架构采用最简单的all-in-one。


2. Kolla简介Kolla是OpenStack社区“Big Tent”开发模式下的项目，该项目由思科于2014年9月提出。Kolla的优势和使用场景体现在如下几个方面：

原子性升级或者回退OpenStack部署。
基于组件升级OpenStack。
基于组件回退OpenStack。

Kolla为OpenStack的部署提供了有效、快捷、方便、易于维护、方便版本更新与回退的方案。
具体而言，Kolla的最终目标是为OpenStack的每一个服务都创建一个对应的Docker镜像，通过Docker镜像将升级的粒度减小到服务级别，从而在升级时对OpenStack的影响降到最小，并且一旦升级失败，也很容易回滚。升级只需要三步：拉取新版本的容器镜像，停止老版本的容器服务，启动新版本的容器。回滚也不需要重新安装包，直接启动老版本的容器服务就行，非常方便。
Kolla可以使用Ansible、Kubernetes或者Mesos来部署OpenStack环境，Kolla负责容器化OpenStack各个服务；后者则负责部署这些容器，搭建出一个可用的OpenStack环境。来实现基于Docker容器的OpenStack服务全生命周期管理，如安装、升级、回滚、迁移等。在部署Docker容器时，默认的网络配置都是Host模式。因为Kolla的Docker镜像粒度很小，它针对每个OpenStack服务都有特定的镜像，所以我们也可以通过Docker命令来操作某个具体的OpenStack服务。
Kolla项目及其相关的其他项目如下：

Kolla项目，负责docker build OpenStack每个服务，如nova-compute容器等；
Kolla-Ansible项目，使用Ansible部署这些容器，搭建OpenStack环境；
Kolla-Kubernetes项目，使用Kubernetes部署这些容器，搭建OpenStack环境；
Kolla-Mesos项目，使用Mesos部署这些容器，搭建OpenStack环境。

上述Kolla简介摘自微信分享DockOne微信分享（一二八）：容器化部署OpenStack的正确姿势。
3. 环境VirtualBox虚拟机一台，系统为ubuntu-16.04.4-server-amd64，4核8G内存40G存储，主机名为controller，eth0的IP为192.168.56.110，eth1为nat上网网卡，eth2为neutron服务提供网络。
4. 网络设置1、切换到root用户sudo -i
2、vim /etc/network/interfaces，设置网卡为：
# This file describes the network interfaces available on your system# and how to activate them. For more information, see interfaces(5).source /etc/network/interfaces.d/*# The loopback network interfaceauto loiface lo inet loopback# The primary network interfaceauto eth0iface eth0 inet staticaddress 192.168.56.110netmask 255.255.255.0auto eth1iface eth1 inet dhcpauto eth2iface eth2 inet manualup ifconfig $IFACE 0.0.0.0 upup ifconfig $IFACE promisc

3、启用网卡ifup eth2
5. 安装配置主要参考OpenStack, Ansible, and Kolla on Ubuntu 16.04、kolla queens on centos7.4和Kolla-Ansible’s documentation!。
5.1. 安装依赖1、安装并升级pip
apt-get updateapt-get install python-pippip install --upgrade pip

2、安装依赖apt-get -y install python-dev libffi-dev gcc libssl-dev python-selinux
3、安装ansibleapt-get -y install ansible
4、vim /etc/ansible/ansible.cfg，添加如下：
line 10, add[defaults]host_key_checking=Falsepipelining=Trueforks=100

5.2. 安装docker1、安装dockercurl -SSL https://get.docker.io | bash
2、为docker和kolla创建配置文件
mkdir -p /etc/systemd/system/docker.service.dvim /etc/systemd/system/docker.service.d/kolla.conf

修改为：
[Service]MountFlags=shared

3、重启docker
systemctl daemon-reloadsystemctl restart docker

4、查看docker信息docker info报错：
docker: Cannot connect to the Docker daemon. Is the docker daemon running on this host?.

5、重启dockersystemctl restart docker报错：
Failed to restart docker.service: Unit docker.service is not loaded properly: Invalid argument.See system logs and &#x27;systemctl status docker.service&#x27; for details.

6、手动启动docker/usr/bin/dockerd报错：
INFO[0000] libcontainerd: new containerd process, pid: 7629 WARN[0000] containerd: low RLIMIT_NOFILE changing to max  current=1024 max=1048576FATA[0001] Error starting daemon: error initializing graphdriver: &quot;/var/lib/docker&quot; contains several valid graphdrivers: aufs, overlay2; Please cleanup or explicitly choose storage driver (-s &lt;DRIVER&gt;)

7、查看docker的aufs挂载情况并取消挂载
cat /proc/mounts | grep &quot;docker&quot;umount /var/lib/docker/aufs

8、删除/var/lib/docker/目录下的文件并重启docker
rm -rf /var/lib/docker/*systemctl start docker

9、再次查看docker信息docker info
5.3. 加速镜像拉取参考Docker入门中的镜像加速器一节。
5.4. 安装Kolla-ansible1、安装kolla-ansiblepip install kolla-ansible
报错：ImportError: cannot import name main，参考升级pip后出现ImportError: cannot import name main，编辑/usr/bin/pip文件，如下修改：
# line 9, changefrom pip import __main__if __name__ == &#x27;__main__&#x27;:    sys.exit(__main__._main())

报红：oslo-config 6.4.0 has requirement PyYAML&gt;=3.12, but you’ll have pyyaml 3.11 which is incompatible.忽略。
2、拷贝globals.yml和passwords.yml到/etc/kolla目录cp -r /usr/local/share/kolla-ansible/etc_examples/kolla /etc/
3、拷贝all-in-one和multinode清单文件到当前目录cp /usr/local/share/kolla-ansible/ansible/inventory/* .
4、生成kolla密码kolla-genpwd我们部署中使用的密码存储在/etc/kolla/passwords.yml文件中。此文件中的所有密码都是空白的，必须手动填写或运行随机密码生成器。
5.5. 配置globals.yml1、查看globals.yml配置grep -vE &#39;^$|^#&#39; /etc/kolla/globals.yml
2、vim /etc/kolla/globals.yml，如下修改：
# line 15,uncommentkolla_base_distro: &quot;centos&quot;# line 18,uncommentkolla_install_type: &quot;binary&quot;# line 21,uncomment and changeopenstack_release: &quot;queens&quot;# line 31,changekolla_internal_vip_address: &quot;192.168.56.120&quot;# line 85,uncomment and changenetwork_interface: &quot;eth0&quot;# line 100,uncomment and changeneutron_external_interface: &quot;eth2&quot;# line 331,uncommentdesignate_backend: &quot;bind9&quot;designate_ns_record: &quot;sample.openstack.org&quot;# line 340,uncomment and changenova_compute_virt_type: &quot;qemu&quot;# othertempest_image_id:tempest_flavor_ref_id:tempest_public_network_id:tempest_floating_network_name:

kolla_internal_vip_address为192.168.56.120，它是一个和OpenStack宿主机内网连接网络（eth0 192.168.56.110）同一个网段的未使用IP。network_interface为eth0，意思是openstack内部网络使用eth0网卡。neutron_external_interface为eth2，意思是openstack的虚拟机外部网络使用eth2网卡。因为是在虚拟机中安装openstack，所以nova_compute_virt_type设置为qemu。
5.6. 部署1、初始化kolla-ansible -i ./all-in-one bootstrap-servers报错：查找资料，猜测是ansible版本问题。（1）ansible --version，查看ansible版本为2.0.0.2，确实非常低。（2）apt install ansible，提示已经是最新版本。（3）更新apt库，然后重装ansible
apt-get install software-properties-commonapt-add-repository ppa:ansible/ansibleapt-get updateapt-get install ansible
然后，重新编辑/etc/ansible/ansible.cfg，再次执行初始化命令，问题解决。时间比较久，请耐心等待。
2、预检查kolla-ansible -i ./all-in-one prechecks
3、拉取镜像kolla-ansible -i ./all-in-one pull这一步的时间特别久，挺耐心等待。如果拉取失败，就多尝试几次。
4、查看镜像docker images
5、部署kolla-ansible -i ./all-in-one deploy
6. 测试使用openstack1、安装openstack客户端pip install python-openstackclient python-glanceclient python-neutronclient --ignore-installed
2、生成admin-openrc.sh等kolla-ansible post-deploy
3、使admin环境生效source /etc/kolla/admin-openrc.sh
4、查看计算服务openstack compute service list
7. 初始化配置1、执行init-runonce脚本. /usr/local/share/kolla-ansible/init-runonce执行初始化之前，可以参考openstack 之 Kolla部署指南，设置一下外部网络。这样，就可以从路由器给虚拟机分配IP，安装完虚拟机就可以宿主机互相ping通。比如：
EXT_NET_CIDR=&#x27;192.168.56.0/24&#x27;EXT_NET_RANGE=&#x27;start=192.168.56.200,end=192.168.56.240&#x27;EXT_NET_GATEWAY=&#x27;192.168.56.1&#x27;
这里配置的网络范围，对应globals.yml中配置的neutron_external_interface，也就是eth2网卡。
2、根据提示，创建实例
openstack server create \--image cirros \--flavor m1.tiny \--key-name mykey \--nic net-id=e52fe099-c798-4ffd-8c3c-06de424a9de7 \demo1

3、访问horizon服务curl 192.168.56.120 -L
在浏览器访问 http://192.168.56.120 ，使用admin账号登录，密码在passwords.yml文件中查看。less /etc/kolla/passwords.yml | grep keystone_admin_password
8. 后记至此，使用kolla安装openstack成功。但是，我们还不知道实例能否正常使用。关于实例的访问和网络配置，放在下一篇文章中研究。
9. 书签Kolla’s documentation!
Kolla让OpenStack部署更贴心
Kolla安装Ocata单节点
kolla部署openstack ocata版
Install and configure OpenStack Ocata with Kolla as a standalone
Kolla OpenStack系统视频课程
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>ansible</tag>
        <tag>kolla</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo使用livere作为评论插件</title>
    <url>/dev-hexo-livere-comment-plugin/</url>
    <content><![CDATA[1. livere简介livere（来必力）是一个社会化评论插件，优点有：

使用社交网站账户登录，免去注册过程。
提高用户的参与和沟通意愿。
管理/删除我的评论内容。
提供管理页面，管理网站文章及评论内容。



2. 注册livere1、访问来必力官网，注册账号。
2、登录后点击导航栏“安装”，选择city版。
3、根据提示，申请获取代码。
4、郝同学获取到的一般网站的代码为：
&lt;!-- 来必力City版安装代码 --&gt;&lt;div id=&quot;lv-container&quot; data-id=&quot;city&quot; data-uid=&quot;MTAyMC8zODU3Mi8xNTEwMA==&quot;&gt;    &lt;script type=&quot;text/javascript&quot;&gt;   (function(d, s) &#123;       var j, e = d.getElementsByTagName(s)[0];       if (typeof LivereTower === &#x27;function&#x27;) &#123; return; &#125;       j = d.createElement(s);       j.src = &#x27;https://cdn-city.livere.com/js/embed.dist.js&#x27;;       j.async = true;       e.parentNode.insertBefore(j, e);   &#125;)(document, &#x27;script&#x27;);    &lt;/script&gt;&lt;noscript&gt; 为正常使用来必力评论功能请激活JavaScript&lt;/noscript&gt;&lt;/div&gt;&lt;!-- City版安装代码已完成 --&gt;

3. 配置使用livere1、在 yilia/layout/_partial/post 目录下，新建livere.ejs，内容为：
&lt;div class=&quot;livere&quot;&gt;    &lt;!-- 来必力City版安装代码 --&gt;    &lt;div id=&quot;lv-container&quot; data-id=&quot;city&quot; data-uid=&quot;&lt;%=theme.livere.uid%&gt;&quot;&gt;        &lt;script type=&quot;text/javascript&quot;&gt;       (function(d, s) &#123;           var j, e = d.getElementsByTagName(s)[0];           if (typeof LivereTower === &#x27;function&#x27;) &#123; return; &#125;           j = d.createElement(s);           j.src = &#x27;https://cdn-city.livere.com/js/embed.dist.js&#x27;;           j.async = true;           e.parentNode.insertBefore(j, e);       &#125;)(document, &#x27;script&#x27;);        &lt;/script&gt;    &lt;noscript&gt; 为正常使用来必力评论功能请激活JavaScript&lt;/noscript&gt;    &lt;/div&gt;    &lt;!-- City版安装代码已完成 --&gt;&lt;/div&gt;

2、编辑 yilia/layout/_partial/article.ejs，添加：
&lt;% if (!index &amp;&amp; theme.livere.enable &amp;&amp; post.comments)&#123; %&gt;&lt;%- partial(&#x27;post/livere&#x27;, &#123;    key: post.slug,    title: post.title,    url: config.url+url_for(post.path)  &#125;) %&gt;&lt;% &#125; %&gt;

3、编辑 yilia/_config.yml，添加：
# 注释所有畅言配置# 注释所有gitalk配置# livere配置livere:   enable: true  uid: MTAyMC8zODU3Mi8xNTEwMA==

4、在 yilia/source/css/_partial/main.stly 中添加：
.livere&#123;  padding: 0 40px;&#125;

至此，配置livere评论插件完成，效果如下：
nice，比gitalk要美观，支持平台也更多。
4. 配置评论提醒访问livere管理页面 - 评论提醒，设置接受提醒的邮箱和提醒周期（建议3小时）。
配置评论提醒后，有新的评论我们就能及时看到并进行回复，给用户更好的体验。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo使用gitalk作为评论插件</title>
    <url>/dev-hexo-gitalk-comment-plugin/</url>
    <content><![CDATA[1. gitalk简介
Gitalk 是一个基于 GitHub Issue 和 Preact 开发的评论插件。

特点：

使用 GitHub 登录
支持多语言（en, zh-CN, zh-TW, es-ES, fr, ru）
支持个人或组织
无干扰模式（设置 distractionFreeMode 为 true 开启）
快捷键提交评论 （cmd|ctrl + enter）

本文中，我们学习使用gitalk作为Hexo评论插件。
参考文档：

如何在 hexo 博客中，集成 gitalk 评论插件
在Hexo、Hugo博客框架中使用Gitalk
Gitalk评论自动初始化
hexo next 主题配置 gitalk 评论后无法初始化创建 issue



2. 准备工作想要使用gitalk，需要申请GitHub Application。
如上图，以郝同学的申请为例：

Application name填入喜欢的应用名称。
Homepage URL填入域名。
Application description填入应用的描述。
Authorization callback URL 填入域名。

然后，Register application，转到应用页面。在该页面，可以看到Client ID和Client Secret。
3. 配置使用gitalk3.1. yilia主题配置具体配置内容参考hexo-theme-yilia - commit/6e339354
最终效果如下图：
打开一些页面的时候，报错：Error: Validation Failed.在gitalk的issue中发现，如果location.pathname大于50，就会报这个错误。
采用md5编码url的方式来解决这个问题，也已经包含在commit/6e339354中了，配置方法参考文档：

处理Gitalk中由于文章URL过长导致的Validation Failed(422)
报错出现 Error: Validation Failed.

3.2. next主题配置具体配置内容参考hexo-theme-next - commit/c5b354d2
4. 批量初始化比较麻烦的是，每篇文章都需要初始化！因此需要搞一个批量初始化的脚本。
具体配置内容参考hexo-deploy - commit/e11e8ec8
1、测试生成 sitemap.xml
hexo gcat public/sitemap.xml

2、批量初始化 issues
node init-gitalk.js

批量初始化可以集中到package.json，添加
&#123;  &quot;scripts&quot;: &#123;      &quot;gitalk&quot;: &quot;node init-gitalk.js&quot;    &#125;,&#125;

对应批量初始化命令改为：
npm run gitalk

注意：如果文章比较多，那么无法一次性初始化成功，因为github api免费版限制每小时写issue请求为60，超过则会报错：
&quot;message&quot;: &quot;You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.&quot;&quot;documentation_url&quot;: &quot;https://docs.github.com/rest/overview/resources-in-the-rest-api#secondary-rate-limits&quot;

参考文档：

Hexo评论功能的实现：Gitalk
自动初始化 Gitalk 和 Gitment 评论
sitemap.xml

5. 后记Hexo评论插件对比

多说，挺好用，下线了
畅言，广告太多，弃用
disqus，靠谱，但是被墙
gitalk，初始化麻烦，加载慢
livere，加载慢，广告需自行屏蔽

]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>使用Open vSwitch进行VxLAN隧道实验</title>
    <url>/dev-openvswitch-vxlan/</url>
    <content><![CDATA[1. 前言为了加深对VxLAN网络模式的理解，郝同学打算使用Open vSwitch进行VxLAN隧道实验。主要参考搭建基于Open vSwitch的VxLAN隧道实验和基于Open vSwitch的VxLAN隧道实验网络。


2. 虚拟机准备使用VirtualBox创建两个虚拟机，主机名分别为controller和compute，系统均为ubuntu-16.04.4-server-amd64。
这两台虚拟机，都有两块网卡。eth0负责主机间相互通信，eth1负责上网。controller的eth0的IP为192.168.56.110，compute的eth0的IP为192.168.56.111。controller和compute可以互相ping通。
3. 安装openvswitch通过源码安装参考Open vSwitch on Linux, FreeBSD and NetBSD，这里我们不使用源码安装，而是使用apt包管理工具直接安装，参考Debian安装openvswitch。
一条命令搞定：
apt -y install  openvswitch-switch openvswitch-common openvswitch-switch-dpdk

4. 实验一4.1. 目标如上图，分别给两台虚拟机的br1指定两个 相同网段 的ip，然后通过搭建VXLAN隧道让这两个不同机器的网桥能够实现通信。
4.2. controller1、在controller上添加br0和br1两个网桥
ovs-vsctl add-br br0ovs-vsctl add-br br1

2、查看网桥ovs-vsctl show
3、在br0上添加一个端口，将eth0挂载到br0上。这样做的目的是方便我们在虚拟网桥上添加多个端口供我们使用，这样不必受限于eth0的有限端口。ovs-vsctl add-port br0 eth0
4、此时我们将原先eth0分配的ip清除并指定给br0，让虚拟机网络能通过br0继续工作。
ifconfig eth0 0 upifconfig br0 192.168.56.110/24 up

5、根据实际情况配置一下br0的网关（可选）。route add default gw 192.168.56.1 br0
6、给网桥br1分配一个ip。ifconfig br1 172.16.0.1/24 up
7、查看ipifconfig
4.3. ip永久生效以上ip配置，在重启后会失效，要想永久有效，需要修改interfaces配置。vim /etc/network/interfaces，如下修改：
auto eth0iface eth0 inet manualup ifconfig $IFACE 0.0.0.0 upup ifconfig $IFACE promiscauto eth1iface eth1 inet dhcpauto br0iface br0 inet staticaddress 192.168.56.110netmask 255.255.255.0auto br1iface br1 inet staticaddress 172.16.0.1netmask 255.255.255.0

4.4. compute1、在compute上添加br0和br1两个网桥
ovs-vsctl add-br br0ovs-vsctl add-br br1

2、在br0上添加一个端口，将eth0挂载到br0上。这样做的目的是方便我们在虚拟网桥上添加多个端口供我们使用，这样不必受限于eth0的有限端口。ovs-vsctl add-port br0 eth0
3、此时我们将原先eth0分配的ip清除并指定给br0，让虚拟机网络能通过br0继续工作。
ifconfig eth0 0 upifconfig br0 192.168.56.111/24 up

4、根据实际情况配置一下br0的网关（可选）。route add default gw 192.168.56.1 br0
5、给网桥br1分配一个ip。ifconfig br1 172.16.0.2/24 up
4.5. 搭建VxLAN隧道1、在controller查看连通性
ping 192.168.56.111 -c3ping 172.16.0.2 -c3
上图可以看到，controller可以ping通过192.168.56.111，却ping不通172.16.0.2。
2、在controller上设置VxLAN，远端ip设置为compute能对外通信的br0的ip。
ovs-vsctl add-port br1 vx1 -- set interface vx1 type=vxlan options:remote_ip=192.168.56.111

此时，依然ping不通172.16.0.2。
3、在compute上设置VxLAN，远端ip设置为controller能对外通信的br0的ip。
ovs-vsctl add-port br1 vx1 -- set interface vx1 type=vxlan options:remote_ip=192.168.56.110

4、在controller上验证VxLAN隧道ping 172.16.0.2 -c3如上图，controller的br1和compute的br1已经连通，实验成功。
5. 实验二5.1. 目标如上图，分别给两台虚拟机的br1指定两个 不同网段 的ip，然后通过搭建VXLAN隧道让这两个不同网段的网桥能够实现通信。
5.2. 搭建VxLAN隧道紧接着实验一，我们已经创建了VxLAN隧道，controller和compute在172.16.0.0/24网段可以相互ping通。
1、在compute上修改ip为其他网段ifconfig br1 172.16.1.1/24 up
2、在controller上验证VxLAN隧道ping 172.16.1.1 -c3如上图，controller无法连通compute，这是因为，不通网段间的通信需要路由。
3、在controller上设置路由route add -net 172.16.1.0/24 gw 172.16.0.1 dev br1
4、在compute上设置路由route add -net 172.16.0.0/24 gw 172.16.1.1 dev br1
5、再次在controller上验证VxLAN隧道ping 172.16.1.1 -c3如上图，controller的br1和compute的br1已经连通，实验成功。
5.3. 路由永久生效以上路由配置，在重启后会失效，要想永久有效，需要修改interfaces配置。controller上，vim /etc/network/interfaces，添加：
up route add -net 172.16.1.0/24 gw 172.16.0.1 dev br1

compute上，vim /etc/network/interfaces，添加：
up route add -net 172.16.0.0/24 gw 172.16.1.1 dev br1

6. 实验三6.1. 目标紧接着实验二，问题来了，两个虚拟机之间可以创建VxLAN隧道，那么三个虚拟机之间该怎么处理？本实验，就要在三个虚拟机之间建立XxLAN隧道，实现通信。
6.2. ip配置参照实验一：controller的br0配置ip为192.168.56.110/24，br1配置ip为172.16.0.1/24。compute1的br0配置ip为192.168.56.111/24，br1配置ip为172.16.1.1/24。compute2的br0配置ip为192.168.56.112/24，br1配置ip为172.16.2.1/24。
6.3. 搭建VxLAN隧道1、在controller上设置VxLAN，远端ip设置为compute1和compute2能对外通信的br0的ip。
ovs-vsctl add-port br1 vx1 -- set interface vx1 type=vxlan options:remote_ip=192.168.56.111ovs-vsctl add-port br1 vx2 -- set interface vx2 type=vxlan options:remote_ip=192.168.56.112

2、在compute1上设置VxLAN，远端ip设置为controller能对外通信的br0的ip。
ovs-vsctl add-port br1 vx1 -- set interface vx1 type=vxlan options:remote_ip=192.168.56.110

3、在compute2上设置VxLAN，远端ip设置为controller能对外通信的br0的ip。
ovs-vsctl add-port br1 vx1 -- set interface vx1 type=vxlan options:remote_ip=192.168.56.110

5、在controller上添加路由
route add -net 172.16.0.0/16 gw 172.16.0.1 dev br1

6、在compute1上添加路由route add -net 172.16.0.0/16 gw 172.16.1.1 dev br1
7、在compute2上添加路由route add -net 172.16.0.0/16 gw 172.16.2.1 dev br1
8、在controller测试连通compute1和compute2
ping 172.16.1.1 -c3ping 172.16.2.1 -c3
神奇的事情发生了，可以ping通172.16.1.1的时候，172.16.2.1不通；可以ping通172.16.2.1的时候，172.16.1.1不通。这是因为，在完成实验二后，郝同学直接复制了compute虚拟机作为compute2，结果导致两台虚拟机的br1的mac地址相同。compute2删除br1重建，问题解决。
9、在compute1测试连通compute2ping 172.16.2.1 -c3此时，compute1和compute2的br1已经连通。由上面的配置我们知道，它们之间并没有建立直接隧道，而是通过controller进行转发。
有意思的是，使用traceroute查看路由，发现它们连接时并没有经过网关（compute1的172.16.1.1和controller的172.16.0.1），而是直连。
traceroute 172.16.2.1traceroute 172.16.2.1 -I

7. 后记至此，使用Open vSwitch进行VxLAN隧道实验完成。三个实验都比较基础，至于更高级的实验，以后再研究，比如在br1网桥上连接cirros虚拟机、添加tun/tap设备、添加veth设备等。
8. 书签Vxlan学习笔记——原理
最详细的Vlan原理介绍
SDN与OpenFlow技术简介
OpenStack网络实战系列一：通过Openvswitch实践了解交换机的基本概念和操作
Open vSwitch使用案例扩展实验
TAP 设备与 VETH 设备
Linux虚拟网络设备之tun/tap
Linux虚拟网络设备之veth
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——vxlan网络进阶</title>
    <url>/dev-ubuntu16-manual-openstack-vxlan-advance/</url>
    <content><![CDATA[1. 目标《Ubuntu16手动安装OpenStack——openvswitch》一文中，配置好了openvswitch作为虚拟交换机。
《Ubuntu16手动安装OpenStack——vxlan网络》一文中，使用linuxbridge虚拟交换机配置过vxlan网络模式。
因为linuxbridge换成了openvswitch，所以，本文就再来研究一下vxlan网络模式的配置。主要参考OpenStack Pike : Neutron Network (VXLAN)和openstack使用openvswitch实现vxlan的方法。如果要配置flat网络模式，参考OpenStack Pike : Neutron Network (FLAT)。


2. 安装配置2.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
2.2. 控制节点0、备份配置（可选操作）：cp /etc/neutron/plugins/ml2/ml2_conf.ini&#123;,.bak&#125;
1、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 130: add a value to tenant_network_typestenant_network_types = vxlan# line 181: add[ml2_type_flat]flat_networks = physnet1# line 235: add[ml2_type_vxlan]vni_ranges = 1:1000

2、重启neutron-serversystemctl restart neutron-server
2.3. 网络节点这里我们的网络节点和控制节点是同一个节点。
1、创建网桥br-eth2ovs-vsctl add-br br-eth2
2、把br-eth2连接到eth2ovs-vsctl add-port br-eth2 eth2
3、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 130: add a value to tenant_network_typestenant_network_types = vxlan# line 181: add[ml2_type_flat]flat_networks = physnet1# line 235: add[ml2_type_vxlan]vni_ranges = 1:1000

4、vi /etc/neutron/plugins/ml2/openvswitch_agent.ini，如下修改：
# line 117: add[agent]tunnel_types = vxlanl2_population = Trueprevent_arp_spoofing = True# line 195: add (specify IP address of this host for local_ip)[ovs]local_ip = 172.16.0.105bridge_mappings = physnet1:br-eth2

5、重启相关服务
for service in dhcp-agent l3-agent metadata-agent openvswitch-agent; dosystemctl restart neutron-$servicedone

2.4. 计算节点1、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 130: add a value to tenant_network_typestenant_network_types = vxlan# line 181: add[ml2_type_flat]flat_networks = physnet1# line 235: add[ml2_type_vxlan]vni_ranges = 1:1000

2、vi /etc/neutron/plugins/ml2/openvswitch_agent.ini，如下修改：
# line 117: add[agent]tunnel_types = vxlanl2_population = Trueprevent_arp_spoofing = True# line 195: add (specify IP address of this host for local_ip)[ovs]local_ip = 172.16.0.106

3、重启openvswitch-agentsystemctl restart neutron-openvswitch-agent
3. 使用在控制节点测试使用vxlan，实际上可以在任意节点使用。
3.1. 内核配置1、vim /etc/sysctl.conf，如下修改：
# line 19, uncomment and changenet.ipv4.conf.default.rp_filter=0net.ipv4.conf.all.rp_filter=0# line 28, uncommentnet.ipv4.ip_forward=1# end line, addnet.bridge.bridge-nf-call-iptables = 1net.bridge.bridge-nf-call-ip6tables = 1

2、使内核配置生效sysctl -p
3.2. 创建vxlan网络1、使admin环境生效. admin-openrc
2、创建路由router01openstack router create router01
3、创建内部网络int_netopenstack network create int_net --provider-network-type vxlan
4、在内部网络中创建子网
openstack subnet create subnet1 --network int_net \--subnet-range 192.168.100.0/24 --gateway 192.168.100.1 \--dns-nameserver 10.0.0.10

5、把内部网络int_net连接到路由router01上openstack router add subnet router01 subnet1
6、创建外部网络ext_net
openstack network create \--provider-physical-network physnet1 \--provider-network-type flat --external ext_net

7、在外部网络中创建子网subnet2
openstack subnet create subnet2 \--network ext_net --subnet-range 10.0.0.0/24 \--allocation-pool start=10.0.0.200,end=10.0.0.254 \--gateway 10.0.0.1 --dns-nameserver 10.0.0.10 --no-dhcp

8、把ext_net的网关设置为router01openstack router set router01 --external-gateway ext_net
此时，在horizon控制台看到的网络拓扑如下：
3.3. 授权网络默认情况下，所有项目都可以访问到外部网络，但对于内部网络，只有管理项目可以访问它。因此需要对其他项目进行授权，使项目中的用户可以使用内部网络。
1、查看rbac网络openstack network rbac list
2、查看rbac网络细节openstack network rbac show [rbac-id]
3、查看网络openstack network list
4、查看项目openstack project list
5、授权int_net给siat项目，权限为access_as_shared
netID=$(openstack network list | grep int_net | awk &#x27;&#123; print $2 &#125;&#x27;) prjID=$(openstack project list | grep siat | awk &#x27;&#123; print $2 &#125;&#x27;) openstack network rbac create --target-project $prjID --type network --action access_as_shared $netID 

3.4. 使用vxlan网络1、切换到siat项目的voidking用户环境. voidkingrc
2、查看实例模板、镜像、网络
openstack flavor listopenstack image list openstack network list


3、安全组和密钥使用《Ubuntu16手动安装OpenStack——创建实例》一文中创建的secgroup01和vkkey。
4、创建实例
netID=$(openstack network list | grep int_net | awk &#x27;&#123; print $2 &#125;&#x27;) openstack server create --flavor m1.tiny --image cirros --security-group secgroup01 --nic net-id=$netID --key-name vkkey cirros1

5、查看实例openstack server list
6、给实例添加浮动IP
openstack floating ip create ext_netopenstack server add floating ip cirros1 10.0.0.206

7、查看浮动IPopenstack floating ip show 10.0.0.206
8、再次查看实例openstack server list可以发现，cirros1已经有了两个IP地址。
9、测试连通ping 10.0.0.206，不通。
4. 网络调试4.1. 连接实例1、测试网关ping 10.0.0.1，不通。
2、将br-eth2的ip设置为10.0.0.1
ifconfig eth2 0.0.0.0 ifconfig br-eth2 10.0.0.1/24

3、测试连通ping 10.0.0.206，已经连通，nice。
4、ssh登录实例ssh cirros@10.0.0.206，默认密码为gocubsgo。
或者vnc登录实例openstack console url show cirros1查看到url后，在浏览器登录cirros1。
4.2. 连接外网参考Ubuntu16手动安装OpenStack——实例访问外网，配置实例连接外网。
1、在控制节点执行
iptables -I INPUT -i br-eth2 -j ACCEPTiptables -I INPUT -i eth2 -j ACCEPTiptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth1 -j SNAT --to 172.16.0.105

2、登录cirros1ssh cirros@10.0.0.206
2、测试访问ping 8.8.8.8 -c3
3、修改resolv.confsudo vi /etc/resolv.conf添加：
nameserver 180.76.76.76

4、测试访问ping www.baidu.com -c3
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——openvswitch</title>
    <url>/dev-ubuntu16-manual-openstack-openvswitch/</url>
    <content><![CDATA[1. 目标《Ubuntu16手动安装OpenStack——实例访问外网》一文中，已经配置好了实例访问外网。但是仍有不足，因为我们看不到更详细网络信息，排查问题不方便。
本文，我们把linuxbridge-agent更换为openvswitch-agent，主要参考OpenStack Pike : Configure Neutron、Neutron Configuration Options和Open vSwitch: Self-service networks。


2. openvswitch简介以下简介摘自openstack底层技术-使用openvswitch。
在过去，数据中心的服务器是直接连在硬件交换机上，后来VMware实现了服务器虚拟化技术，使虚拟服务器(VMs)能够连接在虚拟交换机上。借助这个虚拟交换机，可以为服务器上运行的VMs或容器提供逻辑的虚拟的以太网接口，这些逻辑接口都连接到虚拟交换机上。有三种比较流行的虚拟交换机：VMware virtual switch，Cisco Nexus 1000V和Open vSwitch。
Open vSwitch(OVS)是运行在虚拟化平台上的虚拟交换机，其支持OpenFlow协议，也支持gre/vxlan/IPsec等隧道技术。在OVS之前，基于Linux的虚拟化平台比如KVM或Xen上，缺少一个功能丰富的虚拟交换机，因此OVS迅速崛起并开始在Xen/KVM中流行起来，并且应用于越来越多的开源项目，比如openstack neutron中的网络解决方案。
在虚拟交换机的Flow控制器或管理工具方面，一些商业产品都集成有控制器或管理工具，比如Cisco 1000V的Virtual Supervisor Manager(VSM)，VMware的分布式交换机中的vCenter。而OVS则需要借助第三方控制器或管理工具实现复杂的转发策略。例如OVS支持OpenFlow协议，我们就可以使用任何支持OpenFlow协议的控制器来对OVS进行远程管理。OpenStack Neutron中的ML2插件也能够实现对OVS的管理。但这并不意味着OVS必须要有一个控制器才能工作。在不连接外部控制器情况下，OVS自身可以依靠MAC地址学习实现二层数据包转发功能，就像Linux Bridge。
在基于Linux内核的系统上，应用最广泛的还是系统自带的虚拟交换机Linux Bridge，它是一个单纯的基于MAC地址学习的二层交换机，简单高效，但同时缺乏一些高级特性，比如OpenFlow、VLAN tag、QOS、ACL、Flow等，而且在隧道协议支持上，Linux Bridge只支持vxlan，OVS支持gre/vxlan/IPsec等，这也决定了OVS更适用于实现SDN技术。
3. 控制节点3.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
3.2. 卸载linuxbridge在安装OVS之前，我们先删除实例和网络，卸载掉linuxbridge。
1、使admin环境生效. admin-openrc
2、查看当前网络组件openstack network agent list
3、在dashboard使用admin账户登录，依次删除实例、路由和网络。
4、卸载linuxbridge
systemctl disable neutron-linuxbridge-agentsystemctl stop    neutron-linuxbridge-agentapt remove -y neutron-plugin-linuxbridge-agent

5、查看当前网络组件openstack network agent list此时，controller节点上的Linux bridge agent已经从笑脸变成了XXX。
6、从数据库删除Linux bridge agent
Bridge=`openstack network agent list | grep &#x27;Linux bridge agent&#x27;|awk &#x27;&#123;print $2&#125;&#x27;`echo $Bridgeneutron agent-delete $Bridgeopenstack network agent list

7、重新创建neutron数据库mysql -uroot -p，密码为openstack。
drop database neutron;create database neutron;

8、参考Ubuntu16手动安装OpenStack——neutron篇，创建好证书和服务端点。
3.3. 安装配置1、安装相关组件
apt-get -y install neutron-server neutron-metadata-agent neutron-plugin-ml2 python-neutronclient

2、备份neutron.confmv /etc/neutron/neutron.conf /etc/neutron/neutron.conf.bak
3、vim /etc/neutron/neutron.conf，新建neutron.conf内容为：
[DEFAULT]core_plugin = ml2service_plugins = routerauth_strategy = keystonestate_path = /var/lib/neutrondhcp_agent_notification = Trueallow_overlapping_ips = Truenotify_nova_on_port_status_changes = Truenotify_nova_on_port_data_changes = True# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller[agent]root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = neutronpassword = openstack# MariaDB connection info[database]connection = mysql+pymysql://neutron:openstack@controller/neutron# Nova connection info[nova]auth_url = http://controller:5000auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultregion_name = RegionOneproject_name = serviceusername = novapassword = openstack[oslo_concurrency]lock_path = $state_path/tmp

4、更改权限chmod 640 /etc/neutron/neutron.conf
chgrp neutron /etc/neutron/neutron.conf
5、vi /etc/neutron/metadata_agent.ini，如下修改：
# line 22: uncomment and specify Nova API servernova_metadata_host = controller# line 34: uncomment and specify any secret key you likemetadata_proxy_shared_secret = openstack# line 260: uncomment and specify Memcache Servermemcache_servers = controller:11211

6、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 129: add ( it&#x27;s OK with no value for &quot;tenant_network_types&quot; (set later if need) )[ml2]type_drivers = flat,vlan,gre,vxlantenant_network_types =mechanism_drivers = openvswitch,l2populationextension_drivers = port_security# line 262: uncomment and addenable_security_group = Truefirewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver# end line: uncommentenable_ipset = True

7、vi /etc/nova/nova.conf，如下修改：
# add follows into [DEFAULT] sectionuse_neutron = Truelinuxnet_interface_driver = nova.network.linux_net.LinuxOVSInterfaceDriverfirewall_driver = nova.virt.firewall.NoopFirewallDrivervif_plugging_is_fatal = Truevif_plugging_timeout = 300# add follows to the end : Neutron auth info# the value of metadata_proxy_shared_secret is the same with the one in metadata_agent.ini[neutron]auth_url = http://controller:5000auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultregion_name = RegionOneproject_name = serviceusername = neutronpassword = openstackservice_metadata_proxy = Truemetadata_proxy_shared_secret = openstack

8、创建链接ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini
9、生成数据库数据
su -s /bin/bash neutron -c &quot;neutron-db-manage --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugin.ini upgrade head&quot;

10、重启neutron服务并设置开机启动
systemctl start neutron-server neutron-metadata-agent systemctl enable neutron-server neutron-metadata-agent 

11、重启nova-apisystemctl restart nova-api
4. 网络节点本文中，控制节点和网络节点是同一个节点。
1、安装相关组件
apt-get -y install neutron-plugin-ml2 neutron-plugin-openvswitch-agent neutron-l3-agent neutron-dhcp-agent neutron-metadata-agent python-neutronclient

2、备份neutron.confmv /etc/neutron/neutron.conf /etc/neutron/neutron.conf.bak
3、vim /etc/neutron/neutron.conf，新建neutron.conf如下：
[DEFAULT]core_plugin = ml2service_plugins = routerauth_strategy = keystonestate_path = /var/lib/neutronallow_overlapping_ips = True# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller[agent]root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = neutronpassword = openstack[oslo_concurrency]lock_path = $state_path/lock

4、修改权限chmod 640 /etc/neutron/neutron.conf
chgrp neutron /etc/neutron/neutron.conf
5、vi /etc/neutron/l3_agent.ini，如下修改：
# line 17: addinterface_driver = neutron.agent.linux.interface.OVSInterfaceDriver# line 100: uncomment and changeexternal_network_bridge = br-eth2

6、vi /etc/neutron/dhcp_agent.ini，如下修改：
# line 17: addinterface_driver = neutron.agent.linux.interface.OVSInterfaceDriver# line 28: uncommentdhcp_driver = neutron.agent.linux.dhcp.Dnsmasq# line 37: uncomment and changeenable_isolated_metadata = true

7、vi /etc/neutron/metadata_agent.ini，如下修改：
# line 22: uncomment and specify Nova API servernova_metadata_host = controller# line 34: uncomment and specify any secret key you likemetadata_proxy_shared_secret = openstack# line 260: uncomment and specify Memcache Servermemcache_servers = controller:11211

8、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 129: add ( it&#x27;s OK with no value for &quot;tenant_network_types&quot; (set later if need) )[ml2]type_drivers = flat,vlan,gre,vxlantenant_network_types =mechanism_drivers = openvswitch,l2populationextension_drivers = port_security# line 262: uncomment and addenable_security_group = Truefirewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver# end line: uncommentenable_ipset = True

9、创建链接ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini
10、重启openvswitch并设置开机启动
systemctl restart openvswitch-switch systemctl enable openvswitch-switch 

11、创建网桥br-intovs-vsctl add-br br-int
12、重启相关服务并设置开机启动
for service in dhcp-agent l3-agent metadata-agent openvswitch-agent; dosystemctl start neutron-$servicesystemctl enable neutron-$servicedone 

13、在控制节点查看neutron服务
. admin-openrcopenstack network agent list

5. 计算节点5.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
5.2. 卸载linuxbridge1、卸载linuxbridge
systemctl disable neutron-linuxbridge-agentsystemctl stop    neutron-linuxbridge-agentapt remove -y neutron-plugin-linuxbridge-agent

2、控制节点查看当前网络组件openstack network agent list
3、从数据库删除Linux bridge agent
Bridge=`openstack network agent list | grep &#x27;Linux bridge agent&#x27;|awk &#x27;&#123;print $2&#125;&#x27;`echo $Bridgeneutron agent-delete $Bridgeopenstack network agent list

5.3. 安装配置1、安装相关组件
apt-get -y install neutron-common neutron-plugin-ml2 neutron-plugin-openvswitch-agent

2、备份neutron.confmv /etc/neutron/neutron.conf /etc/neutron/neutron.conf.bak
3、vim /etc/neutron/neutron.conf，新建neutron.conf如下：
[DEFAULT]core_plugin = ml2service_plugins = routerauth_strategy = keystonestate_path = /var/lib/neutronallow_overlapping_ips = True# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller[agent]root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = neutronpassword = openstack[oslo_concurrency]lock_path = $state_path/lock

4、修改权限chmod 640 /etc/neutron/neutron.conf
chgrp neutron /etc/neutron/neutron.conf
5、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 129: add ( it&#x27;s OK with no value for &quot;tenant_network_types&quot; (set later if need) )[ml2]type_drivers = flat,vlan,gre,vxlantenant_network_types =mechanism_drivers = openvswitch,l2populationextension_drivers = port_security# line 262: uncomment and addenable_security_group = Truefirewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver# end line: uncommentenable_ipset = True

6、vi /etc/nova/nova.conf，如下修改：
# add follows into [DEFAULT] sectionuse_neutron = Truelinuxnet_interface_driver = nova.network.linux_net.LinuxOVSInterfaceDriverfirewall_driver = nova.virt.firewall.NoopFirewallDrivervif_plugging_is_fatal = Truevif_plugging_timeout = 300# add follows to the end : Neutron auth info# the value of metadata_proxy_shared_secret is the same with the one in metadata_agent.ini[neutron]auth_url = http://controller:5000auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultregion_name = RegionOneproject_name = serviceusername = neutronpassword = openstackservice_metadata_proxy = Truemetadata_proxy_shared_secret = openstack

7、创建链接ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini
8、重启openvswitch并设置开机启动
systemctl restart openvswitch-switch systemctl enable openvswitch-switch 

9、创建网桥br-intovs-vsctl add-br br-int
10、重启nova-compute服务systemctl restart nova-compute
11、重启neutron-openvswitch-agent并设置开机启动
systemctl restart neutron-openvswitch-agent systemctl enable neutron-openvswitch-agent 

13、在控制节点查看neutron服务
. admin-openrcopenstack network agent list
可以看到，新添加了compute节点的Open vSwitch agent。
至此，openvswitch安装配置完成。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——实例访问外网</title>
    <url>/dev-ubuntu16-manual-openstack-internet/</url>
    <content><![CDATA[1. 问题《Ubuntu16手动安装OpenStack——vxlan网络》一文中，已经配置好了vxlan网络，并且成功创建了cirros1实例。
但是，进入实例后，发现一个问题：实例无法连通外网。这个问题我们在《Ubuntu16手动安装OpenStack——horizon篇》一文中就遇到过，也找到了解决办法。如今更换了网络模式，我们再来研究一下实例访问外网的问题。


2. 解决办法2.1. traceroute1、登录cirros1ssh cirros@10.0.0.201
2、路由跟踪traceroute 8.8.8.8从上图可以看出，从虚拟机发出的包，依次经过了192.168.100.1和10.0.0.1，然后就出不去了。这说明，问题出在网桥（10.0.0.1）和外网的连接。
2.2. snat参照《Linux配置SNAT上网》和《Ubuntu16手动安装OpenStack——horizon篇》，对控制节点网络进行设置。
1、接受虚拟机数据包iptables -I INPUT -i eth2 -j ACCEPT
2、开启路由功能echo 1 &gt; /proc/sys/net/ipv4/ip_forward
3、伪装数据包iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth1 -j SNAT --to 172.16.0.105
2.3. 测试1、登录cirros1ssh cirros@10.0.0.201
2、测试访问ping 8.8.8.8 -c3
3、修改resolv.confsudo vi /etc/resolv.conf添加：
nameserver 180.76.76.76

4、测试访问ping www.baidu.com -c3
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——vxlan网络</title>
    <url>/dev-ubuntu16-manual-openstack-vxlan/</url>
    <content><![CDATA[1. 目标完成了《Ubuntu16手动安装OpenStack——swift篇》，openstack的核心组件就全部安装好了。 
之前的配置中，使用了简单的flatdhcp网络模式配置。本文，我们来把网络模式修改为vxlan，主要参考OpenStack Queens : Neutron Network (VXLAN)。


2. 网络模式参考OpenStack 网络：Neutron 初探和深入理解 Neutron – OpenStack 网络实现，openstack的网络模式可以分为五种，分别是Flat、FlatDHCP、VLAN、GRE和VxLAN。
2.1. FlatFlat模式使用一个网桥实现OpenStack的网络，由于所有虚拟机都属于同一个网段，所以称之为扁平化flat。缺点是虚拟机的IP需要手动注入，而且只支持linux操作系统。
在 Flat 模式下工作流程如下：
（1）为所有租户创建一个 IP 池（2）创建租户（3）租户创建虚拟机，为虚拟机分配 IP 池中的可用 IP
2.2. FlatDHCPFlatDHCP 与 Flat 的不同在于有一个 DHCP 进程，虚拟机启动时会发送 dhcpdiscover 以获取 IP 地址。
2.3. VLANVLAN（Virtual Local Area Network）的中文名为”虚拟局域网”。VLAN 是一种将局域网设备从逻辑上划分成一个个网段，从而实现虚拟工作组的新兴数据交换技术。
在 VLAN 模式下工作流程如下：
（1）创建新的租户，并记下租户的标识（2）为该租户创建独占的私有IP段（3）租户创建虚拟机，从租户的私有IP段内分配IP给虚拟机
与 Flat 模式相比，VLAN 模式为网络增加了：将网络与租户关联和为网络分配一个 VLAN 号。
更多内容参考Neutron Vlan Network 原理- 每天5分钟玩转 OpenStack（92）。
2.4. GREGRE（General Routing Encapsulation）是点对点的IP隧道技术，可以用于虚拟网络互连。GRE和VLAN相比，最大的不同在于VLAN的tag转换方式。
在VLAN模式下，虚拟机集群内部网络与外部网络通信时，内部网络的int-VLAN:tag会转换为外部网络的ex-VLAN:tag；外部网络与虚拟机内部网络通信时，外部网络的ex-VLAN:tag会转换为内部网络的int-VLAN:tag。
而GRE模式下，虚拟机集群内部网络与外部网络通信时，内部网络的int-VLAN:tag会转换为外部网络的tunnul:tag；外部网络与虚拟机内部网络通信时，外部网络的tunnel:tag会转换为内部网络的int-VLAN:tag。

2.5. VxLANVxLAN（Virtual Extensible LAN）一般认为是 VLAN 技术的延伸或替代者。相较于采用物理VLAN实现的网络虚拟化，VxLAN是UDP隧道，可以穿越IP网络，使得两个VLAN可以实现二层联通，并且突破4095的VLAN ID限制，提供多达1600万的虚拟网络容量。
VxLAN 模式下，网络的架构跟 GRE 模式类似，所不同的是，不同节点之间通过 VxLAN 隧道互通，即虚拟化层是采用的 VxLAN 协议。
3. VxLAN配置3.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
3.2. 控制节点1、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 130: add a value to tenant_network_typestenant_network_types = vxlan# line 181: add[ml2_type_flat]flat_networks = physnet1# line 235: add[ml2_type_vxlan]vni_ranges = 1:1000

2、重启neutron-serversystemctl restart neutron-server
3.3. 网络节点这里我们的网络节点和控制节点是同一个节点。
1、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 130: add a value to tenant_network_typestenant_network_types = vxlan# line 181: add[ml2_type_flat]flat_networks = physnet1# line 235: add[ml2_type_vxlan]vni_ranges = 1:1000

2、vi /etc/neutron/plugins/ml2/linuxbridge_agent.ini，如下修改：
# line 118: add[agent]prevent_arp_spoofing = True# line 147: add[linux_bridge]physical_interface_mappings = physnet1:eth2# line 201: add[vxlan]enable_vxlan = Truel2_population = True

3、vi /etc/neutron/dhcp_agent.ini，如下修改：
# line 63: adddnsmasq_config_file = /etc/neutron/dnsmasq-neutron.conf

4、vi /etc/neutron/dnsmasq-neutron.conf，新建dnsmasq-neutron.conf内容如下：
# create newdhcp-option-force=26,1450

5、重启服务
for service in l3-agent dhcp-agent metadata-agent linuxbridge-agent; dosystemctl restart neutron-$servicedone 

3.4. 计算节点1、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 130: add a value to tenant_network_typestenant_network_types = vxlan# line 181: add[ml2_type_flat]flat_networks = physnet1# line 235: add[ml2_type_vxlan]vni_ranges = 1:1000

2、vi /etc/neutron/plugins/ml2/linuxbridge_agent.ini，如下修改：
# line 118: add[agent]prevent_arp_spoofing = True# line 201: add[vxlan]enable_vxlan = Truel2_population = True

3、重启neutron-linuxbridge-agentsystemctl restart neutron-linuxbridge-agent
4. 使用4.1. 清除flat网络《Ubuntu16手动安装OpenStack——创建实例》一文中，已经使用flat网络创建了sharednet1，并在网络中创建了一个实例cirros0。在使用新的网络之前，删除它们。1、使admin环境生效. admin-openrc
2、查看实例openstack server list
3、删除实例openstack server delete cirros0
4、查看网络openstack network list
5、删除sharednet1openstack network delete sharednet1
4.2. 创建vxlan网络在控制节点测试使用vxlan，实际上可以在任意节点使用。
1、使admin环境生效. admin-openrc
2、创建路由router01openstack router create router01
3、创建内部网络int_netopenstack network create int_net --provider-network-type vxlan
4、在内部网络中创建子网
openstack subnet create subnet1 --network int_net \--subnet-range 192.168.100.0/24 --gateway 192.168.100.1 \--dns-nameserver 10.0.0.10

5、把内部网络int_net连接到路由router01上openstack router add subnet router01 subnet1
6、创建外部网络ext_net
openstack network create \--provider-physical-network physnet1 \--provider-network-type flat --external ext_net

7、在外部网络中创建子网subnet2
openstack subnet create subnet2 \--network ext_net --subnet-range 10.0.0.0/24 \--allocation-pool start=10.0.0.200,end=10.0.0.254 \--gateway 10.0.0.1 --dns-nameserver 10.0.0.10 --no-dhcp

8、把ext_net的网关设置为router01openstack router set router01 --external-gateway ext_net
此时，在horizon控制台看到的网络拓扑如下：
4.3. 授权网络默认情况下，所有项目都可以访问到外部网络，但对于内部网络，只有管理项目可以访问它。因此需要对其他项目进行授权，使项目中的用户可以使用内部网络。
1、查看rbac网络openstack network rbac list
2、查看rbac网络细节openstack network rbac show a5c4db79-f467-4b58-892b-b9cc56c3e317
3、查看网络openstack network list
4、查看项目openstack project list
5、授权int_net给siat项目，权限为access_as_shared
netID=$(openstack network list | grep int_net | awk &#x27;&#123; print $2 &#125;&#x27;) prjID=$(openstack project list | grep siat | awk &#x27;&#123; print $2 &#125;&#x27;) openstack network rbac create --target-project $prjID --type network --action access_as_shared $netID 

4.4. 使用vxlan网络1、切换到siat项目的voidking用户环境. voidkingrc
2、查看实例模板、镜像、网络
openstack flavor listopenstack image list openstack network list


3、安全组和密钥使用《Ubuntu16手动安装OpenStack——创建实例》一文中创建的secgroup01和vkkey。
4、创建实例
netID=$(openstack network list | grep int_net | awk &#x27;&#123; print $2 &#125;&#x27;) openstack server create --flavor m1.tiny --image cirros --security-group secgroup01 --nic net-id=$netID --key-name vkkey cirros1

5、查看实例openstack server list
6、给实例添加浮动IP
openstack floating ip create ext_netopenstack server add floating ip cirros1 10.0.0.201

7、查看浮动IPopenstack floating ip show 10.0.0.201
8、再次查看实例openstack server list可以发现，cirros1已经有了两个IP地址。
9、测试连通
ping 10.0.0.201ssh cirros@10.0.0.201

顺利登录，nice。
PS：ping 192.168.100.6是不通的，因为那是openstack内部网络。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——swift篇</title>
    <url>/dev-ubuntu16-manual-openstack-swift/</url>
    <content><![CDATA[1. 目标紧接着《Ubuntu16手动安装OpenStack——cinder篇》，本文，我们来研究一下swift的安装配置。主要参考OpenStack Queens : Configure Swift 和 Object Storage Install Guide。
cinder和swift都是关于存储的组件，那么它们有什么区别呢？cinder的存储，就像是普通硬盘。可以方便快捷地本地存取修改数据，还可以按需添加减少硬盘。swift的存储，就像是百度云盘。只能存取数据，不能在云端进行修改，适用于备份。如果非要修改，那么需要下载到本地修改，修改完再上传。在openstack实际应用中，swift常用来存储镜像这种不需要经常修改的文件。


2. swift简介OpenStack Object Storage 是一个多租户对象存储系统。它具有高度可扩展性，可以通过RESTful HTTP API以低成本管理大量非结构化数据。
代理服务器（swift-proxy-server）接受OpenStack Object Storage API和原始HTTP请求，上传文件，修改元数据和创建容器。它还为Web浏览器提供文件或容器列表。为了提高性能，代理服务器可以使用通常使用memcache部署的可选缓存。
帐户服务器（swift-account-server）管理使用对象存储定义的帐户。
容器服务器（swift-container-server）在对象存储中管理容器或文件夹的映射。
对象服务器（swift-object-server）管理存储节点上的实际对象，例如文件。
Various periodic processes在大型数据存储上执行内务处理任务。复制服务可确保群集的一致性和可用性。其他定期流程包括审计员，更新者和收割者。
WSGI中间件处理身份验证，通常是OpenStack Identity。
swift client允许用户通过命令行客户端进行授权，然后为admin用户、经销商用户或swift用户向REST API提交命令。
swift-init初始化环文件构建的脚本，将守护程序名称作为参数并提供命令。详细文档在这里。
swift-recon一种客户端工具，用于检索由swift-recon中间件收集的有关群集的各种指标和遥测信息。
swift-ring-builder存储环构建和重新平衡实用程序。详细文档在这里。
3. 架构一般来说，swift至少需要三个存储节点，如下结构。
之前的安装中，我们只使用了两个节点，一个控制节点，一个计算节点。本文中，我们把swift安装在控制节点和计算节点，领会精神。如果要增加存储节点，修改Ring files，然后参照计算节点配置即可。
控制节点安装：
Swift ProxySwift-AccountSwift-ContainerSwift-Object

计算节点安装：
Swift ProxySwift-AccountSwift-ContainerSwift-Object

4. 控制节点4.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
4.2. 服务证书和端点1、使admin环境生效. admin-openrc
2、添加swift用户openstack user create --domain default --project service --password openstack swift
3、把admin角色添加给swift用户openstack role add --project service --user swift admin
4、创建swift服务openstack service create --name swift --description &quot;OpenStack Object Storage&quot; object-store
5、创建服务端点
openstack endpoint create --region RegionOne object-store public http://controller:8080/v1/AUTH_%\(tenant_id\)sopenstack endpoint create --region RegionOne object-store internal http://controller:8080/v1/AUTH_%\(tenant_id\)s openstack endpoint create --region RegionOne object-store admin http://controller:8080/v1 

4.3. 安装配置Swift Proxy1、安装swift-proxyapt-get -y install swift swift-proxy python-swiftclient python-keystonemiddleware python-memcache
2、创建swift配置目录mkdir /etc/swift
3、vi /etc/swift/proxy-server.conf，新建proxy-server.conf内容如下：
# create new[DEFAULT]bind_ip = 0.0.0.0bind_port = 8080user = swift[pipeline:main]pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync bulk ratelimit authtoken keystoneauth container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server[app:proxy-server]use = egg:swift#proxyallow_account_management = trueaccount_autocreate = true# Keystone auth info[filter:authtoken]paste.filter_factory = keystonemiddleware.auth_token:filter_factorywww_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = swiftpassword = openstackdelay_auth_decision = true[filter:keystoneauth]use = egg:swift#keystoneauthoperator_roles = admin,SwiftOperator[filter:healthcheck]use = egg:swift#healthcheck[filter:cache]use = egg:swift#memcachememcache_servers = controller:11211[filter:ratelimit]use = egg:swift#ratelimit[filter:domain_remap]use = egg:swift#domain_remap[filter:catch_errors]use = egg:swift#catch_errors[filter:cname_lookup]use = egg:swift#cname_lookup[filter:staticweb]use = egg:swift#staticweb[filter:tempurl]use = egg:swift#tempurl[filter:formpost]use = egg:swift#formpost[filter:name_check]use = egg:swift#name_check[filter:list-endpoints]use = egg:swift#list_endpoints[filter:proxy-logging]use = egg:swift#proxy_logging[filter:bulk]use = egg:swift#bulk[filter:slo]use = egg:swift#slo[filter:dlo]use = egg:swift#dlo[filter:container-quotas]use = egg:swift#container_quotas[filter:account-quotas]use = egg:swift#account_quotas[filter:gatekeeper]use = egg:swift#gatekeeper[filter:container_sync]use = egg:swift#container_sync[filter:xprofile]use = egg:swift#xprofile[filter:versioned_writes]use = egg:swift#versioned_writes

4、vi /etc/swift/swift.conf，新建swift.conf内容为：
# create new (it is shared among Swift Nodes - any words you like)[swift-hash]swift_hash_path_suffix = swift_shared_pathswift_hash_path_prefix = swift_shared_path

5、修改权限chown -R swift. /etc/swift
4.4. 配置Ring files1、创建rings
swift-ring-builder /etc/swift/account.builder create 12 2 1 swift-ring-builder /etc/swift/container.builder create 12 2 1 swift-ring-builder /etc/swift/object.builder create 12 2 1

12、2、1这三个参数，分别代表part_power、replicas、min_part_hours。part_power是以2为底数的指数的幂，即2^ part_power，所得的值表示总共的partition的数目。如果设置为2，则2^2=4，表示总共有4个partition。
replicas表示每个object在swift中储存的数目。
min_part_hours表示一个partiton能够再次更改的最小时间。防止没有进行同步便进行下次更改。
2、添加存储节点
swift-ring-builder /etc/swift/account.builder add r0z0-172.16.0.105:6002/device0 100swift-ring-builder /etc/swift/container.builder add r0z0-172.16.0.105:6001/device0 100 swift-ring-builder /etc/swift/object.builder add r0z0-172.16.0.105:6000/device0 100swift-ring-builder /etc/swift/account.builder add r1z1-172.16.0.106:6002/device1 100 swift-ring-builder /etc/swift/container.builder add r1z1-172.16.0.106:6001/device1 100 swift-ring-builder /etc/swift/object.builder add r1z1-172.16.0.106:6000/device1 100 

3、平衡存储
swift-ring-builder /etc/swift/account.builder rebalanceswift-ring-builder /etc/swift/container.builder rebalance swift-ring-builder /etc/swift/object.builder rebalance

4、更改权限chown swift. /etc/swift/*.gz
5、重启swift-proxysystemctl restart swift-proxy
5. 计算节点5.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
5.2. 磁盘分区1、《Ubuntu16手动安装OpenStack——cinder篇》一文中，因为计算节点（存储节点）的sdb用于cinder服务了，所以我们需要在计算节点再添加一块硬盘sdc，用于swift服务。
2、查看硬盘fdisk -l
3、对sdc进行分区fdisk /dev/sdc

输入 m 显示所有命令提示。
输入 p 显示硬盘分割情形。
输入 a 设定硬盘启动区。
输入 n 设定新的硬盘分割区。输入 e 硬盘为[延伸]分割区(extend)，输入 p 硬盘为[主要]分割区(primary)。
输入 t 改变硬盘分割区属性。
输入 d 删除硬盘分割区属性。
输入 q 结束不存入硬盘分割区属性。
输入 w 结束并写入硬盘分割区属性。

4、命令行输入n，添加一个新的分区；接下来选择p，设置一个主分区（e为扩展分区）；接下来分区号选择1；再接下来设置起止扇区号，选择默认值即可（2048到max）；最后输入w，确认写入硬盘分区。
5.3. 安装配置1、下载安装相关组件apt-get -y install swift swift-account swift-container swift-object xfsprogs
2、使用xfs格式化空闲磁盘mkfs.xfs -i size=1024 -s size=4096 /dev/sdc1
3、创建device1文件夹mkdir -p /srv/node/device1
4、挂载/dev/sdc1到device1mount -o noatime,nodiratime,nobarrier /dev/sdc1 /srv/node/device1
5、修改权限chown -R swift. /srv/node
6、vi /etc/fstab，设置开机挂载
# add to the end/dev/sdc1  /srv/node/device1  xfs  noatime,nodiratime,nobarrier 0 0

7、从控制节点（swift-proxy）拷贝文件到计算节点scp voidking@172.16.0.105:/etc/swift/*.gz /etc/swift/
8、修改权限chown swift. /etc/swift/*.gz
9、vi /etc/swift/swift.conf，新建swift.conf内容如下：
# set the value which is set on Proxy Node[swift-hash]swift_hash_path_suffix = swift_shared_pathswift_hash_path_prefix = swift_shared_path

10、vi /etc/swift/account-server.conf，如下修改：
# line 2: make sure the value is followsbind_ip = 0.0.0.0bind_port = 6002

11、vi /etc/swift/container-server.conf，如下修改：
# line 2: make sure the value is followsbind_ip = 0.0.0.0bind_port = 6001

12、vi /etc/swift/object-server.conf，如下修改：
# line 2: make sure the value is followsbind_ip = 0.0.0.0bind_port = 6000

13、vi /etc/rsyncd.conf，新建rsyncd.conf内容如下：
# create newpid file = /var/run/rsyncd.pidlog file = /var/log/rsyncd.loguid = swiftgid = swift# IP address of this Nodeaddress = 172.16.0.106[account]path            = /srv/noderead only       = falsewrite only      = nolist            = yesincoming chmod  = 0644outgoing chmod  = 0644max connections = 25lock file =     /var/lock/account.lock[container]path            = /srv/noderead only       = falsewrite only      = nolist            = yesincoming chmod  = 0644outgoing chmod  = 0644max connections = 25lock file =     /var/lock/container.lock[object]path            = /srv/noderead only       = falsewrite only      = nolist            = yesincoming chmod  = 0644outgoing chmod  = 0644max connections = 25lock file =     /var/lock/object.lock[swift_server]path            = /etc/swiftread only       = truewrite only      = nolist            = yesincoming chmod  = 0644outgoing chmod  = 0644max connections = 5lock file =     /var/lock/swift_server.lock

14、vi /etc/default/rsync，如下修改：
# line 8: changeRSYNC_ENABLE=true

15、重启rsync并设置开机启动systemctl restart rsync
systemctl enable rsync
16、重启其他swift服务并设置开机启动
for ringtype in account container object; do     systemctl restart swift-$ringtype    systemctl enable swift-$ringtype    for service in replicator updater auditor; do        if [ $ringtype != &#x27;account&#x27; ] || [ $service != &#x27;updater&#x27; ]; then            systemctl start swift-$ringtype-$service            systemctl enable swift-$ringtype-$service        fi    donedone

至此，一个存储节点配置成功，下面再配置一个swift存储节点在控制节点上。
6. 控制节点6.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
6.2. 磁盘分区1、在控制节点再添加一块硬盘sdb，用于swift服务。
2、参考计算节点的分区方法，给sdb分区。
6.3. 安装配置1、下载安装相关组件apt-get -y install swift swift-account swift-container swift-object xfsprogs
2、使用xfs格式化空闲磁盘mkfs.xfs -i size=1024 -s size=4096 /dev/sdb1
3、创建device0文件夹mkdir -p /srv/node/device0
4、挂载/dev/sdc1到device1mount -o noatime,nodiratime,nobarrier /dev/sdb1 /srv/node/device0
5、修改权限chown -R swift. /srv/node
6、vi /etc/fstab，设置开机挂载
# add to the end/dev/sdb1  /srv/node/device0  xfs  noatime,nodiratime,nobarrier 0 0

7、修改权限chown swift. /etc/swift/*.gz
8、vi /etc/swift/account-server.conf，如下修改：
# line 2: make sure the value is followsbind_ip = 0.0.0.0bind_port = 6002

9、vi /etc/swift/container-server.conf，如下修改：
# line 2: make sure the value is followsbind_ip = 0.0.0.0bind_port = 6001

10、vi /etc/swift/object-server.conf，如下修改：
# line 2: make sure the value is followsbind_ip = 0.0.0.0bind_port = 6000

11、vi /etc/rsyncd.conf，新建rsyncd.conf内容如下：
# create newpid file = /var/run/rsyncd.pidlog file = /var/log/rsyncd.loguid = swiftgid = swift# IP address of this Nodeaddress = 172.16.0.105[account]path            = /srv/noderead only       = falsewrite only      = nolist            = yesincoming chmod  = 0644outgoing chmod  = 0644max connections = 25lock file =     /var/lock/account.lock[container]path            = /srv/noderead only       = falsewrite only      = nolist            = yesincoming chmod  = 0644outgoing chmod  = 0644max connections = 25lock file =     /var/lock/container.lock[object]path            = /srv/noderead only       = falsewrite only      = nolist            = yesincoming chmod  = 0644outgoing chmod  = 0644max connections = 25lock file =     /var/lock/object.lock[swift_server]path            = /etc/swiftread only       = truewrite only      = nolist            = yesincoming chmod  = 0644outgoing chmod  = 0644max connections = 5lock file =     /var/lock/swift_server.lock

12、vi /etc/default/rsync，如下修改：
# line 8: changeRSYNC_ENABLE=true

13、重启rsync并设置开机启动systemctl restart rsync
systemctl enable rsync
14、重启其他swift服务并设置开机启动
for ringtype in account container object; do     systemctl restart swift-$ringtype    systemctl enable swift-$ringtype    for service in replicator updater auditor; do        if [ $ringtype != &#x27;account&#x27; ] || [ $service != &#x27;updater&#x27; ]; then            systemctl start swift-$ringtype-$service            systemctl enable swift-$ringtype-$service        fi    donedone

至此，控制节点的swift存储也配置完成。
7. 使用以下操作在控制节点执行。
7.1. 服务角色和用户1、使admin环境生效. admin-openrc
2、创建一个swiftservice服务openstack project create --domain default --description &quot;Swift Service Project&quot; swiftservice
3、创建SwiftOperator角色openstack role create SwiftOperator
4、创建user01用户，密码为openstackopenstack user create --domain default --project swiftservice --password openstack user01
5、给user01添加SwiftOperator角色openstack role add --project swiftservice --user user01 SwiftOperator
7.2. 客户端1、安装swift-clientapt-get -y install python-openstackclient python-keystoneclient python-swiftclient
2、vi ~/swift-openrc，创建环境配置文件swift-openrc，内容为：
export OS_PROJECT_DOMAIN_NAME=defaultexport OS_USER_DOMAIN_NAME=defaultexport OS_PROJECT_NAME=swiftserviceexport OS_USERNAME=user01export OS_PASSWORD=openstackexport OS_AUTH_URL=http://controller:5000/v3export OS_IDENTITY_API_VERSION=3export PS1=&#x27;[\u@\h \W(swift)]\$ &#x27;

3、使环境生效. swift-openrc
4、查看swift状态swift stat
5、创建一个containeropenstack container create test_container
openstack container list
6、vim test.txt，新建test.txt，内容为“hello openstack”。
7、上传test.txt文件到test_containeropenstack object create test_container test.txt
8、查看test_container中的文件openstack object list test_container
9、下载test.txt文件
rm test.txtllopenstack object save test_container test.txt ll

10、删除test_container中的test.txt文件
openstack object list test_containeropenstack object delete test_container test.txtopenstack object list test_container

11、删除test_container
openstack container list openstack container delete test_container openstack container list 

]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——Cinder篇</title>
    <url>/dev-ubuntu16-manual-openstack-cinder/</url>
    <content><![CDATA[1. 目标在《Ubuntu16手动安装OpenStack——修改镜像》一文中，我们通过修改镜像，实现了ssh密码访问实例。看上去，这个openstack系统已经可以投入使用了，实际上，也确实可以投入使用了。
但是，我们总是追求更好更多的服务。所以接下来，我们继续安装openstack的服务组件。本文中要安装配置的是cinder，主要参考OpenStack Queens : Configure Cinder和Cinder Installation Guide。


2. cinder简介块存储服务（cinder）为用户实例提供块存储设备。配置和使用存储的方法由块存储驱动程序确定，或者在多后端配置的情况下由驱动程序确定。有多种驱动程序可用：NAS / SAN，NFS，iSCSI，Ceph等。Block Storage API和调度程序服务通常在控制器节点上运行。根据所使用的驱动程序，卷服务可以在控制器节点，计算节点或独立存储节点上运行。
3. 控制节点3.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
3.2. 数据库1、登录数据库mysql -uroot -p，密码为openstack。
2、创建cinder数据库create database cinder;
3、授权
grant all privileges on cinder.* to cinder@&#x27;localhost&#x27; identified by &#x27;openstack&#x27;;grant all privileges on cinder.* to cinder@&#x27;%&#x27; identified by &#x27;openstack&#x27;; 

4、退出数据库flush privileges;
exit;
3.3. 证书和端点1、使admin环境生效. admin-openrc
2、创建cinder用户，密码为openstackopenstack user create --domain default --project service --password openstack cinder
3、给cinder用户添加admin角色openstack role add --project service --user cinder admin
4、创建cinder服务
openstack service create --name cinderv2 --description &quot;OpenStack Block Storage&quot; volumev2openstack service create --name cinderv3 --description &quot;OpenStack Block Storage&quot; volumev3 

5、创建cinder服务端点
openstack endpoint create --region RegionOne volumev2 public http://controller:8776/v2/%\(project_id\)sopenstack endpoint create --region RegionOne volumev2 internal http://controller:8776/v2/%\(project_id\)sopenstack endpoint create --region RegionOne volumev2 admin http://controller:8776/v2/%\(project_id\)sopenstack endpoint create --region RegionOne volumev3 public http://controller:8776/v3/%\(project_id\)s openstack endpoint create --region RegionOne volumev3 internal http://controller:8776/v3/%\(project_id\)s openstack endpoint create --region RegionOne volumev3 admin http://controller:8776/v3/%\(project_id\)s 

3.4. 安装配置1、下载安装组件apt-get -y install cinder-api cinder-scheduler python-cinderclient
2、备份cinder.confmv /etc/cinder/cinder.conf /etc/cinder/cinder.conf.bak
3、vi /etc/cinder/cinder.conf，新建cinder.conf内容为：
# create new[DEFAULT]# define own IP addressmy_ip = 172.16.0.105rootwrap_config = /etc/cinder/rootwrap.confapi_paste_confg = /etc/cinder/api-paste.inistate_path = /var/lib/cinderauth_strategy = keystone# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller# MariaDB connection info[database]connection = mysql+pymysql://cinder:openstack@controller/cinder# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = cinderpassword = openstack[oslo_concurrency]lock_path = $state_path/tmp

4、更改权限chmod 644 /etc/cinder/cinder.conf
chown root:cinder /etc/cinder/cinder.conf
5、生成数据库数据su -s /bin/bash cinder -c &quot;cinder-manage db sync&quot;
6、重启cinder-schedulersystemctl restart cinder-scheduler
7、查看volumeopenstack volume service list
4. 存储节点本文中，我们的存储节点和计算节点在同一个节点，该节点的IP为172.16.0.106。已经安装的服务和virtualbox计算节点（192.168.56.111）相同，包括nova-compute和neutron。
4.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
4.2. 安装配置1、安装cinder-volumeapt-get -y install cinder-volume python-mysqldb
2、备份cinder.confmv /etc/cinder/cinder.conf /etc/cinder/cinder.conf.bak
3、vi /etc/cinder/cinder.conf，新建cinder.conf内容为：
# create new[DEFAULT]# define own IP addressmy_ip = 172.16.0.106rootwrap_config = /etc/cinder/rootwrap.confapi_paste_confg = /etc/cinder/api-paste.inistate_path = /var/lib/cinderauth_strategy = keystone# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller# Glance connection infoglance_api_servers = http://controller:9292# OK with empty value nowenabled_backends =# MariaDB connection info[database]connection = mysql+pymysql://cinder:openstack@controller/cinder# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = cinderpassword = openstack[oslo_concurrency]lock_path = $state_path/tmp

4、更改权限chmod 644 /etc/cinder/cinder.conf
chown root:cinder /etc/cinder/cinder.conf
5、重启cinder-volumesystemctl restart cinder-volume
5. 使用lvm卷5.1. 存储节点1、在存储节点添加硬盘，保证至少有两块硬盘。
2、创建物理卷ll /dev/sd*
pvcreate /dev/sdb
3、创建volume groupvgcreate -s 32M vg_volume01 /dev/sdb
4、安装lvm相关组件apt-get -y install tgt thin-provisioning-tools
5、vi /etc/cinder/cinder.conf，如下修改cinder.conf：
# add a value for enabled_backendsenabled_backends = lvm# add follows to the end[lvm]iscsi_helper = tgtadm# volume group name just createdvolume_group = vg_volume01# IP address of Storage Nodeiscsi_ip_address = 172.16.0.106volume_driver = cinder.volume.drivers.lvm.LVMVolumeDrivervolumes_dir = $state_path/volumesiscsi_protocol = iscsi

6、重启服务systemctl restart cinder-volume tgt
5.2. 计算节点1、vi /etc/nova/nova.conf，编辑nova.conf文件：
# add to the end[cinder]os_region_name = RegionOne

2、重启compute服务systemctl restart nova-compute
5.3. 控制节点以上，已经准备好了cinder服务，我们在控制节点使用cinder服务（也可以在其他机器使用）。
1、假设我们已经按照《Ubuntu16手动安装OpenStack——创建实例》一文，创建了一个cirros0实例，ip为10.0.0.206。
2、登录cirros0ssh cirros@10.0.0.206，密码默认为gocubsgo。
3、在cirros0中查看硬盘df -h
ls -l /dev/vd*
sudo fdisk -l
可以看到，当前虚拟机只有一块vda硬盘。
4、控制节点中修改voidking用户环境，并使环境生效echo &quot;export OS_VOLUME_API_VERSION=2&quot; &gt;&gt; ~/voidkingrc
. voidkingrc
5、创建卷disk01，大小为1Gopenstack volume create --size 1 disk01
6、查看卷openstack volume list
7、把卷disk01添加给cirros0实例openstack server list
openstack server add volume cirros0 disk01
8、在cirros0中查看硬盘ls -l /dev/vd*
发现cirros0中多出了一块硬盘vdb。
9、在控制节点查看卷openstack volume list显示disk01已经attach到了cirros0的/dev/vdb，和我们在cirros0中看到的结果一致。
10、移除卷openstack server remove volume cirros0 disk01
6. 后记关于NFS（网络文件系统）的使用，参考OpenStack Queens : Use Cinder Storage (NFS)。
关于LVM和NFS格式卷的混合使用，参考OpenStack Queens : Use Cinder Storage (Multi-BackEnds)。
关于使用cinder备份卷，参考OpenStack Queens : Configure Cinder Backup。
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——修改镜像</title>
    <url>/dev-ubuntu16-manual-openstack-modify-mirror/</url>
    <content><![CDATA[1. 问题描述《Ubuntu16手动安装OpenStack——horizon篇》一文中，已经安装了horizon，并且在dashboard中使用官方镜像创建了ubuntu16实例。看上去，很成功。
但是，再次使用官方镜像创建实例，这次居然无法通过ssh密钥访问！！！Permission denied (publickey). 神奇了。。。
重建实例，依然无法通过密钥访问。本文，就来研究一下解决办法。


2. 尝试解决1、查看启动日志，发现：
ci-info: no authorized ssh keys fingerprints found for user ubuntu.
看这个提示，应该是密钥没有注入到实例中。
2、再往上看，发现：
DataSourceEc2.py[CRITICAL]: Giving up on md from [&#x27;http://169.254.169.254/2009-04-04/meta-data/instance-id&#x27;] after 120 secondsurl_helper.py[WARNING]: Calling &#x27;http://10.0.0.200/latest/meta-data/instance-id&#x27; failed [0/120s]: bad status code [404]......DataSourceCloudStack.py[CRITICAL]: Giving up on waiting for the metadata from [&#x27;http://10.0.0.200/latest/meta-data/instance-id&#x27;] after 120 seconds

完整日志查看：start.log
3、参考OpenStack METADATA不工作的分析方法，重启metadata服务：systemctl restart neutron-metadata-agent.service
然后重新创建实例，没有用。
4、查看metadata日志tail /var/log/neutron/neutron-metadata-agent.log
2018-07-10 11:40:45.079 22459 INFO eventlet.wsgi.server [-] 10.0.0.1,&lt;local&gt; &quot;GET /latest/meta-data/instance-id HTTP/1.1&quot; status: 404  len: 297 time: 0.0431259

使用日志关键词搜索，并没有找到相关解决办法。
5、参考Openstack 通过 Metadata 服务请求流程 和 在OpenStack里如何确保虚机正常访问MetaData IP 169.254.169.254，也没有解决问题。
6、重建实例，配置脚本设置ssh允许密码登录。但是，无论是密钥还是密码，都无法登录，和《OpenStack添加镜像》一文中遇到的问题简直一模一样。
3. 修改镜像配置失败，决定从镜像入手，修改镜像，使之支持ssh密码登录。主要参考openstack中镜像的密码修改和使用 guestfish 工具修改OpenStack官方Ubuntu 镜像
以下操作在控制节点切换到root执行。
1、下载镜像wget http://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-amd64-disk1.img
2、安装guestfish工具apt-get install libguestfs-tools -y
3、打开镜像guestfish --rw -a xenial-server-cloudimg-amd64-disk1.img
4、执行修改
&gt;&lt;fs&gt; run&gt;&lt;fs&gt; list-filesystems&gt;&lt;fs&gt; mount /dev/sda1 /&gt;&lt;fs&gt; vi /etc/cloud/cloud.cfg&gt;&lt;fs&gt; quit 

在编辑cloud.cfg的时候，做如下修改：
# line 12, change and adddisable_root: falsessh_pwauth: true# line 93, change and addlock_passwd: falseplain_text_passwd: &quot;ubuntu&quot;

以上，允许了root登录，增加了ssh密码登录，设置ubuntu用户的默认密码为ubuntu。
5、使admin环境生效. admin-openrc
6、添加镜像
openstack image create &quot;ubuntu16-server-modified&quot; \  --file xenial-server-cloudimg-amd64-disk1.img \  --disk-format qcow2 --container-format bare \  --public

7、之后，创建实例ubuntu1，就可以在vnc使用密码登录了。可以使用sudo -i切换到root，等待时间很长，大概半分钟才能切换成功。
但是此时，依然不能使用ssh登录，无论是使用密钥，还是不使用密钥，都会提示：Permission denied (publickey).。
打算在vnc给实例添加公钥，然后把私钥scp给控制节点，但是，无法向authorized_keys写入公钥。神奇了！
而且，在vnc也无法ssh登录本机！同样提示Permission denied (publickey).
8、解决办法
sudo -iapt remove openssh-serverrm /etc/ssh/*apt install openssh-server

4. 继续修改镜像4.1. 定位问题以上，已经实现了实例通过ssh密码登录，但是这个过程比较麻烦。理论上，继续修改镜像，应该就能够实现这个目标。
1、创建一个实例ubuntu2，然后对比ubuntu1和ubuntu2中的/etc/ssh/sshd_config。发现它们最大的不同在于PasswordAuthentication。
ubuntu1中：
# Change to no to disable tunnelled clear text passwords#PasswordAuthentication yes

ubuntu2中：
# Change to no to disable tunnelled clear text passwordsPasswordAuthentication no

啊哈，这就是无法使用ssh登录的罪魁祸首了。
2、在ubuntu2中，修改PasswordAuthentication：
# Change to no to disable tunnelled clear text passwords#PasswordAuthentication no

3、重启sshdservice sshd restart
然后，就可以使用ssh密码登录了。
4.2. 修改镜像那么，该怎么在镜像中修改PasswordAuthentication呢？很简单。
1、打开镜像guestfish --rw -a xenial-server-cloudimg-amd64-disk1.img
2、修改sshd_config
&gt;&lt;fs&gt; run&gt;&lt;fs&gt; list-filesystems&gt;&lt;fs&gt; mount /dev/sda1 /&gt;&lt;fs&gt; vi /etc/ssh/sshd_config&gt;&lt;fs&gt; quit 

在编辑sshd_config的时候，做如下修改：
# Change to no to disable tunnelled clear text passwords#PasswordAuthentication no

3、（可选）为了方便联网，不妨编辑resolv.conf.d/base
&gt;&lt;fs&gt; vi /etc/resolvconf/resolv.conf.d/base

在base中添加：
nameserver 180.76.76.76nameserver 223.6.6.6

4、使admin环境生效. admin-openrc
5、删除镜像，重新添加
openstack image delete &quot;ubuntu16-server-modified&quot;openstack image create &quot;ubuntu16-server-modified&quot; \  --file xenial-server-cloudimg-amd64-disk1.img \  --disk-format qcow2 --container-format bare \  --public

至此，大功告成。
5. 后记在创建实例时无法注入公钥和脚本的问题，最终没有解决，好在找到了其他解决方案。至于这个注入问题，会在后续的学习中继续尝试解决。
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——horizon篇</title>
    <url>/dev-ubuntu16-manual-openstack-horizon/</url>
    <content><![CDATA[1. 目标完成了《Ubuntu16手动安装OpenStack——创建实例》，验证了之前的安装没有大的问题。
本文，我们来研究一下horizon的安装配置，主要参考Horizon: Install and configure for Ubuntu 和 OpenStack Queens : Configure Horizon。


2. 说明由于电脑配置有限，已经没有多余的资源分配给虚拟机，所以接下来的安装配置转移到实体上。实体机系统为ubuntu-16.04.4-server-amd64，配置为32核64GB内存8TB存储。有四张网卡分别为eno1、eno2、eno3和eno4，其中eno1的IP为172.16.0.105，eno2的ip为172.16.101.105，eno3配置为neutron使用的网卡。
172.16.0.105需要通过跳板机访问，172.16.101.105可以本地直连。
实体机同样安装好了keystone、glance、nova和neutron服务，也测试创建了实例。除了配置更高，其他等同于之前的控制节点。
3. horizon简介horizon（dashboard）一般安装在控制器节点上，所需的唯一核心服务是身份认证服务。我们可以将horizon与其他服务结合使用，例如镜像服务，计算和网络。也可以在具有独立服务（如对象存储）的环境中使用horizon。
queens版本的horizon需要以下依赖：
（1）Python 2.7（2）Django 1.11。Django 1.8到1.10也受支持，不过他们的支持将在Rocky版本中被删除。（3）可访问的keystone端点。（4）所有其他服务都是可选的。如果配置了服务的keystone端点，则horizo​​n会检测它并自动启用其支持。queens发布时，Horizo​​n支持以下服务：

cinder: Block Storage
glance: Image Management
neutron: Networking
nova: Compute
swift: Object Storage
Horizon also supports many other OpenStack services via plugins. 

4. 安装配置1、安装horizonapt-get -y install openstack-dashboard
2、编辑horizon配置文件vi /etc/openstack-dashboard/local_settings.py
如下修改：
# line 39 uncomment and add own hostnameALLOWED_HOSTS = [&#x27;controller&#x27;, &#x27;localhost&#x27;]# line 65: uncomment like followsOPENSTACK_API_VERSIONS = &#123;#    &quot;data-processing&quot;: 1.1,    &quot;identity&quot;: 3,    &quot;image&quot;: 2,    &quot;volume&quot;: 2,    &quot;compute&quot;: 2,&#125;# line 76: uncomment and changeOPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True# line 98: uncommentOPENSTACK_KEYSTONE_DEFAULT_DOMAIN = &#x27;Default&#x27;# line 163: change to your own Memcache serverCACHES = &#123;    &#x27;default&#x27;: &#123;        &#x27;BACKEND&#x27;: &#x27;django.core.cache.backends.memcached.MemcachedCache&#x27;,        &#x27;LOCATION&#x27;: &#x27;172.16.0.105:11211&#x27;,    &#125;,&#125;# line 190: change to your own HostOPENSTACK_HOST = &quot;172.16.0.105&quot;OPENSTACK_KEYSTONE_URL = &quot;http://%s:5000/v3&quot; % OPENSTACK_HOSTOPENSTACK_KEYSTONE_DEFAULT_ROLE = &quot;_member_&quot;

3、重启apache2和memcachedsystemctl restart apache2 memcached
5. 简单使用1、测试访问访问地址：http://172.16.101.105/horizon/auth/login/
2、Domain输入default，用户名输入admin，密码输入openstack。登录horizon。
3、查看项目和用户。点击左侧导航栏身份认证，项目或者用户。
4、查看cirros0实例。身份认证，项目，siat，查看使用量。
5、退出登录
6. 简单使用21、测试访问访问地址：http://172.16.101.105/horizon/auth/login/
2、Domain输入default，用户名输入voidking，密码输入openstack。登录horizon。
3、同样可以看到cirros0实例，并且进行管理。
7. 访问实例参考OpenStack添加镜像，添加ubuntu16的官方镜像，创建实例，分配的IP为10.0.0.208。
1、在控制节点通过密钥访问ssh ubuntu@10.0.0.208 -i id_rsa
2、在本地通过网页访问点击实例名称，控制台，即可拿到vnc地址，比如 http://controller:6080/vnc_auto.html?token=a92733f2-e6c5-45e8-8521-5ace9b747957&amp;title=k8s01
把controller替换为172.16.101.105，得到 http://172.16.101.105:6080/vnc_auto.html?token=a92733f2-e6c5-45e8-8521-5ace9b747957&amp;title=k8s01
访问该地址，即可看到网页控制台。
3、在本地通过密钥访问（1）在控制节点新建portmap.sh脚本，内容为：
#!/bin/bashpro=&#x27;tcp&#x27;NAT_Host=&#x27;172.16.0.105&#x27;NAT_Port=20822Dst_Host=&#x27;10.0.0.208&#x27;Dst_Port=22iptables -t nat -A PREROUTING -m $pro -p $pro --dport $NAT_Port -j DNAT --to-destination $Dst_Host:$Dst_Portiptables -t nat -A POSTROUTING -m $pro -p $pro --dport $Dst_Port -d $Dst_Host -j SNAT --to-source $NAT_Host

（2）执行脚本chmod a+x portmap.sh
sudo ./portmap.sh
（3）新建xshell连接名称填入10.0.0.208，主机填入172.16.101.105，端口号填入20822。
用户身份验证，方法选择Public Key，浏览，导入实例对应的id_rsa私钥。
（4）配置完成，连接即可。
8. 实例访问外网在实例中ping百度，是不通的，下面我们进行配置，使实例能够访问外网。参考Linux配置SNAT上网。
8.1. 控制节点在控制节点切换到root进行操作。
1、接收数据包iptables -P INPUT ACCEPT
2、开启路由功能echo 1 &gt; /proc/sys/net/ipv4/ip_forward
3、伪装数据包iptables -t nat -A POSTROUTING -j MASQUERADE
8.2. 实例1、在实例中测试访问ping 120.77.36.182，是可以ping通的。ping www.baidu.com，ping不通。
2、在实例中配置resolv.confsudo vim /etc/resolv.conf，添加nameserver的配置
nameserver 180.76.76.76nameserver 223.6.6.6

sudo vim /etc/resolvconf/resolv.conf.d/base，添加：
nameserver 180.76.76.76nameserver 223.6.6.6

注意：执行sudo的时候，反应非常慢，请耐心等待，大概半分钟才能打开文件。要解决这个问题，需要修改hosts文件，加入主机名的解析：
127.0.0.1  k8s01

3、测试ping www.baidu.com
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——创建实例</title>
    <url>/dev-ubuntu16-manual-openstack-instance/</url>
    <content><![CDATA[1. 目标完成了《Ubuntu16手动安装OpenStack——neutron篇》，意味着keystone、glance、nova和neutron都已经安装完成，也就是说，openstack最小安装已经完成。在安装horizon、cinder和swift之前，我们先来通过命令行创建一个实例，验证一下之前的安装。
主要参考OpenStack Queens : Add Users、OpenStack Queens : Add Users 和 OpenStack Queens : Boot Instances。


2. 说明本文操作全部都是在控制节点。为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
3. 网卡配置参考Openstack的网卡设置和和OpenStack Networking Tutorial: Single-host FlatDHCPManager，配置eth2网卡。
1、查看当前网络，ip add
route -n，brctl show
2、vim /etc/network/interfaces，添加：
auto eth2iface eth2 inet manual  up ifconfig $IFACE 0.0.0.0 up  up ifconfig $IFACE promisc#iface eth2 inet manual#up ifconfig eth2 up

3、启用网卡，ifup eth2
4. 创建子网1、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 181: add[ml2_type_flat]flat_networks = physnet1

2、vi /etc/neutron/plugins/ml2/linuxbridge_agent.ini，如下修改：
# line 147: add[linux_bridge]physical_interface_mappings = physnet1:eth2# line 208: uncomment and changeenable_vxlan = false

3、重启neutron-linuxbridge-agentsystemctl restart neutron-linuxbridge-agent
4、获取projectID. admin-openrc
projectID=$(openstack project list | grep service | awk &#39;&#123;print $2&#125;&#39;)
5、创建名为sharednet1的网络
openstack network create --project $projectID \--share --provider-network-type flat --provider-physical-network physnet1 sharednet1

6、在sharednet1中创建子网10.0.0.0/24
openstack subnet create subnet1 --network sharednet1 \--project $projectID --subnet-range 10.0.0.0/24 \--allocation-pool start=10.0.0.200,end=10.0.0.254 \--gateway 10.0.0.1 --dns-nameserver 10.0.0.10

7、查看网络openstack network list
openstack subnet list

5. 添加用户首先使admin环境生效，. admin-openrc。
1、添加siat项目openstack project create --domain default --description &quot;SIAT Project&quot; siat
2、添加voidking用户，密码为openstackopenstack user create --domain default --project siat --password openstack voidking
3、添加clouduser角色openstack role create clouduser
4、给voidking用户添加clouduser角色openstack role add --project siat --user voidking clouduser
5、因为cirros需要的资源很少，所以创建一个类型模板m1.tinyopenstack flavor create --id 0 --vcpus 1 --ram 256 --disk 5 m1.tiny
6. 创建实例6.1. 环境准备1、vi ~/voidkingrc，创建voidking环境脚本，内容如下：
export OS_PROJECT_DOMAIN_NAME=defaultexport OS_USER_DOMAIN_NAME=defaultexport OS_PROJECT_NAME=siatexport OS_USERNAME=voidkingexport OS_PASSWORD=openstackexport OS_AUTH_URL=http://controller:5000/v3export OS_IDENTITY_API_VERSION=3export OS_IMAGE_API_VERSION=2export PS1=&#x27;\u@\h \W(keystone)\$ &#x27;

2、使环境生效. voidkingrc
3、查看类型模板openstack flavor list
4、查看可用镜像openstack image list
5、查看可用网络openstack network list
6、创建安全组openstack security group create secgroup01

7、查看安全组openstack security group list
8、创建ssh密钥ssh-keygen -q -N &quot;&quot;
密钥文件保存在/home/root/.ssh/目录下。
9、添加公钥openstack keypair create --public-key ~/.ssh/id_rsa.pub vkkey
10、查看公钥openstack keypair list
6.2. 创建实例1、给netID赋值netID=$(openstack network list | grep sharednet1 | awk &#39;&#123; print $2 &#125;&#39;)
2、创建实例
openstack server create --flavor m1.tiny \--image cirros --security-group secgroup01 \--nic net-id=$netID --key-name vkkey cirros0

3、查看实例openstack server list
如上图，已经成功启动实例，ip为10.0.0.206，nice。
PS：删除实例命令，openstack server delete cirros0
6.3. 访问实例配置安全组的安全设置，以便使用SSH和ICMP进行访问。
1、设置允许ICMPopenstack security group rule create --protocol icmp --ingress secgroup01
2、设置允许sshopenstack security group rule create --protocol tcp --dst-port 22:22 secgroup01
3、查看安全组规则openstack security group rule list
4、访问测试ping 10.0.0.206 -c3
ping不通，看来网络出了问题，等会再解决这个问题。
5、查看vnc的url，并复制该urlopenstack console url show cirros0
6、url中的controller替换为192.168.56.110，在浏览器中打开
顺利访问，可以看到，cirros0的ip为192.168.56.102，难怪10.0.0.206无法ping通。
控制节点上，ping 192.168.56.102，依然不通。vnc中，ping 192.168.56.110，也不通。
7. 网络问题7.1. 网桥支持Configure the Linux bridge agent一节中，提到过要启用网桥支持，那就启用网桥试试。
1、查看ip转发cat /proc/sys/net/ipv4/ip_forward如果值为0，那么改为1。
2、启用网桥支持vim /etc/sysctl.conf，添加
net.bridge.bridge-nf-call-iptables = 1net.bridge.bridge-nf-call-ip6tables = 1

3、使配置生效sysctl -p
修改后，依然无法ping通10.0.0.206。
7.2. 网卡配置1、查看ipip add如上图，和本文开始时相比，多了三个接口。
2、查看网桥brctl show
3、vim /etc/network/interfaces，添加
auto brqedda68f7-72iface brqedda68f7-72 inet staticaddress 10.0.0.1netmask 255.255.255.0bridge_stp offbridge_fd 0

然后启用brqedda68f7-72端口，ifup brqedda68f7-72
或者不配置interfaces，直接执行ifconfig brqedda68f7-72 10.0.0.1/24 up
5、查看网络ip add
route -n

不过，依然无法ping通10.0.0.206。毕竟，cirros0的ip不是10.0.0.206，而是192.168.56.102。这个ip，明显是通过VirtualBox分配的，和虚拟机在同一个网段。
7.3. 修改ip既然cirros0的ip不对，那就给它手动修改一下，好主意。1、通过vnc登录cirros0
2、sudo vi /etc/network/interfaces，修改eth0的配置为：
auto eth0iface eth0 inet staticaddress 10.0.0.206netmask 255.255.255.0

3、重启eth0sudo ifdown eth0
sudo ifup eth0
4、在控制节点测试连接ping 10.0.0.206 -c3
至此，问题解决。
8. 访问实例1、cirros当前只支持密码访问：ssh cirros@10.0.0.206，输入密码gocubsgo。
登录后执行ls .ssh，并没有authorized_keys文件，看来创建实例时并没有注入密钥，不知道什么原因。
2、在控制节点，添加密钥到cirros0ssh-copy-id -i .ssh/id_rsa.pub -p 22 cirros@10.0.0.206
3、用密钥测试登录ssh cirros@10.0.0.206 -i .ssh/id_rsa
登录成功。
4、如果重启了控制节点，那么需要手动启动cirros0openstack server start cirros0
5、vnc的token也会改变，所以要重新获取openstack console url show cirros0
9. 后记至此，已经完成了吗？想到一些问题：

cirros0理论上应该通过浮动ip进行访问，我们这种直接访问是闹哪样？
以后每次创建实例，都要通过vnc重新配置ip？
以后每次创建实例，都要手动添加密钥？

也许是因为VirtualBox网卡分配不对，也许是因为在虚拟机中安装，也许是因为控制节点网络配置不对。。。不管了，反正创建实例完成，其他问题放在以后的文章中解决。
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——neutron篇</title>
    <url>/dev-ubuntu16-manual-openstack-neutron/</url>
    <content><![CDATA[1. 目标紧接着《Ubuntu16手动安装OpenStack——nova篇》，本文我们来安装neutron，主要参考Networking service、Install and configure for Ubuntu和OpenStack Queens : Configure Neutron。


2. neutron简介OpenStack Networking（neutron）允许用户创建和连接接口设备，这些设备由其他OpenStack服务管理并连入网络。可以实现插件以适应不同的网络设备和软件，为OpenStack架构和部署提供灵活性。
它包括以下组件：neutron-server接受API请求，并将请求通过路由找到适当的OpenStack Networking插件以进行操作。
OpenStack Networking plug-ins and agents插拔端口，创建网络或子网，并提供IP寻址。这些插件和代理程序因特定云中使用的供应商和技术而异。OpenStack Networking附带了很多插件和代理，可以用于思科虚拟和物理交换机，NEC OpenFlow产品，Open vSwitch，Linux桥接和VMware NSX产品。
公共代理是L3（第3层），DHCP（动态主机IP寻址）和插件代理。
Messaging queue大多数OpenStack Networking安装使用messaging queue，在neutron-server和各种代理之间传递信息。还用于存储特定插件的网络状态，算是一个数据库。
OpenStack Networking主要与OpenStack Compute交互，为其实例提供网络和连接。
更多信息请参考Networking (neutron) concepts。
3. 主机网络在每个节点上安装操作系统后，必须配置网络接口。官方建议禁用任何自动网络管理工具，并手动编辑配置文件。有关如何配置网络的详细信息，请参阅文档。
出于管理目的，所有节点都需要Internet访问，例如程序包安装，安全更新，域名系统（DNS）和网络时间协议（NTP）。在大多数情况下，节点应通过管理网络接口获得Internet访问。为了突出网络分离的重要性，示例体系结构使用专用地址空间用于管理网络，并假设物理网络基础结构通过网络地址转换（NAT）或其他方法提供Internet访问。
在provider网络架构中，所有实例都直接连接到provider网络。在私网体系结构中，实例可以连接到一个私网或多个私网。私网可以完全属于OpenStack，不接入外网；也可以通过provider网络接入外部网络。


4. 控制节点4.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
4.2. 数据库1、登录数据库mysql -uroot -p，密码为openstack。
2、创建neutron数据库CREATE DATABASE neutron;
3、授权
GRANT ALL PRIVILEGES ON neutron.* TO &#x27;neutron&#x27;@&#x27;localhost&#x27; IDENTIFIED BY &#x27;openstack&#x27;;GRANT ALL PRIVILEGES ON neutron.* TO &#x27;neutron&#x27;@&#x27;%&#x27; IDENTIFIED BY &#x27;openstack&#x27;;

4、退出数据库exit;
4.3. 证书和端点1、使admin环境生效. admin-openrc
2、创建neutron用户openstack user create --domain default --password-prompt neutron
根据提示设置密码为openstack。
3、添加admin角色给neutron用户openstack role add --project service --user neutron admin
4、创建neutron服务实体openstack service create --name neutron --description &quot;OpenStack Networking&quot; network
5、创建网络服务端点
openstack endpoint create --region RegionOne network public http://controller:9696openstack endpoint create --region RegionOne network internal http://controller:9696openstack endpoint create --region RegionOne network admin http://controller:9696

4.4. 安装配置1、安装相关组件
apt-get -y install neutron-server neutron-plugin-ml2 neutron-plugin-linuxbridge-agent neutron-l3-agent neutron-dhcp-agent neutron-metadata-agent python-neutronclient

2、备份neutron.confmv /etc/neutron/neutron.conf /etc/neutron/neutron.conf.bak
3、vim /etc/neutron/neutron.conf，新建neutron.conf内容为：
[DEFAULT]core_plugin = ml2service_plugins = routerauth_strategy = keystonestate_path = /var/lib/neutrondhcp_agent_notification = Trueallow_overlapping_ips = Truenotify_nova_on_port_status_changes = Truenotify_nova_on_port_data_changes = True# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller[agent]root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = neutronpassword = openstack# MariaDB connection info[database]connection = mysql+pymysql://neutron:openstack@controller/neutron# Nova connection info[nova]auth_url = http://controller:5000auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultregion_name = RegionOneproject_name = serviceusername = novapassword = openstack[oslo_concurrency]lock_path = $state_path/tmp

4、更改权限chmod 640 /etc/neutron/neutron.conf
chgrp neutron /etc/neutron/neutron.conf
5、vi /etc/neutron/l3_agent.ini，如下修改：
# line 17: addinterface_driver = neutron.agent.linux.interface.BridgeInterfaceDriver

6、vi /etc/neutron/dhcp_agent.ini，如下修改：
# line 17: addinterface_driver = neutron.agent.linux.interface.BridgeInterfaceDriver# line 28: uncommentdhcp_driver = neutron.agent.linux.dhcp.Dnsmasq# line 37: uncomment and changeenable_isolated_metadata = true

7、vi /etc/neutron/metadata_agent.ini，如下修改：
# line 22: uncomment and specify Nova API servernova_metadata_host = controller# line 34: uncomment and specify any secret key you likemetadata_proxy_shared_secret = openstack# line 260: uncomment and specify Memcache Servermemcache_servers = controller:11211

8、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 129: add ( it&#x27;s OK with no value for &quot;tenant_network_types&quot; (set later if need) )[ml2]type_drivers = flat,vlan,vxlantenant_network_types =mechanism_drivers = linuxbridge,l2populationextension_drivers = port_security# line 262: uncomment and addenable_security_group = Truefirewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver# end line: uncommentenable_ipset = True

9、vi /etc/neutron/plugins/ml2/linuxbridge_agent.ini，如下修改：
# line 235: add own IP addresslocal_ip = 192.168.56.110

10、vi /etc/nova/nova.conf，如下修改：
# add follows into [DEFAULT] sectionuse_neutron = Truelinuxnet_interface_driver = nova.network.linux_net.LinuxBridgeInterfaceDriverfirewall_driver = nova.virt.firewall.NoopFirewallDrivervif_plugging_is_fatal = Truevif_plugging_timeout = 300# add follows to the end : Neutron auth info# the value of metadata_proxy_shared_secret is the same with the one in metadata_agent.ini[neutron]auth_url = http://controller:5000auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultregion_name = RegionOneproject_name = serviceusername = neutronpassword = openstackservice_metadata_proxy = Truemetadata_proxy_shared_secret = openstack

4.5. 完成安装1、创建链接ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini
2、生成数据库表结构su -s /bin/bash neutron -c &quot;neutron-db-manage --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugin.ini upgrade head&quot;
3、重启网络服务并设置开机启动
for service in server l3-agent dhcp-agent metadata-agent linuxbridge-agent; dosystemctl restart neutron-$servicesystemctl enable neutron-$servicedone

4、重启novasystemctl restart nova-api nova-compute
5、查看网络agentopenstack network agent list
如上图，看到4个agent，都在controller节点上。
5. 计算节点主要参考OpenStack Queens : Configure Neutron。
5.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
5.2. 安装配置1、安装组件apt-get -y install neutron-common neutron-plugin-ml2 neutron-plugin-linuxbridge-agent
2、备份neutron.confmv /etc/neutron/neutron.conf /etc/neutron/neutron.conf.bak
3、vim /etc/neutron/neutron.conf，新建neutron.conf如下：
[DEFAULT]core_plugin = ml2service_plugins = routerauth_strategy = keystonestate_path = /var/lib/neutronallow_overlapping_ips = True# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller[agent]root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = neutronpassword = openstack[oslo_concurrency]lock_path = $state_path/lock

4、修改权限chmod 640 /etc/neutron/neutron.conf
chgrp neutron /etc/neutron/neutron.conf
5、vi /etc/neutron/plugins/ml2/ml2_conf.ini，如下修改：
# line 129: add ( it&#x27;s OK with no value for &quot;tenant_network_types&quot; (set later if need) )[ml2]type_drivers = flat,vlan,vxlantenant_network_types =mechanism_drivers = linuxbridge,l2populationextension_drivers = port_security# line 262: uncomment and addenable_security_group = Truefirewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver# end line: uncommentenable_ipset = True

6、vi /etc/neutron/plugins/ml2/linuxbridge_agent.ini，如下修改：
# line 235: add own Ip addresslocal_ip = 192.168.56.111

7、vi /etc/nova/nova.conf，如下修改：
# add follows into [DEFAULT] sectionuse_neutron = Truelinuxnet_interface_driver = nova.network.linux_net.LinuxBridgeInterfaceDriverfirewall_driver = nova.virt.firewall.NoopFirewallDrivervif_plugging_is_fatal = Truevif_plugging_timeout = 300# add follows to the end: Neutron auth info# the value of metadata_proxy_shared_secret is the same with the one in metadata_agent.ini[neutron]auth_url = http://controller:5000auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultregion_name = RegionOneproject_name = serviceusername = neutronpassword = openstackservice_metadata_proxy = Truemetadata_proxy_shared_secret = openstack

8、创建链接ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini
9、重启网络服务systemctl restart nova-compute neutron-linuxbridge-agent
10、设置开启自启动systemctl enable neutron-linuxbridge-agent
6. 验证操作在控制节点执行以下命令。
1、使admin环境生效. admin-openrc
2、查看agentsopenstack network agent list如上图，此时就能看到5个agent，其中4个在controller节点，1个在compute节点。
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——nova篇</title>
    <url>/dev-ubuntu16-manual-openstack-nova/</url>
    <content><![CDATA[1. 目标紧接着《Ubuntu16手动安装OpenStack——glance篇》，本文我们来安装nova，主要参考Compute service、Install and configure controller node for Ubuntu、Install and configure a compute node for Ubuntu。


2. nova简介使用OpenStack Compute来托管和管理云计算系统。OpenStack Compute是基础架构即服务（IaaS）系统的主要部分。主要模块用Python实现。
OpenStack Compute与OpenStack Identity交互以进行身份​​验证；OpenStack 镜像服务用于磁盘和镜像管理；OpenStack Dashboard用于用户界面和管理界面。镜像访问受项目和用户的限制；配额受每个项目限制（例如，实例数）。OpenStack Compute可以在标准硬件上水平扩展，下载镜像，然后启动实例。
OpenStack Compute包含以下组件：
nova-api service接受并响应终端用户compute API调用。该服务支持OpenStack Compute API。它强制执行某些策略并启动大多数活动，例如运行实例。
nova-api-metadata service接受来自实例的元数据请求。在nova-network的多主机模式下运行时，通常会使用nova-api-metadata服务。
nova-compute service通过虚拟机管理程序API，创建和终止虚拟机实例。例如：

XenAPI for XenServer/XCP
libvirt for KVM or QEMU
VMwareAPI for VMware

处理相当复杂。基本上，nova-compute守护程序接受来自队列的操作，并执行一系列系统命令，例如启动KVM实例并更新其在数据库中的状态。
nova-placement-api service跟踪每个provider的库存和使用情况。
nova-scheduler service从队列中获取虚拟机实例请求，并确定它运行的计算服务器主机。
nova-conductor module负责nova-compute服务和数据库之间的交互。它消除了nova-compute服务对云数据库的直接访问。nova-conductor module可以水平伸缩。不要将其部署在运行nova-compute服务的节点上。
nova-consoleauth daemon在控制台代理为用户授权tokens，参见nova-novncproxy和nova-xvpvncproxy。必须运行此服务才能使控制台代理生效。可以针对群集配置中的单个nova-consoleauth服务运行任一类型的代理。
nova-novncproxy daemon提供正在运行的实例的代理，该代理通过VNC连接访问。支持基于浏览器的novnc客户端。
nova-spicehtml5proxy daemon提供正在运行的实例的代理，该代理通过SPICE连接访问。支持基于浏览器的HTML5客户端。
nova-xvpvncproxy daemon提供正在运行的实例的代理，该代理通过VNC连接访问。支持OpenStack特定的Java客户端。
The queue用于在守护进程之间传递消息的中央集线器。通常用RabbitMQ实现，也可以用另一个AMQP消息队列实现，比如ZeroMQ。
SQL database存储云基础架构的大多数构建时和运行时状态，包括：

Available instance types
Instances in use
Available networks
Projects

从理论上讲，OpenStack Compute可以支持SQLAlchemy支持的任何数据库。常见的数据库是用于测试和开发工作的SQLite3，MySQL，MariaDB和PostgreSQL。
3. 控制节点3.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
3.2. 数据库1、登录数据库mysql -uroot -p，密码为openstack。
2、创建nova_api，nova，nova_placement和nova_cell0数据库
CREATE DATABASE nova_api;CREATE DATABASE nova;CREATE DATABASE nova_placement; CREATE DATABASE nova_cell0;

3、授权
GRANT ALL PRIVILEGES ON nova_api.* TO &#x27;nova&#x27;@&#x27;localhost&#x27; IDENTIFIED BY &#x27;openstack&#x27;;GRANT ALL PRIVILEGES ON nova_api.* TO &#x27;nova&#x27;@&#x27;%&#x27; IDENTIFIED BY &#x27;openstack&#x27;;GRANT ALL PRIVILEGES ON nova.* TO &#x27;nova&#x27;@&#x27;localhost&#x27; IDENTIFIED BY &#x27;openstack&#x27;;GRANT ALL PRIVILEGES ON nova.* TO &#x27;nova&#x27;@&#x27;%&#x27; IDENTIFIED BY &#x27;openstack&#x27;;GRANT ALL PRIVILEGES ON nova_placement.* TO &#x27;nova&#x27;@&#x27;localhost&#x27; IDENTIFIED BY &#x27;openstack&#x27;;GRANT ALL PRIVILEGES ON nova_placement.* TO &#x27;nova&#x27;@&#x27;%&#x27; IDENTIFIED BY &#x27;openstack&#x27;;GRANT ALL PRIVILEGES ON nova_cell0.* TO &#x27;nova&#x27;@&#x27;localhost&#x27; IDENTIFIED BY &#x27;openstack&#x27;;GRANT ALL PRIVILEGES ON nova_cell0.* TO &#x27;nova&#x27;@&#x27;%&#x27; IDENTIFIED BY &#x27;openstack&#x27;;

4、退出数据库exit;
3.3. 服务证书和端点1、使admin环境生效. admin-openrc
2、创建nova用户openstack user create --domain default --password-prompt nova
根据提示设置密码为openstack。
3、添加admin角色给nova用户openstack role add --project service --user nova admin
4、创建nova服务实体openstack service create --name nova --description &quot;OpenStack Compute&quot; compute
5、创建计算服务API endpoints
openstack endpoint create --region RegionOne compute public http://controller:8774/v2.1openstack endpoint create --region RegionOne compute internal http://controller:8774/v2.1openstack endpoint create --region RegionOne compute admin http://controller:8774/v2.1

3.4. 服务证书和端点21、创建Placement service useropenstack user create --domain default --password-prompt placement
按照提示设置密码为openstack。
2、添加Placement user到service project，并且赋予admin角色openstack role add --project service --user placement admin
3、创建placement服务实体openstack service create --name placement --description &quot;Placement API&quot; placement
4、创建Placement API服务端点
openstack endpoint create --region RegionOne placement public http://controller:8778openstack endpoint create --region RegionOne placement internal http://controller:8778openstack endpoint create --region RegionOne placement admin http://controller:8778

3.5. 安装配置组件1、安装组件apt-get -y install nova-api nova-placement-api nova-conductor nova-consoleauth nova-scheduler nova-novncproxy python-novaclient
2、备份nova.confmv /etc/nova/nova.conf /etc/nova/nova.conf.bak
3、vim /etc/nova/nova.conf，新建nova.conf内容为：
[DEFAULT]# define own IPmy_ip = 192.168.56.110state_path = /var/lib/novaenabled_apis = osapi_compute,metadatalog_dir = /var/log/nova# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller[api]auth_strategy = keystone# Glance connection info[glance]api_servers = http://controller:9292[oslo_concurrency]lock_path = $state_path/tmp# MariaDB connection info[api_database]connection = mysql+pymysql://nova:openstack@controller/nova_api[database]connection = mysql+pymysql://nova:openstack@controller/nova# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = novapassword = openstack[placement]auth_url = http://controller:5000os_region_name = RegionOneauth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = placementpassword = openstack[placement_database]connection = mysql+pymysql://nova:openstack@controller/nova_placement[wsgi]api_paste_config = /etc/nova/api-paste.ini


4、更改权限chmod 640 /etc/nova/nova.conf
chgrp nova /etc/nova/nova.conf
5、生成数据库表结构
su -s /bin/bash nova -c &quot;nova-manage api_db sync&quot;su -s /bin/bash nova -c &quot;nova-manage cell_v2 map_cell0&quot;su -s /bin/bash nova -c &quot;nova-manage db sync&quot;su -s /bin/bash nova -c &quot;nova-manage cell_v2 create_cell --name cell1&quot;

完成后查看日志：tail /var/log/nova/nova-manage.log不放心的话，就登录数据库查看数据。
6、验证一下cell0和cell1是否被正确注册nova-manage cell_v2 list_cells
7、重启服务，完成安装
for service in api conductor scheduler consoleauth novncproxy; dosystemctl restart nova-$servicedone

8、查看计算服务列表openstack compute service list
4. 计算节点本节介绍如何在计算节点上安装和配置Compute服务，该服务支持多个虚拟机管理程序来部署实例或虚拟机（VM）。为简单起见，此配置使用Quick EMUlator（QEMU）虚拟机管理程序，和支持虚拟机硬件加速的KVM扩展。可以按照教程进行小改，以便水平扩展计算节点。
安装方法主要参考OpenStack Queens : Add Compute Nodes。
4.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
4.2. 安装配置1、安装组件apt-get -y install nova-compute-kvm
2、备份nova.confmv /etc/nova/nova.conf /etc/nova/nova.conf.bak
3、vim /etc/nova/nova.conf，新建nova.conf内容为：
[DEFAULT]# define own IP addressmy_ip = 192.168.56.111state_path = /var/lib/novaenabled_apis = osapi_compute,metadatalog_dir = /var/log/nova# RabbitMQ connection infotransport_url = rabbit://openstack:openstack@controller[api]auth_strategy = keystone# enable VNC[vnc]enabled = Trueserver_listen = 0.0.0.0server_proxyclient_address = $my_ipnovncproxy_base_url = http://controller:6080/vnc_auto.html# Glance connection info[glance]api_servers = http://controller:9292[oslo_concurrency]lock_path = $state_path/tmp# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = novapassword = openstack[placement]auth_url = http://controller:5000os_region_name = RegionOneauth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = placementpassword = openstack[wsgi]api_paste_config = /etc/nova/api-paste.ini

4、设置权限chmod 640 /etc/nova/nova.conf
chgrp nova /etc/nova/nova.conf
5、硬件加速（可选）egrep -c &#39;(vmx|svm)&#39; /proc/cpuinfo
如果看到大于等于1的数字，说明主机支持硬件加速，不需要额外的配置。
如果此命令返回零值，则计算节点不支持硬件加速，您必须将libvirt配置为使用QEMU而不是KVM。
编辑/etc/nova/nova-compute.conf文件，libvirt部分修改为：
[libvirt]# ...virt_type = qemu

6、重启nova-compute服务systemctl restart nova-compute
5. 添加计算节点到数据库以下操作在controller节点操作。
1、使admin环境生效. admin-openrc
2、查看计算服务列表openstack compute service list
发现，此时已经多了nova-compute服务。
3、查看计算节点openstack compute service list --service nova-compute
4、如果没有看到计算节点，那么需要查找计算节点，并且进行注册su -s /bin/bash nova -c &quot;nova-manage cell_v2 discover_hosts&quot;
添加新的compute节点时，必须在控制器节点上运行nova-manage cell_v2 discover_hosts以注册这些新计算节点。或者，可以在/etc/nova/nova.conf中设置适当的间隔来自动发现和注册节点：
[scheduler]discover_hosts_in_cells_interval = 300

6. 验证操作以下操作在controller节点操作。
1、使admin环境生效. admin-openrc
2、列出服务组件，以验证每个进程的成功启动和注册openstack compute service list
成功的话可以看到四个服务：nova-scheduler、nova-consoleauth、nova-conductor和nova-compute。
3、列出Identity服务中的API端点，以验证与Identity服务的连接openstack catalog list

4、列出镜像服务中的镜像，以验证与镜像服务的连接openstack image list

5、检查cells and placement API是否工作正常nova-status upgrade check
7. 控制节点安装计算服务1、安装组件apt -y install nova-compute-kvm
2、vim /etc/nova/nova.conf，添加vnc配置：
# enable VNC[vnc]enabled = Trueserver_listen = 0.0.0.0server_proxyclient_address = $my_ipnovncproxy_base_url = http://controller:6080/vnc_auto.html

3、硬件加速（可选）egrep -c &#39;(vmx|svm)&#39; /proc/cpuinfo
如果看到大于等于1的数字，说明主机支持硬件加速，不需要额外的配置。
如果此命令返回零值，则计算节点不支持硬件加速，您必须将libvirt配置为使用QEMU而不是KVM。
编辑/etc/nova/nova-compute.conf文件，libvirt部分修改为：
[libvirt]# ...virt_type = qemu

4、重启nova-compute服务systemctl restart nova-compute
5、添加到数据库su -s /bin/bash nova -c &quot;nova-manage cell_v2 discover_hosts&quot;
6、查看计算节点openstack compute service list --service nova-compute
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——glance篇</title>
    <url>/dev-ubuntu16-manual-openstack-glance/</url>
    <content><![CDATA[1. 目标紧接着《Ubuntu16手动安装OpenStack——keystone篇》，本文我们来安装glance，主要参考Glance Installation Tutorial for Ubuntu。


2. glance简介Image服务（glance）使用户能够发现、注册和检索虚拟机映像。它提供了一个REST API，使用户可以查询虚拟机镜像元数据并检索实际镜像。用户可以通过Image服务，把虚拟机映像存储在各种位置，从简单的文件系统到OpenStack Object Storage等对象存储系统。
为简单起见，官方教程中介绍如何配置Image服务以使用文件后端，该文件后端上载并存储在托管Image服务的控制器节点上的目录中。默认情况下，此目录为/var/lib/glance/ images/。在继续之前，请确保控制器节点在此目录中至少有几千兆字节的可用空间。请记住，由于文件后端通常是控制器节点的本地，因此通常不适合多节点glance部署。
OpenStack Image服务是Infrastructure-as-a-Service（IaaS）的核心。它接受来自磁盘或服务器映像的API请求，以及来自终端用户或OpenStack Compute组件的元数据定义。它还支持在各种存储库类型（包括OpenStack Object Storage）上存储磁盘或服务器映像。许多定期进程在OpenStack Image服务上运行以支持缓存。Replication服务可确保群集的一致性和可用性，其他定期进程包括auditors，updaters和reapers。
OpenStack Image服务包括以下组件：
glance-api接受Image API调用以进行镜像发现，检索和存储。
官方建议将Glance作为独立服务器以传统方式运行。
glance-registry存储，处理和检索有关镜像的元数据。元数据包括大小和类型等选项。
注册表是一个私有的内部服务，供OpenStack Image服务使用，不要将此服务公开给用户。
Glance注册服务及其API已在queens版本中弃用，并且在“S”开发周期开始后，可根据OpenStack标准弃用政策进行删除。
Database存储镜像元数据，您可以根据自己的喜好选择数据库，大多数部署使用MySQL或SQLite。
Storage repository for image files支持各种存储库类型，包括常规文件系统（或安装在glance-api控制器节点上的任何文件系统），Object Storage，RADOS块设备，VMware数据存储和HTTP。
请注意，某些存储库仅支持只读用法。
Metadata definition service供应商、管理员、服务和用户的通用API，用于定义自己的元数据。此元数据可用于不同类型的资源，如 images、artifacts、volumes、flavors和aggregates。定义包括新属性的关键字、描述、约束和它可以关联的资源类型。
3. 安装和配置本节介绍如何在控制器节点上安装和配置镜像服务（glance）。为简单起见，此配置将镜像存储在本地文件系统上。
3.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
3.2. 数据库1、登录数据库mysql -uroot -p，密码为openstack。
2、创建glance数据库CREATE DATABASE glance;
3、授权GRANT ALL PRIVILEGES ON glance.* TO &#39;glance&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;openstack&#39;;
GRANT ALL PRIVILEGES ON glance.* TO &#39;glance&#39;@&#39;%&#39; IDENTIFIED BY &#39;openstack&#39;;
4、退出数据库exit;
3.3. 服务证书和端点1、使admin环境生效. admin-openrc
2、创建glance用户openstack user create --domain default --password-prompt glance
根据提示设置密码为openstack。
3、将admin角色添加到glance用户和service项目上openstack role add --project service --user glance admin
4、创建glance服务实体openstack service create --name glance --description &quot;OpenStack Image&quot; image
5、创建镜像服务API endpoints
openstack endpoint create --region RegionOne image public http://controller:9292openstack endpoint create --region RegionOne image internal http://controller:9292openstack endpoint create --region RegionOne image admin http://controller:9292

3.4. 安装和配置组件1、安装glanceapt -y install glance
2、备份glance-api.confmv /etc/glance/glance-api.conf /etc/glance/glance-api.conf.bak
3、vim /etc/glance/glance-api.conf，新建glance-api.conf文件内容为：
[DEFAULT]bind_host = 0.0.0.0[glance_store]default_store = filefilesystem_store_datadir = /var/lib/glance/images/[database]# MariaDB connection infoconnection = mysql+pymysql://glance:openstack@controller/glance# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = glancepassword = openstack[paste_deploy]flavor = keystone

4、备份glance-registry.confmv /etc/glance/glance-registry.conf /etc/glance/glance-registry.conf.bak
5、vim /etc/glance/glance-registry.conf，新建glance-registry.conf内容为：
[DEFAULT]bind_host = 0.0.0.0[database]# MariaDB connection infoconnection = mysql+pymysql://glance:openstack@controller/glance# Keystone auth info[keystone_authtoken]www_authenticate_uri = http://controller:5000auth_url = http://controller:5000memcached_servers = controller:11211auth_type = passwordproject_domain_name = defaultuser_domain_name = defaultproject_name = serviceusername = glancepassword = openstack[paste_deploy]flavor = keystone

6、更改权限
chmod 644 /etc/glance/glance-api.conf /etc/glance/glance-registry.confchown glance. /etc/glance/glance-api.conf /etc/glance/glance-registry.conf

7、生成glance数据库结构su -s /bin/bash glance -c &quot;glance-manage db_sync&quot;
完成后登录mysql查看数据。
查看日志命令：tail /var/log/glance/glance-api.log
tail /var/log/glance/glance-registry.log
8、重启镜像服务systemctl restart glance-api glance-registry
4. 验证操作1、使admin环境生效. admin-openrc
2、下载corris镜像wget http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img
3、上传corris镜像到glance，存储为QCOW2格式，并且设置为所有人可见
openstack image create &quot;cirros&quot; \  --file cirros-0.4.0-x86_64-disk.img \  --disk-format qcow2 --container-format bare \  --public


有关OpenStack映像创建参数的信息，请参阅OpenStack用户指南中的创建或更新映像（glance）。
有关映像的磁盘和容器格式的信息，请参阅OpenStack虚拟机映像指南中的映像的磁盘和容器格式。
4、查看镜像openstack image list
看到镜像ID、Name和Status。
cd /var/lib/glance/images，可以看到名为镜像ID的文件。

]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——keystone篇</title>
    <url>/dev-ubuntu16-manual-openstack-keystone/</url>
    <content><![CDATA[1. 目标紧接着《Ubuntu16手动安装OpenStack——环境篇》，本文我们来安装keystone，主要参考Keystone Installation Tutorial for Ubuntu。


2. keystone简介OpenStack身份识别服务集成了身份验证，授权和服务目录。
身份服务通常是用户与之交互的第一个服务。一旦通过身份验证，终端用户就可以使用他们的身份访问其他OpenStack服务。同样，其他OpenStack服务利用身份服务来确保用户是他们所说的人，并发现其他服务的位置。身份识别服务还可以与一些外部用户管理系统（如LDAP）集成。
用户和服务可以通过使用由身份服务管理的服务目录来定位其他服务。顾名思义，服务目录是OpenStack部署中可用服务的集合。每个服务可以有一个或多个端点，每个端点可以是以下三种类型之一：admin，internal或public。在生产环境中，出于安全原因，不同类型的终端类型可能会驻留在暴露给不同类型用户的单独网络中。例如，公共API网络可能从互联网上可见，因此客户可以管理他们的云。管理API网络可能仅限于管理云基础架构的组织中的运营商。内部API网络可能仅限于包含OpenStack服务的主机。另外，OpenStack支持多个区域的可伸缩性。为简单起见，本指南针对所有端点类型和默认的RegionOne区域使用管理网络。在身份服务中创建的区域，服务和端点一起构成部署的服务目录。部署中的每个OpenStack服务都需要一个服务条目，并在Identity服务中存储相应的端点。这可以在Identity Service安装和配置完成后完成。
身份服务包含以下组件：服务器集中式服务器使用RESTful接口提供认证和授权服务。
驱动程序驱动程序或服务后端集成到中央服务器。它们用于访问OpenStack外部存储库中的身份信息，并且可能已存在于部署OpenStack的基础架构中（例如，SQL数据库或LDAP服务器）。
模块中间件模块运行在使用Identity服务的OpenStack组件的地址空间中。这些模块拦截服务请求，提取用户凭据并将其发送到中央服务器进行授权。中间件模块和OpenStack组件之间的集成使用Python Web服务器网关接口。
3. 安装记录本节记录如何在控制节点上安装和配置代号为keystone的OpenStack Identity服务。出于可伸缩性的目的，此配置会部署Fernet令牌和Apache HTTP服务器来处理请求。
3.1. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
3.2. 数据库配置1、登录mariadbmysql -uroot -p，密码为openstack。
2、创建keystone数据库CREATE DATABASE keystone;
3、创建keystone用户，密码为openstack，并授权访问keystone数据库GRANT ALL PRIVILEGES ON keystone.* TO &#39;keystone&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;openstack&#39;;
GRANT ALL PRIVILEGES ON keystone.* TO &#39;keystone&#39;@&#39;%&#39; IDENTIFIED BY &#39;openstack&#39;;
4、退出exit;
5、测试登录
mysql -h localhost -ukeystone -p
mysql -h controller -ukeystone -p
3.3. 安装组件1、安装keystone、apache2和libapache2-mod-wsgiapt -y install keystone  apache2 libapache2-mod-wsgi
2、vim /etc/keystone/keystone.conf编辑配置：
# line 606: uncomment and specify Memcache Servermemcache_servers = controller:11211# line 740: change ( MariaDB connection info )connection = mysql+pymysql://keystone:openstack@controller/keystone# line 2891: add[token]provider = fernet

3、生成keystone数据库的数据
su -s /bin/bash keystone -c &quot;keystone-manage db_sync&quot;
（非root用户执行sudo keystone-manage db_sync）
查看日志：tail /var/log/keystone/keystone-manage.log，看到done说明执行成功。
不放心的话，可以登录mariadb查看keystone数据库的数据，有数据的话说明执行成功。
如果在日志中看到报错：
2018-06-30 18:37:40.845 4964 WARNING oslo_db.sqlalchemy.engines [-] SQL connection failed. 6 attempts left.: DBConnectionError: (pymysql.err.OperationalError) (2003, &quot;Can&#x27;t connect to MySQL server on &#x27;controller&#x27; ([Errno 111] Connection refused)&quot;) (Background on this error at: http://sqlalche.me/e/e3q8)

这是因为，mysql可能绑定了IP。执行netstat -an | grep 3306查看，如果只看到127.0.0.1，那么说明确实绑定了IP。
解决办法是编辑50-server.cnf，把bind-address = 127.0.0.1注释掉，然后重启mariadb，重新执行命令。
4、初始化Fernet密钥存储库keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
keystone-manage credential_setup --keystone-user keystone --keystone-group keystone
5、引导身份服务，管理密码为openstack
keystone-manage bootstrap --bootstrap-password openstack \  --bootstrap-admin-url http://controller:5000/v3/ \  --bootstrap-internal-url http://controller:5000/v3/ \  --bootstrap-public-url http://controller:5000/v3/ \  --bootstrap-region-id RegionOne


3.4. 配置apache1、vim /etc/apache2/apache2.conf，添加：
# line 70: specify server nameServerName controller

2、重启apacheservice apache2 restart
3、新建环境变量配置keystonerc
export OS_USERNAME=adminexport OS_PASSWORD=openstackexport OS_PROJECT_NAME=adminexport OS_USER_DOMAIN_NAME=Defaultexport OS_PROJECT_DOMAIN_NAME=Defaultexport OS_AUTH_URL=http://controller:5000/v3export OS_IDENTITY_API_VERSION=3

4、使环境变量生效source keystonerc
5、测试echo $OS_USERNAME
4. 使用记录4.1. 创建域Identity服务为每个OpenStack服务提供身份验证服务。身份验证服务使用域，项目，用户和角色的组合。
1、引导身份服务步骤中已存在“默认”域，但创建新域的正式方法是：openstack domain create --description &quot;An Example Domain&quot; example
虚拟机中执行成功，但是实体机报错：
Failed to discover available identity versions when contacting http://controller:5000/v3. Attempting to parse version from URL.Internal Server Error (HTTP 500)

netstat -an | grep 5000，发现5000端口服务已经启动。
查看keystone日志，没有新的内容，说明根本没有连接成功。
尝试了重新导入数据，重新安装数据库，都失败了。
如果看到同样的错误，那么极有可能之前安装devstack遗留的问题，最终重装系统后再次安装，执行成功。
2、在default域下创建service项目：openstack project create --domain default --description &quot;Service Project&quot; service
3、常规（非管理员）任务应使用非特权项目和用户。创建demo项目：openstack project create --domain default --description &quot;Demo Project&quot; demo
4、创建demo用户：openstack user create --domain default --password-prompt demo
按照提示设置密码为openstack。
5、创建user角色：openstack role create user
6、将user角色添加到demo项目和demo用户openstack role add --project demo --user demo user
7、查看域、项目、角色、用户
openstack domain listopenstack project listopenstack role listopenstack user list

5. 验证操作在安装其他服务之前验证Identity服务的操作。
1、取消设置临时OS_AUTH_URL和OS_PASSWORD环境变量：unset OS_AUTH_URL OS_PASSWORD
2、作为admin用户，请求身份验证令牌：
openstack --os-auth-url http://controller:5000/v3 \  --os-project-domain-name Default --os-user-domain-name Default \  --os-project-name admin --os-username admin token issue

按照提示输入密码openstack。
3、作为demo用户，请求身份验证令牌：
openstack --os-auth-url http://controller:5000/v3 \  --os-project-domain-name Default --os-user-domain-name Default \  --os-project-name demo --os-username demo token issue

按照提示输入密码openstack。
6. 创建OpenStack客户端环境脚本前面几节使用了环境变量和命令选项的组合，通过openstack客户端与Identity服务进行交互。为了提高客户端操作的效率，OpenStack支持简单的客户端环境脚本，也称为OpenRC文件。这些脚本通常包含所有客户端的常用选项，但也支持独特的选项。
6.1. 创建脚本为admin和demo项目和用户创建客户端环境脚本。接下来引用这些脚本来加载客户端操作的适当凭据。这些脚本在控制节点和计算节点都适用。
1、创建并编辑admin-openrc文件并添加以下内容：
export OS_PROJECT_DOMAIN_NAME=Defaultexport OS_USER_DOMAIN_NAME=Defaultexport OS_PROJECT_NAME=adminexport OS_USERNAME=adminexport OS_PASSWORD=openstackexport OS_AUTH_URL=http://controller:5000/v3export OS_IDENTITY_API_VERSION=3export OS_IMAGE_API_VERSION=2

2、创建并编辑demo-openrc文件并添加以下内容：
export OS_PROJECT_DOMAIN_NAME=Defaultexport OS_USER_DOMAIN_NAME=Defaultexport OS_PROJECT_NAME=demoexport OS_USERNAME=demoexport OS_PASSWORD=openstackexport OS_AUTH_URL=http://controller:5000/v3export OS_IDENTITY_API_VERSION=3export OS_IMAGE_API_VERSION=2

6.2. 使用脚本要将客户端作为特定项目和用户运行，只需在运行它们之前加载关联的客户端环境脚本即可。例如：
1、加载admin-openrc文件以使用Identity服务的位置以及管理项目和用户凭据填充环境变量：. admin-openrc或者source admin-openrc
2、请求身份验证令牌：openstack token issue


]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16手动安装OpenStack——环境篇</title>
    <url>/dev-ubuntu16-manual-openstack-env/</url>
    <content><![CDATA[1. 前言《Ubuntu16安装OpenStack》一文中，使用devstack在物理机上安装了OpenStack的Queens版本。但是，在后来的《OpenStack添加镜像》实验中，遇到了很多奇怪的问题，遂决定重装OpenStack。
本次安装，计划采用一步步手动安装的方式，在两个节点上，最小安装OpenStack的Queens版本。主要参考OpenStack Queens Installation Guides，首先配置环境，然后安装必要的组件，包括keystone、glance、nova、neutron。再之后，选择安装horizon、cinder和swift。
本文，就记录一下配置环境的过程。


2. 准备本次安装使用两台VirtualBox虚拟机，一个控制节点和一个计算节点，系统均为ubuntu-16.04.4-server-amd64。控制节点1核4G内存40G存储，计算节点1核2G内存40G存储。
这两台虚拟机，都有三块网卡。eth0负责主机间相互通信，eth1负责上网，eth2负责neutron网络服务。两张eth0网卡在同一网络，两张eth2网卡在同一网络。
2.1. IP配置参考VirtualBox下CentOS7和Ubuntu16.04网络配置，修改网卡名，然后配置静态IP。控制节点eth0配置为192.168.56.110，计算节点eth0配置为192.168.56.111。
2.2. 主机名配置参考VirtualBox下CentOS7和Ubuntu16.04网络配置，配置控制节点主机名为controller，计算节点主机名为compute。
然后编辑/etc/hosts，添加：
192.168.56.110  controller192.168.56.111  compute

2.3. 设置sudo免密编辑sudoers，vim /etc/sudoers在最后添加一行：
test ALL = NOPASSWD: ALL

2.4. 更换源列表修改/etc/apt/sources.list，参考Ubuntu更换源列表，更换为Ubuntu官方中国的源列表。
最后重启主机，sudo reboot。
2.5. root用户为了避免权限问题，建议切换到root用户进行操作（否则要加很多sudo）。sudo -i
3. 环境在安装openstack之前，需要先安装好必须的环境，包括：

Security
Host networking
Network Time Protocol (NTP)
OpenStack packages
SQL database
Message queue
Memcached
Etcd

安装方法参考Environment。
3.1. SecurityOpenStack服务支持各种安全方法，包括密码，策略和加密。
本节没有什么安装操作，过。
3.2. Host networking在选择部署的体系结构的每个节点上安装操作系统后，必须配置网络接口。openstack官方建议禁用任何自动网络管理工具，并手动编辑网络配置文件。
本文准备工作中已经配置好了IP，过。
3.3. NTPNTP服务主要用来各节点之间同步时间。
3.3.1. 控制节点1、安装chronyapt -y install chrony
2、配置chronyvim /etc/chrony/chrony.conf
取消两处注释：
allow 0/0allow ::/0

3、重启chronyservice chrony restart
3.3.2. 计算节点1、安装chronyapt -y install chrony
2、配置chronyvim /etc/chrony/chrony.conf注释修改如下：
#pool 2.debian.pool.ntp.org offline iburstserver controller iburst

3、重启chronyservice chrony restart
3.3.3. 验证安装官方建议在继续操作之前验证NTP同步。某些节点，尤其是那些引用控制器节点的节点，可能需要几分钟才能同步。1、在控制节点执行chronyc sources
“Name/IP”列中的内容表示NTP服务器的主机名或IP地址。MS列中的内容，*代表当前NTP服务器同步成功。
2、在计算节点执行chronyc sources“Name/IP”列中的内容应该表示控制器节点的主机名。
3.4. OpenStack packages由于发布计划不同，发行版将OpenStack软件包作为发行版的一部分或使用其他方法发布。在所有节点上执行这些操作。
1、安装openstack queens仓库apt install software-properties-common
add-apt-repository cloud-archive:queens
2、更新软件包apt update &amp;&amp; apt dist-upgrade
3、安装openstack-clientapt -y install python-openstackclient
3.5. SQL database大多数OpenStack服务使用SQL数据库来存储信息，数据库通常在控制器节点上运行。本文使用MariaDB（MySQL），OpenStack服务还支持其他SQL数据库，包括PostgreSQL。
1、安装mariadb和连接工具apt -y install mariadb-server python-pymysql
2、编辑50-server.cnfvim /etc/mysql/mariadb.conf.d/50-server.cnf
命令模式下输入:set nu显示行号：
# line 29: changebind-address = 0.0.0.0# line 105: changecharacter-set-server = utf8 #collation-server = utf8mb4_general_ci

3、重启mariadb关闭mariadb，service mysql stop。然后查看netstat -an | grep 3306，确认3306已经关闭。
启动mariadb，service mysql start。
4、设置mariadb密码mysql_secure_installation
按照提示设置密码为openstack。如果设置后无法登录，那么参考MySQL重置密码。
3.6. Message queueOpenStack使用消息队列来协调服务之间的操作和状态信息。消息队列服务通常在控制器节点上运行。OpenStack支持多种消息队列服务，包括RabbitMQ，Qpid和ZeroMQ。这里我们安装RabbitMQ，因为大多数发行版都支持它。
1、安装rabbitmsqapt -y install rabbitmq-server
2、创建openstack用户，密码为openstackrabbitmqctl add_user openstack openstack
3、允许openstack用户配置和读写rabbitmqctl set_permissions openstack &quot;.*&quot; &quot;.*&quot; &quot;.*&quot;
3.7. MemcachedOpenStack的各个服务的身份认证服务使用Memcached缓存令牌。memcached服务通常在控制器节点上运行。对于生产部署，我们建议启用防火墙，身份验证和加密的组合来保护它。
1、安装memcachedapt -y install memcached python-memcache
2、编辑/etc/memcached.conf文件-l 127.0.0.1修改为-l 0.0.0.0
3、重启memcachedservice memcached restart
4、测试
telnet controller 11211statsquit

3.8. EtcdOpenStack服务可能使用Etcd，它是一个可靠的分布式键值存储工具。用于分布式密钥锁定，存储配置，跟踪服务的实时性等。同样安装在控制节点即可。
1、安装etcdapt -y install etcd
2、查看版本etcd -version，这里看到是2.2.5。按照Etcd for Ubuntu文档配置的话，最终会失败，所以，我们换一种配置方法。
3、查看服务状态etcdctl member list
应该会看到：
ce2a822cea30bfca: name=controller peerURLs=http://localhost:2380,http://localhost:7001 clientURLs=http://localhost:2379,http://localhost:4001

4、官方文档给出的/etc/etcd/etcd.conf.yml文件内容为：
name: controllerdata-dir: /var/lib/etcdinitial-cluster-state: &#x27;new&#x27;initial-cluster-token: &#x27;etcd-cluster-01&#x27;initial-cluster: controller=http://controller:2380initial-advertise-peer-urls: http://controller:2380advertise-client-urls: http://controller:2379listen-peer-urls: http://0.0.0.0:2380listen-client-urls: http://controller:2379

参考etcd.conf.yml，我们在/etc/default/etcd的最后添加：
ETCD_NAME=&quot;controller&quot;ETCD_DATA_DIR=&quot;/var/lib/etcd/default&quot;ETCD_INITIAL_CLUSTER_STATE=&quot;new&quot;ETCD_INITIAL_CLUSTER_TOKEN=&quot;etcd-cluster-01&quot;ETCD_INITIAL_CLUSTER=&quot;controller=http://controller:2380&quot;ETCD_INITIAL_ADVERTISE_PEER_URLS=&quot;http://controller:2380&quot;ETCD_ADVERTISE_CLIENT_URLS=&quot;http://controller:2379,http://127.0.0.1:2379&quot;ETCD_LISTEN_PEER_URLS=&quot;http://0.0.0.0:2380&quot;ETCD_LISTEN_CLIENT_URLS=&quot;http://controller:2379,http://127.0.0.1:2379&quot;

5、设置开机启动systemctl enable etcd
6、重启etcdsystemctl restart etcd
7、查看服务状态etcdctl member list
会看到发生了变化：
ce2a822cea30bfca: name=controller peerURLs=http://localhost:2380,http://localhost:7001 clientURLs=http://127.0.0.1:2379,http://controller:2379

8、在控制节点和计算节点测试curl http://controller:2379/v2/stats/leader
可以看到：
&#123;&quot;leader&quot;:&quot;ce2a822cea30bfca&quot;,&quot;followers&quot;:&#123;&#125;&#125;

4. 后记如果修改了/etc/hosts中的controller对应的IP，那么需要重启memcached和etcd，因为它们的配置中使用了主机名而不是IP。
至此，两台虚拟机上，openstack需要的环境已经安装完成。接下来，准备安装keystone。
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：OpenStack添加镜像</title>
    <url>/dev-openstack-add-mirror/</url>
    <content><![CDATA[1. 前言本文实验最终失败，失败，失败！重要的事情说三遍。
想要用openstack创建两台ubuntu16的虚拟机，于是上传了ubuntu-16.04.4-server-amd64.iso镜像，然后用它创建了实例，分配了浮动IP。理论上，应该可以访问了。但是，ping该IP会提示Destination Host Unreachable。
找到了制作 OpenStack Linux 镜像 - 每天5分钟玩转 OpenStack（151）一文，发现作者使用的是cloud镜像。十有八九是镜像的原因了，那就尝试一下。


2. 下载cloud镜像最简单的方法是使用标准镜像。主流的Linux发行版都提供可以在OpenStack中直接使用的cloud镜像，下面有几个下载地址：

CentOS6：http://cloud.centos.org/centos/6/images/
CentOS7：http://cloud.centos.org/centos/7/images/
Ubuntu14.04：http://cloud-images.ubuntu.com/trusty/current/
Ubuntu16.04：http://cloud-images.ubuntu.com/xenial/current/

这里我们访问 http://cloud-images.ubuntu.com/xenial/current/ ，然后下载xenial-server-cloudimg-amd64-disk1.img。
3. 导入镜像1、在horizon控制台，项目，计算，镜像，创建镜像。
2、镜像上传完成，即可看到新添加的镜像。
3、使用ubuntu16的镜像创建实例u16-n0。
4、测试登录。给实例分配浮动IP为172.24.4.10，在ccrfox105上可以ping通。然后，查看日志，想要找到用户名和密码。但是，不同于cirros，在ubuntu16日志中并没有找到。
参考openstack入门二十六：创建实例，发现大多数 cloud images支持公钥授权而不是传统的用户名密码授权。
4. 使用密钥4.1. dashboard方法1、项目，计算，密钥对，创建密钥对。
2、输入密钥名ccrfox105-key，创建密钥对，然后公钥就会上传到openstack，显示在控制台。同时会自动下载私钥到本地，名为ccrfox105-key.pem。
3、重新创建实例u16-n0（这次不要忘记选择密钥对），然后分配浮动IP为172.24.4.10。
4、通过scp把ccrfox105-key.pem上传到ccrfox105scp ccrfox105-key.pem test@ccrfox105:~
5、等待实例启动完全（5分钟左右），在ccrfox105测试登录ssh ubuntu@172.24.4.10 -i ccrfox105-key.pem
报错：修改了密钥权限为600，依然无法登录。
换成root用户，ssh root@172.24.4.10 -i ccrfox105-key.pem，依然无法登录。
尴尬，这是什么鬼？
4.2. 命令行方法1、切换到stack用户sudo su - stack
2、生成密钥对ssh-keygen，三次回车。
3、添加公钥到openstack环境nova keypair-add --pub-key ~/.ssh/id_rsa.pub ccrfox105-stack-key
报错：
ERROR (CommandError): You must provide a username or user ID via --os-username, --os-user-id, env[OS_USERNAME] or env[OS_USER_ID]

参考[完整部署CentOS7.2+OpenStack+kvm 云平台环境（5）–问题解决]，需要source admin-openrc.sh，然而我的系统中并没有admin-openrc.sh这个文件。
（1）新建admin-openrc.sh，内容如下：
export OS_TENANT_NAME=adminexport OS_USERNAME=adminexport OS_PASSWORD=secretexport OS_AUTH_URL=http://172.16.0.105:35357/v2.

（2）执行source admin-openrc.sh
（3）再次执行nova keypair-add --pub-key ~/.ssh/id_rsa.pub ccrfox105-stack-key进行测试，错误变了
No handlers could be found for logger &quot;keystoneauth.identity.generic.base&quot;ERROR (DiscoveryFailure): Could not determine a suitable URL for the plugin

（4）sudo netstat -antupo | egrep &#39;(5000|35357)&#39;，发现35357端口服务没有启动。
（5）ps -ef | grep keystone，查看keystone服务，已经启动。
（6）vim /etc/keystone/keystone.conf，打开几处注释：
admin_bind_host = 0.0.0.0admin_port = 35357public_bind_host = 0.0.0.0public_port = 5000

（6）重启keystone，sudo /etc/init.d/apache2 restart
然后，没有用。
4.3. dashboard与命令行结合1、切换到stack用户sudo su - stack
2、生成密钥对ssh-keygen，三次回车。
3、拷贝公钥到本地
4、在openstack的dashboard导入公钥。
5、创建实例，选择新导入的公钥。
6、再次测试登录。ssh ubuntu@172.24.4.10 -i ~./ssh/id_rsassh root@172.24.4.10 -i ~./ssh/id_rsa
和dashboad方法报同样的错误：Permission denied (publickey).
至此，三种方法全部失败，猜测openstack的keystone服务没有安装好，留个坑吧。
5. 不使用密钥参考如何在OpenStack上安装Ubuntu系统、Openstack使用官方ubuntu和Centos镜像和openstack中镜像的密码修改，配置过程如下：
创建实例，创建过程中不添加密钥对，而是配置脚本：
#!/bin/sh  sed -i &#x27;s/PermitRootLogin without-password/PermitRootLogin yes/g&#x27; /etc/ssh/sshd_config  sed -i &#x27;s/PasswordAuthentication no/PasswordAuthentication yes/g&#x27; /etc/ssh/sshd_config  cp -f /home/ubuntu/.ssh/authorized_keys /root/.ssh/  service ssh restart  passwd ubuntu&lt;&lt;EOF  123456123456EOF 

如下图：
然后，ssh ubuntu@172.24.4.10，Permission denied (publickey).
如果是CentOS，那么输入：
#!/bin/sh  sed -i &#x27;s/PermitRootLogin without-password/PermitRootLogin yes/g&#x27; /etc/ssh/sshd_config  sed -i &#x27;s/PasswordAuthentication no/PasswordAuthentication yes/g&#x27; /etc/ssh/sshd_config  cp -f /home/centos/.ssh/authorized_keys /root/.ssh/  service sshd restart  passwd centos&lt;&lt;EOF  123456123456EOF


6. 后记本次实验过程中，各种报错不断，大部分都没有找到解决方案。
最坑的是，除了cirros，其他镜像在实例创建成功后都无法登录，尝试了各种办法也没有解决，只能无奈放弃。
控制台也无法访问，参考OpenStack 控制台不能访问的问题进行配置，依然无法访问。
怀疑是因为openstack没有安装完整，毕竟安装完是有报错的，看来有必要重新安装一下openstack了。然后再写一篇《OpenStack添加镜像2.0》版本。
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：Ubuntu16安装OpenStack</title>
    <url>/dev-ubuntu16-openstack/</url>
    <content><![CDATA[1. 前言在《VirtualBox中安装OpenStack》一文中，已经成功安装过openstack。如今，确定了研究方向就是OpenStack。第一步要做的，就是在实体机安装配置OpenStack。
由于版本更新，devstack目前只支持ubuntu16，而机房服务器使用的都是ubuntu14.04，很尴尬。尝试了旧的newton、mitaka、liberty，但是devstack依赖的keystone项目中没有了这三个分支，失败；尝试了新的pike、queens，但是ubuntu14中缺少相应的包，失败。
找到了DevStack 安装 grizzly-eol 版本 OpenStack一文，以为在local.conf中把分支改为tag就可以了。然而，报错接连不断，解决了N个小怪，随便出来个大BOSS就我拦住了。
最终，把系统换成了Ubuntu16，计划使用devstack安装最新版Queens，主要参考DevStack官方文档、《优雅安装OpenStack》和使用devstack安装OpenStack 双节点部署。


2. 目标本文的目标是搭建一个 all-in-one OpenStack，所有核心服务都安装在ccrfox105节点上，节点IP为172.16.0.105。
核心服务包括：身份认证服务keystone，镜像服务glance，计算服务nova（默认使用KVM虚拟化），网络服务neutron，仪表板horizon。也需要包含一些支持服务，例如：SQL数据库，消息队列和NTP。
3. 环境准备3.1. 更换sources.list（可选）参考《Ubuntu更换源列表》。
1、备份源列表文件sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
2、编辑源列表文件sudo vim /etc/apt/sources.list
修改为：
deb http://mirrors.aliyun.com/ubuntu/ xenial main restricted universe multiverse  deb http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted universe multiverse  deb http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted universe multiverse  deb http://mirrors.aliyun.com/ubuntu/ xenial-proposed main restricted universe multiverse  deb http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ xenial main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ xenial-proposed main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse

3、更新资源包sudo apt-get update
3.2. 时间同步1、同步时区执行命令sudo dpkg-reconfigure tzdata，然后选择Asia，Shanghai。
2、安装时间同步工具
sudo apt-get install ntpdatesudo ntpdate cn.pool.ntp.orgdate

3.3. devstack下载1、安装gitsudo apt-get install git
2、下载devstack并切换到queens分支git clone https://git.openstack.org/openstack-dev/devstack -b stable/queens
3.4. 创建stack用户方法一：1、执行创建用户脚本sudo devstack/tools/create-stack-user.sh
2、将devstack目录放到/opt/stack中并设置权限sudo mv devstack /opt/stack
sudo chown -R stack:stack /opt/stack
3、切换到stack用户sudo su - stack
方法二：1、添加stack用户sudo useradd -s /bin/bash -d /opt/stack -m stack
2、给stack用户添加sudo权限echo &quot;stack ALL=(ALL) NOPASSWD: ALL&quot; | sudo tee /etc/sudoers.d/stack
3、将devstack目录放到/opt/stack中并设置权限sudo mv devstack /opt/stack
sudo chown -R stack:stack /opt/stack
4、切换到stack用户sudo su - stack
3.5. 更换pip源（可选）参考《python pip更换国内源》。
1、安装pythonsudo apt-get install python
2、创建pip.confmkdir ~/.pip &amp;&amp; vim ~/.pip/pip.conf
写入内容如下：
[global]index-url = http://mirrors.aliyun.com/pypi/simple/[install]trusted-host = http://mirrors.aliyun.com/pypi/simple/

3.6. 编译安装1、拷贝local.confcd devstack &amp;&amp; cp samples/local.conf ./
2、修改local.conf密码配置为：
ADMIN_PASSWORD=secretDATABASE_PASSWORD=$ADMIN_PASSWORDRABBIT_PASSWORD=$ADMIN_PASSWORDSERVICE_PASSWORD=$ADMIN_PASSWORD

同时在最后添加：
GIT_BASE=http://git.trystack.cnNOVNC_REPO=http://git.trystack.cn/kanaka/noVNC.gitSPICE_REPO=http://git.trystack.cn/git/spice/spice-html5.git

3、拷贝local.shcp samples/local.sh ./
4、执行安装./stack.sh
安装报错：E: Unable to locate package libsystemd-dev。（1）安装libsystemd-dev，sudo apt install libsystemd-dev，没有找到libsystemd-dev这个包。
（2）查找libsystemd-dev，sudo apt search libsystemd-dev，果然没有找到libsystemd-dev。
（3）猜测是sources.list的锅，改成原sources.list，问题解决。
然后报错pip版本太低，升级却失败，删除.pip目录，问题解决。。。
接下来，顺利安装，最后报错：
2018-06-27 00:47:54.156 | More than one SecurityGroup exists with the name &#x27;default&#x27;.2018-06-27 00:47:55.193 | More than one SecurityGroup exists with the name &#x27;default&#x27;.2018-06-27 00:47:55.246 | ++./stack.sh:main:1390                   err_trap2018-06-27 00:47:55.254 | ++./stack.sh:err_trap:551                   local r=12018-06-27 00:47:55.261 | stack.sh failed: full log in /opt/stack/logs/stack.sh.log.2018-06-27-0828302018-06-27 00:47:55.264 | Error on exit

参考如何修改默认OpenStack安全组中的规则，查看securitygroup规则，openstack security group list
在多租户OpenStack环境中，存在多个名为“default”的安全组。在这种情况下，请使用安全组ID而不是安全组名称。云管理员可以使用OpenStack安全组列表来显示所有安全组及其当前分配的名称。
3.7. 再次安装我觉得上面的安装好像失败了，打算重装。1、先在devstack目录中，执行卸载
./unstack.sh./clean.sh

2、再次安装./stack.sh
最后还是报同样的错误。不管了，测试下看看。
3.8. 测试使用1、访问 http://172.16.0.105/dashboard ，用户名输入admin，密码输入secret，登录控制台。
2、项目，网络，创建网络和路由
3、项目，计算，创建实例
报错：
Error: Failed to perform requested operation on instance &quot;vm1&quot;, the instance has an error status: Please try again later [Error: Host &#x27;ccrfox105&#x27; is not mapped to any cell].

我们查看一下openstack的服务是否正常：
openstack service listopenstack endpoint listopenstack compute service list

提示：Missing value auth-url required for auth plugin password解决办法：进入devstack目录，执行
source openrc voidking projectsource openrc admin admin

虽然Missing value auth-url required for auth plugin password的问题解决了，但是还是无法创建实例。
参考OpenStack应用、报错，执行nova-manage cell_v2 discover_hosts，问题解决。
3.9. 网络问题实例创建成功了，也分配了浮动IP为172.24.4.7，但是从ccrfox105上却ping不通实例。参考Openstack创建实例–horizon篇，进行如下设置：
1、项目，网络，安全组，创建安全组。
2、管理规则，添加出口入口icmp规则和tcp规则。
3、实例的安全组选择新建的规则。
然后，再次ping 172.24.4.7，网络就通了。
3.10. 测试访问1、项目，计算，实例，实例名称，日志。即可看到cirros系统的用户名和密码。
2、然后在ccrfox105上，ssh登录cirros系统。
3.11. 卸载devstack如果devstack安装失败或者不再需要，那么可以对它进行卸载。
执行./unstack.sh和./clean.sh，卸载openstack。然后删除stack用户，删除/opt/stack目录。
或者，干脆重装系统，因为卸载后会有很多遗留问题。
4. 后记至此，完成了单节点OpenStack的安装，也进行了简单地创建实例和测试，nice。
接下来有两个计划，一个是尝试OpenStack更多的操作和设置，以便熟悉这个平台。另一个是使用devstack安装多节点OpenStack，或者手动安装多节点Openstack。计划搭建一个Controller节点和两个Compute节点。
在安装时，自作聪明地更换了sources.list和pip.conf，以为可以加速安装，没想到却给自己挖了坑，同学们引以为鉴啊。
]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
        <tag>devstack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux系统中使用VirtualBox</title>
    <url>/dev-linux-virtualbox/</url>
    <content><![CDATA[1. 前言在windows图形界面上使用virtualbox很简单，而在没有图形的界面的linux系统上，同样可以使用virtualbox，只不过操作方式变成了命令行。
本文我们学习一下在ubuntu16系统下使用virtualbox的方法。
参考文档：

How to Install Oracle VirtualBox 5.2 on Ubuntu 17.10/16.04, Debian 9/8
VirtualBox官网下载页面
Manage Virtualbox virtual machines under command line
How to run VirtualBox virtual machines from the command line



2. 安装virtualbox1、添加安装源
sudo add-apt-repository &quot;deb http://download.virtualbox.org/virtualbox/debian `lsb_release -cs` contrib&quot;

2、安装包签名密钥
wget -q https://www.virtualbox.org/download/oracle_vbox_2016.asc -O- | sudo apt-key add -wget -q https://www.virtualbox.org/download/oracle_vbox.asc -O- | sudo apt-key add -

3、执行安装
sudo apt-get updatesudo apt-get install virtualbox-5.2

报错：
virtualbox-5.2 is already the newest version (5.2.12-122591~Ubuntu~xenial).You might want to run &#x27;apt-get -f install&#x27; to correct these:The following packages have unmet dependencies: virtualbox-5.2 : Depends: libcurl3 (&gt;= 7.16.2) but it is not going to be installed                  Depends: libgl1-mesa-glx but it is not going to be installed or                           libgl1                  Depends: libx11-xcb1 but it is not going to be installed                  Depends: libxcursor1 (&gt; 1.1.2) but it is not going to be installed                  Depends: libxinerama1 but it is not going to be installed                  Recommends: libsdl-ttf2.0-0 but it is not going to be installed                  Recommends: pdf-viewer                  Recommends: libgl1E: Unmet dependencies. Try &#x27;apt-get -f install&#x27; with no packages (or specify a solution).

执行sudo apt-get -f install，修复包依赖问题。
3. 使用virtualbox3.1. 常用命令参考virtualbox命令行。

vboxmanage -v，查看版本。
vboxmanage showvminfo &quot;Windows XP&quot;，查看某一虚拟机的信息。
vboxmanage startvm &quot;Windows XP&quot;，开启虚拟机（指定name）。
vboxmanage startvm 670e746d-abea-4ba6-ad02-2a3b043810a5，开启虚拟机（指定UUID）。
vboxmanage startvm ubuntu gui，图形化界面启动（这是默认值）。
vboxmanage startvm ubuntu headless，命令行界面启动。
vboxmanage list vms，列出所有的虚拟机。
vboxmanage list runningvms，列出现在正在运行的虚拟机。
vboxmanage list hostinfo，列出宿主机的详细信息（CPU 内存 操作系统版本等）。
vboxmanage list hostonlyifs，列出系统的hostonly网络的信息。
vboxmanage list dhcpservers，DBCP服务器的信息。

3.2. 创建虚拟机很麻烦，就不实践了。具体用法参考VirtualBox命令行VBoxManage创建与管理虚拟机教程。
4. 图形界面操作virtualbox详情参考文档《Ubuntu中配置使用VNCServer》
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>ubuntu</tag>
        <tag>virtualbox</tag>
      </tags>
  </entry>
  <entry>
    <title>Ubuntu中配置使用VNCServer</title>
    <url>/dev-ubuntu-vncserver/</url>
    <content><![CDATA[1. 前言《Linux下使用VirtualBox》一文中，使用命令行操作virtualbox，有很多不方便的地方，比如创建虚拟机就很麻烦。
能不能在远程linux使用VirtualBox的时候，通过图形界面使用呢？可以的。要远程使用图形界面，服务端需要安装vncserver，客户端需要安装vncclient。
参考文档：

XShell+Xmanager实现在XShell中显示远程服务器的图形界面
Ubuntu14.04服务器:远程桌面连接
Linux下VNC配置使用总结：开启+桌面配置+安全访问

2. xfce4桌面参考文档Ubuntu如何安装vncserver。
1、安装xfce4桌面系统（twm桌面）apt-get install xfce4 　　xfce4桌面系统非常简单，界面只有一个灰色背景加上一个shell窗口。
2、安装vnc4server和xrdp
apt-get install -y vnc4server xrdp

VNC4Server 是一个基于 VNC（Virtual Network Computing）协议的远程桌面服务器软件，它允许用户通过 VNC 客户端在远程计算机上查看和操作图形界面。VNC4Server 通过在远程计算机上创建虚拟显示设备并将图形渲染传输到客户端来实现远程桌面功能。
XRDP 是一种远程桌面协议，它允许用户通过 Microsoft 的 RDP（Remote Desktop Protocol）协议访问远程计算机的桌面。XRDP 提供了在 Linux 系统上实现 RDP 功能的方式。
XRDP 与 VNC4Server 结合使用，以在 Linux 系统上提供远程桌面访问。通常的配置是，XRDP 充当 RDP 服务器，接受来自 Windows 或其他 RDP 客户端的连接请求，并将这些连接请求转发到 VNC4Server。VNC4Server 在远程 Linux 计算机上运行，负责处理图形界面渲染，并将渲染的结果传输回 RDP 客户端。这样，用户可以使用 RDP 客户端连接到远程 Linux 计算机并在其中运行图形应用程序。
3、配置默认使用xfce4作为登录界面
echo &quot;xfce4-session&quot; &gt;~/.xsession

4、重启xrdp
service xrdp restart

5、启动vncserver
vncserver
根据提示设置远程密码，默认放在/root/.vnc/passwd中，同时生成/root/.vnc/xstartup。其中:1表示在5900的基础上加1，就是vnc端口。
xstartup内容为：
#!/bin/sh# Uncomment the following two lines for normal desktop:# unset SESSION_MANAGER# exec /etc/X11/xinit/xinitrc[ -x /etc/vnc/xstartup ] &amp;&amp; exec /etc/vnc/xstartup[ -r $HOME/.Xresources ] &amp;&amp; xrdb $HOME/.Xresourcesxsetroot -solid greyvncconfig -iconic &amp;x-terminal-emulator -geometry 80x24+10+10 -ls -title &quot;$VNCDESKTOP Desktop&quot; &amp;x-window-manager &amp;

6、本地打开vncviewer client，输入ip:5901进行连接。连接成功后发现，桌面是灰色背景，桌面上只有一个shell。这就是传说中的twm界面。
7、启动virtualbox在shell中，输入virtualbox
至此，就能愉快地使用virtualbox图形界面了。
3. gnome桌面上面的xfce4桌面，可以满足我们使用virtualbox的需求。但是，这款桌面实在太简单了，快捷键只能切换窗口（Alt+Tab），无法快捷打开terminal，右键鼠标啥也没有。所以，还得折腾下，搞个更好用的桌面，参考文档Ubuntu如何安装vncserver。
1、关闭vncserver
vncserver --listvncserver -kill :1

2、安装gnome桌面
apt-get install -y gnome-panel gnome-settings-daemon metacity nautilus gnome-terminal

3、修改xstartup
cp /root/.vnc/xstartup&#123;,.xfce4&#125;vim /root/.vnc/xstartup

修改后内容如下：
#!/bin/shexport XKL_XMODMAP_DISABLE=1  unset SESSION_MANAGER  unset DBUS_SESSION_BUS_ADDRESS  gnome-panel &amp;  gnome-settings-daemon &amp;  metacity &amp;  nautilus &amp;  gnome-terminal &amp;

4、启动vncserver
vncserver

5、连接vncservergonme桌面，已经比xfce4好了很多，但是还是比较简陋。
4. ubuntu-desktop桌面参考文档：ubuntu20.04 vncservr配置
1、关闭vncserver
vncserver --listvncserver -kill :1

2、安装ubuntu-desktop桌面
apt-get install ubuntu-desktopls -l /usr/share/xsessions/

3、修改xstartup
cp /root/.vnc/xstartup&#123;,.gnome&#125;ls /usr/share/xsessions/cat /usr/share/xsessions/ubuntu.desktopvim /root/.vnc/xstartup

修改后内容如下：
#!/bin/shexport XKL_XMODMAP_DISABLE=1  unset SESSION_MANAGER  unset DBUS_SESSION_BUS_ADDRESS  # Set the default window managerexport XDG_SESSION_DESKTOP=ubuntuexport XDG_DATA_DIRS=/usr/share/ubuntu:/usr/local/share:/usr/share:/var/lib/snapd/desktopexport XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg# Start the window managerexec /usr/bin/gnome-session

其中最关键的是exec /usr/bin/gnome-session，这个值是在/usr/share/xsessions/ubuntu.desktop中获取到的。
4、启动vncserver
vncserver# orvncserver :1 -localhost no# orvncserver :1 -xstartup /root/.vnc/xstartup -localhost no

再次连接后，看到的就是正常的桌面了，nice。
5、修改窗口大小vncserver默认的窗口大小为1024x768，使用时如果感觉窗口比较小，可以进行修改。vim /usr/bin/vncserver，如下修改：
# line 44, change$geometry = &quot;1280x1024&quot;;
然后kill掉已经启动的vncserver，启动新的即可。
5. 错误解决在某次远程桌面连接时，提示too many security failures，解决办法参考vncserver too many security failures。
1、重置黑名单
vncconfig -display :1 -set BlacklistTimeout=0 -set BlacklistThreshold=1000000

2、连接成功后还原黑名单
vncconfig -display :1 -set BlacklistTimeout=600000000000 -set BlacklistThreshold=10




]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>virtualbox</tag>
      </tags>
  </entry>
  <entry>
    <title>Ganglia配置Grid架构</title>
    <url>/dev-ganglia-grid/</url>
    <content><![CDATA[1. Ganglia简介《Ubuntu14.04安装配置Ganglia》一文中，我们已经学会了安装配置ganglia的方法。但是，我们的配置方法，只适用于一个集群，如果我们有多个集群怎么办？本文就来探讨一下。


2. Ganglia层级参考YOUYOU深入学习Ganglia之一（建立Grid），可以得到如下结论：
Ganglia从数据中心的结构上划分了三个层次：Node，Cluster，Grid。一句话概括：多个Node组成一个Cluster，多个Cluster组成一个Grid。
3. 配置Grid如果只有一个Cluster，没有添加Grid配置，那么这个Cluster默认在名为“Grid”的Grid下。
如上图，假设我们有两个Cluster，名为Cloud Computing和k8s，它们的管理机分别是cluster1-master和cluster2-master。除此之外，还有一台Grid管理机为grid-master。
cluster1-master的IP为192.168.56.101，cluster2-master的IP为192.168.56.201，grid-master的IP为192.168.56.56。
现在，我们想要把Cloud Computing和k8s这两个Cluster合并成一个Grid。
1、cluster1-master和cluster2-master配置允许共享gmated的XML文件，同时把grid-master地址设置为信任的主机IP。sudo vi /etc/ganglia/gmetad.conf
找到：
# List of machines this gmetad will share XML with. Localhost# is always trusted. # default: There is no default value# trusted_hosts 127.0.0.1 169.229.50.165 my.gmetad.org# If you want any host which connects to the gmetad XML to receive# data, then set this value to &quot;on&quot;# default: off# all_trusted on
修改为：
# List of machines this gmetad will share XML with. Localhost# is always trusted. # default: There is no default valuetrusted_hosts 192.168.56.56# If you want any host which connects to the gmetad XML to receive# data, then set this value to &quot;on&quot;# default: offall_trusted on

2、cluster1-master和cluster2-master重启gmetadsudo /etc/init.d/gmetad restart
3、grid-master的gmetad，要添加数据源。sudo vi /etc/ganglia/gmetad.conf
找到data_source，添加：
data_source &quot;Cloud Computing&quot; 10 192.168.56.101:8649 192.168.56.102:8649data_source &quot;k8s&quot; 10 192.168.56.201:8649 192.168.56.202:8649

4、grid-master重启gmetadsudo /etc/init.d/gmetad restart
然后，就能在grid-master的gweb上看到一个Grid，包含两个Cluster。
对比最开始时的图可以发现，cluster1-master的gweb标题为“Cloud Computing Cluster Report at…”，而grid-master的gweb标题为“unspecified Grid Report at…”。之所以unspecified，是因为我们在cluster1-master和cluster2-master的gmetad中没有指定Grid的名字。
5、在cluster1-master和cluster2-master上修改gmetad，设置Grid名称为”Center of Cloud Computing”。sudo vi /etc/ganglia/gmetad.conf
找到gridname，打开注释，修改为：
gridname &quot;Center of Cloud Computing&quot;

6、cluster1-master、cluster2-master、grid-master重启gmetadsudo /etc/init.d/gmetad restart
然后，访问grid-master的gweb，看到Grid有了名字。
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>监控</tag>
        <tag>ubuntu</tag>
        <tag>ganglia</tag>
      </tags>
  </entry>
  <entry>
    <title>批量重命名和批量替换字符串</title>
    <url>/dev-batch-rename-and-replace/</url>
    <content><![CDATA[1. 前言完成了《Hexo添加categories页面》，接下里的工作，就是给以前写的文章进行重新分类。以前分为五大类：设计开发、点滴发现、生活札记、精华转载和读者集萃。现在打算分成四大类：随笔（essay）、专业（dev）、英语（en）和爱好（hobby），然后在每个大类下再按需要细分为小类。
分类需要修改md文件的文件名和文件内容，面对近五百篇文章，必须要批量处理。


2. 批量修改文件名假设要把所有以“disc-”开头的文件改为“dev-”开头，那么在linux下执行命令：
# macosbrew install renamerename &#x27;s/disc-/dev-/&#x27; *# ubunturename &#x27;s/disc-/dev-/&#x27; *# centosrename disc- dev- *

3. 批量修改文件内容1、假设我们要把文件中的“设计开发”改为“专业”，那么执行命令：
sed -i &quot;s/设计开发/专业/g&quot; `grep 设计开发 -rl .`

2、假设我们要把文件中的http://7oxjrx.com1.z0.glb.clouddn.com改为http://cdn.voidking.com，那么执行命令：
sed -i &quot;s/http:\/\/7oxjrx.com1.z0.glb.clouddn.com/http:\/\/cdn.voidking.com/g&quot; `grep http:\/\/7oxjrx.com1.z0.glb.clouddn.com -rl .`# 或者sed -i &#x27;s!http://7oxjrx.com1.z0.glb.clouddn.com!http://cdn.voidking.com!g&#x27; `grep http:\/\/7oxjrx.com1.z0.glb.clouddn.com -rl .`

3、假设我们要把文件中的https://www.voidking.com/2018/06/11/dev-hexo-categories/改为https://www.voidking.com/dev-hexo-categories/，这个就比较难了，可以先进行调试：
grep -P &#x27;\d&#123;4&#125;\/\d&#123;2&#125;\/\d&#123;2&#125;\/&#x27; -rl .echo &quot;https://www.voidking.com/2018/06/11/dev-hexo-categories/&quot; | sed &quot;s/\d&#123;4&#125;\/\d&#123;2&#125;\/\d&#123;2&#125;\///g&quot;echo &quot;https://www.voidking.com/2018/06/11/dev-hexo-categories/&quot; | sed &quot;s/\([0-9][0-9][0-9][0-9]\)\/\([0-9][0-9]\)\/\([0-9][0-9]\)\///g&quot; 

因为grep和sed都不支持完全的正则表达式，所以grep要加P参数，sed要改写正则表达式。最后得到的命令为：
sed -i &quot;s/\([0-9][0-9][0-9][0-9]\)\/\([0-9][0-9]\)\/\([0-9][0-9]\)\///g&quot; `grep -P &#x27;\d&#123;4&#125;\/\d&#123;2&#125;\/\d&#123;2&#125;\/&#x27; -rl .`

4、假设我们想要把 [engineering,devops] 替换成 [专业,开发,nginx]，那么执行命令（请使用linux而不是mac）：
sed -i &quot;s/专业,开发,nginx/engineering,devops/g&quot; `grep &#x27;专业,开发,nginx&#x27; -rl .`




]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>macos</tag>
        <tag>shell</tag>
        <tag>hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo添加categories页面</title>
    <url>/dev-hexo-categories/</url>
    <content><![CDATA[1. 前言随着文章越来越多，在网站中寻找文章越来越麻烦。现在有两个方案可供选择：一个是建立搜索系统；另一个是建立分类检索页面。于是，郝同学选择了更简单的后者，本文就记录一下创建categories检索页面的过程。


2. 目标以郝同学的网站为例，左侧导航栏很小。这就意味着，我们不能像其他其他主题一样，直接把分类显示在导航栏，不然会很丑。
所以，我们需要一个categories页面，在这个页面里进行分类显示。
3. 实践3.1. 添加categories链接假设主题是yilia，那么打开yilia/_config.yml文件，找到：
# Headermenu:  主页: /  归档: /archives  关于: /about  # 随笔: /tags/随笔

修改为：
# Headermenu:  主页: /  分类: /categories  归档: /archives  关于: /about  # 随笔: /tags/随笔

3.2. categories页面1、新建categories页面hexo new page categories该命令在source目录下生成一个categories目录，categories目录下有一个index.md文件。
2、修改categories/index.md为：
---title: 文章分类date: 2018-06-11 10:13:21type: &quot;categories&quot;comments: false---

3、生成htmlhexo g，hexo s
4、测试访问访问 http://localhost:4000/categories/ ，即可看到categories页面，只不过现在的页面只有标题。
参考创建分类页面 和 hexo next 为文章添加分类，发现next主题在生成categories后直接就完成了分类显示。而我们的yilia，明显不行，那就自己动手，风衣足食。
3.3. 修改yilia主题1、修改categories/index.md为：
---title: 文章分类date: 2018-06-11 10:13:21type: &quot;categories&quot;layout: &quot;categories&quot;comments: false---

指定layout为categories，渲染时就会使用categories.ejs进行渲染。
2、新建yilia/layout/categories.ejs，内容如下：
&lt;article class=&quot;article article-type-post show&quot;&gt;  &lt;header class=&quot;article-header&quot;&gt;  &lt;h1 class=&quot;article-title&quot; itemprop=&quot;name&quot;&gt;    &lt;%= page.title %&gt;  &lt;/h1&gt;  &lt;/header&gt;  &lt;% if (site.categories.length)&#123; %&gt;  &lt;div class=&quot;category-all-page article-type-post show&quot;&gt;    &lt;h3&gt;共计&amp;nbsp;&lt;%= site.categories.length %&gt;&amp;nbsp;个分类&lt;/h3&gt;    &lt;ul class=&quot;category-list&quot;&gt;    &lt;% site.categories.sort(&#x27;name&#x27;).each(function(item)&#123; %&gt;      &lt;% if(item.posts.length)&#123; %&gt;        &lt;li class=&quot;category-list-item&quot;&gt;          &lt;a href=&quot;&lt;%- config.root %&gt;&lt;%- item.path %&gt;&quot; title=&quot;&lt;%= item.name %&gt;&quot;&gt;&lt;%= item.name %&gt;&lt;sup&gt;[&lt;%= item.posts.length %&gt;]&lt;/sup&gt;&lt;/a&gt;        &lt;/li&gt;      &lt;% &#125; %&gt;    &lt;% &#125;); %&gt;    &lt;/ul&gt;  &lt;/div&gt;  &lt;% &#125; %&gt;&lt;/article&gt;

3、新建yilia/source/css/_partial/categories.styl，内容如下：
.category-all-page &#123;  margin: 30px 40px 30px 40px;  position: relative;  min-height: 70vh;  h3&#123;    margin: 20px 0;  &#125;  .category-all-title &#123; text-align: center; &#125;  .category-all &#123; margin-top: 20px; &#125;  .category-list &#123;    margin: 0;    padding: 0;    list-style: none;  &#125;  .category-list-item &#123; margin: 10px 10px; &#125;  .category-list-count &#123;    color: $grey;    &amp;:before &#123;      display: inline;      content: &quot; (&quot;    &#125;    &amp;:after &#123;      display: inline;      content: &quot;) &quot;    &#125;  &#125;  .category-list-child &#123; padding-left: 10px; &#125;&#125;

4、在yilia/source/css/style.styl引入categories.styl：
@import &quot;_extend&quot;@import &quot;_partial/main&quot;@import &quot;_partial/archive&quot;@import &quot;_partial/article&quot;@import &quot;_partial/archive&quot;@import &quot;_partial/highlight&quot;@import &quot;_partial/footer&quot;@import &quot;_partial/share&quot;@import &quot;_partial/page&quot;@import &quot;_partial/instagram&quot;@import &quot;_partial/tagcloud&quot;@import &quot;_partial/scroll&quot;@import &quot;_partial/mobile-slider&quot;@import &quot;_partial/categories&quot;

5、测试访问再次访问 http://localhost:4000/categories/ ，发现已经成功显示分类。
3.4. 多层分类以上，已经完成了categories分类页面，但是只有一层分类。假设，现在有一篇文章的分类为多层分类，例如：
---title: Linux设置邮件提醒toc: truedate: 2018-05-24 11:00:00tags:- linux- ubuntu- centos- mailcategories: - [专业,测试]---

显示的效果为所有类别平级显示，不是我们想要的效果，如下图：
本节就实现多层分类的显示效果，具体操作如下：
1、修改yilia/layout/categories.ejs为：
&lt;article class=&quot;article article-type-post show&quot;&gt;  &lt;header class=&quot;article-header&quot; style=&quot;border-bottom: 1px solid #ccc&quot;&gt;  &lt;h1 class=&quot;article-title&quot; itemprop=&quot;name&quot;&gt;    &lt;%= page.title %&gt;  &lt;/h1&gt;  &lt;/header&gt;  &lt;% if (site.categories.length)&#123; %&gt;  &lt;div class=&quot;category-all-page&quot;&gt;    &lt;h2&gt;共计&amp;nbsp;&lt;%= site.categories.length %&gt;&amp;nbsp;个分类&lt;/h2&gt;    &lt;%- list_categories(site.categories, &#123;      show_count: true,      class: &#x27;category-list-item&#x27;,      style: &#x27;list&#x27;,      depth: 2,      separator: &#x27;&#x27;    &#125;) %&gt;  &lt;/div&gt;  &lt;% &#125; %&gt;&lt;/article&gt;

2、yilia/source/css/_partial/categories.styl修改为：
.category-all-page &#123;  margin: 30px 40px 30px 40px;  position: relative;  min-height: 70vh;  h2&#123;    margin: 20px 0;  &#125;  .category-all-title &#123; text-align: center; &#125;  .category-all &#123; margin-top: 20px; &#125;  .category-list &#123;    margin: 0;    padding: 0;    list-style: none;  &#125;  .category-list-item-list-item&#123;    margin: 10px 15px;  &#125;  .category-list-item-list-count&#123;    color: $grey;    &amp;:before &#123;      display: inline;      content: &quot; (&quot;    &#125;    &amp;:after &#123;      display: inline;      content: &quot;) &quot;    &#125;  &#125;  .category-list-item &#123; margin: 10px 10px; &#125;  .category-list-count &#123;    color: $grey;    &amp;:before &#123;      display: inline;      content: &quot; (&quot;    &#125;    &amp;:after &#123;      display: inline;      content: &quot;) &quot;    &#125;  &#125;  .category-list-child &#123; padding-left: 10px; &#125;&#125;

3、再次访问categories，达到了预期效果，如下图：
4. 后记多看了几眼next主题，比几年前棒多了，现在也是一个很好的选择，适合小白。不过，它的项目结构也变得更加复杂，不方便个性化修改。
5. 书签Hexo文档
Hexo变量
Hexo之list-categories
hexo-auto-category
Hexo主题实现多级分类显示
Hexo使用攻略：（四）Hexo的分类和标签设置
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>Windows安装配置Gym</title>
    <url>/dev-windows-gym/</url>
    <content><![CDATA[1. 前言最近在研究强化学习，各种概念公式，让人心烦意乱。打算搞点有意思的东西，提高下学习的热情。
听说用强化学习玩游戏很有意思，不妨搞一下。第一步，安装OpenAI Gym，本文做下记录。


2. 安装篇2.1. 环境准备win10系统，python3.6.3。
2.2. 更换pip源参考python pip更换国内源，打开%appdata%目录，新建pip目录，pip目录下新建pip.ini文件，内容为：
[global]index-url = https://mirrors.aliyun.com/pypi/simple/[install]trusted-host = https://mirrors.aliyun.com/pypi/simple/

2.3. 下载安装参考gym官方文档，使用一条命令安装即可：pip install gym
3. 测试篇1、新建python脚本demo.py：
import gymenv = gym.make(&#x27;CartPole-v0&#x27;)env.reset()for _ in range(1000):    env.render()    env.step(env.action_space.sample()) # take a random action

2、运行脚本python demo.py

4. 后记gym安装配置很简单，接下来，就可以愉快地进行游戏训练了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>gym</tag>
        <tag>rflearning</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：使用PSSH批量管理Linux进阶篇</title>
    <url>/dev-pssh-manage-linux-advance/</url>
    <content><![CDATA[1. 前言《使用PSSH批量管理Linux》一文中，学习了pssh的基本用法，实际上已经涵盖了pssh的大部分内容。
本文主要记录利用pssh和expect脚本来解决一些实际问题，就算是进阶篇了。


2. 批量添加密钥《添加密钥认证访问》一节中，已经写清楚了挨个给客户机添加密钥的方法。但是，在客户机数量非常多的时候，挨个添加也很麻烦，写个脚本吧！（假设所有客户机用户都是test，密码都是123456）
参考无密钥登录的自动脚本实现 和 expect - 自动交互脚本
2.1. 基线版本1、新建脚本addkey.sh，内容为：
#!/usr/bin/expect set timeout 60set password 123456spawn ssh-copy-id -i /home/test/.ssh/id_rsa.pub -p 22 test@192.168.56.101expect &quot;install the new keys&quot;send &quot;\r&quot;expect &quot;password:&quot;send &quot;$password\r&quot;expect eof

2、执行脚本chmod a+x addkey.sh
./addkey.sh
注意：

如果密码是六个空格，那么set password &quot;      &quot;即可。
expect &quot;install the new keys&quot;这里其实是不需要交互的，但是密码会直接出现在这里，所以加一个回车。

2.2. 升级版本在使用ssh-copy-id命令的时候，有两种情况，一种是直接输入密码，就像基线版本；另一种情况是会先提示添加主机到known_hosts。
1、新建脚本addkey.sh，主要目的是测试，内容为：
#!/usr/bin/expect set timeout 60set password 123456spawn ssh-copy-id -i /home/test/.ssh/id_rsa.pub -p 22 test@192.168.56.101expect &#123;    #first connect, no public key in ~/.ssh/known_hosts    &quot;Are you sure you want to continue connecting (yes/no)?&quot; &#123;        send &quot;yes\r&quot;; exp_continue    &#125;    #already has public key in ~/.ssh/known_hosts    &quot;install the new keys&quot; &#123;        send &quot;\r&quot;; exp_continue    &#125;    &quot;password:&quot; &#123;        send &quot;$password\r&quot;; exp_continue    &#125;    &quot;Now try logging into the machine&quot; &#123;        #it has authorized, do nothing!        ; exp_continue    &#125;&#125;

2、执行脚本chmod a+x addkey.sh
./addkey.sh
2.3. 批量版本1、升级脚本，新建addkeys.sh，内容为：
#!/usr/bin/expect set timeout 60set password 123456for &#123;set i 101&#125; &#123;$i &lt; 201&#125; &#123;incr i&#125; &#123;    spawn ssh-copy-id -i /home/test/.ssh/id_rsa.pub -p 22 test@192.168.56.$i    expect &#123;        #first connect, no public key in ~/.ssh/known_hosts        &quot;Are you sure you want to continue connecting (yes/no)?&quot; &#123;            send &quot;yes\r&quot;; exp_continue        &#125;        #already has public key in ~/.ssh/known_hosts        &quot;install the new keys&quot; &#123;            send &quot;\r&quot;; exp_continue        &#125;        &quot;password:&quot; &#123;            send &quot;$password\r&quot;; exp_continue        &#125;        &quot;Now try logging into the machine&quot; &#123;            #it has authorized, do nothing!            ; exp_continue        &#125;    &#125;&#125;

2、执行脚本chmod a+x addkeys.sh
./addkeys.sh
2.4. 批量删除添加完密钥之后，发现部分机器不能免密登录。检查客户机中的authorized_keys文件，发现添加了两次密钥。于是决定删除重新添加。
1、管理机新建脚本delkeys.sh
#!/usr/bin/expect set timeout 30set password 123456for &#123;set i 101&#125; &#123;$i &lt; 201&#125; &#123;incr i&#125; &#123;    spawn ssh test@192.168.56.$i &quot;rm /home/test/.ssh/authorized_keys&quot;    expect &#123;        &quot;password:&quot; &#123; send &quot;$password\r&quot;&#125;        &quot;No route to host&quot; &#123; send &quot;\r&quot; &#125;    &#125; &#125;

2、执行脚本chmod a+x delkeys.sh
./delkeys.sh
注意，执行完之后检查一下最后一台客户机，也许会删除失败，不知道为什么。
2.5. 批量修改权限删除后重新添加，依然无法登录，后来发现，是目录权限的问题。用户目录权限是750，~/.ssh权限是700， ~/.ssh/authorized_keys权限是600。
1、权限修改脚本chmods.sh
#!/usr/bin/expect set timeout 30set password 123456for &#123;set i 101&#125; &#123;$i &lt; 201&#125; &#123;incr i&#125; &#123;    spawn ssh test@192.168.56.$i -t &quot;sudo chmod 750 /home/test/ &amp;&amp; sudo chmod 700 /home/test/.ssh &amp;&amp; sudo chmod 600 /home/test/.ssh/authorized_keys&quot;    expect &#123;        &quot;password:&quot; &#123; send &quot;$password\r&quot;; exp_continue &#125;        &quot;password for test&quot; &#123; send &quot;$password\r&quot;; exp_continue &#125;        &quot;No route to host&quot; &#123; send &quot;\r&quot; &#125;    &#125;&#125;

2、执行脚本chmod a+x chmods.sh
./chmods.sh
2.6. 修改StrictModes设置完权限，依然无法免密登录，猜测是StrictModes的问题。
1、编辑sshd_configsudo vi /etc/ssh/sshd_config
2、找到StrictModes yes，改成StrictModes no。
3、重启sshd，sudo service ssh restart
然后，依然失败。
2.7. 调试1、不服，进行ssh登录调试，ssh -v test@192.168.56.102或者ssh -vvv test@192.168.56.102。
debug1: Authentications that can continue: publickey,passworddebug1: Next authentication method: publickeydebug1: Offering RSA public key: /home/test/.ssh/id_rsadebug1: Authentications that can continue: publickey,passworddebug1: Trying private key: /home/test/.ssh/id_dsadebug1: Trying private key: /home/test/.ssh/id_ecdsadebug1: Trying private key: /home/test/.ssh/id_ed25519debug1: Next authentication method: password

一番折腾，失败，失败，失败。。。
2、参考ssh免密登录失败问题排查思路，在客户机查看日志，cat /var/log/auth.log。
调试方法：（1）客户机执行tail -f /var/log/auth.log（2）管理机执行ssh -v test@192.168.56.102
然而，各种尝试依然失败，失败，失败。。。
3、然后找到一篇重建home分区后出现的ssh公钥认证失败问题，猜测是SElinux的问题，于是在客户机执行：
sudo apt-get install policycoreutils
restorecon -r -vv /home/
然而，依然没有用。
2.8. 使用dsa密钥1、生成dsa密钥ssh-keygen -t dsa -P &#39;&#39; -f ~/.ssh/id_dsa
2、添加公钥到客户机ssh-copy-id -i /home/test/.ssh/id_dsa.pub -p 22 test@192.168.56.102
3、管理机测试登录ssh test@192.168.56.102
依然失败。换一个管理机重新添加密钥，依然失败，看来确实是客户机的锅。好吧，放弃了，不玩了！
3. 设置sudo免密以上，假设已经实现了批量添加密钥，也就是实现了所有客户机的免密登录。但是，在客户机执行sudo命令的时候，会提示输入密码，也是很麻烦，所以接下来设置sudo免密执行。
1、管理机新建脚本addsudo.sh，内容为：
#!/usr/bin/expect set timeout 10set password 123456for &#123;set i 101&#125; &#123;$i &lt;= 200&#125; &#123;incr i&#125; &#123;    spawn ssh test@192.168.56.$i -t &quot;sudo -i&quot;    expect &quot;password for test:&quot;    send &quot;$password\r&quot;    expect &quot;*#&quot;    send &quot;echo test ALL = NOPASSWD: ALL &gt;&gt; /etc/sudoers \r&quot;    expect &quot;*#&quot;    send &quot;exit\r&quot;&#125;

2、执行脚本chmod a+x addsudo.sh
./addsudo.sh
4. 批量修改密码《批量修改密码》一节中，我们采用的修改密码的方式是给所有的客户机都安装expect，然后发送脚本到客户机并执行脚本。给每台机器安装expect，不是一个好的做法。不妨参照设置sudo免密，在管理机执行脚本，虽然无法并行，但是胜在不需要给客户机安装expect。
不过，这次我们换一种脚本写法，使用bash脚本循环调用expect脚本。这种写法更好，更加稳定。因为在expect脚本中写循环语句，有些字符串会不显示，影响执行效果。
1、新建expect脚本m-chpasswd.sh
#!/usr/bin/expect set timeout 10set newpassword 12345678set i [lindex $argv 0]spawn ssh test@192.168.56.$i -t &quot;sudo passwd test&quot;expect &quot;Enter new UNIX password:&quot;send &quot;$newpassword\r&quot;expect &quot;Retype new UNIX password:&quot;send &quot;$newpassword\r&quot;expect &quot;closed&quot; send &quot;\r&quot;expect eof

2、新建bash脚本loop-passwd.sh
#!/bin/bashfor ((i=101;i&lt;=200;i++))do    echo &quot;begin...&quot;    echo $i    ./m-chpasswd.sh $i    echo &quot;end...&quot;done

3、执行脚本chmod a+x m-chpasswd.sh
chmod a+x loop-passwd.sh
./loop-passwd.sh
5. 批量修改hosts《批量修改hostname》一节中，我们给三台机器设置了主机名，比较简单。现在假设我们有100台机器需要管理，IP为192.168.56.101-200，现在要给它们设置主机名，同时修改它们的/etc/hosts文件，方便使用主机名相互寻找。
5.1. 自动生成hosts1、在管理机新建脚本makehosts.sh，内容为：
#!/bin/bashecho &quot;127.0.0.1    localhost&quot; &gt; /home/test/hostsNUM=101while [ $NUM -lt 201 ]do    echo &quot;192.168.56.$NUM    vk$NUM&quot; &gt;&gt; /home/test/hosts    NUM=`expr $NUM + 1`done

2、执行脚本sudo chmod a+x makehosts.sh
./makehosts.sh
3、替换hostssudo cp hosts /etc/hosts
PS：添加一条新的记录到 /etc/hosts ，sudo bash -c &quot;echo 192.168.56.201 vk201 &gt;&gt; /etc/hosts&quot;
5.2. 修改客户机hosts1、拷贝hosts文件到客户机pscp -h hosts.txt ./hosts /home/test
2、替换hosts文件pssh -h hosts.txt -i &quot;sudo cp /home/test/hosts /etc/hosts&quot;
6. 后记本文在写作之前，思路很明确，以为可以半天搞定，没想到遇到了一个大坑：部分机器无法免密登录。大大小小试了十几种解决办法，未果，折腾了一整天，还是无奈放弃。
设置sudo免密时还有一个方案，就是写一个脚本发送到客户机，在客户机执行修改/etc/sudoers文件。但是要求所有客户机都安装了expect，这个很麻烦，不如在管理机操作。
7. 20180614更新今天，突然冒出一个想法，如果换一个用户呢？不再用test，而是使用test2用户。于是，进行了测试。
1、客户机新建test2用户sudo useradd -m test2 -s /bin/bash
sudo passwd test2
根据需要给予sudo权限，sudo adduser test2 sudo
2、管理机添加免密登录ssh-copy-id -i .ssh/id_rsa.pub -p 22 test2@192.168.56.102
3、测试登录ssh test2@192.168.56.102
登录成功！！！
那么，如果我删除test用户，再添加新的test用户，是不是也可以呢？
1、客户机删除test用户sudo userdel -r test提示userdel: user test is currently used by process 62901
2、查看最近登录情况w，last
并没有人在使用这个账号，那就继续想办法删除。
3、使用vipwsudo vipw，找到test用户那行，删除。
sudo vipw -s，找到test用户那行，删除。
4、删除test目录cd /home &amp;&amp; sudo rm -rf test
5、新建test用户sudo useradd -m test -g test -s /bin/bash
sudo passwd test
sudo adduser test sudo
7、管理机添加免密登录ssh-copy-id -i .ssh/id_rsa.pub -p 22 test@192.168.56.102
8、测试登录ssh test@192.168.56.102
登录成功！！！问题完美结局，不过确实很麻烦。
如果，单纯地拷贝其他客户机的整个test用户目录呢？假设客户机A的IP为192.168.56.102，已经配置完成，可以免密登录；客户机B的IP为192.168.56.103，不可以免密登录。
1、删除客户机B的test目录下所有文件rf -rf ./*
rf -rf ./.*
2、拷贝配置好的客户机A的test目录到客户机Bscp test@192.168.56.102:~/.* .
scp test@192.168.56.102:~/.ssh/* .ssh/
3、在管理机测试登录ssh test@192.168.56.103
登录成功！！！这个方法明显更好，nice。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
        <tag>pssh</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux设置邮件提醒</title>
    <url>/dev-linux-mail/</url>
    <content><![CDATA[1. 前言《Linux设置定时任务》一文中，我们学习了使用crontab设置定时任务的方法，并用它来获取ganglia的运行状态。这样还不够，我们想要在ganglia的运行状态出现问题时通知我们，这样，我们就不用每天自己查看日志了。
参考文档：

Ubuntu mail命令发送邮件
如何通过Shell脚本实现发送邮件通知功能？
Linux命令大全——LMNOPQ
如何在Ubuntu环境下搭建邮件服务器（一）



2. 安装配置mail2.1. ubuntu1、安装mailutils
sudo apt-get install mailutils

2、General type of mail configuration 选择 Internet Site
3、System mail name 输入 mail.voidking.com
2.2. centos7安装配置过程参考 Centos 7 安装 Mail发送邮件
1、获取发送者邮箱授权密码登录 https://mail.163.com设置，POP3/SMTP/IMAP，授权密码管理
授权码是用于登录第三方邮件客户端的专用密码。适用于登录以下服务: 您开启的服务（例如POP3/IMAP/SMTP）、Exchange/CardDAV/CalDAV服务。
2、安装邮件MUA
yum install -y mailx

3、配置 /etc/mail.rc，在尾部添加：
set from=quizthink@163.comset smtp=smtps://smtp.163.com:465set smtp-auth=loginset smtp-auth-user=quizthink@163.comset smtp-auth-password=xxxset ssl-verify=ignoreset nss-config-dir=/root/.certs

不要使用25端口，阿里云服务器和163邮件服务器25端口不通。
4、配置数字证书
mkdir -p /root/.certs/echo -n | openssl s_client -connect smtp.163.com:465 | sed -ne &#x27;/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p&#x27; &gt; ~/.certs/163.crtcertutil -A -n &quot;GeoTrust SSL CA&quot; -t &quot;C,,&quot; -d ~/.certs -i ~/.certs/163.crtcertutil -A -n &quot;GeoTrust Global CA&quot; -t &quot;C,,&quot; -d ~/.certs -i ~/.certs/163.crtcertutil -L -d /root/.certscd /root/.certs/certutil -A -n &quot;GeoTrust SSL CA - G3&quot; -t &quot;Pu,Pu,Pu&quot; -d ./ -i 163.crt

5、测试发送邮件
echo &#x27;Welcome to mail world！&#x27; | mail -s &quot;Hello VoidKing&quot; voidking@qq.com
查看邮箱，正常的话应该已经收到邮件了。如果没有收到邮件，可以查看日志进行排查：
tail /var/log/maillog

2.3. macos参考文档：《MacOS配置发送邮件》
3. mail命令3.1. 命令格式mail [OPTION...] [address...]
常用到的选项，一个是s，代表主题；一个是A，代表附件。其他的需要时查看help。
3.2. 发送普通邮件目标：给邮箱 &#118;&#111;&#105;&#100;&#x6b;&#105;&#x6e;&#x67;&#x40;&#x71;&#113;&#x2e;&#x63;&#x6f;&#109; 发送邮件，主题为“Hello VoidKing”，内容为“Welcome to mail world！”
echo &#x27;Welcome to mail world！&#x27; | mail -s &quot;Hello VoidKing&quot; voidking@qq.com

但是，等待了几分钟，并没有收到邮件，垃圾箱也没有。mail，查看email的发送状态为Undelivered Mail Returned，原来没有投递出去。
查看邮件系统日志：
tail -n 50 /var/spool/mail/test
提示host mx3.qq.com[183.57.48.35] said: 550 Mail content denied。看来qq邮箱的垃圾邮件防御机制做的不错，那就换个163邮箱，没想到依然失败。
换一台服务器？发送给 &#x76;&#x6f;&#105;&#100;&#107;&#x69;&#x6e;&#x67;&#64;&#x71;&#113;&#46;&#99;&#111;&#109; ，成功！很尴尬。。。就这么着吧
3.3. 发送高级邮件1、从文本读入内容发送邮件
mail -s &#x27;Error report&#x27; voidking@qq.com &lt; /tmp/log/ganglia/error-20180524.log

2、发送附件
mail -s &#x27;Error report&#x27; voidking@qq.com -A /tmp/log/ganglia/ganglia-20180524.log &lt; /tmp/log/ganglia/error-20180524.log

3、指定发件人
echo &#x27;Welcome to mail world！&#x27; | mail -s &quot;Hello VoidKing&quot; voidking@qq.com -aFrom:haojin@qq.com

失败了，收不到邮件，不知道什么原因，留个坑吧。
4. 设置邮件提醒目标：每天获取一次ganglia的状态日志，然后通过checkganglia.sh脚本进行审阅。审阅完毕，发送审阅结果到管理员的邮箱。
修改checkganglia.sh脚本为：
#!/bin/bashDATE=`date +%Y%m%d`filename=&quot;ganglia-$&#123;DATE&#125;.log&quot;prefix=&quot;ganglia-$&#123;DATE&#125;&quot;hosts=`grep test@ /tmp/log/ganglia/$&#123;filename&#125; | wc -l`pids=`grep gmond.pid /tmp/log/ganglia/$&#123;filename&#125; | wc -l`if [ $&#123;hosts&#125; != 10 ]then    echo &quot;Some hosts are offline!&quot; &gt;&gt; /tmp/log/ganglia/error-$&#123;DATE&#125;.logfiif [ $&#123;hosts&#125; != $&#123;pids&#125; ]then    echo &quot;Some ganglia services have stopped!&quot; &gt;&gt; /tmp/log/ganglia/error-$&#123;DATE&#125;.log    cd /tmp/log/ganglia/    csplit /tmp/log/ganglia/$&#123;filename&#125; /test@/ -n2 -s &#123;*&#125; -f $&#123;prefix&#125; -b &quot;.log.%02d&quot;    rm $&#123;prefix&#125;.log.00    for file in /tmp/log/ganglia/$&#123;prefix&#125;.log.*    do     if [ -f &quot;$&#123;file&#125;&quot; ]    then        #echo &quot;$&#123;file&#125; is file&quot;        if [ `grep gmond.pid $&#123;file&#125; | wc -l` == 0 ]        then            echo `grep test@ $&#123;file&#125;` &gt;&gt; /tmp/log/ganglia/error-$&#123;DATE&#125;.log        fi    fi    donefi# mail infoif [ ! -f &quot;/tmp/log/ganglia/error-$&#123;DATE&#125;.log&quot; ]then    mail -s &quot;HappyDay$&#123;DATE&#125;&quot; voidking@qq.com &lt;&lt;&lt; &quot;All services are running!&quot;else    mail -s &quot;SadDay$&#123;DATE&#125;&quot; voidking@qq.com -A /tmp/log/ganglia/ganglia-$&#123;DATE&#125;.log &lt; /tmp/log/ganglia/error-$&#123;DATE&#125;.logfi

5. 后记至此，大功告成！《Linux设置定时任务》配合《Linux设置邮件提醒》，简直完美！两篇文章完成了一个小系统：定时生成ganglia日志，定时审阅ganglia日志，并且把审阅结果通过邮件发送给管理员。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux设置定时任务</title>
    <url>/dev-linux-crontab/</url>
    <content><![CDATA[1. 前言很多时候，我们需要定时执行一些任务，或者需要定时执行一些批量任务。本文我们来学习一下linux设置定时任务的方法。
参考文档：

Linux Crontab 定时任务
Linux定时任务Crontab命令详解
Linux 定时任务详解
Linux 定时任务调度(crontab)，太实用了！



2. 理论篇2.1. crond简介cron（crond）是linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程。linux系统上面原本就有非常多的计划性工作，因此这个系统服务是默认启动的。crond进程每分钟会定期检查是否有要执行的任务，如果有要执行的任务，则自动执行该任务。另外，由于使用者自己也可以设置计划任务，所以，linux系统也提供了使用者控制计划任务的命令：crontab命令。
crontab命令是cron table的简写，它是cron的配置文件，也可以叫它作业列表，我们可以在以下文件夹内找到相关配置文件。

/var/spool/cron/crontabs/ 目录下存放的是每个用户包括root的crontab任务，每个任务以创建者的名字命名。
/etc/crontab 这个文件负责调度各种管理和维护任务。
/etc/cron.d/ 这个目录用来存放任何要执行的crontab文件或脚本。
我们还可以把脚本放在/etc/cron.hourly、/etc/cron.daily、/etc/cron.weekly、/etc/cron.monthly目录中，让它每小时/天/星期/月执行一次。

linux下的任务调度分为两类，系统任务调度和用户任务调度。
系统任务调度：系统周期性所要执行的工作，比如写缓存数据到硬盘、日志清理等。/etc/crontab文件就是系统任务调度的配置文件。
用户任务调度：用户定期要执行的工作，比如用户数据备份、定时邮件提醒等。用户可以使用 crontab 工具来定制自己的计划任务。所有用户定义的crontab文件都被保存在 /var/spool/cron/crontabs/目录中，其文件名与用户名一致。
2.2. crontab文件假设我们使用的是Ubuntu14.04.5 Server版，查看/etc/crontab，内容为：
# /etc/crontab: system-wide crontab# Unlike any other crontab you don&#x27;t have to run the `crontab&#x27;# command to install the new version when you edit this file# and files in /etc/cron.d. These files also have username fields,# that none of the other crontabs do.SHELL=/bin/shPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin# m h dom mon dow user  command17 *    * * *   root    cd / &amp;&amp; run-parts --report /etc/cron.hourly25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / &amp;&amp; run-parts --report /etc/cron.daily )47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / &amp;&amp; run-parts --report /etc/cron.weekly )52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / &amp;&amp; run-parts --report /etc/cron.monthly )

第一行SHELL变量指定了系统要使用哪个shell；第二行PATH变量指定了系统执行 命令的路径。接下来的命令格式为：m h dom mon dow user  command英文全拼为：minute hour day month week user commond

minute：表示分钟，可以是从0到59之间的任何整数。
hour：表示小时，可以是从0到23之间的任何整数。
day：表示日期，可以是从1到31之间的任何整数。
month：表示月份，可以是从1到12之间的任何整数。
week：表示星期几，可以是从0到7之间的任何整数，这里的0或7代表星期日。
user：表示用户。
command：要执行的命令，可以是系统命令，也可以是自己编写的脚本文件。

注意，/var/spool/cron目录中的用户调度任务，没有user一项，因为文件名已经代表了user。
在以上各个字段中，还可以使用以下特殊字符：

星号（*）：代表所有可能的值，例如month字段如果是星号，则表示在满足其它字段的制约条件后每月都执行该命令操作。
逗号（,）：可以用逗号隔开的值指定一个列表范围，例如，“1,2,5,7,8,9”
中杠（-）：可以用整数之间的中杠表示一个整数范围，例如“2-6”表示“2,3,4,5,6”
正斜线（/）：可以用正斜线指定时间的间隔频率，例如“0-23/2”表示每两小时执行一次。
同时正斜线可以和星号一起使用，例如*/10，如果用在minute字段，表示每十分钟执行一次。

2.3. crontab命令crontab命令格式为：crontab [-u username] [file] [ -e | -l | -r ]

-u username，指定设置某个用户的crontab，省略则表示操作当前用户的crontab。
file，将file做为crontab的任务列表文件并载入crontab。如果没有指定这个文件，crontab命令将接受标准输入（键盘）上键入的命令，并将它们载入crontab。
-e，编辑某个用户的crontab文件内容。如果不指定用户，则表示编辑当前用户的crontab文件。
-l，显示某个用户的crontab文件内容，如果不指定用户，则表示显示当前用户的crontab文件内容。
-r，删除某个用户的crontab文件，如果不指定用户，则默认删除当前用户的crontab文件。

PS：第一次编辑 crontab 时，会提示选择默认编辑器，如果选择错了，可以使用下面的命令重新设置。
select-editor

2.4. crontab脚本对于比较长的命令，我们一般不写在crontab文件里，而是单独搞一个脚本，定时任务调用这个脚本。例如：
5 0 * * * /home/voidking/restart.sh
怎样测试定时任务是否好用？最简单的办法就是改一下定时任务调用时间为 * * * * * ，测试完成再改回去。如果发现手动执行 restart.sh 脚本可以成功，但是通过定时任务调用无法成功，那么大概率是因为环境变量问题。可以在 restart.sh 脚本里先调用一次 source ~/.bash_profile，或者执行env，然后把所有变量都写到 restart.sh 脚本中。详情参考《bash_profile和bashrc的区别》
需要注意的是，如果想要后台执行脚本，其中nohup可以省略。例如：
#0 12 * * * nohup /home/voidking/restart.sh &gt; /home/voidking/log/daily-`date +\%Y\%m\%d\%H\%M\%S`.log 2&gt;&amp;1 &amp;0 12 * * * /home/voidking/restart.sh &gt; /home/voidking/log/daily-`date +\%Y\%m\%d\%H\%M\%S`.log 2&gt;&amp;1 &amp;

3. 实践篇3.1. 设置系统时间设置定时任务和时间紧密相关，如果服务器的时区时间设置和本地不同，就不能保证计划任务的正确执行。所以使用crontab的第一步，是调节好服务器的时间。
下面参考Ubuntu 16.04将系统时间写入到硬件时间BIOS，对服务器时间进行调节。
时间是有时区的，无论硬件时间还是操作系统时间。hwclock的时区在/etc/default/rcS文件中设置，里面有一个参数UTC，默认值为yes，表示使用UTC时区，如果设置为no，那表示使用osclock的时区。建议hwclock与osclock设置相同的时区，也就是no。
1、查看服务器硬件时间
sudo hwclock -r

看到的时间格式为：Wed 23 May 2018 11:02:17 AM HKT  -0.031663 seconds
2、查看服务器系统时间
date
看到的时间格式为：Wed May 23 11:02:41 HKT 2018
3、设置hwclock和osclock时区相同
sudo vim /etc/default/rcS

找到UTC=yes，修改为UTC=no
4、将系统时间写入硬件时间
sudo hwclock -w

5、修改系统时区osclock的时区配置文件为/etc/timezone，不建议直接修改配置文件。
如果想修改为CST时间，那么执行sudo tzselect命令时，选择Asia-&gt;China-&gt;Beijing Time即可，这时会提示使用Asia/Shanghai时区。（ubuntu和centos通用）
或者直接替换 /etc/timezone ：
sudo cp /usr/share/zoneinfo/Asia/Shanghai  /etc/localtime

再次执行date，发现时区已经变成了CST。
6、从NTP Server同步时间
sudo yum install ntp # centossudo apt install ntpdate # ubuntusudo ntpdate ntp.ntsc.ac.cn

PS：时间同步也可以使用其他工具，比如 chronyd 。
更多NTP Server，可以从阿里云NTP服务器和腾讯云NTP服务器获取。
7、硬件时间同步
sudo hwclock -rsudo hwclock -w

3.2. 实例实例1：每分钟、每小时、每天、每周、每月、每年执行
* * * * * myCommand0 * * * * myCommand0 0 * * * myCommand0 0 * * 0 myCommand0 0 1 * * myCommand0 0 1 1 * myCommand

实例2：每小时的第3和第15分钟执行3,15 * * * * myCommand
实例3：在上午8点到11点的第3和第15分钟执行3,15 8-11 * * * myCommand
实例4：每隔两天的上午8点到11点的第3和第15分钟执行3,15 8-11 */2  *  * myCommand
实例5：每周一上午8点到11点的第3和第15分钟执行3,15 8-11 * * 1 myCommand
实例6：每晚的21:30重启smb30 21 * * * /etc/init.d/smb restart
实例7：每月1、10、22日的4 : 45重启smb45 4 1,10,22 * * /etc/init.d/smb restart
实例8：每周六、周日的1 : 10重启smb10 1 * * 6,0 /etc/init.d/smb restart
实例9：每天18 : 00至23 : 00之间每隔30分钟重启smb0,30 18-23 * * * /etc/init.d/smb restart
实例10：每星期六的晚上11 : 00 pm重启smb0 23 * * 6 /etc/init.d/smb restart
实例11：每一小时重启smb0 * * * * /etc/init.d/smb restart
实例12：晚上11点到早上7点之间，每隔一小时重启smb0 23-7/1 * * * /etc/init.d/smb restart
实例13：每月的4号与每周一到周三的11点重启smb0 11 4 * mon-wed /etc/init.d/smb restart
实例14：一月一号的4点重启smb0 4 1 jan * /etc/init.d/smb restart
实例15：每小时执行/etc/cron.hourly目录内的脚本01 * * * * root run-parts /etc/cron.hourlyrun-parts这个参数了，如果去掉这个参数的话，后面就可以写要运行的某个脚本名，而不是目录名了。
3.3. 查看ganglia的状态目标：每分钟查看一下ganglia的状态，并保存到/tmp/log/ganglia目录。
1、创建/tmp/log/ganglia目录
sudo mkdir -p /tmp/log/gangliasudo chmod a+w /tmp/log/ganglia

2、编辑crontabcrontab -e，选择编辑器为vim
3、在crontab文件中添加一行（注意该命令是错误的，不要直接使用）
* * * * * pssh -h /home/test/hosts.txt -t 30 -i &#x27;ps aux | grep gmond&#x27; &gt; /tmp/log/ganglia/ganglia-`date +%Y%m%d-%H%M%S`.log

4、查看crontab任务crontab -l，看到任务已经添加成功。
5、等待了五分钟，发现/tmp/log/ganglia目录下啥也没有。sudo service cron status，状态正常。sudo /etc/init.d/cron restart，重启cron试试。又等待了五分钟，发现/tmp/log/ganglia目录下依然空空。
莫非是因为pssh没有使用绝对路径？whereis pssh，找到pssh路径为/usr/lib/pssh，修改crontab为：
* * * * * /usr/lib/pssh -h /home/test/hosts.txt -t 30 -i &#x27;ps aux | grep gmond&#x27; &gt; /tmp/log/ganglia/ganglia-`date +%Y%m%d-%H%M%S`.log
然而，并没有用。还是查看下crontab日志吧！
3.4. 开启crontab日志以下主要参考Ubuntu下用crontab 部署定时任务。
1、编辑50-default.confsudo vim /etc/rsyslog.d/50-default.conf
2、把cron前的井号去掉，也就是修改为：
cron.*                         /var/log/cron.log

3、重启rsyslog服务sudo service rsyslog restart
4、重启crontab服务sudo service cron restart
5、查看crontab日志less /var/log/cron.log
果然发现了问题：
(test) CMD (/usr/lib/pssh -h /home/test/hosts.txt -t 30 -i &#x27;ps aux | grep gmond&#x27; &gt; /tmp/log/ganglia/ganglia-`date +)
也就是说，命令确实按时执行了，只不过没有执行完，被百分号截断了，导致log文件没有正常生成！
修改crontab为：
* * * * * /usr/lib/pssh -h /home/test/hosts.txt -t 30 -i &#x27;ps aux | grep gmond&#x27; &gt; /tmp/log/ganglia/ganglia-`date +\%Y\%m\%d-\%H\%M\%S`.log

终于，log文件成功生成，nice！但是，文件内容是空的！因为，/usr/lib/pssh是一个目录，不是pssh命令！真正的pssh命令是parallel-ssh，找到它的位置为/usr/bin/parallel-ssh，修改crontab：
* * * * * /usr/bin/parallel-ssh -h /home/test/hosts.txt -t 30 -i &#x27;ps aux | grep gmond&#x27; &gt; /tmp/log/ganglia/ganglia-`date +\%Y\%m\%d-\%H\%M\%S`.log

至此，问题圆满解决。实际使用的时候，一天获取一次ganglia的状态就够了，所以crontab改成：
0 0 * * * /usr/bin/parallel-ssh -h /home/test/hosts.txt -t 30 -i &#x27;ps aux | grep gmond&#x27; &gt; /tmp/log/ganglia/ganglia-`date +\%Y\%m\%d`.log

3.5. 定时执行脚本以上，每天执行一次定时任务，抓取ganglia的运行状态保存到日志文件中。紧接着，我们的目标是使用脚本检查当天的日志文件，如果发现ganglia运行异常，则产生一个错误日志。
1、假设日志文件ganglia-20180524.log的内容为：
[1] 00:00:01 [SUCCESS] test@192.168.56.102ganglia  24810  0.0  0.0  57536  3396 ?        Ssl  May23   0:20 /usr/sbin/gmond --pid-file /var/run/gmond.pidtest     27619  0.0  0.0  11376  2800 ?        Ss   09:05   0:00 bash -c ps aux | grep gmondtest     27621  0.0  0.0  10720  2276 ?        S    09:05   0:00 grep gmond[2] 00:00:01 [SUCCESS] test@192.168.56.103ganglia  25710  0.0  0.0  57536  3396 ?        Ssl  May23   0:20 /usr/sbin/gmond --pid-file /var/run/gmond.pidtest     27622  0.0  0.0  11376  2800 ?        Ss   09:05   0:00 bash -c ps aux | grep gmondtest     27645  0.0  0.0  10720  2276 ?        S    09:05   0:00 grep gmond

2、参考grep命令最经常使用的功能总结，编写脚本checkganglia.sh
#!/bin/bashDATE=`date +%Y%m%d`filename=&quot;ganglia-$&#123;DATE&#125;.log&quot;hosts=`grep test@ /tmp/log/ganglia/$&#123;filename&#125; | wc -l`pids=`grep gmond.pid /tmp/log/ganglia/$&#123;filename&#125; | wc -l`if [ $&#123;hosts&#125; == $&#123;pids&#125; ]then    echo &quot;All services are runing!&quot;else    echo &quot;Error occurred!&quot; &gt; /tmp/log/ganglia/error-$&#123;DATE&#125;.logfi

3、执行
chmod a+x checkganglia.sh./checkganglia.sh

如果所有客户机的ganglia运行正常，就会输出All services are runing!。如果有的客户机ganglia进程不存在，则会在/tmp/log/ganglia/目录下生成当天的错误日志。
4、设置定时运行因为日志的检查工作要在日志生成之后，所以时间上延后十分钟。
10 0 * * * /bin/bash /home/test/checkganglia.sh

4. 脚本进阶上面的脚本，还有很多要改进的地方。比如有的客户机宕机了，上面的脚本检查不出来。比如有的客户机ganglia服务没有启动，那么具体是哪几台？针对这两个问题，下面进行改进。假设已经知道客户机的数量为10。
参考csplit命令，checkganglia.sh脚本修改为：
#!/bin/bashDATE=`date +%Y%m%d`filename=&quot;ganglia-$&#123;DATE&#125;.log&quot;prefix=&quot;ganglia-$&#123;DATE&#125;&quot;hosts=`grep test@ /tmp/log/ganglia/$&#123;filename&#125; | wc -l`pids=`grep gmond.pid /tmp/log/ganglia/$&#123;filename&#125; | wc -l`if [ $&#123;hosts&#125; != 10 ]then    echo &quot;Some hosts are offline!&quot; &gt;&gt; /tmp/log/ganglia/error-$&#123;DATE&#125;.logfiif [ $&#123;hosts&#125; != $&#123;pids&#125; ]then    echo &quot;Some ganglia services have stopped!&quot; &gt;&gt; /tmp/log/ganglia/error-$&#123;DATE&#125;.log    cd /tmp/log/ganglia/    csplit /tmp/log/ganglia/$&#123;filename&#125; /test@/ -n2 -s &#123;*&#125; -f $&#123;prefix&#125; -b &quot;.log.%02d&quot;    rm $&#123;prefix&#125;.log.00    for file in /tmp/log/ganglia/$&#123;prefix&#125;.log.*    do     if [ -f &quot;$&#123;file&#125;&quot; ]    then        #echo &quot;$&#123;file&#125; is file&quot;        if [ `grep gmond.pid $&#123;file&#125; | wc -l` == 0 ]        then            echo `grep test@ $&#123;file&#125;` &gt;&gt; /tmp/log/ganglia/error-$&#123;DATE&#125;.log        fi    fi    donefi

以上脚本，实现了当客户机数量不为10的时候，进行报错；当客户机ganglia服务没有启动时，进行报错，并且筛选出所有没有启动ganglia的客户机。
5. 小技巧5.1. 使用脚本任务执行逻辑，最好写在脚本中，然后配置定时任务执行脚本。
5.2. 导入环境变量脚本中导入环境变量，可以避免很多错误。
#!/bin/bashsource /etc/profilesource /home/voidking/.bash_profile# business code

5.3. 使用绝对路径配置定时任务时，用到的命令，使用命令的绝对路径。使用which查看绝对路径，例如which bash
5.4. 错开执行时间如果有多个定时任务，都需要每隔5分钟执行一次，此时如果都配置为*/5 * * * *，那么可能造成资源竞争。
针对这种场景，更好的做法是将这些任务错开。
方法一：明确定义执行时间
# 任务1 - 在每小时的0,5,10,15...分钟执行0,5,10,15,20,25,30,35,40,45,50,55 * * * * /path/to/script1.sh# 任务2 - 在每小时的1,6,11,16...分钟执行（错开1分钟）1,6,11,16,21,26,31,36,41,46,51,56 * * * * /path/to/script2.sh# 任务3 - 在每小时的2,7,12,17...分钟执行（错开2分钟）2,7,12,17,22,27,32,37,42,47,52,57 * * * * /path/to/script3.sh# 任务4 - 在每小时的3,8,13,18...分钟执行（错开3分钟）3,8,13,18,23,28,33,38,43,48,53,58 * * * * /path/to/script4.sh# 任务5 - 在每小时的4,9,14,19...分钟执行（错开4分钟）4,9,14,19,24,29,34,39,44,49,54,59 * * * * /path/to/script5.sh

方法二：使用步长值（推荐）
# 每5分钟执行，从第0分钟开始*/5 * * * * /path/to/script1.sh# 每5分钟执行，从第1分钟开始1-59/5 * * * * /path/to/script2.sh# 每5分钟执行，从第2分钟开始  2-59/5 * * * * /path/to/script3.sh# 每5分钟执行，从第3分钟开始3-59/5 * * * * /path/to/script4.sh# 每5分钟执行，从第4分钟开始  4-59/5 * * * * /path/to/script3.sh

6. 后记本文中，我们先学习了crontab的基础知识和基本用法。然后通过监控ganglia这一个应用场景来具体学习crontab的详细使用方法，包括查看cron日志的方法，crontab中命令转义的方法，定时执行脚本的方法，以及审阅日志脚本的编写和进阶。
至此，还不够完美，因为我们需要每天登录管理机查看有没有错误日志。下一篇Linux设置邮件提醒中，我们将会研究linux设置邮件提醒的方法。审阅完日志后，如果脚本能够给我们发送一封邮件，告知我们审阅的结果，那么我们就不必再每天查看错误日志。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
        <tag>crontab</tag>
        <tag>pssh</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Ubuntu/Debian更换软件安装源</title>
    <url>/dev-ubuntu-change-software-repo/</url>
    <content><![CDATA[1. 前言在使用基于Debian的操作系统，如Ubuntu，我们经常需要安装和更新软件。这些操作通常是通过APT（Advanced Package Tool）软件包管理器完成的，它利用了一系列定义在系统中的软件源（repositories）来安装和管理软件。
Ubuntu默认的软件安装源是 http://archive.ubuntu.com ，在国内访问速度慢。Debian（Ubuntu是Debian的衍生品）的默认软件安装源是 http://deb.debian.org，在国内访问速度慢。
如果换成国内的软件安装源，能够节省很多时间，因此安装Ubuntu/Debian系统后建议先更换软件安装源。
本文中，我们会学习如何在Ubuntu/Debian系统中更换软件安装源。
参考文档：

ubuntu 14.04source
源列表
Ubuntu 源使用帮助
Debian 源使用帮助
Debian Security 源使用帮助
如何修改Ubuntu的源列表(source list)



2. 备选国内源
Ubuntu中国：http://cn.archive.ubuntu.com
中科大：https://mirrors.ustc.edu.cn
清华：https://mirrors.tuna.tsinghua.edu.cn
阿里：https://mirrors.aliyun.com
腾讯：https://mirrors.cloud.tencent.com
网易：https://mirrors.163.com

3. 什么是 DEB822 (.sources) 文件格式？自新版本的 Debian 与 Ubuntu 起，例如：

Debian 12 的容器镜像
Ubuntu 24.04

默认预装的系统中 APT 的系统源配置文件不再是传统的 /etc/apt/sources.list。传统格式（又被称为 One-Line-Style 格式）类似如下：
deb http://mirrors.ustc.edu.cn/debian/ bookworm main contrib

新的 DEB822 格式自 APT 1.1（2015 年发布）起支持，后缀为 .sources，存储在 /etc/apt/sources.list.d/ 目录下，格式类似如下：
Types: debURIs: https://mirrors.ustc.edu.cn/debianSuites: bookwormComponents: main contribSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg
在切换软件源时，需要根据实际情况选择对应的格式进行修改。
关于 DEB822 格式的设计考虑，可以参考官方文档（英文）。
4. Ubuntu更换软件安装源4.1. Ubuntu14.04系统版本Ubuntu14.04（大版本别名trusty），计划换成Ubuntu中国的安装源。
1、备份现有的软件源列表
sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup

2、修改软件安装源配置
sudo sed -i &#x27;s#http://archive.ubuntu.com#http://cn.archive.ubuntu.com#g&#x27; /etc/apt/sources.list

也可以手动修改配置：
vim /etc/apt/sources.list

修改后的 sources.list 的内容为：
deb http://cn.archive.ubuntu.com/ubuntu/ trusty main restricted universe multiversedeb http://cn.archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiversedeb http://cn.archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe multiversedeb http://cn.archive.ubuntu.com/ubuntu/ trusty-backports main restricted universe multiverse##測試版源deb http://cn.archive.ubuntu.com/ubuntu/ trusty-proposed main restricted universe multiverse# 源碼deb-src http://cn.archive.ubuntu.com/ubuntu/ trusty main restricted universe multiversedeb-src http://cn.archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiversedeb-src http://cn.archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe multiversedeb-src http://cn.archive.ubuntu.com/ubuntu/ trusty-backports main restricted universe multiverse##測試版源deb-src http://cn.archive.ubuntu.com/ubuntu/ trusty-proposed main restricted universe multiverse# Canonical 合作夥伴和附加# deb http://archive.canonical.com/ubuntu/ trusty partner# deb http://extras.ubuntu.com/ubuntu/ trusty main

ubuntu14的别名是trusty，所以souces.list中出现了很多trusty，其他版本的ubuntu就不是trusty了。
3、更新安装源
sudo apt-get update

如果报错Error connecting: Could not connect: Connection refused解决方法参考：

“Unable to connect” error with apt-get?
apt-get update Error connecting Could not connect Connection refused

4、更新软件
sudo apt-get upgrade

PS：其他版本的Ubuntu修改方法类似，主要是大版本别名的差别。
4.2. Ubuntu24.04sudo cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.baksudo sed -i &#x27;s/security.ubuntu.com/mirrors.ustc.edu.cn/g&#x27; /etc/apt/sources.list.d/ubuntu.sourcessudo apt update

可能会收到GPG错误，显示没有公钥，或者公钥不可用。这是因为APT管理系统通过GPG密钥验证软件包的安全性。如果遇到这样的问题，须要导入对应软件源的公钥。
5. Debian更换软件安装源5.1. Debian bustersudo cp /etc/apt/sources.list /etc/apt/sources.list.baksudo sed -i &#x27;s#http://deb.debian.org#http://mirrors.tuna.tsinghua.edu.cn#g&#x27; /etc/apt/sources.listsudo sed -i &#x27;s#http://security.debian.org/debian-security#http://mirrors.tuna.tsinghua.edu.cn/debian-security#g&#x27; /etc/apt/sources.listsudo apt-get update

5.2. Debian bookwormsudo sed -i &#x27;s/deb.debian.org/mirrors.ustc.edu.cn/g&#x27; /etc/apt/sources.list.d/debian.sources

从 Debian 12 (bookworm) 开始，仓库添加了非自由固件组件 non-free-firmware。如果正在使用 bookworm, testing 或 sid，并且需要使用非自由固件，则在编辑配置时需要添加 non-free-firmware。
详情参考 https://wiki.debian.org/Firmware 与 Debian bug #1030189。
sudo sed -i &#x27;s/Components: main/Components: main contrib non-free non-free-firmware/g&#x27; /etc/apt/sources.list.d/debian.sources

可能会收到GPG错误，显示没有公钥，或者公钥不可用。这是因为APT管理系统通过GPG密钥验证软件包的安全性。如果遇到这样的问题，须要导入对应软件源的公钥。
5.3. Debian版本名称
Debian 12 - Bookworm（2023年发布，当前稳定版）
Debian 11 - Bullseye（2021年发布）
Debian 10 - Buster（2019年发布）
Debian 9 - Stretch（2017年发布）
Debian 8 - Jessie（2015年发布）

6. GPG错误解决办法6.1. public key is not available基于Docker运行新版本的Ubuntu和Debian，修改软件安装源之后，执行apt update，可能会报错：
The following signatures couldn&#x27;t be verified because the public key is not available: NO_PUBKEY 0E98404D386FA1D9

解决办法：
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 0E98404D386FA1D9

或者：
gpg --keyserver  keyserver.ubuntu.com --recv-keys 0E98404D386FA1D9gpg --export --armor 0E98404D386FA1D9 | sudo apt-key add -

6.2. 缺少gnupg但是，执行apt-key和gpg命令，都需要依赖 gnupg
apt install gnupg

而想要安装gpupg需要先执行 apt update，死循环了。。。
解决办法：
apt update --allow-unauthenticated --allow-insecure-repositories

6.3. Problem executing scripts APT可能还会报错：
E: Problem executing scripts APT::Update::Post-Invoke &#x27;rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true&#x27;E: Sub-process returned an error code

解决办法：
sed -i -e &#x27;s/^APT/# APT/&#x27; -e &#x27;s/^DPkg/# DPkg/&#x27; /etc/apt/apt.conf.d/docker-clean

6.4. 安装软件报错重新更新软件安装源，安装gnupg
apt update --allow-unauthenticated --allow-insecure-repositoriesapt install --allow-unauthenticated -y gnupg

报错：
...dpkg-deb (subprocess): decompressing archive &#x27;/tmp/apt-dpkg-install-0nckSy/24-libsasl2-modules_2.1.28+dfsg-10_amd64.deb&#x27; (size=66596) member &#x27;control.tar&#x27;: lzma error: Cannot allocate memorytar: This does not look like a tar archivetar: Exiting with failure status due to previous errorsdpkg-deb: error: tar subprocess returned error exit status 2dpkg: error processing archive /tmp/apt-dpkg-install-0nckSy/24-libsasl2-modules_2.1.28+dfsg-10_amd64.deb (--unpack): dpkg-deb --control subprocess returned error exit status 2Errors were encountered while processing: /tmp/apt-dpkg-install-0nckSy/00-readline-common_8.2-1.3_all.deb /tmp/apt-dpkg-install-0nckSy/01-libassuan0_2.5.5-5_amd64.deb /tmp/apt-dpkg-install-0nckSy/02-libreadline8_8.2-1.3_amd64.deb /tmp/apt-dpkg-install-0nckSy/03-gpgconf_2.2.40-1.1_amd64.deb /tmp/apt-dpkg-install-0nckSy/04-libksba8_1.6.3-2_amd64.deb /tmp/apt-dpkg-install-0nckSy/05-libsasl2-modules-db_2.1.28+dfsg-10_amd64.deb /tmp/apt-dpkg-install-0nckSy/06-libsasl2-2_2.1.28+dfsg-10_amd64.deb /tmp/apt-dpkg-install-0nckSy/07-libldap-2.5-0_2.5.13+dfsg-5_amd64.deb /tmp/apt-dpkg-install-0nckSy/08-libnpth0_1.6-3_amd64.deb /tmp/apt-dpkg-install-0nckSy/09-dirmngr_2.2.40-1.1_amd64.deb /tmp/apt-dpkg-install-0nckSy/10-gnupg-l10n_2.2.40-1.1_all.deb /tmp/apt-dpkg-install-0nckSy/11-gnupg-utils_2.2.40-1.1_amd64.deb /tmp/apt-dpkg-install-0nckSy/12-libsqlite3-0_3.40.1-2_amd64.deb /tmp/apt-dpkg-install-0nckSy/13-gpg_2.2.40-1.1_amd64.deb /tmp/apt-dpkg-install-0nckSy/14-libncursesw6_6.4-4_amd64.debE: Sub-process /usr/bin/dpkg returned an error code (1)

终极解决办法：升级Docker！升级Docker之后，GPG错误消失，后续所有问题也都不存在了。
7. 扩展：Ubuntu/Debian配置时间同步sudo ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtimesudo echo &quot;Asia/Shanghai&quot; &gt; /etc/timezonedatesudo apt install ntpdatesudo ntpdate time.windows.comdate

如果是在容器中操作ntpdate time.windows.com，可能会报错ntpdate[542]: step-systime: Operation not permitted解决办法：启动容器时直接挂载宿主机的/etc/localtime
docker run --name test -d \  -v /etc/localtime:/etc/localtime \  python:3.7.10-slim-buster sleep 7200




]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>ubuntu</tag>
        <tag>debian</tag>
      </tags>
  </entry>
  <entry>
    <title>使用PSSH批量管理Linux</title>
    <url>/dev-pssh-manage-linux/</url>
    <content><![CDATA[1. 前言《Ubuntu系统批量自动安装》一文中，配置好了PXE服务器，也通过它安装了几台机器。每个机器都重新配置好了IP，已经可以远程访问了。现在新的问题来了，每个机器的主机名都相同，需要修改；每个机器的sources.list都有问题，需要替换。以后，肯定也有很多其他需要批量操作的问题，比如批量安装ganglia，总不能上百台机器一个个手动操作吧！
本文，就研究下linux批量操作的相关方法和工具，重点研究下pssh。


2. 批量操作思路首先定义两个概念：管理机和客户机，本文中的管理机是指管理其他服务器的服务器，客户机是指普通服务器。管理机IP为192.168.56.101，客户机IP为192.168.56.102-104，用户名都是test。
2.1. 思路一说到批量操作，最容易想到的，肯定是在管理机写一个脚本，里面有个循环语句，挨个连接客户机进行操作。而循环语句里面，主要是ssh，然后执行交互命令。参考 shell实现SSH自动登陆 、 关于SSH 远程执行命令你要知道的二三事 和 shell脚本实现同时多台远程主机执行命令的代码分享。
但是，这种方式很难并行处理，比较浪费时间。
2.2. 思路二另一个简单的思路，是在管理机写一个在客户机执行的脚本，然后推送给客户机，再在客户机里执行脚本。主要参考SSH 远程执行任务。
这种方式同样很难并行处理，比较浪费时间。如果非要并行处理，那么就只能牺牲反馈信息。
2.3. 思路三最后一种思路就是借助工具，比如mussh、pdsh、pssh等等。
3. pssh3.1. pssh简介pssh是一个python编写可以在多台服务器上执行命令的工具，同时支持拷贝文件，是同类工具中很出色的。类似pdsh，但是相对pdsh更为简便，使用前必须在各个服务器上配置好密钥认证访问。参考pssh命令和pssh HOWTO。
3.2. 安装1、ubuntu安装pssh，sudo apt-get install pssh
2、ubuntu安装完pssh后，输入pssh，也许会提示：No command ‘pssh’ found, did you mean:…
解决办法参考Why pssh command is not working?，一条命令解决：echo &quot;alias pssh=parallel-ssh&quot; &gt;&gt; ~/.bashrc &amp;&amp; . ~/.bashrc，其中&amp;&amp; . ~/.bashrc代表立即生效。
3、设置相关命令安装完pssh后，实际上还安装了pscp、prsync、pnuke和pslurp。和pssh命令无效的问题相同，它们默认也只能使用全名，不能只用简称。需要执行如下命令：
echo &quot;alias pscp=parallel-scp&quot; &gt;&gt; ~/.bashrc &amp;&amp; . ~/.bashrcecho &quot;alias prsync=parallel-rsync&quot; &gt;&gt; ~/.bashrc &amp;&amp; . ~/.bashrcecho &quot;alias pnuke=parallel-nuke&quot; &gt;&gt; ~/.bashrc &amp;&amp; . ~/.bashrcecho &quot;alias pslurp=parallel-slurp&quot; &gt;&gt; ~/.bashrc &amp;&amp; . ~/.bashrc

其中，pscp把文件并行地复制到多个客户机；prsync使用rsync协议从管理机同步到客户机；pslurp将文件从客户机复制到管理机；pnuke并行地在客户机杀进程。
3.3. 命令格式命令格式：pssh [OPTIONS] command [...]
选项：
--version：查看版本--help：查看帮助，即此信息-h：主机文件列表，内容格式&quot;[user@]host[:port]&quot;-H：主机字符串，内容格式&quot;[user@]host[:port]&quot;-l：登录使用的用户名-p：并发的线程数【可选】-o：输出的文件目录【可选】-e：错误输入文件【可选】-t：TIMEOUT 超时时间设置，0无限制【可选】-O：SSH的选项-v：详细模式-A：手动输入密码模式-x：额外的命令行参数使用空白符号，引号，反斜线处理-X：额外的命令行参数，单个参数模式，同-x-i：每个服务器内部处理信息输出-P：打印出服务器返回信息

4. 实践篇4.1. 添加密钥认证访问参考Linux之SSH密钥认证和ssh使用密钥进行认证，在管理机上制作密钥对，将公钥添加给客户机，然后通过ssh免密登录。
1、确认管理机和客户机都安装了ssh。ps aux | grep ssh
2、在管理机上创建密钥对ssh-keygen所有的提示按enter键即可，完成后在home目录执行ll .ssh，即可看到创建好的id_rsa和id_rsa.pub文件。
3、把公钥拷贝到所有客户机中ssh-copy-id -i .ssh/id_rsa.pub -p 22 test@192.168.56.102
4、测试登录上一步拷贝完成后，会提示使用ssh -p &#39;22&#39; &#39;test@192.168.56.102&#39;测试登录。
在管理机中，使用ssh test@192.168.56.102测试登录，我们发现已经不需要输入密码了。
5、查看公钥登录102客户机，ll .ssh，我们发现有一个authorized_keys文件，文件的内容和管理机的id_rsa.pub相同。
5、测试命令ssh test@192.168.56.102 &#39;/sbin/ifconfig&#39;
返回了102客户机的ifconfig执行结果，测试成功。
6、设置sudo命令免密码ssh test@192.168.56.102 &#39;sudo iptables --list&#39;报错：sudo: no tty present and no askpass program specified
这个问题，需要在每个客户机下进行sudo免密设置。进入客户机之后，sudo vim /etc/sudoers，添加：
test ALL = NOPASSWD: ALL

再次执行ssh test@192.168.56.102 &#39;sudo iptables --list&#39;，成功。
4.2. 获取每台机器的uptime1、在管理机上新建hosts.txt，内容为：
test@192.168.56.102test@192.168.56.103test@192.168.56.104

2、执行uptimepssh -h hosts.txt -i uptime
3、保存执行结果pssh -h hosts.txt -i -o /tmp/pssh/ uptime
ll /tmp/pssh
cat /tmp/pssh
4.3. 批量修改hostname参考Linux批量修改多台服务器的主机名（hostname），我们把客户机的主机名改为vk102、vk103和vk104。
1、新建hosts文件，内容为：
192.168.56.102    vk102192.168.56.103    vk103192.168.56.104    vk104

2、新建hostname.sh文件，内容为：
#!/bin/baship=`ifconfig eth0 | grep &#x27;inet&#x27; | awk &#x27;&#123;print $2&#125;&#x27; | tr -d &#x27;addr:&#x27;`hostname=`cat /home/test/hosts | grep $ip | awk &#x27;&#123;print $2&#125;&#x27;`echo $ipecho $hostnamehostnamectl set-hostname --static $hostnamehostname $hostname

3、发送到hosts和hostname.sh到客户机/home/test目录下pscp -h hosts.txt ./hosts /home/testpscp -h hosts.txt ./hostname.sh /home/test
4、批量授予hostname.sh可执行权限pssh -h hosts.txt -i  &#39;chmod +x /home/test/hostname.sh&#39;
5、批量执行hostname.shpssh -h hosts.txt -i  &#39;sudo sh /home/test/hostname.sh&#39;报错：Stderr: hostname: you must be root to change the host name命令修改为：pssh -h hosts.txt -i  &#39;sudo sh /home/test/hostname.sh&#39;执行成功。
4.4. 批量替换sources.list1、新建sources.list，内容为：
deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse  deb http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse  deb http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse  deb http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse  deb http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse  deb-src http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse  

2、客户机备份原sources.listpssh -h hosts.txt -i  &#39;sudo mv /etc/apt/sources.list /etc/apt/sources.list.bak&#39;
3、复制新的sources.list到客户机pscp -h hosts.txt sources.list /home/test/pssh -h hosts.txt &#39;sudo mv /home/test/sources.list /etc/apt/&#39;
4、更新安装源pssh -h hosts.txt -i &#39;sudo apt-get update&#39;
4.5. 批量安装ganglia参考Ubuntu14.04安装配置Ganglia，假设我们已经配置好了ganglia主节点。
1、把ganglia主节点的/etc/ganglia/gmond.conf文件拷贝到管理机当前目录。
2、编写ganglia安装脚本install-gmond.sh（不要照抄，下面有修改版）
#!/bin/bashsudo apt-get -y install ganglia-monitor rrdtool &amp;&amp; \sudo mv /etc/ganglia/gmond.conf /etc/ganglia/gmond.conf.bak &amp;&amp; \sudo mv /home/test/gmond.conf /etc/ganglia/ &amp;&amp; \sudo /etc/init.d/ganglia-monitor restart &amp;&amp; \sudo rm -rf /home/test/install-gmond.sh

3、拷贝gmond.conf和install-gmond.sh到客户机pscp -h hosts.txt gmond.conf /home/test/pscp -h hosts.txt install-gmond.sh /home/test/
4、添加执行权限pssh -h hosts.txt -i  &#39;chmod +x /home/test/install-gmond.sh&#39;
5、执行安装pssh -h hosts.txt -i &#39;sudo apt-get update&#39;
pssh -h hosts.txt -i &#39;sudo sh /home/test/install-gmond.sh&#39;
脚本虽然顺利执行了，但是报错：Stderr: debconf: unable to initialize frontend: Dialog解决办法是修改脚本为：
#!/bin/bashexport DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true &amp;&amp; \sudo apt-get -y -q install ganglia-monitor rrdtool &amp;&amp; \sudo mv /etc/ganglia/gmond.conf /etc/ganglia/gmond.conf.bak &amp;&amp; \sudo mv /home/test/gmond.conf /etc/ganglia/ &amp;&amp; \sudo /etc/init.d/ganglia-monitor restart &amp;&amp; \sudo rm -rf /home/test/install-gmond.sh

6、查看运行状态pssh -h hosts.txt -i -o /tmp/pssh/ &#39;ps aux | grep ganglia&#39;
4.6. 批量修改密码参考shell实现SSH自动登陆 和 6个Expect脚本示例，使用expect命令。
1、管理机上新建chpasswd.sh脚本，内容如下：
#!/usr/bin/expect set timeout 3set user testset password 123456spawn sudo passwd $user  expect &quot;Enter new UNIX password:&quot;  send &quot;$&#123;password&#125;\r&quot;  expect &quot;Retype new UNIX password:&quot;  send &quot;$&#123;password&#125;\r&quot;  expect eof

2、在客户机上安装expect（可以用whereis expect查看是否安装）pssh -h hosts.txt -i &#39;sudo apt-get install expect -y&#39;
3、拷贝chpasswd.sh脚本到客户机pscp -h hosts.txt chpasswd.sh /home/test
5、添加执行权限pssh -h hosts.txt -i &#39;sudo chmod a+x /home/test/chpasswd.sh&#39;
5、执行修改密码pssh -h hosts.txt -i &#39;sudo /home/test/chpasswd.sh&#39;
6、删除chpasswd.sh脚本pssh -h hosts.txt -i &#39;sudo rm -rf /home/test/chpasswd.sh&#39;
4.7. 批量杀进程假设需要杀死的进程为gmond。方法一：pnuke -h hosts.txt gmondpssh -h hosts.txt -i &#39;ps -ef | grep gmond&#39;这种方法虽然显示success，但是查看进程依然存在，看来存在不确定性。猜测对于sudo启动的进程难以杀死。
方法二：pssh -h hosts.txt -i &#39;sudo pkill -9 gmond&#39;这种方法杀的很彻底，是个好方法。
方法三：pssh -h hosts.txt &#39;sudo ps -ef | grep gmond | awk &#39;&#123;print $2&#125;&#39; | xargs kill -9&#39;这种方法也显示success，但是查看进程依然存在，还是有问题。猜测因为sudo作用在了ps上，所以对于sudo启动的进程难以杀死。
方法四：
pssh -h hosts.txt -i &#x27;sudo kill -s 9 `pgrep gmond`&#x27;
这种方法杀的很彻底，是个好方法。
PS：启动gmond命令pssh -h hosts.txt -i &#39;sudo /etc/init.d/ganglia-monitor start
5. 后记以上实践，已经包含了pssh的最常见用法。更高级的用法，就在需要时再去学习吧！
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>shell</tag>
        <tag>pssh</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS7安装配置DNS服务</title>
    <url>/dev-centos-dns-server/</url>
    <content><![CDATA[1. 前言本文中，我们学习在CentOS7中安装配置DNS服务。DNS服务选择使用BIND，BIND（Berkeley Internet Name Domain）是一种开源的DNS服务器软件，可以安装在CentOS 7上。
参考文档：BIND官网


2. 安装BINDyum install -y bind bind-utils

3. 配置BIND1、编辑bind配置
vim /etc/named.conf

默认配置内容为：named.conf
引用zone配置文件和反向数据文件：
zone &quot;example.com&quot; &#123;    type master;    file &quot;/var/named/example.com.zone&quot;;&#125;;zone &quot;1.168.192.in-addr.arpa&quot; &#123;    type master;    file &quot;/var/named/1.168.192.in-addr.arpa.zone&quot;;&#125;;

2、创建zone配置文件
vim /var/named/example.com.zone

内容为：
$TTL 86400@   IN  SOA     ns1.example.com. admin.example.com. (            2023101601 ; Serial            3600       ; Refresh            1800       ; Retry            604800     ; Expire            86400 )    ; Minimum TTL@   IN  NS  ns1.example.com.@   IN  NS  ns2.example.com.ns1 IN  A   192.168.1.10ns2 IN  A   192.168.1.11www IN  A   192.168.1.100

其中 @ IN SOA ns1.example.com. admin.example.com. 是DNS区域文件（zone file）中的一行，它定义了域的”Start of Authority”（SOA）记录。

@: 在这种上下文中，@ 代表当前域的根（例如，如果您定义的是example.com，那么@ 表示 example.com）。这是一种特殊的缩写，表示当前域。
IN: 表示资源记录的类别（Class），通常为“IN”（Internet）。
SOA: 这是资源记录类型，表示”Start of Authority”，它包含有关域的基本信息。
ns1.example.com.: 这是主要名称服务器（Primary Name Server）的域名。它指定了此域的主要权威DNS服务器的域名。
admin.example.com.: 这是负责管理此域的负责人的电子邮件地址。通常，电子邮件地址以.替换@。

此行的示例告诉DNS服务器，在当前域（例如，example.com）中，主要的权威DNS服务器是 ns1.example.com ，同时负责人的电子邮件地址是admin@example.com。
SOA记录还包括其他信息，如序列号、刷新时间、重试时间、过期时间等，这些信息对于域名的管理和更新非常重要。
这是DNS区域文件的一部分，用于配置DNS服务器以提供有关域名的信息，以便其他计算机能够解析域名并找到与之相关的IP地址。
其中 @   IN  NS ns1.example.com. 用于指定当前域的NS服务器的域名。
其中 www IN  A   192.168.1.100 是一条解析，www.example.com 域名解析到 192.168.1.100。
3、创建反向数据文件 
vim /var/named/1.168.192.in-addr.arpa.zone

内容为：
$TTL 86400@   IN  SOA     ns1.example.com. admin.example.com. (            2023101601 ; Serial            3600       ; Refresh            1800       ; Retry            604800     ; Expire            86400 )    ; Minimum TTL@   IN  NS  ns1.example.com.@   IN  NS  ns2.example.com.10  IN  PTR ns1.example.com.11  IN  PTR ns2.example.com.100 IN  PTR www.example.com.

其中 100 IN  PTR www.example.com. 是一条反向解析，192.168.1.100 反向解析到 www.example.com 。
4. 启动BIND1、启动BIND
systemctl start namedsystemctl enable named

2、查看服务状态
systemctl status namednetstat -nlp | grep 53

3、配置防火墙（可选）
firewall-cmd --add-service=dns --permanentfirewall-cmd --reload

5. 测试DNS服务器1、正向解析测试
nslookup www.example.com 127.0.0.1dig @127.0.0.1 -p 53 www.example.com 
正常的话，会看到解析结果为 192.168.1.100
2、反向解析测试
nslookup 192.168.1.100 127.0.0.1dig @127.0.0.1 -p 53 -x 192.168.1.100
正常的话，会看到反向解析结果为 www.example.com
6. 配置上游DNS服务器自建的DNS服务器无法解析的查询请求，会进行递归查询。DNS具体解析流程，可以参考《DNS入门篇》。此时，我们可以配置指定的上游DNS服务器，从而减少自建DNS服务器的递归查询数量，提高性能。
具体配置方法为：
1、编辑named.conf
vim /etc/named.conf

添加 forwarders 配置：
options &#123;    forwarders &#123; 180.76.76.76; 114.114.114.114; &#125;;    forward first;&#125;;
这里的 forwarders 指定了需要前向查询的DNS服务器，多个服务器之间用分号间隔。forward first 的意思是首先尝试将请求转发给forwarders中的服务器，如果失败，那么才会尝试使用传统的DNS递归查询来解析。
2、重启named
systemctl restart named

3、测试访问
nslookup www.baidu.com 127.0.0.1nslookup www.google.com 127.0.0.1

7. 配置使用DNS服务器1、配置允许其他主机使用DNS服务器
vim /etc/named.conf

修改options部分的参数：
options &#123;    listen-on port 53 &#123; any; &#125;;    listen-on-v6 port 53 &#123; any; &#125;;    allow-query &#123; any; &#125;;&#125;;

2、重启named
systemctl restart named

3、其他主机配置使用DNS服务器
vim /etc/resolv.conf

假设DNS服务器的IP地址为 192.168.56.101 ，那么配置为：
nameserver 192.168.56.101

8. no valid DS resolving 问题8.1. 问题描述查看bind服务的日志：
cd /var/named/datagrep &quot;xxx.oss-cn-hangzhou.aliyuncs.com&quot; named.run

发现很多错误的解析：
no valid DS resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 114.114.114.114#53no valid DS resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 180.76.76.76#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:503:d414::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:502:8cc::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:502:1ca1::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:503:eea3::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:501:b1f9::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:503:83eb::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:502:7094::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:503:a83e::2:30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:500:d937::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:503:d2d::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:500:856e::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:503:39c1::30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2001:503:231d::2:30#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2401:b180:4100::12#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2401:b180:4100::13#53network unreachable resolving &#x27;xxx.oss-cn-hangzhou.aliyuncs.com/A/IN&#x27;: 2401:b180:4100::11#53

8.2. 解决办法1、对于 no valid DS resolving ，需要关闭dnssec。
vim /etc/named.conf

修改：
options &#123;    dnssec-enable no;    dnssec-validation no;&#125;

2、对于 network unreachable resolving ，需要关闭IPv6解析。
vim /etc/sysconfig/named

添加：
OPTIONS=&quot;-4&quot;

3、重启named
systemctl restart named

9. TCP client quota reached 问题9.1. 问题描述CoreDNS上游DNS配置了 named ，使用一段时候，发现大量报错：
[ERROR] plugin/errors: 2 kong-db. SRV: read udp 100.119.197.225:38457-&gt;192.168.56.101:53: i/o timeout[ERROR] plugin/errors: 2 test.example.com. A: read udp 100.119.197.225:35334-&gt;192.168.56.101:53: i/o timeout

查看 named 状态，发现正常运行，但是出现飘红日志：
Oct 26 14:29:49 named-host named[5564]: socket: file descriptor exceeds limit (21000/21000)Oct 26 14:29:57 named-host named[5564]: FORMERR resolving &#x27;smlopenapi.esign.cn/AAAA/IN&#x27;: 114.114.114.114#53

9.2. 问题排查解决1、测试解析
nslookup www.voidking.com 192.168.56.101

发现可以正常解析，说明问题是偶发的。
2、重启named
systemctl restart namedsystemctl status named

重启named，查看named状态，发现出现了明显的提示：
Oct 26 15:03:55 named-host named[5564]: client @0x7f2f7bb4b050 (no-peer): TCP client quota reached: quota reachedOct 26 15:03:55 named-host named[5564]: client @0x7f2f7bbcac90 (no-peer): TCP client quota reached: quota reachedOct 26 15:03:55 named-host named[5564]: client @0x7f2f7bc3eed0 (no-peer): TCP client quota reached: quota reached

从日志可以看出，TCP client quota达到了阈值。
解决办法：调整 TCP client quota为一个更大值。参考文档no more TCP clients: quota reached
options &#123;  tcp-clients &lt;NUMBER_OF_MAX_CLIENT&gt;;&#125;;

更好的解决办法：使用adguardhome替换bind9。
10. 后记如果想要配置多个顶级域名，那么在 named.conf 中添加多个 zone 即可。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>网络</tag>
        <tag>问题排查</tag>
        <tag>dns</tag>
      </tags>
  </entry>
  <entry>
    <title>Ubuntu14.04安装配置DNS服务</title>
    <url>/dev-ubuntu-dns-server/</url>
    <content><![CDATA[1. 前言《Ubuntu系统批量自动安装》一文中，用到了DNS服务，本文中我们来学习安装配置一下。
配置目标：在Ubuntu14.04上搭建一个DNS服务器，服务器地址为10.0.0.100。使用这个DNS，可以把dns.voidking.com域名解析到10.0.0.100这个地址。
参考文档：BIND官网


2. 安装DNS服务安装命令：
sudo apt-get updatesudo apt-get install bind9

安装完成后，会创建/etc/bind目录，里面有很多配置文件。
3. 配置DNS正反解区域1、添加DNS正反解区域
sudo vim /etc/bind/named.conf.default-zones

在最后添加：
zone &quot;voidking.com&quot; &#123;        type master;        file &quot;/etc/bind/db.voidking.com&quot;;&#125;;zone &quot;0.0.10.in-addr.arpa&quot; &#123;        type master;        file &quot;/etc/bind/db.10.0.0&quot;;&#125;;
添加正解区域“voidking.com”，以及反解区域“10.0.0.0”
2、创建 db.voidking.com 配置文件
cd /etc/bindsudo cp db.local db.voidking.comsudo vim db.voidking.com

添加一个A记录到10.0.0.100：
;; BIND data file for local loopback interface;$TTL    604800@       IN      SOA     localhost. root.localhost. (                              2         ; Serial                         604800         ; Refresh                          86400         ; Retry                        2419200         ; Expire                         604800 )       ; Negative Cache TTL;@       IN      NS      localhost.@       IN      A       127.0.0.1@       IN      AAAA    ::1dns     IN      A       10.0.0.100

3、创建 db.10.0.0 配置文件
sudo cp db.127 db.10.0.0sudo vim db.10.0.0

添加100的反向解析到dns.voidking.com：
;; BIND reverse data file for local loopback interface;$TTL    604800@       IN      SOA     localhost. root.localhost. (                              1         ; Serial                         604800         ; Refresh                          86400         ; Retry                        2419200         ; Expire                         604800 )       ; Negative Cache TTL;@       IN      NS      voidking.com.100     IN      PTR     dns.voidking.com.

4. 测试使用DNS服务1、测试访问
ping dns.voidking.com
这次访问会提示 connection timed out: no servers could be reached，或者提示 ping: unknown host dns.voidking.com
2、重启dns服务
sudo /etc/init.d/bind9 restart

3、测试访问
ping dns.voidking.com
这次访问，就可以正常ping到服务器100的ip地址。
host 10.0.0.100，可以看到10.0.0.100绑定到了dns.voidking.com。
至此，DNS服务器已经配置成功。
5. 配置上游DNS服务器自建的DNS服务器无法解析的查询请求，会进行递归查询。DNS具体解析流程，可以参考《DNS入门篇》。此时，我们可以配置指定的上游DNS服务器，从而减少自建DNS服务器的递归查询数量，提高性能。
具体配置方法为：
1、编辑named.conf.options
sudo vim named.conf.options

forwarders设置转发DNS为180.76.76.76，allow-query设置为0.0.0.0/0：
options &#123;        directory &quot;/var/cache/bind&quot;;        // If there is a firewall between you and nameservers you want        // to talk to, you may need to fix the firewall to allow multiple        // ports to talk.  See http://www.kb.cert.org/vuls/id/800113        // If your ISP provided one or more IP addresses for stable        // nameservers, you probably want to use them as forwarders.        // Uncomment the following block, and insert the addresses replacing        // the all-0&#x27;s placeholder.        forwarders &#123;                180.76.76.76;        &#125;;        allow-query &#123;                0.0.0.0/0;        &#125;;        //========================================================================        // If BIND logs error messages about the root key being expired,        // you will need to update your keys.  See https://www.isc.org/bind-keys        //========================================================================        dnssec-validation auto;        auth-nxdomain no;    # conform to RFC1035        listen-on-v6 &#123; any; &#125;;&#125;;

2、重启DNS服务
sudo /etc/init.d/bind9 restart

3、测试访问
ping www.baidu.com

6. 其他主机使用DNS服务1、添加DNS配置同局域网主机在 /etc/resolv.conf 中添加：
nameserver 10.0.0.100

2、测试使用DNS服务然后ping dns.voidking.com，ping www.baidu.com
至此，DNS Server安装配置完成。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>ubuntu</tag>
        <tag>dns</tag>
      </tags>
  </entry>
  <entry>
    <title>Ubuntu系统批量自动安装</title>
    <url>/dev-ubuntu-auto-install/</url>
    <content><![CDATA[1. 前言《Ubuntu 14.04.5 Server物理机安装》一文中，我们安装了一台Ubuntu Server服务器，整个过程花费了很多时间。令人难过的是，还有15台机器需要安装！本来打算每天安装个一两台，慢慢来，今天老师提醒说为什么不使用脚本呢？对啊，为什么不试试脚本呢！这是一个思路。突然想起师兄曾说，机房的机器系统安装都是通过网络，这也是一个很好的思路啊！
按照上面两个思路，那就研究下批量安装Ubuntu系统的方法！


2. 原理篇2.1. 历史参考PXE、kickstart与preseed简介、Linux装机利器Cobbler简述和ubuntu 14.04 无人职守全自动安装-基础知识中，郝同学简单归纳了批量装机的发展。
历史上，很多同学都遇到过和我同样的问题，机器太多，需要挨个装机，简直就是折磨！
于是，为了降低批量装机的难度，出现了PXE技术，它将操作系统远程下载到本地运行，有了这个技术，我们就不需要再挨个机器插U盘安装系统。
有了PXE技术，依然需要在安装的过程中不停地与机器进行交互，设置选项，费时费力。为了减轻同学们的工作量，RedHat推出了kickstart，在安装操作系统前将其中出现的各种问题的应答内容提前写好，使整个操作系统的安装可以自动完成。Debian系的Ubuntu等感觉kickstart对自己不够友善，不能完全满足自己的要求，于是推出了preseed。
再后来，因为kickstart难度系数太高，RedHad又推出了cobbler，把批量装机的难度降低到了小学水平。
2.2. PXEPXE是Intel公司开发的将操作系统远程下载到本地运行的一种技术。
本地网卡ROM中包含有PXE客户端软件。网卡启动时会发出DHCP请求，从PXE服务器端获得动态IP地址、网关及TFTP服务器信息后，它会自动下载一个用于引导操作系统的启动软件包到本地内存（不同的操作系统使用不同的PXE引导文件）中，再通过此软件加载操作系统启动文件，从而开始操作系统的安装工作。
PXE的详细介绍请参考自动化运维之-PXE实现系统批量自动安装和PXE网络装机攻略。
2.3. kickstart与preseed在我们手动安装操作系统时，需要回答各种提示问题，而针对某种特定应用时，这种回答完全有章可循，如果我们能在安装操作系统时将其中出现的各种问题的应答内容提前写好，那么整个操作系统的安装就可以自动完成。
kickstart是RedHat公司针对自动安装RedHat、Fedora与CentOS这3种同一体系的操作系统而制定的问答规范。它一般会以.cfg作为文件后缀名，不仅可以自动应答一些简单问题，还可以指定操作系统需要安装的各种软件包，更可以在操作系统完装完成后自动执行一些脚本，这些脚本可以让我们直接配置系统。
通常，kickstart配置文件通过命令行工具system-config-kickstart生成。当然，我们更喜欢在CentOS图形界面环境下生成用于定制操作系统安装的配置文件。
preseed则是Debian/Ubuntu操作系统自动安装的问答规范，同样可以预定义Ubuntu如何安装，其配置更多通过手动处理。
2.4. cobblerRedHat在2008年发布了网络安装服务器套件 cobbler（补鞋匠），它集成了PXE、DHCP、HTTP、TFTP、kickstart等服务，相对之前的 kickstart ，能够更加快捷、方便的批量布署redhat、centos类系统。
2.5. 选择由上面的基础概念我们了解到，可选择的技术组合有：

PXE+DHCP+HTTP+TFTP+kickstart
PXE+DHCP+HTTP+TFTP+preseed
PXE+DHCP+HTTP+TFTP+kickstart+preseed
PXE+cobbler
PXE+cobbler+preseed

本文，我们选择PXE+DHCP+HTTP+TFTP+kickstart+preseed，主要参考PXE无人值守网络安装Ubuntu14.04 、 PXE+kickstart自动安装ubuntu14.04 和 在 Ubuntu 14.04 中配置 PXE 服务器。
3. 实践篇3.1. PXE服务器PXE服务器选择我们手动安装好的那一台，系统是Ubuntu 14.04.5 Server (64-bit)版，当前IP为172.16.0.213。
配置为PXE服务器后网段为10.0.0.0/8，IP为10.0.0.100。
3.2. DHCP1、安装isc-dhcp-serversudo apt-get update
sudo apt-get install isc-dhcp-server -y
2、查看网卡和ipip add或者ifconfig，查看正在使用的局域网网卡，假设为eth0。
3、指定dhcp网卡sudo vim /etc/default/isc-dhcp-server拉到最底部，修改为：
INTERFACES=&quot;eth0&quot;

4、配置dhcp服务sudo vim /etc/dhcp/dhcpd.conf在文件末尾添加：
subnet 10.0.0.0 netmask 255.0.0.0 &#123;    range 10.0.0.101 10.0.0.200;    option subnet-mask 255.0.0.0;    option routers 10.0.0.100;    option broadcast-address 10.255.255.255;    filename &quot;pxelinux.0&quot;;    next-server 10.0.0.100;&#125;

子网和掩码可以根据自己的需要设置。
filename &quot;pxelinux.0&quot;;，指定PXE启动文件名。next-server 10.0.0.100;，指定PXE服务器IP地址，TFTP服务器IP地址。
5、重启服务service isc-dhcp-server restart
备注：也可以安装DNSmasq，DNSmasq实际上是一个集成工具，包含了DNS、DHCP、TFTP服务器。
3.3. TFTP主要参考Tftpd-hpa
1、安装tftpd-hpaapt-get install tftpd-hpa -y
2、给根目录添加读写权限sudo chmod 777 /var/lib/tftpboot/
3、测试tftp
apt-get install tftp -ycd ~tftp 127.0.0.1get version.info

4、tftpd开启关闭命令sudo /etc/init.d/tftpd-hpa start
sudo /etc/init.d/tftpd-hpa stop
3.4. apache21、安装apache2sudo apt-get install apache2 -y
web根目录是 /var/www/html/
2、测试apache2curl 127.0.0.1
3、重启命令sudo /etc/init.d/apache2 restart
3.5. 文件准备1、上传ubuntu-14.04.5-server-amd64.iso到用户目录。使用xftp上传，或者使用scp命令，这里使用scp命令。打开git bash，执行：scp ubuntu-14.04.5-server-amd64.iso test@172.16.0.213:~
2、挂载ubuntu-14.04.5-server-amd64.iso到ubuntu目录。在用户目录执行：sudo mkdir /var/www/html/ubuntu
sudo mount ubuntu-14.04.5-server-amd64.iso /var/www/html/ubuntu
3、拷贝部分文件到tftpsudo cp -r /var/www/html/ubuntu/install/netboot/* /var/lib/tftpboot/
4、拷贝seed文件到web根目录sudo cp /var/www/html/ubuntu/preseed/ubuntu-server.seed /var/www/html/
5、编辑seed文件sudo vim /var/www/html/ubuntu-server.seed
在文件末尾添加：
d-i live-installer/net-image string http://10.0.0.100/ubuntu/install/filesystem.squashfsd-i pkgsel/include string openssh-server

（1）因为在ubuntu12.10版本以后，安装一些包会依赖于预配置的文件系统，这就是导致使用kickstart方式无法成功安装的原因。（2）自动安装ssh服务。
3.6. kickstartks.cfg中保存的是安装系统过程中默认的系统配置，没有这个文件就需要安装中对系统手动交互设置。kickstart则是方便编辑这个文件的，不用它的话也可以手工编辑。
kickstart需要GUI界面，我因为是安装的server，所以需要安装桌面（如果是desktop版本就不需要）。
1、安装图形界面sudo apt-get install ubuntu-desktop -y
过程非常久，请先去继续读论文。
2、安装kickstartsudo apt-get install system-config-kickstart -y
3、使用屏幕键盘连接主机，进入图形界面sudo startx，或者直接重启sudo reboot
4、点击右上角“Search your computer and online resources”，搜索kickstart，启动kickstart。
5、然后，按照图示选择kickstart的配置。
附：上图的长图合成工具为 美图秀秀网页版
6、保存ks.cfg后，查看ks.cfg内容为：
#Generated by Kickstart Configurator#platform=AMD64 or Intel EM64T#System languagelang en_US#Language modules to installlangsupport en_US#System keyboardkeyboard us#System mousemouse#System timezonetimezone Asia/Shanghai#Root passwordrootpw --disabled#Initial useruser test --fullname &quot;test&quot; --iscrypted --password $1$AKq0i3Yu$Tunuha7bYwq5uUV62F2nF0#Reboot after installationreboot#Use text mode installtext#Install OS instead of upgradeinstall#Use Web installationurl --url http://10.0.0.100/ubuntu#System bootloader configurationbootloader --location=mbr #Clear the Master Boot Recordzerombr yes#Partition clearing informationclearpart --all --initlabel #System authorization infomationauth  --useshadow  --enablemd5 #Network informationnetwork --bootproto=dhcp --device=eth0#Firewall configurationfirewall --disabled --trust=eth0 --ssh #Do not configure the X Window Systemskipx

7、移动ks.cfg到web根目录sudo mv ks.cfg /var/www/html/
8、修改txt.cfgsudo vim /var/lib/tftpboot/ubuntu-installer/amd64/boot-screens/txt.cfg，原txt.cfg为：
default installlabel install        menu label ^Install        menu default        kernel ubuntu-installer/amd64/linux        append vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet label cli        menu label ^Command-line install        kernel ubuntu-installer/amd64/linux        append tasks=standard pkgsel/language-pack-patterns= pkgsel/install-language-support=false vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet 

修改为：
default installlabel install        menu label ^Install        menu default        kernel ubuntu-installer/amd64/linux        append ks=http://10.0.0.100/ks.cfg preseed/url=http://10.0.0.100/ubuntu-server.seed netcfg/get_nameservers=10.0.0.100 vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet label cli        menu label ^Command-line install        kernel ubuntu-installer/amd64/linux        append tasks=standard pkgsel/language-pack-patterns= pkgsel/install-language-support=false vga=788 initrd=ubuntu-installer/amd64/initrd.gz --- quiet

3.7. pxelinux.cfg编辑pxelinux.cfg配置文件sudo vim /var/lib/tftpboot/pxelinux.cfg/default
# D-I config version 2.0include ubuntu-installer/amd64/boot-screens/menu.cfgdefault ubuntu-installer/amd64/boot-screens/vesamenu.c32prompt 0timeout 60
timeout默认是0，改为60（6秒后自动选择install选项）。
3.8. 收尾1、重新配置IPsudo vim /etc/network/interfaces，修改eth0的IP为10.0.0.100：
# This file describes the network interfaces available on your system# and how to activate them. For more information, see interfaces(5).# The loopback network interfaceauto loiface lo inet loopback# The primary network interfaceauto eth0iface eth0 inet staticaddress 10.0.0.100gateway 10.0.0.100netmask 255.0.0.0

2、重启eth0sudo ifdown eth0，sudo ifup eth0
3、重启dhcpsudo service isc-dhcp-server restart
4、重新挂载镜像sudo mount ubuntu-14.04.5-server-amd64.iso /var/www/html/ubuntu
5、检查各个服务ps aux | grep dhcp
ps aux | grep apache
ps aux | grep tftp
6、把PC和PXE服务器连接到同一个交换机，测试一下PC能否获取到IP。
7、PC浏览器访问 http://10.0.0.100/ubuntu/ ，测试能否正常访问Web服务。
4. 安装篇4.1. 安装步骤1、把PXE服务器和需要安装系统的主机（下文统一称为目标主机）连接到同一个交换机。
2、目标主机开机启动进入BIOS，首选启动项选择PXE，保存退出。
3、然后，目标主机就会自动获取IP地址，连接PXE服务器，进入到安装界面。
4、在自动网络配置的时候失败，报错“Network autoconfiguration failed”，多次尝试依然失败。Alt+F2，进入命令行界面，ip add，查看到已经获取到IP地址。
Alt+F1，切换回安装界面，这时需要手动配置，一次不行就再来一次。

5、安装过程中，分区后卡住，需要手动选择确认分区。
6、之后，无需任何操作，直到安装成功。
7、测试ssh服务ssh test@localhost，顺利登录。
以上安装过程，已经省去了很多交互步骤，但是依然比较麻烦，主要存在两个问题：一个是自动配置网络的问题，一个是需要分区确认的问题。下面尝试解决这两个问题。
4.2. 网络问题1、对于网络配置问题，在14.04 server PXE installation fails at “Configure the network” page 找到了可能的答案，也许是/var/lib/tftpboot/pxelinux.cfg/default 文件的问题，修改为：
# D-I config version 2.0include ubuntu-installer/amd64/boot-screens/menu.cfgdefault linuxlabel linux  menu default  menu label Linux  kernel ubuntu-installer/amd64/linux  append ks=http://10.0.0.100/ks.cfg preseed/url=http://10.0.0.100/ubuntu-server.seed vga=normal initrd=ubuntu-installer/amd64/initrd.gz --prompt 0timeout 60
问题依旧。最好再改回原来的/var/lib/tftpboot/pxelinux.cfg/default文件配置，因为这样启动时通过DHCP获取IP的时间明显变长了，有时还会出现无法获取IP以至于无法启动安装的情况。如果没有获取到IP，记得及时按下ctrl+alt+del，进行重启，不然要等很久才能开始第二次尝试。
2、莫非是dhcp配置的问题？参考14.04 server PXE installation fails at “Configure the network” page修改网卡配置为：
# This file describes the network interfaces available on your system# and how to activate them. For more information, see interfaces(5).# The loopback network interfaceauto loiface lo inet loopback# The primary network interfaceauto eth0iface eth0 inet staticaddress 10.0.0.100netmask 255.0.0.0gateway 10.0.0.100broadcast 10.255.255.255dns-nameservers 10.0.0.100

重启网络，sudo ifdown eth0，sudo ifup eth0。重启dhcp，sudo service isc-dhcp-server restart。问题依旧。
3、参考Install And Configure PXE Server On Ubuntu 14.04 LTS，sudo vim /etc/dhcp/dhcpd.conf，在最后添加： 
allow booting;allow bootp;option option-128 code 128 = string;option option-129 code 129 = text;next-server 10.0.0.100;filename &quot;pxelinux.0&quot;;

重启dhcp，sudo service isc-dhcp-server restart。问题依旧。
4、莫非是因为没有安装DNS？参考《Ubuntu14.04配置DNS Server》，安装好DNS。问题依旧。
5、参考preseeds，sudo vim /var/www/html/ubuntu-server.seed，在尾部添加：
d-i netcfg/choose_interface select autod-i netcfg/get_hostname string $hostname
问题依旧。
6、删除步骤第5次尝试的内容，参考，sudo vim /var/www/html/ubuntu-server.seed，在尾部添加：
d-i netcfg/choose_interface select eth0d-i netcfg/dhcp_timeout string 60d-i netcfg/get_hostname string ubuntud-i netcfg/get_domain string ubuntu-domain
问题依旧。
7、删除步骤第6次尝试的内容，参考基于PXE和preseed安装Ubuntu-14.04 Server 64位操作系统，sudo vim /var/www/html/ubuntu-server.seed，在尾部添加：
d-i netcfg/choose_interface select eth0d-i netcfg/dhcp_failed noted-i netcfg/dhcp_options select Do not configure the network at this timed-i netcfg/get_hostname string ubuntud-i netcfg/get_domain string ubuntu-domain
问题依旧。
8、参考附录 B. 使用预置自动进行安装，sudo vim /var/www/html/ubuntu-server.seed，在尾部添加：
# If you prefer to configure the network manually, uncomment this line and# the static network configuration below.d-i netcfg/disable_dhcp boolean trued-i netcfg/disable_autoconfig boolean trued-i netcfg/use_autoconfig boolean false# If you want the preconfiguration file to work on systems both with and# without a dhcp server, uncomment these lines and the static network# configuration below.d-i netcfg/dhcp_failed noted-i netcfg/dhcp_options select Configure network manually# Static network configuration.## IPv4 exampled-i netcfg/get_ipaddress string 10.0.0.134d-i netcfg/get_netmask string 255.0.0.0d-i netcfg/get_gateway string 10.0.0.100d-i netcfg/get_nameservers string 10.0.0.100d-i netcfg/confirm_static boolean true
问题依旧。感觉netcfg这个系列的命令根本没有生效！
附录 B. 使用预置自动进行安装一文中说，例如，为网卡设置静态地址。它使加载了预置文件以后网络预置再运行一次，这需要将下面的命令包含在 “preseed/run” 脚本里面：kill-all-dhcp; netcfg
于是在Is it possible to download a bash script and execute it from a preseed file?和kill-all-dhcp中找到了使用脚本的方法。（1）创建脚本sudo vim /var/www/html/run.sh写入内容为：
#!/bin/sh# Killall for dhcp clients.kill-all-dhcp;netcfg;for client in dhclient udhcpc pump dhcp6c; do        pid=$(pidof $client) || true        [ &quot;$pid&quot; ] || continue        if kill -0 $pid 2&gt;/dev/null; then                kill -TERM $pid                sleep 1                # Still alive? Die!                if kill -0 $pid 2&gt;/dev/null; then                        kill -KILL $pid                fi        fidone

（2）变更权限sudo chmod a+x /var/www/html/run.sh
（3）在ubuntu-server.seed文件中添加一行：
d-i preseed/run string run.sh
问题依旧，但是Alt+F2进入命令行，发现获取的IP确实没有了。
9、莫非，是因为DHCP的网段问题？折腾了一个多小时，PXE服务器地址换成192.168.34.1，DHCP换成了192.168.34.0网段，然而并没有什么用。
最终，我猜测这个锅是ubuntu14的，或者是电脑硬件的。在askubuntu上进行了提问，但是没有得到满意的回复。
4.3. 分区问题1、参考Ubuntu Kickstart installation using LVM waits for input，修改ks.cfg文件。
sudo vim /var/www/html/ks.cfg，添加：
preseed partman-lvm/confirm_nooverwrite boolean true
问题依旧。
2、sudo vim /var/www/html/ks.cfg，添加：
preseed partman-lvm/confirm_nooverwrite boolean truepreseed partman-lvm/device_remove_lvm boolean truepreseed partman/confirm_write_new_label boolean truepreseed partman/confirm boolean truepreseed partman/confirm_nooverwrite boolean true

问题依旧。
3、删除ks.cfg中添加的内容，参考Automated Kickstart Partitioning，sudo vim /var/www/html/ubuntu-server.seed，在末尾加上：
d-i partman/confirm_write_new_label boolean trued-i partman/choose_partition select Finish partitioning and write changes to diskd-i partman/confirm boolean true

分区成功，吼吼吼！折腾了一整天，总算有点成就，泪流满面。。。让我安静的哭一会。。。
5. 后记配置Ubuntu批量自动安装的过程中，发现最重要的两个文件是/var/www/html/ks.cfg 和 /var/www/html/ubuntu-server.seed，因为他们两个是控制交互的。
折腾的过程中学到了很多东西，而最终的成果，除了网络需要手动配置之外，其他都很满意。我会继续尝试解决网络配置的问题，有了进展，再更新本文。
6. 书签PXE无人值守网络安装Ubuntu14.04
KICKSTART无人值守安装
COBBLER无人值守安装
Cobbler+preseed自动化安装Ubuntu系统
Ubuntu 16.04.2 LTS PXE全自动安装
u盘全自动安装 ubuntu server 12.04
CentOS/Ubuntu制作自动安装iso实操
Installation/UnattendedCD
PXELINUX
第二章、安裝伺服器與 kickstart 大量部署用戶端電腦
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>ubuntu</tag>
        <tag>操作系统</tag>
        <tag>pxe</tag>
      </tags>
  </entry>
  <entry>
    <title>Ubuntu 14.04.5 Server物理机安装</title>
    <url>/dev-ubuntu-14.04.5-server-install/</url>
    <content><![CDATA[1. 前言机房里有十几台服务器需要安装系统，这里选择Ubuntu 14.04.5 Server版。本文，就图文记录一下Ubuntu 14.04.5 Server (64-bit)的物理机的安装过程。


2. 思路我们知道，使用虚拟机安装ubuntu很简单，选好配置，设置好镜像，启动虚拟机一步步安装就可以了。但是，在物理机上安装却没有那么简单，尤其是server版，过程中有很多坑。假设使用U盘安装，一般安装思路如下：
1、下载Ubuntu 14.04.5 Server (64-bit)的镜像。2、使用usb-installer、Win32DiskImager、UltraISO等把镜像写入U盘。3、插入U盘到服务器，设置BIOS把U盘作为第一启动项。4、一步步安装配置，解决各种坑。
3. 第一次尝试3.1. 下载镜像访问Ubuntu官网，下载Ubuntu 14.04.5 Server (64-bit)。
3.2. 镜像写入1、访问Universal USB Installer – Easy as 1 2 3，下载usb-installer。
2、双击启动usb-installer，按照下图选择，然后Create。
3、把镜像拷贝进U盘。
3.3. BIOS设置开机时，提示可能有：

Press Esc to enter SETUP
Press F2 go to Setup Utility
Press DEL to enter EFI BIOS SETUP
Press Del run Setup
Press F1 run Setup
Press F2 to Setup

等等，按照提示狂点几下提示的按键就可以进入BIOS界面。选择选择U盘作为第一启动项，保存退出。
3.4. 安装配置安装过程如下图：
不知道大家注意到了没有，到了最后一张图，实际上和倒第四张图相同，也就是说，开始循环了。而此时的主机还没有网络，无法从网络下载镜像。
U盘安装Ubuntu-14.04.2-server一文给出了解决办法：挂载iso文件到/cdrom目录。然而，我的操作却失败了。
继续查找安装失败的原因，找到了14.04server LTS安装为啥跳不过choose mirror 这一步骤和14.04.1 server install without internet access，和我的问题一毛一样。怀疑是usb-installer的问题，决定换成Win32DiskImager。
备注：之前在同一台机器上，用usb-installer安装ubuntu desktop版，一路畅通无阻安装成功。。。
4. 第二次尝试第二次尝试，主要是重新制作镜像。
4.1. 镜像写入1、访问Win32DiskImager和Win32DiskImager makes an Ubuntu family USB boot drive in Windows，下载Win32DiskImager。
2、双击启动Win32DiskImager，按照下图选择，然后点击“写入”。
此时，出现一个坑，U盘空间变小了，变成了2.18MB。而且U盘里只有一个文件夹：efi。空间太小，也就无法把镜像再拷贝进U盘里。
使用这样一个2.18MB的U盘，能安装成功？我保持怀疑。
4.2. 安装配置一直到上图，都和第一次尝试相同。接下来却发生了变化：注意，到上图的步骤时，点击空格选择安装OpenSSH server。
然后，就安装成功了吗？并没有！卡在了最后一步Installing GRUB boot loader！等了20分钟，没有丝毫动静。重启看看，果然无法启动系统！
离成功如此接近，最后却失败了，蓝瘦！莫非，是因为某个步骤没有配置好？于是，重装，在Partition disks一步，选择yes后继续，依然卡在最后一步。
再来一次，在Partition disks一步，选择Guided-reuse partition…，然后，居然安装成功！
但是，高兴的太早了，选择Ubuntu，并没有进入到登录页面，而且键盘失效了，插哪个USB接口都没有任何反应。折腾继续。。。
再次重装，配置选择和之前没有什么不同，没想到一路顺利，安装成功！对于这种莫名其妙的成功，我也是很无奈啊。。。
5. 后记以前在物理机上安装过很多次Linux，只不过没有记录。因为安装系统的过程无法截图，只能挨个拍照，很麻烦。这次下了很大的决心，完成了本文，给自己点个赞，真是个勤劳的郝同学啊！
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>ubuntu</tag>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>iptables实现端口映射</title>
    <url>/dev-iptables-port-map/</url>
    <content><![CDATA[1. 需求描述主机A和主机B在同一个局域网。主机A eth0的IP为内网IP：192.168.56.101，eth1为公网IP：120.77.36.222。主机B eth0的IP为内网IP：192.168.56.102，80端口开启了Web服务。
通过外网访问到主机A，无法访问到主机B。现在想要访问外网访问主机B的Web服务，怎么办？答：使用端口映射。
参考文档：

《iptables入门篇》
linux下用用iptables做端口映射的shell
Ubuntu 14.04 端口映射



2. 具体配置2.1. 开启路由功能主机A上开启路由功能
sudo echo 1 &gt; /proc/sys/net/ipv4/ip_forward

如果报错：-bash: /proc/sys/net/ipv4/ip_forward: Permission denied那就切换到root用户：
sudo -iecho 1 &gt; /proc/sys/net/ipv4/ip_forward

要想永久生效，还要把 /etc/sysctl.conf 文件中 net.ipv4.ip_forward 的值改为1。
2.2. 配置端口映射1、新建portmap.sh，内容如下：
#!/bin/bashpro=&#x27;tcp&#x27;NAT_Host=&#x27;192.168.56.101&#x27;NAT_Port=3480Dst_Host=&#x27;192.168.56.102&#x27;Dst_Port=80iptables -t nat -A PREROUTING -m $pro -p $pro --dport $NAT_Port -j DNAT --to-destination $Dst_Host:$Dst_Portiptables -t nat -A POSTROUTING -m $pro -p $pro --dport $Dst_Port -d $Dst_Host -j SNAT --to-source $NAT_Host

2、添加执行权限
sudo chmod a+x portmap.sh

3、执行脚本
sudo ./portmap.sh

4、查看iptables规则
sudo iptables -t nat -vnL PREROUTING --line-numbersudo iptables -t nat -nL --line-number

5、如果不需要端口映射了，可以删除端口映射规则
sudo iptables -t nat -D PREROUTING 1 # 实际规则编号通过上一步确定sudo iptables -t nat -D POSTROUTING 1 # 实际规则编号通过上一步确定

6、设置重启后依然有效参考《Linux配置SNAT上网》。
2.3. 测试效果主机A上测试
curl 192.168.56.102 # 正常curl 192.168.56.101:3480 # 报错 curl: (7) Failed to connect to 192.168.56.103 port 3480: Connection refusedtelnet 192.168.56.101 3480 # 不通

主机B上测试
curl 192.168.56.102 # 正常curl 192.168.56.101:3480 # 正常telnet 192.168.56.101 3480 # 正常

在配置iptables的主机A上，直接测试端口映射是没有效果的，必须从另外的主机上进行测试才能确认端口映射已经生效。原理不明，了解的大佬欢迎留言解惑。
3. 本机端口映射有主机A，eth0的IP为内网IP：192.168.56.101，eth1为公网IP：120.77.36.222。
访问 http://192.168.56.101 正常，访问 http://120.77.36.222 失败。这是因为，电信会封锁80端口和8080端口。为了使外网也可以访问80端口的服务，可以把10180端口映射为80端口。
sudo iptables -t nat -A PREROUTING -p tcp --dport 10180 -j REDIRECT --to-ports 80

然后，就可以通过 http://120.77.36.222:10180 来访问80端口的服务了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>ubuntu</tag>
        <tag>iptables</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo加速访问</title>
    <url>/dev-hexo-accelerate/</url>
    <content><![CDATA[1. 前言《Hexo环境搭建2018年5月版》一文中，重新记录了hexo的安装部署方法，比较详细完整。但是，在访问的时候，总感觉访问速度太慢，本文就记录下hexo加速访问的方法。


2. 思路访问速度慢，其实很好理解，毕竟coding提供了千千万万个pages服务，分给你的流量和带宽不会很多。想要加速，最简单的思路，就是更换服务器，换成自己的服务器。没错，有钱就是任性。
而且操作极其简单，git clone一下即可。
3. 操作步骤1、服务器安装nginx和git。
2、创建www目录mkdir /opt/www
3、clone项目cd /opt/www
git clone https://git.coding.net/voidking/voidking.git
4、nginx配置参考《Hexo启用https加密连接》，修改www.voidking.com.conf。
cd /etc/nginx/conf.d/cp www.voidking.com.conf www.voidking.com.conf.bakvim www.voidking.com.conf

www.voidking.com.conf 内容为：
server &#123;    listen 80;    listen 443 ssl;    server_name www.voidking.com;    charset utf-8;    ssl_certificate /etc/nginx/ssl/1_www.voidking.com_bundle.crt;    ssl_certificate_key  /etc/nginx/ssl/2_www.voidking.com.key;    ssl_session_timeout  5m;    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;    ssl_ciphers  HIGH:!ADH:!EXPORT56:RC4+RSA:+MEDIUM;    ssl_prefer_server_ciphers on;    if ($ssl_protocol = &quot;&quot;) &#123;        return 301 https://$host$request_uri;    &#125;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        root /opt/www/voidking/;        index index.html;    &#125;    error_page 404 /404.html;    location = /404.html &#123;        root /opt/www/voidking/;        index 404.html;    &#125;    location ~ /\.git &#123;        return 404;    &#125;&#125;

重启nginx，systemctl restart nginx
4. 更新每次文章有更新时，hexo g，hexo d操作和以前相同。
然后，登录服务器，cd /opt/www/voidking，git pull，即可完成更新。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo环境搭建2018年5月版</title>
    <url>/dev-hexo-build-environment-2018-05/</url>
    <content><![CDATA[1. 前言15年的时候，写了一篇《Hexo环境搭建》，记录了hexo的详细安装部署步骤。三年了，node和hexo都有了很大的变化。而郝同学刚刚换了magicbook，又要再次搭建hexo，那就推出新版的安装记录。


2. 环境准备2.1. git1、访问git官网，下载git。
2、双击exe文件安装，一路next即可。
3、安装好git之后，配置用户名和邮箱git config --global user.name &quot;voidking&quot;
git config --global user.email &quot;voidking@qq.com&quot;
2.2. nvm1、访问nvm-windows项目，下载nvm-noinstall.zip。
2、解压nvm-noinstall.zip到D:\develop\nvm目录。
3、双击install.cmd，生成settings.txt文件。打开settings.txt文件，添加：
node_mirror: https://npm.taobao.org/mirrors/node/npm_mirror: https://npm.taobao.org/mirrors/npm/

4、右键此电脑，属性，高级系统设置，环境变量。
5、系统变量中，添加JVM_HOME为D:\develop\nvm；添加JVM_SYMLINK为D:\develop\nvm\nodejs；Path中添加;%NVM_HOME%;%NVM_SYMLINK%;。
6、用户变量中，添加JVM_HOME为D:\develop\nvm；添加JVM_SYMLINK为D:\develop\nvm\nodejs；Path中添加;%NVM_HOME%;%NVM_SYMLINK%;。
2.3. node1、查看可获得的node版本nvm list available
2、安装node8.11.1nvm install 8.11.1
2、使用node8.11.1nvm listnvm install 8.11.1
3、测试使用node -v
3. 安装hexo1、切换国内源npm config set registry=&quot;http://registry.cnpmjs.org&quot;
2、安装hexonpm install -g hexo
3、初始化新建hexo目录，并安装依赖包。
hexo init hexocd hexonpm install

4、测试hexo g，hexo s，然后查看 http://localhost:4000
4. 个性化4.1. 添加RSSnpm install hexo-generator-feed --save，
注意，后面的参数--save绝对不能省，否则该插件信息不会写入package.json。hexo clean，hexo g，查看public文件夹，可以看到atom.xml文件。
4.2. 添加sitemapnpm install hexo-generator-sitemap --save
npm install hexo-generator-baidu-sitemap --save
hexo clean，hexo g，查看public文件夹，可以看到sitemap.xml和baidusitemap.xml文件。sitemap的初衷是给搜索引擎看的，为了提高搜索引擎对自己站点的收录效果，我们最好手动到google和百度等搜索引擎提交sitemap.xml。具体参考《hexo生成sitemap》。
4.3. 主题进入hexo/themes目录，下载自己维护的yilia主题git clone https://github.com/voidking/hexo-theme-yilia.git yilia
该主题的修改，可以参考《hexo主题优化》。
4.4. config.ymlhexo目录中的_config.yml修改为：
# Hexo Configuration## Docs: http://hexo.io/docs/configuration.html## Source: https://github.com/hexojs/hexo/# Sitetitle: VoidKingsubtitle: 学而不思则罔，思而不学则殆！description: VoidKingauthor: VoidKinglanguage: zh-CNtimezone:# URL## If your site is put in a subdirectory, set url as &#x27;http://yoursite.com/child&#x27; and root as &#x27;/child/&#x27;url: http://voidking.comroot: /permalink: :year/:month/:day/:title/permalink_defaults:# Directorysource_dir: sourcepublic_dir: publictag_dir: tagsarchive_dir: archivescategory_dir: categoriescode_dir: downloads/codei18n_dir: :langskip_render:# Writingnew_post_name: :title.md # File name of new postsdefault_layout: posttitlecase: false # Transform title into titlecaseexternal_link: true # Open external links in new tabfilename_case: 0render_drafts: falsepost_asset_folder: falserelative_link: falsefuture: truehighlight:  enable: true  line_number: true  auto_detect: true  tab_replace:# Category &amp; Tagdefault_category: uncategorizedcategory_map:tag_map:# Date / Time format## Hexo uses Moment.js to parse and display date## You can customize the date format as defined in## http://momentjs.com/docs/#/displaying/format/date_format: YYYY-MM-DDtime_format: HH:mm:ss# Pagination## Set per_page to 0 to disable paginationper_page: 5pagination_dir: pageindex_generator:  per_page: 5archive_generator:  per_page: 500  yearly: true    monthly: true tag_generator:  per_page: 100 category_generator:   per_page: 100 # Extensions## Plugins: http://hexo.io/plugins/## Themes: http://hexo.io/themes/theme: yilia# Deployment## Docs: http://hexo.io/docs/deployment.htmldeploy:  type: git  repository: https://git.coding.net/voidking/voidking.git  branch: coding-pages## gitcafe### deploy:###   type: git###   repository: git@gitcafe.com:voidking/voidking.git###   branch: gitcafe-pages## github### deploy:###   type: git###   repository: https://git.coding.net/voidking/voidking.git###   branch: coding-pages

4.5. mathjax本来打算参考《Hexo中使用Mathjax的冲突问题》，修改一下修改hexo的渲染代码。但是，郝同学惊奇地发现，在新版的hexo中，并不需要修改任何代码，mathjax就渲染正常，nice。
5. 发布5.1. coding1、在coding上注册一个帐号。
2、新建一个项目，和帐号名相同。
3、项目启动Pages服务。
5.2. 本地操作1、生成本地代码hexo g
2、本地测试hexo s，然后访问 http://localhost:4000
3、发布到codinghexo d
提示ERROR Deployer not found: git，需要先执行：npm install hexo-deployer-git --save
4、线上测试http://voidking.coding.me
5.3. 域名1、申请域名。
2、域名解析添加CNAME记录指向voidking.coding.me。
3、在coding上绑定域名。
4、测试访问。
6. 后记至此，hexo安装部署完成。有些步骤写的比较简单，不理解的可以自行谷歌百度。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>hexo</tag>
        <tag>mathjax</tag>
        <tag>好好学Hexo</tag>
        <tag>nvm</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux配置SNAT上网</title>
    <url>/dev-linux-snat/</url>
    <content><![CDATA[1. 需求描述有主机A，eth0的IP为内网IP：192.168.56.101，eth1为公网IP：120.77.36.222。有主机B，eth0的IP为内网IP：192.168.56.102。主机A可以访问外网，主机B和主机A在同一个局域网。现在我们想让主机B也可以上网，该怎么办？最简单的方法有两个：1）SNAT；2）代理
本文中，我们学习使用配置SNAT的方式，实现局域网中的所有主机可以上网。
参考文档：

《iptables入门篇》
Linux下配置SNAT上网
利用iptables的SNAT功能实现局域网共享上网
ubuntu配置静态路由及重启生效



2. SNAT数据包流向2.1. 发送数据包B发送数据包时，A也就是NAT主机，分析数据包表头，将此数据包转到可以连接公网的IP上去。由于私有IP与公有IP不能互通，A会通过iptables的NAT table内的Postrounting链将数据包表头伪装成A的公网IP，并且将这两个不同来源的数据包对应关系写入暂存内存中，然后将数据包传送出去。此时传到互联网的这个数据包，已经表现为来自公网IP，而非来自局域网。
2.2. 接收响应包当互联网把数据响应给B时，会首先进入NAT主机A，A分析数据包的序号，对比刚刚记录到内存中的数据，由于发现该数据包为后端主机之前传送出去的，因此在NAT Prerouting链中，会将目标IP修改成为后端主机，即B的IP，然后发现目标已经不是A的公网IP，开始通过路由分析，将数据包传送到A的局域网接口，再传送到最终目标192.168.56.102上去。
3. snat具体配置3.1. 主机A3.1.1. 接收数据包允许接收局域网网卡的数据包sudo iptables -I INPUT -i eth0 -j ACCEPT
或者sudo iptables -P INPUT ACCEPT
3.1.2. 开启路由功能sudo echo 1 &gt; /proc/sys/net/ipv4/ip_forward如果报错：-bash: /proc/sys/net/ipv4/ip_forward: Permission denied那就切换到root用户：sudo -iecho 1 &gt; /proc/sys/net/ipv4/ip_forward
要想永久有效，还要把/etc/sysctl.conf文件里边的net.ipv4.ip_forward的值改为1。
3.1.3. 伪装数据包sudo iptables -t nat -A POSTROUTING -s 192.168.56.102 -o eth1 -j MASQUERADE
或者用SNAT直接修改IP数据包的表头来源IPsudo iptables -t nat -A POSTROUTING -s 192.168.56.102 -o eth1 -j SNAT --to 192.168.56.101
如果需要支持整个网段：sudo iptables -t nat -A POSTROUTING -s 192.168.56.0/24 -o eth1 -j SNAT --to 192.168.56.101
如果需要支持连续IP：sudo iptables -t nat -A POSTROUTING -m iprange --src-range 192.168.56.102-192.168.56.104 -o eth1 -j SNAT --to 192.168.56.101
注意：

亲测以上所有命令中的-o eth1可以省略，所以，在分不清哪个网卡是内网哪个网卡是外网的情况下，直接省略即可。
如果想要允许所有IP，-s 192.168.56.0/24也可以省略。
-j SNAT --to 192.168.56.101可以换成-j SNAT --to 120.77.36.222。
-j SNAT --to 120.77.36.222建议换成-j MASQUERADE，尤其在外网IP非固定的情况下。
综上，最简单的万能SNAT命令为sudo iptables -t nat -A POSTROUTING -j MASQUERADE

3.1.4. 查看与删除查看NAT表链规则sudo iptables -t nat -nL --line-number
删除POSTROUTING第一条规则sudo iptables -t nat -D POSTROUTING 1
3.2. 重启后依旧生效1、保存规则sudo chmod a+w -R /optsudo iptables-save &gt; /opt/iptables.rules
2、手动导入规则sudo iptables-restore &lt; /opt/iptables.rules
3、开机自动导入规则在ubuntu下要把一个程序加入开机启动，一般可以通过修改rc.local来完成，但ubuntu下有两个rc.local文件，分别是/etc/rc.local和/etc/init.d/rc.local。因为/etc/init.d/rc.local调用了/etc/rc.local，所以我们的开机脚本最好直接写入/etc/rc.local，写在exit 0之前即可。
sudo vim /etc/rc.local在exit 0之前添加：
/sbin/iptables-restore &lt; /opt/iptables.rules

3.3. 主机B3.3.1. centos如果是centos，那么配置ifcfg文件sudo vim /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0BOOTPROTO=staticONBOOT=yesIPADDR=192.168.56.102NETMASK=255.255.255.0GATEWAY=192.168.56.101

然后service network restart
3.3.2. ubuntu如果是ubuntu，那么配置interfaces文件sudo vim /etc/network/interfaces
auto loiface lo inet loopbackauto em1iface em1 inet staticaddress 192.168.56.102netmask 255.255.255.0gateway 192.168.56.101

然后sudo /etc/init.d/networking restart
总之，B主机配置的关键在于网关，网关要设置成主机A的内网IP。
请问，网关不设置成主机A的内网IP可以吗？可以，那就是另外一种稍微复杂的设置方法了。
此时，使用主机B，已经可以ping通外网的IP了。但是ping域名会提示“ping: unknown host”。
4. DNS配置4.1. 方法一1、编辑resolv.confsudo vim /etc/resolv.conf，添加nameserver的配置
nameserver 180.76.76.76nameserver 223.6.6.6
上面的两个IP地址，分别是百度和阿里的公共DNS。
2、测试ping www.baidu.com
3、不过，这种方法在重启之后会失效。想要永久生效，还需要编辑base文件。sudo vim /etc/resolvconf/resolv.conf.d/base，添加：
nameserver 180.76.76.76nameserver 223.6.6.6

4.2. 方法二1、编辑interfacessudo vim /etc/network/interfaces
2、添加nameserver的配置
dns-nameserver 180.76.76.76 223.5.5.5

3、重启服务器sudo reboot实际上，重启后的dns配置会写入到resolv.conf中。
4、测试ping www.baidu.com
5. 结语至此，snat完美配置成功！在这个过程中，顺带复习了一下iptables，收获满满。感觉iptables和route有很大关系，但是在配置过程中没有用到route。书签中有关于route的文档，用到的时候再深入学习。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>网络</tag>
        <tag>iptables</tag>
      </tags>
  </entry>
  <entry>
    <title>Ganglia认证授权</title>
    <url>/dev-ganglia-auth/</url>
    <content><![CDATA[1. 前言《Ubuntu14.04安装配置Ganglia》一文中，我们已经完成了Ganglia的安装配置。但是，我们并不想允许所有人都看到集群的使用情况，所以，本文就研究一下怎样给ganglia添加认证授权（加密设置），主要参照《使用 Ganglia 对 Linux 网格和集群服务器进行实时监控》。


2. 原理在ganglia的wiki中包含对Authorization System的描述：

Ganglia contains a simple authorization system to selectively allow or deny users access to certain parts of the gweb application. We rely on the web server to provide authentication, so any Apache authentication system (htpasswd, LDAP, etc) is supported.

由此，我们可以得知，直接利用apache的认证机制即可完成对ganglia的加密设置。
3. 详细步骤1、安装apache工具sudo apt-get install apache2-utils
2、创建密码文件sudo touch /usr/share/ganglia-webfrontend/auth.basic
sudo chmod a+rw /usr/share/ganglia-webfrontend/auth.basic
3、为ganglia创建用户并设定密码htpasswd -c /usr/share/ganglia-webfrontend/auth.basic adminganglia
会提示两次输入密码，以上命令创建了一个adminganglia的用户，密码存放在/usr/share/ganglia-webfrontend/auth.basic（可以任意目录或文件名，只要 Apache 对此有读取权限就可以。） 
4、修改配置文件ganglia.confsudo vim /etc/apache2/sites-enabled/ganglia.conf原配置文件为：
Alias /ganglia /usr/share/ganglia-webfrontend&lt;Directory &quot;/usr/share/ganglia-webfrontend&quot;&gt;        AllowOverride All        Order allow,deny        Allow from all        Deny from none&lt;/Directory&gt;

修改为：
Alias /ganglia /usr/share/ganglia-webfrontend&lt;Directory &quot;/usr/share/ganglia-webfrontend&quot;&gt;    AuthType basic    AuthName &quot;Ganglia web UI&quot;    AuthBasicProvider file    AuthUserFile &quot;/usr/share/ganglia-webfrontend/auth.basic&quot;    Require user adminganglia&lt;/Directory&gt;

5、重启apachesudo /etc/init.d/apache2 restart
6、测试访问访问 http://192.168.56.103/ganglia/ ，这次需要密码才能访问。
至此，给ganglia添加认证授权完成。
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>监控</tag>
        <tag>ubuntu</tag>
        <tag>apache</tag>
        <tag>ganglia</tag>
      </tags>
  </entry>
  <entry>
    <title>使用Certbot申请泛域名证书</title>
    <url>/dev-certbot-domain-certificate/</url>
    <content><![CDATA[1. 需求描述《Hexo启用https加密连接》一文中，我们在腾讯云申请了一个三级域名证书，然后给网站配置上了域名证书。
那如果我们有一百个域名，一个个申请域名证书就太麻烦了，这是我们就需要一个泛域名证书。怎么申请泛域名证书呢？方案一：通过阿里云、腾讯云等云平台申请，但是需要付费，便宜的也要年费两千左右。方案二：使用certbot申请泛域名证书，然后配置好自动续期。
因为想要省点钱，所以本文中我们就来研究学习一下方案二，给dnspod上的域名申请一个泛域名证书。
参考文档：

使用 Certbot 自动申请和续期泛域名证书
使用 certbot 申请泛域名证书和自动续签
使用Certbot申请泛域名SSL证书（Let’s Encrypt）
certbot instructions - Nginx on CentOS7
Certbot Document



2. Certbot简介
为了在您的网站上启用 HTTPS，您需要从证书颁发机构（CA）获取证书（一种文件）。 Let’s Encrypt 是一个证书颁发机构（CA）。 要从 Let’s Encrypt 获取您网站域名的证书，您必须证明您对域名的实际控制权。 您可以在您的 Web 主机上运行使用 ACME 协议的软件来获取 Let’s Encrypt 证书。

而 Certbot，就是Let’s Encrypt推荐的 ACME 客户端，可以用来申请Let’s Encrypt颁发的域名证书。
Let’s Encrypt 的证书有效期为 90 天， 建议每 60 天自动续期一次证书。
Let’s Encrypt 对证书申请进行了限制，每个注册域名（顶级域名）可签发证书数量为每周50张。证书续期遵守特殊规则：它们不计入每个注册域名的证书数量的限制，但它们受到每周最多 5 张重复证书的限制。
PS：与 Let’s Encrypt 类似的，还有一个 ZeroSSL。
参考文档：

Let’s Encrypt - 入门指南
Let’s Encrypt - 常见问题
Let’s Encrypt - 速率限制
Let’s Encrypt
Certbot
ZeroSSL

3. 安装配置思路1、安装certbot（docker方式）2、配置dns自动验证3、申请泛域名4、配置使用证书5、配置证书自动续期
4. 安装Certbot1、下载certbot镜像
docker pull certbot/certbot:v1.32.0
该镜像里包含python3.10.8，能够方便我们后续使用DNS自动验证脚本。
2、创建挂载目录
mkdir -p /etc/letsencrypt

3、测试运行
docker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 certonly
当我们运行certbot容器时，默认会执行certbot命令。certonly是个参数，表示只申请证书，不会对nginx等web服务器进行自动配置。
5. 配置DNS自动验证申请域名证书时，需要验证对域名的所有权，这个验证操作可以通过手动修改域名解析完成，也可以通过域名供应商提供的API自动完成。
本文中，对域名供应商API的自动操作，使用 certbot-letencrypt-wildcardcertificates-alydns-au
5.1. 申请DNS API密钥要使用DNS API，需要先申请密钥。常见域名供应商获取 DNS API 密钥的入口：

ALY_KEY 和 ALY_TOKEN：阿里云 API key 和 Secrec 官方申请文档。
TXY_KEY 和 TXY_TOKEN：腾讯云 API 密钥官方申请文档。
HWY_KEY 和 HWY_TOKEN：华为云 API 密钥官方申请文档
GODADDY_KEY 和 GODADDY_TOKEN：GoDaddy API 密钥官方申请文档。

5.2. 脚本配置本节会对DNS自动验证脚本简单修改，适配certbot/certbot:v1.32.0镜像，觉得麻烦的同学可以直接跳到【脚本配置2.0】一节，使用修改好的脚本。
1、下载DNS自动验证脚本
mkdir -p /etc/letsencrypt/auth-hookcd /etc/letsencrypt/auth-hookgit clone https://github.com/ywdblog/certbot-letencrypt-wildcardcertificates-alydns-au auchmod 0777 au/au.sh

2、修改DNS API密钥au.sh中，填入我们申请的DNS API密钥
3、指定使用sh运行脚本容器中不存在bash，只有sh所以au.sh头部，!#/bin/bash改为#/bin/sh
否则申请证书时会报错：Hook ‘–manual-auth-hook’ for voidking.com reported error code 127Hook ‘–manual-auth-hook’ for voidking.com ran with error output: /bin/sh: /etc/letsencrypt/auth-hook/au/au.sh: not found
详情参考Certbot 无法运行挂钩脚本
4、修改python路径容器中的python路径是/usr/local/bin/python所以au.sh中，/usr/bin/python 改为 /usr/local/bin/python
否则申请证书时会报错：Hook ‘–manual-auth-hook’ for voidking.com ran with error output: /etc/letsencrypt/auth-hook/au/au.sh: line 112: /usr/bin/python: not found
5、添加根域名（可选）domain.ini中，如果没有需要的根域名，需要自行添加
5.3. 脚本配置2.01、下载DNS自动验证脚本
mkdir -p /etc/letsencrypt/auth-hookcd /etc/letsencrypt/auth-hookgit clone https://github.com/voidking/certbot-dns-au auchmod 0777 au/au.sh

2、修改DNS API密钥au.sh中，填入我们申请的DNS API密钥
3、添加根域名（可选）domain.ini中，如果没有需要的根域名，需要自行添加
6. 申请泛域名证书1、测试申请泛域名证书
docker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 certonly \    -d *.voidking.com \    -d voidking.com \    --manual \    --preferred-challenges dns \    --manual-auth-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy add&quot; \    --manual-cleanup-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy clean&quot; \    --dry-run
输出：
Saving debug log to /var/log/letsencrypt/letsencrypt.logSimulating a certificate request for *.voidking.comThe dry run was successful.

参数说明：

-d：要申请证书的域名。这里使用两个-d参数，使证书同时支持泛域名 *.voidking.com 和主域名 voidking.com
–manual-auth-hook：指定要添加dns记录的脚本，调用域名供应商提供的API添加要验证的dns记录
–manual-cleaup-hook：指定要删除dns记录的脚步，调用域名供应商提供的API删除要验证的dns记录
–dry-run：测试运行，并不真实申请证书

2、申请泛域名证书（去掉–dry-run）
docker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 certonly \    -d *.voidking.com \    -d voidking.com \    --manual \    --preferred-challenges dns \    --manual-auth-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy add&quot; \    --manual-cleanup-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy clean&quot;
根据提示，填写邮箱，同意协议等。
申请的证书文件，会被放到 /etc/letsencrypt/archive 路径中，同时 /etc/letsencrypt/live 路径中会创建软链，续期时会自动修改软链。申请证书的相关参数，会被放到 /etc/letsencrypt/renewal 路径中
证书文件有四个，官方说明如下：
This directory contains your keys and certificates.`privkey.pem`  : the private key for your certificate.`fullchain.pem`: the certificate file used in most server software.`chain.pem`    : used for OCSP stapling in Nginx &gt;=1.3.7.`cert.pem`     : will break many server configurations, and should not be used                 without reading further documentation (see link below).WARNING: DO NOT MOVE OR RENAME THESE FILES!         Certbot expects these files to remain in this location in order         to function properly!We recommend not moving these files. For more information, see the CertbotUser Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.

一般使用fullchain.pem和privkey.pem就可以了。如果开启了OCSP stapling，那么需要使用chain.pem。
7. 查看证书1、查看证书详细信息
docker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 certificates

2、查看证书过期时间
openssl x509 -noout -enddate -in /etc/letsencrypt/live/voidking.com/fullchain.pem

8. 配置证书自动续期因为申请证书的相关参数放到了 /etc/letsencrypt/renewal 路径中，所以我们可以通过一条命令实现续期。
8.1. 续期所有证书docker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 renew \    --manual \    --preferred-challenges dns \    --manual-auth-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy add&quot; \    --manual-cleanup-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy clean&quot;
证书有效期小于 30 天的情况才会被 renew。
8.2. 续期指定证书docker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 renew \    --cert-name voidking.com \    --manual \    --preferred-challenges dns \    --manual-auth-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy add&quot; \    --manual-cleanup-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy clean&quot;

8.3. 配置定时自动续期1、证书续期脚本写入 cert-renew.sh
cat &lt;&lt;EOF &gt; /etc/letsencrypt/auth-hook/cert-renew.shdocker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 renew --force-renewal \    --cert-name voidking.com \    --manual \    --preferred-challenges dns \    --manual-auth-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy add&quot; \    --manual-cleanup-hook &quot;/etc/letsencrypt/auth-hook/au/au.sh python txy clean&quot; \    &gt;&gt; /etc/letsencrypt/auth-hook/cert-renew.logEOFchmod a+x /etc/letsencrypt/auth-hook/cert-renew.sh

2、添加定时任务
crontab -e

添加每周定时执行：
0 0 * * 0 /bin/bash /etc/letsencrypt/auth-hook/cert-renew.sh
因为证书有效期小于 30 天的情况才会被 renew，所以 crontab 的周期可以配置为 1 天或 1 周。
9. 撤销证书docker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 revoke \    --cert-name voidking.com# ordocker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/certbot:v1.32.0 revoke \    --cert-path /etc/letsencrypt/live/voidking.com/cert.pem

10. 使用证书server &#123;    listen 80;    listen 443 ssl;    server_name www.voidking.com;    charset utf-8;    ssl_certificate /etc/letsencrypt/live/voidking.com/fullchain.pem;    ssl_certificate_key  /etc/letsencrypt/live/voidking.com/privkey.pem;    ssl_stapling on;    ssl_stapling_verify on;    ssl_trusted_certificate /etc/letsencrypt/live/voidking.com/chain.pem;    ssl_session_timeout  5m;    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;    ssl_ciphers  HIGH:!ADH:!EXPORT56:RC4+RSA:+MEDIUM;    ssl_prefer_server_ciphers on;    ...&#125;

详情参考文档《Nginx入门篇》
11. dns-cloudflare上文中的 certbot/certbot:v1.32.0 镜像，适用的域名供应商包括阿里云、腾讯云、华为云、Godaddy等，但是不适用于CloudFlare。
如果一个域名的DNS服务是由CloudFlare提供的，那么需要另外安装dns_cloudflare插件，才可以正常进行域名证书申请。否则会报错：
Client with the currently selected authenticator does not support any combination of challenges that will satisfy the CA. You may need to use an authenticator plugin that can do challenges over DNS.

这里，我们可以选择使用安装好插件的镜像 certbot/dns-cloudflare:v2.8.0 ，使用方法如下：
1、申请一个cloudflare api-token
2、创建一个token配置文件 /etc/letsencrypt/cloudflare/cloudflare.ini内容为：
dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567

3、申请域名证书
docker run -it --rm --name certbot \    -v &quot;/etc/letsencrypt:/etc/letsencrypt&quot; \    -v &quot;/var/log/letsencrypt:/var/log/letsencrypt&quot; \    certbot/dns-cloudflare:v2.8.0 certonly \    -d *.voidking.com \    -d voidking.com \    --dns-cloudflare \    --dns-cloudflare-credentials /etc/letsencrypt/cloudflare/cloudflare.ini \    --dns-cloudflare-propagation-seconds 60

12. 注意事项老版本的操作系统或浏览器可能不支持 Let’s Encrypt 的根证书。报错：
错误: 无法验证 test.voidking.com 的由 “/C=US/O=Let&#x27;s Encrypt/CN=R3” 颁发的证书:  颁发的证书已经过期。

这种情况下，需要更新系统或浏览器。
CentOS中：
yum install -y ca-certificates

Ubuntu中：
apt-get install -y ca-certificates

13. 后记对于泛域名证书的配置，建议配置在内网最外层的网关入口处，这样方便统一配置。例如：
Request -&gt; Nginx(certificate)  -&gt; Ingress                               -&gt; Nginx                               -&gt; Real Server

除了certbot，还有一款acme.sh也很好用，感兴趣的同学可以自行研究下。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>https</tag>
        <tag>tls</tag>
        <tag>ssl</tag>
        <tag>cloudflare</tag>
      </tags>
  </entry>
  <entry>
    <title>浅谈数据加密</title>
    <url>/dev-encryption-algorithm/</url>
    <content><![CDATA[1. 为什么需要数据加密？数据加密，就是加密算法和加密密钥将明文转变为密文，而解密则是通过解密算法和解密密钥将密文恢复为明文。它的核心是密码学。
数据加密是计算机系统对信息进行保护的一种最可靠的办法。它利用密码技术对信息进行加密，实现信息隐蔽，从而起到保护信息安全的作用。
最典型的应用场景就是数据网络传输和数据存储，为了避免敏感信息泄露，我们就会用到加密算法。


2. 加密算法分类加密算法可以分三类：对称加密算法、非对称加密算法、哈希算法（散列算法）。

对称加密算法的作用是加密和解密，通信双方事先约定好密钥，使用同一个密钥进行明文加密和密文解密。优点是速度快，缺点是不够安全。
非对称加密算法的作用是安全约定密钥（对称加密密钥），服务端保留私钥公开公钥，客户端使用公钥加密密钥明文发送给服务端，服务端用私钥解密获得密钥明文。优点是安全，缺点是速度慢。
哈希算法在加密领域的作用是生成信息摘要，用来验证信息的完整性和正确性。

对称加密算法速度快，但是约定密钥时不够安全，容易被拦截，所以出现了非对称加密算法，可以安全地约定密钥。
详情参考漫画：什么是加密算法？
3. HTTPS(SSL/TLS)加密原理3.1. 基础概念3.1.1. HTTPS
超文本传输安全协议（英语：HyperText Transfer Protocol Secure，缩写：HTTPS；常称为HTTP over TLS、HTTP over SSL或HTTP Secure）是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信，但利用SSL/TLS来加密数据包。HTTPS开发的主要目的，是提供对网站服务器的身份认证，保护交换资料的隐私与完整性。这个协议由网景公司（Netscape）在1994年首次提出，随后扩展到互联网上。

参考文档：维基百科 - 超文本传输安全协议
3.1.2. SSL/TLS
传输层安全性协议（Transport Layer Security，TLS）及其前身安全套接层（Secure Sockets Layer，SSL）是一种安全协议，目的是为互联网通信提供安全及数据完整性保障。

SSL包含记录层（Record Layer）和传输层，记录层协议确定传输层数据的封装格式。传输层安全协议使用X.509认证，之后利用非对称加密演算来对通信方做身份认证，之后交换对称密钥作为会谈密钥（Session key）。这个会谈密钥是用来将通信两方交换的数据做加密，保证两个应用间通信的保密性和可靠性，使客户与服务器应用之间的通信不被攻击者窃听。
虽然TLS是SSL的经过更新的、更安全的版本，但是一般情况下我们仍将自己的安全证书称为SSL。
参考文档：维基百科 - 传输层安全性协议
3.1.3. PKI
公开密钥基础建设（英语：Public Key Infrastructure，缩写：PKI），又称公开密钥基础架构、公钥基础建设、公钥基础设施、公开密码匙基础建设或公钥基础架构，是一组由硬件、软件、参与者、管理政策与流程组成的基础架构，其目的在于创造、管理、分配、使用、存储以及撤销数字证书。密码学上，公开密钥基础建设借着数字证书认证机构（CA）将用户的个人身份跟公开密钥链接在一起。对每个证书中心用户的身份必须是唯一的。链接关系通过注册和发布过程创建，取决于担保级别，链接关系可能由CA的各种软件或在人为监督下完成。PKI的确定链接关系的这一角色称为注册管理中心（Registration Authority，RA）。RA确保公开密钥和个人身份链接，可以防欺诈。在微软的公开密钥基础建设之下，注册管理中心（RA）又被叫做从属数字证书认证机构（Subordinate CA）。

参考文档：维基百科-公开密钥基础架构
3.1.4. CA
数字证书认证机构（英语：Certificate Authority，缩写为CA），也称为电子商务认证中心、电子商务认证授权机构，是负责发放和管理数字证书的权威机构，并作为电子商务交易中受信任的第三方，承担公钥体系中公钥的合法性检验的责任。

参考文档：维基百科-证书颁发机构
3.1.5. 数字证书
公钥证书（英语：Public key certificate），又称数字证书（digital certificate）或身份证书（identity certificate）。是用于公开密钥基础建设的电子文件，用来证明公开密钥拥有者的身份。此文件包含了公钥信息、拥有者身份信息（主体）、以及数字证书认证机构（发行者）对这份文件的数字签名，以保证这个文件的整体内容正确无误。拥有者凭着此文件，可向电脑系统或其他用户表明身份，从而对方获得信任并授权访问或使用某些敏感的电脑服务。电脑系统或其他用户可以透过一定的程序核实证书上的内容，包括证书有否过期、数字签名是否有效，如果你信任签发的机构，就可以信任证书上的密钥，凭公钥加密与拥有者进行可靠的通信。


公钥证书包括自签证书、根证书、中介证书、授权证书、终端实体证书（TLS服务器证书和TLS客户端证书）。

参考文档：维基百科-公钥证书
3.2. HTTPS(SSL/TLS)加密原理TLS/SSL 的功能实现主要依赖于三类基本算法：散列函数 Hash、对称加密和非对称加密。其利用非对称加密实现身份认证和密钥协商，对称加密算法采用协商的密钥对数据加密，基于散列函数验证信息的完整性。
TLS 的基本工作方式是，客户端使用非对称加密与服务器进行通信，实现身份验证并协商对称加密使用的密钥，然后对称加密算法采用协商密钥对信息以及信息摘要进行加密通信，不同的节点之间采用的对称密钥不同，从而可以保证信息只能通信双方获取。例如，在 HTTPS 协议中，客户端发出请求，服务端会将公钥发给客户端，客户端验证过后生成一个密钥再用公钥加密后发送给服务端（非对称加密），双方会在 TLS 握手过程中生成一个协商密钥（对称密钥），成功后建立加密连接。通信过程中客户端将请求数据用协商密钥加密后发送，服务端也用协商密钥解密，响应也用相同的协商密钥。后续的通信使用对称加密是因为对称加解密快，而握手过程中非对称加密可以保证加密的有效性，但是过程复杂，计算量相对来说也大。
HTTPS通信原理：对称加密：HTTPS 的最终加密形式。非对称加密：解决单向对称密钥的传输问题。数字证书：解决公钥传输信任问题。
HTTPS的最终加密形式是对称加密，因为对称加密速度快。但是对称加密需要双方约定好一个密钥，这里就存在一个问题，就是这个密钥在传输过程中有可能会被别人窃取。对于这个问题，HTTPS中的解决办法是使用非对称加密，就是使用一个密钥对，公钥公开，私钥保密。公钥加密的内容只有私钥才能解密，私钥签名的内容只有公钥才能验签。server把公钥发送给client，client使用公钥加密对称加密的密钥，然后发送给server，server使用私钥进行解密获取到对称密钥。因为私钥只有server有，所以client向server的传输是安全的。但是这里又有一个问题，server把公钥传送给client的过程怎么保证安全？怎么保证返回请求的server是真正的server？对于这个问题，HTTPS中的解决办法是使用数字证书。数字证书就是权威第三方机构（CA）出具的证明，经过这个机构认证的证书是可靠的证书。那这个数字证书怎样申请和使用呢？1、首先server也就是域名所有者会向CA申请SSL证书，申请的时候会使用dns解析等方法验证，保证只有域名所有者才能申请到证书明文和CA私钥签名（证书签名），证书中包含公钥。2、client和server在进行通信时，client从server获取到了域名的证书明文和CA私钥签名。3、client根据摘要算法对证书明文生成证书摘要；同时使用内置在操作系统中的CA公钥对CA私钥签名进行验签获取证书摘要。如果两份证书摘要是一致的，域名也和证书上对的上，说明server是可信的，那我们从证书中获取到的公钥也就是可信的。4、client使用公钥加密对称加密的密钥，发送给server，server使用私钥进行解密获取到对称密钥，然后双方就可以进行安全通信了。
如果黑客拿到了server的证书和证书签名，是不是可以冒充server？不可以，因为没有私钥，无法解密client通过公钥加密的内容。
参考文档：

HTTPS百度百科
18 张图彻底弄懂 HTTPS 的原理
SSL/TLS 详解
SSL/TLS 原理详解
OpenSSL 与 SSL 数字证书概念贴

3.3. HTTPS配置实践
网站配置HTTPS加密访问：《好好学Hexo：Hexo启用HTTPS加密连接》
自建CA并颁发证书：《好好学Linux：Linux中安装使用mkcert签发域名证书》
K8S中的数字证书：《好好学K8S：K8S中创建用户账户和服务账户》

4. 信封加密原理
信封加密就是用一个密钥加密明文数据，然后用另一个密钥加密这个密钥。

比如战争时期，Alice和Bob要就军事行动进行沟通。他们已经提前在线下碰过头，约定了一个共享的对称密钥S。某天，Alice想发电报给Bob，通知内容C具体为老大决定今晚11点动手。
正常的加密思路是：Alice用对称密钥S加密通知内容C，然后将密文发送给Bob。Bob在收到密文消息之后，用对应密钥S解密得到通知内容C。 这样就知道行动内容了。但是，这样做会有什么问题吗？
当然有。假设Alice它们按照计划行动了，那以后但凡Alice和Bob之间的通知内容是老大决定今晚11点动手，敌方就得会知晓，因为密文是保持不变的。那怎么解决这个问题呢？
Alice首先临时生成一个对称密钥R，并用该对称密钥R加密通知内容C，然后将密文发送给Bob。但问题是，Bob在收到密文消息之后，它该如何解密呢？Bob的手上只有之前约定的对称密钥S，并没有Alice临时生成的对称密钥R，因此Bob无法解密。
聪明的Alice也想到了这个问题。因此，Alice使用对称密钥S对密钥R做了个加密，并将加密的结果同时发给了Bob。这样，Bob在收到消息之后，首先用对称密钥S解密出临时的对称密钥R，然后用R解密出通知内容C。
如果Alice和Bob再次传输相同的内容C，因为使用的临时对称密钥R是不同的，因此加密后的C也是不同的，即便加密前的明文C还是同一个。
在这个场景中，对称密钥R实际上就是一个会话密钥，对称密钥S就是一个主密钥。因为对称密钥R每次都不一样，但是对称密钥S是一直被重复使用的。
参考文档：

信封加密

]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>k8s</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>k8s</tag>
        <tag>ssh</tag>
        <tag>https</tag>
        <tag>加密</tag>
        <tag>tls</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo启用HTTPS加密连接</title>
    <url>/dev-hexo-https/</url>
    <content><![CDATA[1. 前言访问个人网站，偶尔会跳出广告，看来网站被运营商劫持了。为了避免这种情况，郝同学决定对网站进行https加密。早在2015年，美国政府就要求所有联邦政府网站使用https加密连接，这次郝同学追赶一下国际潮流的尾巴。
HTTPS(SSL/TLS)加密原理，参考文档《浅谈数据加密》。


2. HTTPS配置方法https是在http下加入ssl层，那么，怎么加入呢？很简单，在nginx、apache等web服务器中加入ssl安全证书的配置即可。
3. SSL证书申请很多网站都提供免费的ssl证书：

阿里云免费SSL证书，保护类型选择1个域名，选择品牌选择Symantec，即可看到免费型DV SSL。
腾讯云免费SSL证书
七牛免费SSL证书

推荐使用腾讯云，注册登录，根据提示填写信息，不久就可以拿到证书。
解压www.voidking.com.zip，可以得到下列文件：
www.voidking.com│  www.voidking.com.csr│├─Apache│      1_root_bundle.crt│      2_www.voidking.com.crt│      3_www.voidking.com.key│├─IIS│      keystorePass.txt│      www.voidking.com.pfx│├─Nginx│      1_www.voidking.com_bundle.crt│      2_www.voidking.com.key│└─Tomcat        keystorePass.txt        www.voidking.com.jks

4. 配置思路4.1. GitLabgithub和coding，都不支持上传ssl证书。但是，gitlab支持啊！最简单的思路就是，把网站迁到gitlab上。
按照这个思路，参考《利用gitlab pages和hexo搭建一个个人博客》和《零成本打造安全博客的简单办法》。
1、打开gitlab的项目，Settings，Pages，New Domain，即可进入域名配置页面。
2、Domain填入www.voidking.com，Certificate (PEM)填入1_www.voidking.com_bundle.crt中的内容，Key (PEM)填入2_www.voidking.com.key中的内容。
4.2. CloudflareCloudflare是一家CDN提供商，它提供了免费的https服务（但不是应用SSL证书）。实现模式就是，用户到CDN服务器的连接为https，而CDN服务器到真实服务器的连接为http。也就是说，在CDN服务器那里加上反向代理。没错，又是反向代理，只不过这次的代理服务器归属于Cloudflare，不是我们自己的。
这种方式，好处是你不需要拥有自己的公网服务器，坏处是你必须把域名的dns服务器换成cloudflare。
具体设置步骤参考《让个人域名下GithubPage完美支持https》。
4.3. Nginx反向代理反向代理是普遍思路，无论我们的真实服务器在github、coding还是gitlab，都可以使用反向代理的方式完成https加密连接。前提是你有一个公网服务器，上面安装了nginx。
用户访问域名；域名解析到公网IP，访问到nginx；nginx反向代理到coding等真实服务器，通过http方式拿到页面；nginx拿到页面后进行ssl加密，然后返回给用户加密后的页面。
4.4. 选择配置方式相对而言，Nginx反向代理是最通用的方式。
这里我们就选择Nginx反向代理的方式，通过Nginx反向代理coding pages上的博客，来实现https加密。
5. 详细步骤5.1. 修改域名解析原来的域名解析位CNAME记录，指向 pages.coding.me 。
现在修改为A记录，指向自己的主机公网IP。
5.2. 上传证书1、创建证书目录
mkdir /etc/nginx/ssl

2、上传证书上传  1_www.voidking.com_bundle.crt 和 2_www.voidking.com.key 到 ssl 目录。
5.3. 配置Nginx1、coding pages配置不变，依然绑定 www.voidking.com 域名。
2、/etc/nginx/conf.d 中新建配置文件 www.voidking.com.conf ，内容如下：
server &#123;    listen 80;    listen 443 ssl;    server_name www.voidking.com;    charset utf-8;    ssl_certificate /etc/nginx/ssl/1_www.voidking.com_bundle.crt;    ssl_certificate_key  /etc/nginx/ssl/2_www.voidking.com.key;    ssl_session_timeout  5m;    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;    ssl_ciphers  HIGH:!ADH:!EXPORT56:RC4+RSA:+MEDIUM;    ssl_prefer_server_ciphers on;    if ($ssl_protocol = &quot;&quot;) &#123;        return 301 https://$host$request_uri;    &#125;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        proxy_pass http://voidking.coding.me;    &#125;&#125;

这个配置中，http 自动重定向到 https。
注意SSL的配置不要加 ssl on ，否则启动时会存在一个警告：
[warn] 1#1: the &quot;ssl&quot; directive is deprecated, use the &quot;listen ... ssl&quot; directive instead in /etc/nginx/conf.d/www.voidking.com.conf:11

3、重启nginx
systemctl restart nginx

访问 http://www.voidking.com ，会自动跳转到 https://www.voidking.com ，说明配置成功。
4、测试访问访问 https://www.voidking.com ，已经可以正常访问网址。
有些脚本是通过http方式引用的，会提示不安全，需要修改hexo主题。
6. 修改Hexo主题修改Hexo主题，使之支持https。修改详情参考 voidking/hexo-theme-yilia/commit/a228365
7. 后记这里并没有配置 voidking.com 和 blog.voidking.com ，我们可以参照上面的步骤进行配置。但是，更加简单的配置方式，是在dnspod上配置显性url，直接跳转到 http://www.voidking.com 。
本文主要研究Hexo启用https加密连接。其实所有Web站点都可以启用https加密连接，原理相同。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>hexo</tag>
        <tag>https</tag>
        <tag>加密</tag>
        <tag>tls</tag>
        <tag>ssl</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>七牛对象存储添加HTTPS代理</title>
    <url>/dev-qiniu-oss-https-proxy/</url>
    <content><![CDATA[1. 前言七牛对象存储（图床），http访问不收费，https收费。收费标准参考文档价格 | CDN
为了给图床省钱，不妨使用nginx给七牛图床配置一层https代理。


2. 域名配置七牛对象存储配置中，添加三个域名：

https类型的 qiniu.cdn.voidking.com
http类型的  http.cdn.voidking.com
http类型的 cdn.voidking.com

DNSPod域名解析，添加三个域名解析：

qiniu.cdn.voidking.com 解析到七牛对象存储CNAME
http.cdn.voidking.com 解析到七牛对象存储CNAME
cdn.voidking.com 解析到nginx主机IP

因为 cdn.voidking.com 没有配置解析到七牛云对象存储，所以会一直处于等待CNAME解析状态，没有关系。
3. Nginx配置nginx添加配置文件 cdn.voidking.com.conf ，内容为：
server &#123;    listen 80;    listen 443 ssl;    server_name cdn.voidking.com;    charset utf-8;    #ssl配置    ssl_certificate /etc/nginx/ssl/1_cdn.voidking.com_bundle.crt;    ssl_certificate_key  /etc/nginx/ssl/2_cdn.voidking.com.key;    ssl_session_timeout  5m;    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;    ssl_ciphers  HIGH:!ADH:!EXPORT56:RC4+RSA:+MEDIUM;    if ($ssl_protocol = &quot;&quot;) &#123;        return 301 https://$host$request_uri;    &#125;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        proxy_pass http://qiniu.cdn.voidking.com;    &#125;&#125;

重新加载nginx配置后，就可以通过 https://cdn.voidking.com 这个https类型的URL来访问七牛图床了。因为没有使用七牛的https流量，所以是免费的。
如果通过 https://cdn.voidking.com 这个https类型的URL来访问七牛图床，就会收费了，当前速度也会更快。
4. 2023年10月14日 404问题2023年10月14日，发现通过 cdn.voidking.com 这个域名获取图片都是404，估计是七牛云做了什么变动。
尝试把 proxy_pass 地址配置为 cdn.voidking.com 域名的 CNAME 地址，问题解决。
proxy_pass http://cdn-voidking-com-idvmpix.qiniudns.com;



]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>troubleshooting</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>问题排查</tag>
        <tag>hexo</tag>
        <tag>https</tag>
        <tag>tls</tag>
        <tag>ssl</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：CentOS安装配置Jupyter</title>
    <url>/dev-centos-jupyter/</url>
    <content><![CDATA[1. 前言如果在服务器上安装一个jupyter，那么就可以随时随地使用jupyter，很方便。
本文主要记录centos7上jupyter的安装方法，设置密码的方法，添加守护进程的方法，顺便给它设置个域名。


2. 安装jupyter参考《Jupyter notebook》或者官网文档，完成jupyter的安装。
1、升级pippip install --upgrade pip
2、安装jupyterpip install jupyter
如果安装报错：
_scandir.c:14:20: fatal error: Python.h: No such file or directory #include &lt;Python.h&gt;                    ^compilation terminated.error: command &#x27;gcc&#x27; failed with exit status 1

那么需要先安装python-develyum install python-devel
3、启动jupyter notebook
如果报错：socket.error: [Errno 99] Cannot assign requested address
那么在启动时加上参数：jupyter notebook --ip 0.0.0.0 --allow-root
4、开放端口firewall-cmd --zone=public --add-port=8888/tcp --permanentfirewall-cmd --reload
5、测试访问访问地址：http://ip:8888进入到jupyter登录页面，如下图：
3. 设置密码在安装完成jupyter后，我们看到，登录页面给出了三种登录方式。方式一：复制启动时的token，粘贴到页面中，直接登录。方式二：参照the documentation on how to enable a password进行配置。方式三：复制启动时的token，粘贴到页面中，同时输入新密码，修改密码并登录。
这里，我们选择方式二，首先关闭jupyter，然后具体配置步骤如下：
1、生成jupyter配置文件jupyter notebook --generate-config该命令会生成文件/root/.jupyter/jupyter_notebook_config.py
2、设置密码jupyter notebook password提示两次输入密码，然后密码被hash化之后保存到jupyter_notebook_config.json中。
3、测试访问再次启动jupyter，使用密码访问成功。
4、禁止启动浏览器此时，启动jupyter时会默认启动本地浏览器，这个需要关闭。vim /root/.jupyter/jupyter_notebook_config.py，修改一行：
c.NotebookApp.open_browser = False

5、指定根目录在哪里执行jupyter notebook，哪里就是根目录，这里，我们也可以指定根目录。（1）创建根目录mkdir /opt/notebook
（2）vim /root/.jupyter/jupyter_notebook_config.py，修改一行：
c.NotebookApp.notebook_dir = u&#x27;/opt/notebook&#x27;

4. 配置保活参考《CentOS安装配置Supervisor》，安装配置好supervisor。
1、新建配置文件jupyter.confvim /etc/supervisor/jupyter.conf写入内容如下：
[program:jupyter]command=/usr/bin/jupyter notebook --ip 0.0.0.0 --allow-rootuser=rootautostart=trueautorestart=truestdout_logfile=/var/log/supervisor/%(program_name)s.logstderr_logfile=/var/log/supervisor/%(program_name)s.log

2、重启supervisorsystemctl restart supervisord
5. 设置域名假设nginx的安装目录为/usr/local/nginx/。1、创建vhost目录mkdir /usr/local/nginx/conf/vhost
2、修改nginx.conf，添加子配置目录
include /usr/local/nginx/conf/vhost/*.conf;

3、在vhost目录中新建jupyter.voidking.com.conf文件，内容为：
server &#123;    listen 80;    server_name jupyter.voidking.com;    charset utf-8;    location ~* /(api/kernels/[^/]+/(channels|iopub|shell|stdin)|terminals/websocket)/? &#123;        proxy_pass http://127.0.0.1:8888;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header Host $host;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        # WebSocket support        proxy_http_version 1.1;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection &quot;upgrade&quot;;    &#125;&#125;

4、重启nginx/usr/local/nginx/sbin/nginx -s reload
5、本机测试（1）修改hosts文件，添加
127.0.0.1 jupyter.notebook.com
然后用curl命令测试：curl -L jupyter.notebook.com
6、设置域名解析在dnspod上，设置A记录，主机记录填入jupyter，记录值填入服务器IP，稍等几分钟。理论上，此时即可使用域名访问自己的jupyter。
然而，遇到了坑，神坑。。。浏览器访问 http://jupyter.voidking.com ，看到的是nginx的欢迎页面。excuse me? 再次使用curl命令测试，原来，拿到的确实是nginx的页面。那么问题来了？这次解析为什么没有生效？
使用systemctl重启nginx，欢迎页面；指定nginx配置文件重启nginx，欢迎页面；修改jupyter.voidking.com.conf，反向代理到8080的tomcat，欢迎页面；删除jupyter.voidking.com.conf文件，把内容写入nginx.conf，欢迎页面；把新添加内容放在nginx.conf的上部，作为第一个server配置，欢迎页面；换另外一个域名（jupyter.imsnail.com）设置解析，欢迎页面。。。
至此我严重怀疑，nginx有毛病！
6. 卸载nginx上次安装nginx是编译安装，按照下列步骤卸载。
1、删除安装目录
rm -rf /usr/local/nginxrm -rf /var/log/nginxrm -rf /var/temp/nginxrm -rf /var/temp/run/nginx

2、删除开机自启动文件find / -name &quot;nginx*&quot;
rm -rf /usr/lib/systemd/system/nginx.service
systemctl daemon-reload
7. 重新安装nginx接下来，参考nginx官方文档，安装nginx。
1、创建/etc/yum.repos.d/nginx.repo，内容如下：
[nginx]name=nginx repobaseurl=http://nginx.org/packages/centos/7/$basearch/gpgcheck=0enabled=1

2、安装nginxyum install nginx
3、设置开机启动systemctl enable nginx
4、杀死nginx进程，重新启动ps aux | grep nginx
kill -9 [nginx_id]
systemctl start nginx
5、查看运行状态systemctl status nginx -l发现配置文件位置为：/etc/nginx/nginx.conf
6、在/etc/nginx/conf.d/中创建jupyter.voidking.com.conf，内容同上。
7、重启nginxsystemctl restart nginx
接下来，见证奇迹的时刻！再次访问 http://jupyter.voidking.com ，依然是欢迎页面。。。
8. 挣扎大哥，好歹给个报错也行啊！没有报错，访问就欢迎，这是要闹哪样！决定再次换成反向代理8080端口的tomcat，测试一下是不是仍然出现欢迎页面。
1、先创建/var/data/client_body_temp和/var/data/proxy_temp两个目录mkdir -p /var/data/client_body_tempmkdir -p /var/data/proxy_temp
2、然后修改jupyter.voidking.com.conf文件为：
server &#123;    listen 80;    server_name jupyter.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        client_max_body_size       1024m;        client_body_buffer_size    128k;        client_body_temp_path      /var/data/client_body_temp;        proxy_connect_timeout      90;        proxy_send_timeout         90;        proxy_read_timeout         90;        proxy_buffer_size          4k;        proxy_buffers              4 32k;        proxy_busy_buffers_size    64k;        proxy_temp_file_write_size 64k;        proxy_temp_path            /var/data/proxy_temp;        proxy_pass http://127.0.0.1:8080;    &#125;&#125;

3、重启nginxsystemctl restart nginx
4、测试访问访问 http://jupyter.voidking.com/ ，成功访问到tomcat！看来上一个nginx确实有问题，因为重装nginx前，同样的配置，上一个nginx给我返回的是欢迎界面！
5、jupyter.voidking.com.conf修改成：
server &#123;    listen 80;    server_name jupyter.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        client_max_body_size       1024m;        client_body_buffer_size    128k;        client_body_temp_path      /var/data/client_body_temp;        proxy_connect_timeout      90;        proxy_send_timeout         90;        proxy_read_timeout         90;        proxy_buffer_size          4k;        proxy_buffers              4 32k;        proxy_busy_buffers_size    64k;        proxy_temp_file_write_size 64k;        proxy_temp_path            /var/data/proxy_temp;        proxy_pass http://127.0.0.1:8888;    &#125;&#125;
重启nginx，测试访问，成功！但是，上面的配置可以访问jupyter页面，却无法连接kernel，也就是说无法执行python代码。
6、jupyter.voidking.com.conf修改为：
server &#123;    server_name jupyter.voidking.com;     listen 80;    location / &#123;        proxy_pass http://127.0.0.1:8888/;         proxy_set_header X-Real-IP $remote_addr;        proxy_set_header Host $host;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_http_version 1.1;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection &quot;upgrade&quot;;        proxy_redirect off;    &#125;&#125;

重启nginx，测试访问，成功；执行代码，成功。
9. 后记绕了好大一个圈。
首先是jupyter.voidking.com.conf的配置文件写错了，我也认为是配置文件写错了，于是各种修改配置文件，可无论我怎样修改配置文件，nginx只返回一个欢迎页面。
这就误导我认为nginx出了问题，于是开始了针对nginx的各种测试。修改配置也好，重启nginx也好，nginx只返回一个欢迎页面，于是我认定nginx除了问题。
重装nginx后，保持最开始的jupyter.voidking.com.conf配置，依然返回欢迎页面，但是修改配置文件，正常访问到tomcat，这就说明配置文件写错了。
综上，我认为这个锅是配置文件的。之所以最开始修改配置文件nginx毫无反应，孜孜不倦地返回欢迎页面，要么是存在僵尸进程，要么是浏览器存在缓存，干扰了测试，因为nginx坏的可能性太低。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>python</tag>
        <tag>nginx</tag>
        <tag>jupyter</tag>
      </tags>
  </entry>
  <entry>
    <title>Jupyter调试</title>
    <url>/dev-jupyter-debug/</url>
    <content><![CDATA[1. 前言除了PyCharm，另一个常用的编辑器就是Jupyter了。本文研究记录一下Jupyter的调试方法。


2. 使用1、在jupyter中新建文件，运行如下代码：
def func1(a,b):    return a/bdef func2(x):    a=x    b=x-1    return func1(a,b)func2(1)
出现报错：
2、出错后，输入%debug，进入调试模式。调试模式会进入直接出错函数func1，我们可以在调试模式自带的对话框中输入变量名来查看函数中的变量情况，输入“quit”则退出该模式。
同时，在调试模式下，我们也可以通过输入“up”来对外层函数进行调试，查看其中的变量情况。同样的，也可以通过“down”进入内层函数。
3、在notebooke中执行 %pdb on 可以设置为当异常发生时自动进入调试模式，在某些特殊的情况下，这么做可能会更为方便：
def func1(a,b):    return a/bdef func2(x):    a=x    b=x-1    return func1(a,b)%pdb onfunc2(1)


3. 书签Python Jupyter Notebook 中的错误异常与代码调试
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>debug</tag>
        <tag>jupyter</tag>
        <tag>调试</tag>
      </tags>
  </entry>
  <entry>
    <title>PyCharm调试</title>
    <url>/dev-pycharm-debug/</url>
    <content><![CDATA[1. PyCharmPyCharm是一种Python IDE，带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具，比如调试、语法高亮、Project管理、代码跳转、智能提示、自动完成、单元测试、版本控制。此外，该IDE提供了一些高级功能，以用于支持Django框架下的专业Web开发。
本文要记录的，是PyCharm的调试功能。


2. 使用2.1. 一般调试一般调试，不需要对PyCharm做什么设置，直接使用就可以了。
1、打开PyCharm，新建文件test.py
def my_abs(x):    if x &gt;= 0:        return x    else:        return -xprint(my_abs(-100))

2、双击代码左侧行号旁的空白处，添加断点。
3、在菜单栏点击Run，Debug…，选择test。再次调试的时候，只需要在菜单栏点击Run，Debug ‘test’即可。
4、在PyCharm底部的Debugger窗口中，就可以看到Step Over、Step Into、Step Into My Code、Step Out、Run to Cursor等按钮。同时在Variables窗口里，会出现变量的信息。
2.2. django调试django的调试，需要对PyCharm进行设置，这里以调试djsite项目为例。
1、在PyCharm中打开djsite项目，点击djsite的目录，然后点击菜单栏的Run，Edit Configurations…。弹出一个Run/Debug Configurations窗口。
2、点击配置窗口左上角的加号，再点击Python，出现配置项目的页面。主要修改Name为djsite，Script为D:\github\djsite\manage.py，Script parameters为runserver。然后点击OK即可。
3、在菜单栏点击Run，Debug…，选择djsite。在Console中，就会显示出运行情况。（注意，Console可以缩小到最右边，记得把它展开）
4、双击代码左侧行号旁的空白处，添加断点。在浏览器访问该方法，就会跳转到断点处。
3. 书签PyCharm下载地址
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>debug</tag>
        <tag>pycharm</tag>
        <tag>调试</tag>
      </tags>
  </entry>
  <entry>
    <title>CentOS7中firewall的使用</title>
    <url>/dev-firewall/</url>
    <content><![CDATA[1. firewall简介根据官方文档，firewall简介如下：

firewalld daemon provides a dynamically managed firewall with support for network “zones” to assign a level of trust to a network and its associated connections and interfaces. It has support for IPv4 and IPv6 firewall settings. It supports Ethernet bridges and IP set and has a separation of runtime and permanent configuration options. It also has an interface for services or applications to add firewall rules directly. The complete communication with firewalld is done using D-Bus.

firewalld守护进程提供了一个动态管理的防火墙，支持网络“区域”为网络及其关联的连接和接口分配信任级别。它支持ipv4和ipv6防火墙设置。它支持以太网桥和ip集，并且有运行时和永久配置选项的分离。它还具有用于直接添加防火墙规则的服务或应用程序的界面。与firewalld的完整通信是使用d-bus完成的。
简单来说，CentOS6中使用iptables作为防火墙；而在CentOS7中，firewall作为防火墙，是iptables的升级。本文只讨论自己常用的firewall命令和配置，不涉及高级用法。


2. firewall常用命令
systemctl start firewalld，启动。
systemctl stop firewalld，关闭。
systemctl status firewalld，查看状态。
systemctl enable firewalld，设置开机启动。
systemctl disable firewalld，关闭开机启动。


firewall-cmd --version，查看版本。 
firewall-cmd --help，查看帮助。
firewall-cmd --state，显示状态。
firewall-cmd --list-all，查看所有规则。
firewall-cmd --zone=public --list-ports，查看所有打开的端口。 
firewall-cmd --zone=public --add-port=8080/tcp --permanent，打开端口。
firewall-cmd --zone=public --remove-port=8080/tcp --permanent，关闭端口。
firewall-cmd --reload，更新防火墙规则。

3. firewall配置文件firewall的配置文件是xml格式的，存储在/usr/lib/firewalld/ 和 /etc/firewalld/ 两个目录下。
这样就有很大的灵活性，因为文件可以被编辑，写入，备份，用作其他安装的模板等等。
1、系统配置目录ll /usr/lib/firewalld/services
目录中存放定义好的网络服务和端口参数，系统参数，不能修改。
2、用户配置目录ll /etc/firewalld/
3、自定义添加端口常用命令中，我们知道用户可以通过命令的方式添加端口，添加的端口会写入/etc/firewalld 目录下的相应配置文件中。用户也可以通过直接修改配置文件的方式添加端口，比如添加8080端口。
vim /etc/firewalld/zones/public.xml，添加内容：
&lt;port protocol=&quot;tcp&quot; port=&quot;8080&quot;/&gt;
重启firewalld，即可生效。
4、public.xml文件中还可以配置放行的ip范围、端口范围等等，在此不作记录，需要的时候再说。
4. 使用iptables很多同学在使用时，喜欢使用iptables代替firewall，毕竟iptables用习惯了。1、关闭firewall，禁止开机启动systemctl stop firewalldsystemctl disable firewalld
2、安装iptables防火墙yum install iptables-services
3、编辑iptables防火墙配置文件vi /etc/sysconfig/iptables添加开放22、80、3306端口：
iptables -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPTiptables -A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPTiptables -A INPUT -m state --state NEW -m tcp -p tcp --dport 3306 -j ACCEPT

4、启动iptables，设置开机启动systemctl start iptablessystemctl enable iptables
5、iptables常用命令

iptables --list，查看防火墙详情。
iptables-save，查看防火墙详情。
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT，开放端口。
iptables -L -n --line-numbers，将所有规则以序号标记显示。
iptables -D INPUT 1，删除INPUT里序号为1的规则。
service iptables save，保存iptables设置（仅限RedHat系列）。

5. 书签CentOS 7中firewall防火墙详解和配置以及切换为iptables防火墙
CentOS7使用firewalld打开关闭防火墙与端口
CentOS 7 开放3306端口访问
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>firewall</tag>
        <tag>iptables</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：CentOS7中安装配置MySQL</title>
    <url>/dev-centos-mysql/</url>
    <content><![CDATA[1. 前言CentOS7中安装MySQL，有yum安装和编译安装两种方式，本文学习一下两种安装方法。
参考文档：

CentOS 7.0下使用yum安装MySQL
《CentOS安装多版本MySQL》



2. yum安装MySQL1、下载mysql的repo源
wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm

2、安装repo源
rpm -ivh mysql-community-release-el7-5.noarch.rpm

安装这个包后，会获得两个mysql的yum repo源：

/etc/yum.repos.d/mysql-community.repo
/etc/yum.repos.d/mysql-community-source.repo

3、安装mysql
yum install mysql-server

4、登录mysql
mysql -u root

登录时可能报错：ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/var/lib/mysql/mysql.sock’ (2)原因是/var/lib/mysql的访问权限问题，解决办法是把 /var/lib/mysql 的拥有者改为当前用户，并重启服务：
chown -R root:root /var/lib/mysqlservice mysqld restart

5、重置密码，设置远程访问参考文档《MySQL重置密码》
PS：查看mysql版本
mysql -V

6、设置开机自启动
systemctl enable mysqld

3. 编译安装MySQL编译安装MySQL的具体方法，参考文档《CentOS安装多版本MySQL》。
安装好MySQl之后，配置开机自启动的方法如下：
1、准备mysql启停脚本
cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqldchmod +x /etc/init.d/mysqld

2、添加开机自启动服务
chkconfig --add mysqldchkconfig --list

如果看到mysql的服务，并且3、4、5都是on的话，则说明配置成功。如果是off，则执行：
chkconfig --level 345 mysql on

3、重启验证mysql自启动
rebootnetstat -na | grep 3306

如果看到mysql在监听3306端口，说明服务自启动成功。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>database</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>mysql</tag>
        <tag>数据库</tag>
        <tag>好好学MySQL</tag>
        <tag>问题排查</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS7中安装配置Tomcat</title>
    <url>/dev-centos-tomcat/</url>
    <content><![CDATA[1. 前言本文中，我们学习在CentOS7中安装配置Tomcat的方法。


2. 安装JDKTomcat依赖JDK，首先参考《全平台安装JDK》，安装配置好JDK。
3. 安装Tomcat1、下载tomcat8.0.50
wget http://www-eu.apache.org/dist/tomcat/tomcat-8/v8.0.50/bin/apache-tomcat-8.0.50.tar.gz

2、解压tomcat到/opt目录
tar -xzvf apache-tomcat-8.0.50.tar.gz -C /opt

3、启动tomcat
cd /opt/apache-tomcat-8.0.50/bin/./startup.sh

4、测试访问
curl localhost:8080

测试时，遇到一个坑，curl命令卡在那里不动了，浏览器访问也是一直转圈，十几分钟了还在转。。。
4. 访问Tomat卡住问题猜测是tomcat版本有问题？换了8.5.29，依然不行；换了7.0.85，依然不行。猜测是jdk版本有问题？换了1.8.0_161，依然不行。
最终找到了答案：Centos7+Tomcat8配置javaweb环境，tomcat启动巨慢的问题，需要安装rng服务。
yum install rng-toolssystemctl start rngdsystemctl enable rngd

安装rngd后，就可以正常访问tomcat了。
PS：关闭tomcat方法
cd /opt/apache-tomcat-8.0.50/bin/./shutdown.sh

5. 基于supervisor设置开机自启动参考《CentOS安装配置Supervisor》，安装配置好supervisor。
1、在/etc/supervisor中新建tomcat.conf文件
[program:tomcat]directory=/opt/apache-tomcat-8.0.50command=/opt/apache-tomcat-8.0.50/bin/catalina.sh runenvironment=JAVA_HOME=&quot;/usr/lib/jvm/jdk1.8.0_161&quot;,JAVA_BIN=&quot;/usr/lib/jvm/jdk1.8.0_161/bin&quot;user=rootautostart=trueautorestart=truestdout_logfile=/var/log/supervisor/%(program_name)s.logstderr_logfile=/var/log/supervisor/%(program_name)s.log

2、重启supervisor
systemctl retart supervisord

3、重启tomcat
supervisorctl -c /etc/supervisord.conf restart tomcat

4、测试访问
curl localhost:8080

5、重启验证tomcat开启自启动
reboot# 重启后执行supervisorctl status

6. 失败的开机自启动配置（选读）6.1. 基于systemd设置开机自启动参考《在CentOS7中设置Tomcat开机自启动》，设置开机自启动：
1、为Tomcat添加启动参数catalina.sh 在执行的时候会调用同级路径下的setenv.sh来设置额外的环境变量，因此在/opt/tomcat/bin路径下创建setenv.sh文件，内容如下：
# 设置JAVA_HOMEexport JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161export JRE_HOME=$JAVA_HOME/jreexport CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATHexport PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATHexport CATALINA_HOME=/opt/apache-tomcat-8.0.50export CATALINA_BASE=/opt/apache-tomcat-8.0.50# 设置Tomcat的PID文件CATALINA_PID=&quot;$CATALINA_BASE/tomcat.pid&quot;# 添加JVM选项JAVA_OPTS=&quot;-server -XX:PermSize=256M -XX:MaxPermSize=1024m -Xms512M -Xmx1024M -XX:MaxNewSize=256m&quot;

2、创建systemd服务配置文件 tomcat.service
cd /etc/systemd/system/vim tomcat.service

tomcat.service 内容如下：
[Unit]Description=tomcatAfter=syslog.target network.target remote-fs.target nss-lookup.target[Service]Type=forkingPIDFile=/opt/apache-tomcat-8.0.50/tomcat.pidExecStart=/opt/apache-tomcat-8.0.50/bin/startup.shExecReload=/bin/kill -s HUP $MAINPIDExecStop=/bin/kill -s QUIT $MAINPIDPrivateTmp=true[Install]WantedBy=multi-user.target

3、把tomcat加入开机自启动
systemctl enable tomcat.service

enable之后，会多出两个文件：

/etc/systemd/system/multi-user.target.wants/tomcat.service
/etc/selinux/targeted/active/modules/100/tomcat

5、重启验证tomcat开启自启动
reboot# 重启后执行systemctl status tomcat.service

看到tomcat已经启动，正想着大功告成，却发现浏览器访问又开始转圈了。检查rng服务，启动正常。
直接使用 ./startup.sh 启动正常，而使用 systemctl start tomcat 就转圈，还能是什么原因？参照《CentOS7下Tomcat启动慢的原因及解决方案》一文，修改java.security，无效；修改catalina.sh，无效。是不是tomcat.service写的有问题，参照其他配置方案修改，依然无效。想到《Dockerfile使用Supervisor管理Tomcat》一文中，supervisor启动tomcat也会有些问题。那么，修改startup.sh呢？依然无效。
最终没有配置成功，留个坑。
6.2. 基于SysVinit设置开机自启动1、在/etc/init.d下创建服务脚本 tomcat
vim /etc/init.d/tomcatchmod a+x /etc/init.d/tomcat

tomcat 脚本内容如下：
#!/bin/bash## tomcat startup script for the Tomcat server### chkconfig: 345 80 20# description: start the tomcat deamon## Source function library. /etc/rc.d/init.d/functionsprog=tomcat# 根据自己的路径改写JAVA_HOMEJAVA_HOME=/usr/lib/jvm/jdk1.8.0_161/  export JAVA_HOME# 根据自己的路径改写CATALANA_HOMECATALANA_HOME=/opt/apache-tomcat-8.0.50/   export CATALINA_HOMEcase &quot;$1&quot; instart)    echo &quot;Starting Tomcat...&quot;    $CATALANA_HOME/bin/startup.sh    ;;stop)    echo &quot;Stopping Tomcat...&quot;    $CATALANA_HOME/bin/shutdown.sh    ;;restart)    echo &quot;Stopping Tomcat...&quot;    $CATALANA_HOME/bin/shutdown.sh    sleep 2    echo    echo &quot;Starting Tomcat...&quot;    $CATALANA_HOME/bin/startup.sh    ;;*)    echo &quot;Usage: $prog &#123;start|stop|restart&#125;&quot;    ;;esacexit 0

2、测试启动
service tomcat start
启动成功，然而，还是转圈。。。
实际上，因为系统版本的原因，init.d方法添加的自启动服务，也会加入到systemctl的管理。所以，报错表现和systemd配置的开机自启动问题相同。
无奈，最终换成了supervisor来设置开机自启动，配置成功。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
        <tag>tomcat</tag>
        <tag>supervisor</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Nginx：CentOS7中安装配置Nginx</title>
    <url>/dev-centos-nginx/</url>
    <content><![CDATA[1. 前言本文学习在CentOS7中安装配置Nginx的方法。
参考文档：

如何在CentOS 7上安装Nginx
Centos7安装Nginx实战



2. yum安装nginx1、添加epel库
yum install epel-release

2、安装nginx
yum install nginx
安装完成后，nginx的默认配置路径为/etc/nginx/。
3、启动nginx
systemctl start nginx

4、测试访问
curl localhost

浏览器如果不能访问，可能需要关闭防火墙或者打开端口。
# 关闭防火墙systemctl stop firewalld.service# 开放端口firewall-cmd --zone=public --add-port=80/tcp --permanentfirewall-cmd --reload

5、设置开机启动
systemctl enable nginx````# 编译安装nginx## 普通编译安装1、安装编译工具```bashyum install -y gcc-c++yum install -y pcre pcre-develyum install -y zlib zlib-develyum install -y openssl openssl-devel

2、下载nginx1.12.0并解压
wget http://nginx.org/download/nginx-1.12.0.tar.gztar -xzvf nginx-1.12.0.tar.gzcd nginx-1.12.0

3、创建临时目录
mkdir -p /var/temp/nginxmkdir -p /var/temp/run/nginxchmod a+wrx -R /var/temp

4、配置编译选项
./configure \  --prefix=/usr/local/nginx \  --pid-path=/var/temp/run/nginx/nginx.pid \  --lock-path=/var/lock/nginx.lock \  --error-log-path=/var/log/nginx/error.log \  --http-log-path=/var/log/nginx/access.log \  --http-client-body-temp-path=/var/temp/nginx/client \  --http-proxy-temp-path=/var/temp/nginx/proxy \  --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \  --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \  --http-scgi-temp-path=/var/temp/nginx/scgi \  --with-http_gzip_static_module

注意：pid-path 不能设置为/var/run/nginx/nginx.pid。因为CentOS每次重启后，都会删除 /var/run 目录中的自建目录和文件，从而导致nginx自启动失败。
5、编译安装
make &amp;&amp; make install

进入/usr/local/nginx查看文件是否存在conf、sbin、html文件夹，若存在则安装成功
6、测试启动
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
如果不指定-c，nginx在启动时默认加载conf/nginx.conf文件。
7、测试访问
curl localhost

3. 编译安装所有模块1、安装编译工具
yum install -y gcc-c++yum install -y pcre pcre-develyum install -y zlib zlib-develyum install -y openssl openssl-develyum install -y libxml2 libxml2-develyum install -y libxslt libxslt-develyum install -y gd-develyum install -y perl-ExtUtils-Embedyum install -y GeoIP GeoIP-devel GeoIP-datayum install -y gperftoolsyum install -y libatomic_ops-devel

2、下载nginx1.12.0并解压
wget http://nginx.org/download/nginx-1.12.0.tar.gztar -xzvf nginx-1.12.0.tar.gzcd nginx-1.12.0

3、创建临时目录
mkdir -p /var/temp/nginxmkdir -p /var/temp/run/nginxchmod a+wrx -R /var/temp

4、下载ngx_http_upstream_check_module模块
git clone https://github.com/yaoweibin/nginx_upstream_check_module

5、配置编译选项
./configure \  --prefix=/usr/local/nginx \  --pid-path=/var/temp/run/nginx/nginx.pid \  --lock-path=/var/lock/nginx.lock \  --error-log-path=/var/log/nginx/error.log \  --http-log-path=/var/log/nginx/access.log \  --http-client-body-temp-path=/var/temp/nginx/client \  --http-proxy-temp-path=/var/temp/nginx/proxy \  --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \  --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \  --http-scgi-temp-path=/var/temp/nginx/scgi \  --with-http_gzip_static_module \  --with-select_module \  --with-poll_module \  --with-threads \  --with-file-aio \  --with-ipv6 \  --with-http_ssl_module \  --with-http_v2_module \  --with-http_realip_module \  --with-http_addition_module \  --with-http_xslt_module \  --with-http_xslt_module \  --with-http_image_filter_module \  --with-http_image_filter_module \  --with-http_geoip_module \  --with-http_geoip_module \  --with-http_sub_module \  --with-http_dav_module \  --with-http_flv_module \  --with-http_mp4_module \  --with-http_gunzip_module \  --with-http_auth_request_module \  --with-http_random_index_module \  --with-http_secure_link_module \  --with-http_degradation_module \  --with-http_slice_module \  --with-http_stub_status_module \  --with-http_perl_module \  --with-mail \  --with-mail_ssl_module \  --with-stream \  --with-stream \  --with-stream_ssl_module \  --with-google_perftools_module \  --with-cpp_test_module \  --with-pcre \  --with-libatomic \  --add-module=./nginx_upstream_check_module

6、编译安装
make &amp;&amp; make install

3.1. 设置开机启动参考文档：

《Linux服务配置开机自启动》
Nginx+Center OS 7.2 开机启动设置(转载)

1、创建systemd服务文件 nginx.service
vi /usr/lib/systemd/system/nginx.service

写入内容如下：
[Unit]Description=nginxAfter=network.target  [Service]Type=forkingExecStart=/usr/local/nginx/sbin/nginxExecReload=/usr/local/nginx/sbin/nginx -s reloadExecStop=/usr/local/nginx/sbin/nginx -s quitPrivateTmp=true  [Install]WantedBy=multi-user.target

3、设置开机自启动
systemctl enable nginx.service

4、查看nginx状态
systemctl status nginx.service

5、重启服务器验证开机自启动
rebootsystemctl status nginx.service

看到nginx已经启动，至此nginx自启动配置成功。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>nginx</tag>
        <tag>好好学Nginx</tag>
      </tags>
  </entry>
  <entry>
    <title>Ubuntu14.04安装配置Ganglia</title>
    <url>/dev-ubuntu-ganglia/</url>
    <content><![CDATA[1. Ganglia简介Ganglia是UC Berkeley发起的一个开源集群监视项目，设计用于测量数以千计的节点。Ganglia的核心包含gmond、gmetad以及一个Web前端。主要是用来监控系统性能，如：cpu 、mem、硬盘利用率， I/O负载、网络流量情况等，通过曲线很容易见到每个节点的工作状态，对合理调整、分配系统资源，提高系统整体性能起到重要作用。


2. Ganlia结构Ganglia在结构上由三种守护进程组成gmond 、gmetad和gweb。在操作上，每种守护进程都是独立的，运行时只需要自己的配置文件来操作即可，任意守护进程在缺少其他两种守护进程的情况下也可以正常启动和运行。然而，三者在结构上又是相互协作的，需要同时使用才能发挥功效。
2.1. gmondgmond和普通代理一样，安装在每一台需要监控的主机上，负责与操作系统交互以获得需要关注的指标数据，例如CPU负载和硬盘容量。 
gmond在内部采用模块化设计，采用基于C语言编写的、根据操作系统定制的插件进行监控。和其他监控系统采用的客户端代理软件不同，gmond不需要等待外部轮询引擎的数据监测请求，也不将监控数据直接上传至集中式轮询器，而是根据自己本地配置文件定义的调度方案进行轮询。监测数据时使用简单的监听／通告协议，通过 XDR (External Data Representation)在集群内的主机之间共享。因此，Ganglia集群内的每个节点都知道同一集群内所有主机的当前指标数据。远程轮询器可以通过端口8649向集群内任意节点请求获得该集群XML格式的所有数据。 
gmond并不是消极等待被监控系统服务器唤醒，而总是处于激活态，以便进行测量、传输和共享。轮询器不再需要知道从哪些主机获取哪些服务，而只需要一个包含每个集群内至少一台主机名称的列表即可。
2.2. gmetadgmetad的作用是整合所有信息。
gmeted是一个简单的轮询器，对网络中每个集群进行轮询，并将每台主机上返回的所有指标数据写入各个集群对应的轮询数据库RRD。作为数据存储的一种流行的解决方案，RRDtool是很好的选择。指标数据存储于轮询数据库（Round Robin Database），这种数据库包含了多个时间块内静态分配的数值。如果每10秒进行一次轮询，每次数据都进行存储，一天将需要8640次存储。考虑到数据保留需求， RRDtool内部以“循环覆盖”的方式管理数据，将新数据的值叠加到原来的数值上来覆盖原有数据。
2.3. gwebGanglia可视化工具——gweb无需用户进行任何自定义设置即可便捷、及时地访问网络中任意一台主机的任意一种指标数据。
3. 主节点安装配置ganglia是一个集群监控项目，要分主节点和从节点。首先，我们来完成主节点的安装配置。
1、更新软件源中的软件列表sudo apt-get update
2、安装gmond、gmetad和gwebsudo apt-get install ganglia-monitor rrdtool gmetad ganglia-webfrontend
3、配置gmondsudo vi /etc/ganglia/gmond.conf分别找到：
cluster &#123;  name = &quot;unspecified&quot;  owner = &quot;unspecified&quot;  latlong = &quot;unspecified&quot;  url = &quot;unspecified&quot;&#125;udp_send_channel &#123;  mcast_join = 239.2.11.71  port = 8649  ttl = 1&#125;udp_recv_channel &#123;  mcast_join = 239.2.11.71  port = 8649  bind = 239.2.11.71&#125;

修改为：
cluster &#123;  name = &quot;my cluster&quot;   owner = &quot;unspecified&quot;  latlong = &quot;unspecified&quot;  url = &quot;unspecified&quot;&#125;udp_send_channel &#123;  # mcast_join = 239.2.11.71  host = 192.168.56.103  port = 8649  ttl = 1&#125;udp_recv_channel &#123;  # mcast_join = 239.2.11.71  port = 8649  # bind = 239.2.11.71&#125;

4、配置gmetadsudo vi /etc/ganglia/gmetad.conf
查找cluster，注释掉该行，修改为：
# data_source &quot;my cluster&quot; localhostdata_source &quot;my cluster&quot; 10 192.168.56.103:8649

data_source后面有三个参数，第一个参数”my cluster”是集群名称；第二个参数10是轮询时间（单位：秒）；第三个参数192.168.56.103:8649是要监听的机器的IP和端口（可写多个，用空格隔开）。
5、配置gwebsudo cp /etc/ganglia-webfrontend/apache.conf /etc/apache2/sites-enabled/ganglia.conf
6、重启服务
sudo /etc/init.d/ganglia-monitor restartsudo /etc/init.d/gmetad restartsudo /etc/init.d/apache2 restart

7、测试访问ubuntu测试：curl http://192.168.56.103/ganglia/浏览器访问测试：http://192.168.56.103/ganglia/ 
4. 从节点安装配置以上，已经完成了ganglia主节点的安装配置，但是往往我们要监控的是一个集群。接下里，我们就来研究一下从节点的配置。
Ganglia结构一节中，我们了解到，gmond要安装在每一个节点，负责监控主机情况；gmetad（包括rrdtool）只要安装在主节点就可以，负责收集各个节点的数据；gweb只要安装在主节点，负责显示gmetad收集的数据。
综上，从节点只要安装gmond就可以了。
4.1. 从节点假设我们有一个从节点，IP为192.168.56.104，那么它的安装配置步骤如下：
1、更新软件源中的软件列表sudo apt-get update
2、安装gmondsudo apt-get install ganglia-monitor
3、配置gmondsudo vi /etc/ganglia/gmond.conf分别找到：
cluster &#123;  name = &quot;unspecified&quot;  owner = &quot;unspecified&quot;  latlong = &quot;unspecified&quot;  url = &quot;unspecified&quot;&#125;udp_send_channel &#123;  mcast_join = 239.2.11.71  port = 8649  ttl = 1&#125;udp_recv_channel &#123;  mcast_join = 239.2.11.71  port = 8649  bind = 239.2.11.71&#125;

修改为：
cluster &#123;  name = &quot;my cluster&quot;   owner = &quot;unspecified&quot;  latlong = &quot;unspecified&quot;  url = &quot;unspecified&quot;&#125;udp_send_channel &#123;  # mcast_join = 239.2.11.71  host = 192.168.56.103  port = 8649  ttl = 1&#125;udp_recv_channel &#123;  # mcast_join = 239.2.11.71  port = 8649  # bind = 239.2.11.71&#125;

没错，从节点的gmond配置和主节点完全相同。
4、重启服务sudo /etc/init.d/ganglia-monitor restart
4.2. 主节点配置完从节点就好了吗？并没有，主节点也要做相应修改，主要是修改下gmetad配置。
1、sudo vi /etc/ganglia/gmetad.conf
查找cluster，修改为：
# data_source &quot;my cluster&quot; localhostdata_source &quot;my cluster&quot; 10 192.168.56.103:8649 192.168.56.104:8649

这样，就添加好了192.168.56.104这个从节点。
注意：gmetad会依次检测指定主机，并从第一台响应主机开始收集状态数据，而每一台主机上的gmond都知道所在集群的所有状态数据，所以无需在data_source定义里指定集群内的所有主机。假若有节点失效，通常指定两三个主机也就足够保证数据的收集。
2、重启gmetadsudo /etc/init.d/gmetad restart
3、测试访问等待10秒，再次访问 http://192.168.56.103/ganglia/ ，就能看到新加入的从节点。
5. 删除数据由于重设了管理机的系统时间，发现所有的机器状态都变成了Hosts down，这时我们需要删除数据重新开始统计。
1、管理机删除rrds目录下所有文件cd /var/lib/ganglia/rrds &amp;&amp; sudo rm -rf ./*
2、管理机重启gmond和gmetadsudo /etc/init.d/ganglia-monitor restartsudo /etc/init.d/gmetad restart
3、客户机重启gmondsudo /etc/init.d/ganglia-monitor restart
6. 删除机器ganglia默认服务器down机也不会在web前端清除该设备，官方文档介绍的办法如下：
1、登录管理机
2、编辑gmond.conf，sudo vim /etc/ganglia/gmond.conf在globals中找到host_dmax，它的默认值为0，意思是不清除节点。host_dmax的单位为秒，我们把host_dmax的值修改为7天，就是60x60x24x7=604800，即超过7天未汇报数据的节点会从前端清除。
7. 书签Ganglia_简述
ubuntu14.04系统中安装Ganglia
Ganglia 监控实战！
gmetad.conf：gmetad配置文件（1）
]]></content>
      <categories>
        <category>engineering</category>
        <category>monitoring</category>
      </categories>
      <tags>
        <tag>监控</tag>
        <tag>ubuntu</tag>
        <tag>ganglia</tag>
      </tags>
  </entry>
  <entry>
    <title>域名被clienthold解封方法</title>
    <url>/essay-clienthold/</url>
    <content><![CDATA[1. 前言2017年10月13日，突然收到来自阿里云的短信和邮件：

尊敬的用户：您的网站voidking.com涉及违法不良信息，违反了《互联网信息服务管理办法》第十五条规定，目前阿里云已经对您域名hold处理。如果您对本通知的内容存有疑问，请及时工单或者电话联系我们。 谢谢您对阿里云的支持。阿里云计算有限公司

收到这条信息时，郝同学一脸懵逼，马上就致电了阿里云客服，表示不服。客服告诉我需要实名认证，我说认证过了。最终客服解决不了，让我提交工单。


2. 工单第二天，果断提交了工单。以下是工单截图。
3. 从折腾到放弃工单说，必须到公安局网警办公室做申报，听起来就好麻烦。但是，为了我的域名，决心麻烦一把。因为 voidking.com 这个域名用了很多年，和郝同学感情深厚。
当时身在长春，打电话给110，问到了网警中心的电话，想咨询下具体怎么处理，需要准备什么材料，然而，打电话无人接听。连续几天，打了几十个电话，依旧无人接听。
不甘心的我，想要在网上找到网警中心的其他联系方式。意外发现了全国公安机关互联网站安全服务平台，莫非，所谓的申报就是公安备案？于是，花了一周左右完成了公安备案。但是，并没有什么用，申报和公安备案并没有任何关系。惊喜的是，公安备案成功后，给我的通知中附带了长春的网警中心联系方式。果断打电话过去，接听了，有戏！但是，接听电话的人员告诉我，网警中心搬到另外一个办公室了，给了我另外一个号码。然后，再也没有打通过。。。在微博上咨询长春网警，也没有得到回复，遂放弃。
PS：公安备案成功后拿到了备案号，但是现在查询，居然没有备案信息，也是一头雾水，不明所以。
4. 从折腾到放弃2.0一直想着，很快就要去北京实习了，到时候去北京网警中心处理。这一等，就是近五个月。没有去北京，而是来到了深圳。深圳也是大城市，网警中心的服务应该很周到，我这么想着。
给110打电话，询问网警中心的电话，警察说让我打114查询。114查询到网警中心电话后，致电网警中心，网警中心说他们只处理网络诈骗类案件，我这种网站方面的要找通信管理局。然后，从114那里拿到了深圳通信管理局的电话，致电通信管理局，空号。然后，从114那里拿到了广东省通信管理局的电话，一直忙线中，十几个电话没有打通。
决定还是再咨询一下网警，毕竟是公安部下发的hold要求。微博关注了深圳网警，咨询域名被hold怎么处理，没收到回复。了解到他们周末不上班，决定哪天请假去一趟网警中心。
不知道准备什么材料，因为网上的案例极少。比较靠谱的，有以下两篇文章。域名被clientHold的解封办法 和 域名被停止解析该如何恢复正常 中都提到了标准解封流程：
1、写好整顿报告提交本地公安局
2、公安局提交整顿报告到负责网络信息的公安部相关部门
3、公安部相关部门经过对整顿报告的批复把解除域名锁定的通知下达给工信部。
4、工信部接到公安部相关部门的通知把解除域名锁定的通知下达给通信管理局。
5、通信管理局在把通知下达给新网，此时域名才可以正常。
复杂，好复杂，但是郝同学还是决定搞一下。于是从网上找了网站整顿报告的模板，写了一份整顿报告。但是，该提交到哪里呢？这份报告模板是否标准，需不需要重新写？
5. 工单2.0带着这些疑问，郝同学提交了第二份工单，希望阿里云能够提供一些帮助，实在是不知道该找谁了。
6. 解决办法阿里云并没有提供什么有用的信息，几乎走投无路的郝同学，继续寻找希望。在重读域名被停止解析该如何恢复正常 这篇文章时，“域名转移”让我眼前一亮！clienthold是因为注册商hold了域名，但是在国外，注册商是没有权力hold域名的！那我能不能把域名转移到国外呢？查看了一下域名转出阿里云的条件：

必须是在阿里云申请的域名。
域名转出时距离域名申请日（域名注册日期） 60 天以上。
域名转出时距离域名最后一次成功转移日大于 60 天。
域名转出时距该域名到期日大于 15 天。
域名转出时状态正常（不能是禁止转出状态），不欠费、不处于任何仲裁及法律程序中、不存在该域名持有者的身份不清楚或者存在争议。
域名过期后完成续费/域名赎回已超过 45 天。

对比了这些，发现自己符合条件，激动！于是，开始了域名转移，从阿里云到godaddy。
具体流程百度即可，很简单，比较坑的是，在开始转移前，要先等五天！

尊敬的用户：我方注册商（见下表“转出注册商”列）于北京时间2018-03-05 16:27:22收到有关您希望将域名转到其他注册商的通知。通知转出的域名列表如下：（表略）重要提示：1、如果您希望继续该转出，您不需要做任何操作，根据上述域名相关注册局的规定，域名将在5-7天后自动转出。2、如果您希望取消此次转出，请您务必在北京时间2018-03-10 15:27:22前按如下方法操作取消：登录“阿里云管理控制台” —“域名与网站—域名—域名列表”，选择域名进行“管理”，进入“域名转出万网” —“取消转出”按钮。如果登录后没有“取消转出”按钮，则意味着此域名已经转出成功，将无法取消转出。3、如果我方注册商在北京时间2018-03-10 15:27:22前未收到您的取消转出请求，该转移将继续。

五天中，我很焦躁忐忑，在想会不会等了五天最后提示我转移失败。看了很多相关文章帖子，后来域名被注册商强制停止解析，域名状态 clientHold ~~怎么办？这篇帖子中找到了信心。首先，只要不是cn的域名，都可以向APNIC申请强制转出，这样转出就没问题了。其次，转移到国外，因为没有clienthold一说，也就相当于自动解封。
就这样，等到了3月10日的15:27，没有消息。继续等待，直到3月11日13:23，终于收到了godaddy的域名转入确认邮件，感动到哭。
赶紧设置了解析，测试能不能使用。几分钟后，www.voidking.com终于可以正常访问了！本次历时近五个月的clienthold事件，终于圆满解决。
晚上，更换了dnspod作为dns服务器。解析正常，联通4g可以正常访问，但是中科院的网络却不可以访问。很奇怪，后来在网站域名解析之后为什么有些地方还是不能访问？这篇短文中找到了答案：有些站长在做网站域名解析的时候，会遇见这样的问题，网站域名解析之后为什么有些地方还是不能访问？这个是由于各地电信或网络提供部门的DNS刷新频率不一样，因此，全球域名刷新有8-72小时的误差，如果您刚做完解析，那么就有可能出现某些地方暂时还不能访问的情况，这是正常的。
7. 后记原本，在百度搜索“voidking”，基本都是郝同学网站上的文章。而现在，都是郝同学在各大博客网站上的文章，排名下降了太多，从头再来吧。早知道域名转移可以解决clienthold，就不用这么多折腾了。早知道，多好。写下本篇博客，希望能给遇到同样问题的同学提供一些帮助，少走一些弯路。
8. 书签域名被锁定冻结（域名被 hold）的原因及解锁
违法违规信息处理方案
违法信息处置FAQ
]]></content>
      <categories>
        <category>essay</category>
      </categories>
      <tags>
        <tag>域名</tag>
      </tags>
  </entry>
  <entry>
    <title>机器学习相关原则与方法</title>
    <url>/dev-ml-principle-and-method/</url>
    <content><![CDATA[1. 前言越学习，越发现自己的无知。——笛卡尔笛卡尔的这句话，用在郝同学学习机器学习的进展，刚好合适。从开始搞机器学习到现在，一年多了，学习了很多课程，查阅了很多文档，阅读了很多书籍，编写了很多代码，然而，并没有什么实质性的进展。没有任何可以拿得出手的成绩，可以让面试官眼前一亮。寒假了，继续“搞机”，参加kaggle和天池，边比赛边学习，学以致用，为自己书写一份优秀的简历。
接下来一段时间，郝同学会参考七月在线的《Kaggle案例实战班》教程，选择一些项目进行实战。


2. 问题解决流程1、了解场景和目标2、了解评估准则3、认识数据4、数据预处理（清洗，调权）5、特征工程6、模型选择7、模型调参（选择最佳超参数：交叉验证）8、模型状态分析9、模型融合
2.1. 了解场景和目标1、这是一个什么样的问题？2、可以采集到哪些数据？3、需要完成什么样的目标？
2.2. 了解评估标准https://www.kaggle.com/wiki/Metrics
2.3. 认识数据编程显示出前几行、前几列，观察特点。
1、数据有哪些维度？2、数据是不是分布平衡？3、数据是不是有缺省值？
2.4. 数据预处理1、数据清洗

不可信的样本丢掉
缺省值极多的字段考虑不用

2、数据采样

上/下采样
保证样本均衡（加权）
bagging，vote

3、工具

hive sql/spark sql
pandas

2.5. 特征工程
1、特征处理

数值型
类别型
时间类（离散型，间隔型，组合型）
文本类（n-gram，bag of words，tf-idf，word2vec）
统计型
组合特征

2、特征抽取
http://scikit-learn.org/stable/modules/preprocessing.html
http://scikit-learn.org/stable/modules/classes.html#module-sklearn.feature_extraction
3、特征选择http://scikit-learn.org/stable/modules/feature_selection.html
过滤型：sklearn.feature_selection.SelectKBest包裹型：sklearn.feature_selection.RFE嵌入型：feature_selection.SelectFromModel，Linear model，L1正则化 
2.6. 模型选择

sklearn cheat-sheet提供的候选 
课程案例经验 
交叉验证（cross validation）：K折交叉验证（K-fold cross validation）、Cross-validation: evaluating estimator performance

2.7. 模型参数选择交叉验证：

Tuning the hyper-parameters of an estimator
sklearn.model_selection.GridSearchCV

2.8. 模型状态评估1、模型状态过拟合还是欠拟合
2、学习曲线（learning curve）
3、绘制学习曲线（plot learning curve）
2.9. 模型融合1、群众的力量是伟大的，集体智慧是惊人的（1）Bagging（2）随机森林/Random forest 
2、站在巨人的肩膀上，能看得更远（1）模型stacking 
3、一万小时定律（1）Adaboost（2）逐步增强树/Gradient Boosting Tree
2.9.1. Bagging1、模型很多时候效果不好的原因是什么？（1）过拟合啦！！！
2、如何缓解？（1）少给点题，别让它死记硬背这么多东西（2）多找几个同学来做题，综合一下他们的答案
3、sklearn.ensemble.BaggingClassifier
（1）用一个算法不用全部的数据集，每次取一个子集训练一个模型分类：用这些模型的结果做vote回归：对这些模型的结果取平均
（2）用不同的算法用这些模型的结果做vote 或 求平均
2.9.2. Stacking用多种predictor结果作为特征训练
2.9.3. Boosting考得不好的原因是什么？（1）还不够努力，练习题要多次学习解决办法：重复迭代和训练（2）时间分配要合理，要多练习之前做错的题解决办法：每次分配给分错的样本更高的权重（3）我不聪明，但是脚踏实地，用最简单的知识不断积累，成为专家解决办法：最简单的分类器的叠加
Ensemble Methods
3. 原则和技巧1、画出数据2、简单实现3、画出图表4、根据图表优化

数据的重要性超过模型。

4. 注意点与核心思路1、拿到数据后怎么了解数据（可视化）2、选择最贴切的机器学习算法3、定位模型状态（过/欠拟合）以及解决方法4、大量级的数据的特征分析与可视化5、各种损失函数（loss function）的优缺点及如何选择
5. 书签机器学习系列(4)_机器学习算法一览，应用建议与解决思路
机器学习系列(19)_通用机器学习流程与问题解决架构模板
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>机器学习</tag>
        <tag>kaggle</tag>
      </tags>
  </entry>
  <entry>
    <title>代理服务概述</title>
    <url>/dev-proxy-server/</url>
    <content><![CDATA[1. 代理服务简介
代理（Proxy）也称网络代理，是一种特殊的网络服务，允许一个终端（一般为客户端）通过这个服务与另一个终端（一般为服务器）进行非直接的连接。

通信双方原本可以直接通信，为什么需要加一层代理？因为代理可以为整个通信带来更多的功能，例如：

拦截：代理可以选择性拦截传输的网络流量，比如一些公司限制员工在上班的时候不能访问某些游戏或者电商网站，再比如把我们和世界隔离开来的 GFW，还有在数据中心中拒绝恶意访问的网关
统计：既然所有的流量都经过代理，那么代理也可以用来统计网络中的数据信息，比如了解哪些人在访问哪些网站，通信的应答延迟等
缓存：如果通信双方比较”远“，访问比较慢，那么代理可以把最近访问的数据缓存在本地，后面的访问不用访问后端来做到加速。CDN 就是这个功能的典型场景
分发：如果某个通信方有多个服务器后端，代理可以根据某些规则来选择如何把流量发送给多个服务器，也就是我们常说的负载均衡功能。比如著名的 Nginx 软件
跳板：如果 A、B 双方因为某些原因不能直接访问，而代理可以和双方通信，那么通过代理，双方可以绕过原来的限制进行通信。这应该广大中国网民比较熟悉的场景
注入：既然代理可以看到流量，那么它也可以修改网络流量，可以自动在收到的流量中添加一些数据，比如有些宽带提供商的弹窗广告

参考文档：

Proxy server
代理服务器
Istio 是啥？一文带你彻底了解



2. 正向代理和反向代理正向代理和反向代理的不同，关键在于“代理”这个家伙的所有权。假设我要去小明那里拿文件，这时可以有两种情况。正向代理：我委托自己的秘书（代理）去找小明拿文件。反向代理：我去找小明拿文件，小明委托他的秘书（代理）把文件交给我。
同样的，在网络世界中，正向代理就是自己的秘书，自己是隐藏的，不需要亲自出马，秘书帮助自己发送请求获取数据，你要给他开工资（买VPN，做一些设置等）。
反向代理就是服务器的秘书，服务器委托自己的秘书接收请求。秘书拿到请求后交给服务器处理，服务器处理完成后再交给秘书。秘书把拿到的结果返回给用户，并且很得意地告诉用户：我就是服务器。
很多情况下，多个服务器会共用一个秘书（反向代理Nginx），这个秘书同时冒充着很多服务器，一会说我是Tomcat服务器，一会说我是PHP服务器，一会又说我是Django服务器。这个秘书还很机智，有的请求多个Tomcat服务器都能处理，他会送到空闲的那个来处理（负载均衡）。
引用自郝同学自己的回答：反向代理为何叫反向代理？
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>代理</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django部署到线上（修改版）</title>
    <url>/dev-django-deploy-advance/</url>
    <content><![CDATA[1. 前言《Django部署到线上》一文中，很多步骤不是必须的，有些部分甚至是错误的，本文就精简修改一下。目标：把djsite项目部署到/home/web目录中，并且给它分配一个域名为djsite.voidking.com。


2. 环境准备2.1. supervisor参考《CentOS安装配置Supervisor》，安装配置好supervisor。
2.2. python虚拟机参考《CentOS安装配置pyenv》，安装好python3.6.1。
3. 项目部署3.1. 代码准备1、在/home/web目录中，执行命令克隆项目git clone https://github.com/voidking/djsite.git
2、安装djangopip install django==1.11.7
3、安装pymysqlpip install pymysql
3.2. 数据库准备1、创建数据库
# mysql -uroot -pmysql&gt; create database `djsite` default character set utf8 collate utf8_general_ci; 

2、修改mysql的binlog格式为混合模式：
mysql&gt; set global binlog_format=mixed;mysql&gt; exit;

3、修改djsite/djsite/settings.py中的数据库配置vim djsite/djsite/settings.py
4、创建表结构
python manage.py makemigrationspython manage.py migrate

3.3. 启动项目1、启动命令python manage.py runserver
2、服务器测试访问curl localhost:8000/blog/index
3、本地测试访问使用浏览器查看 http://ip:8000/blog/index ，无法访问。启动命令改为：python manage.py runserver 0.0.0.0:8000，此时即可在浏览器看到部署好的项目。
如果还是不能访问，尝试先关闭防火墙：systemctl stop firewalld
4. nginx配置1、首先，在万网上配置域名解析，添加A记录，解析到阿里云服务器IP。假设解析好的域名为django.voidking.com。
2、在nginx的vhost中，添加django.voidking.com.conf，内容为：
server &#123;    listen 80;    server_name django.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        client_max_body_size       1024m;        client_body_buffer_size    128k;        client_body_temp_path      data/client_body_temp;        proxy_connect_timeout      90;        proxy_send_timeout         90;        proxy_read_timeout         90;        proxy_buffer_size          4k;        proxy_buffers              4 32k;        proxy_busy_buffers_size    64k;        proxy_temp_file_write_size 64k;        proxy_temp_path            data/proxy_temp;        proxy_pass http://127.0.0.1:8000;    &#125;&#125;

3、重启nginx，./nginx -s reload
4、测试访问服务器：curl django.voidking.com/blog/index本地浏览器：http://django.voidking.com/blog/index
至此，django项目已经部署成功，没有用到uwsgi。如果给django添加守护进程，那么我们的部署就接近完美了。那么，uwsgi又能干什么呢，我们继续研究。
5. uwsgi5.1. 安装uwsgipip install uwsgi
编写测试：
# test.pydef application(env, start_response):    start_response(&#x27;200 OK&#x27;, [(&#x27;Content-Type&#x27;,&#x27;text/html&#x27;)])    return [b&quot;Hello World&quot;]

启动测试：uwsgi --http :8001 --wsgi-file test.py
访问 http://ip:8001 ，即可看到Hello World 。
5.2. 一般启动1、编写wsgi.py文件编写django_wsgi.py文件，将其放在与文件manage.py同一个目录下。
#!/usr/bin/env python# coding: utf-8import os,djangofrom django.core.handlers.wsgi import WSGIHandleros.environ.setdefault(&quot;DJANGO_SETTINGS_MODULE&quot;, &quot;djsite.settings&quot;)django.setup()application = WSGIHandler()

2、启动项目uwsgi --http :8000 --chdir /home/web/djsite/ --module django_wsgi
3、查看启动结果lsof -i :8000，ps aux | grep uwsgi
4、测试访问http://ip:8000/blog/index此时，页面是没有样式的，也就是说静态资源加载失败。
5、配置静态资源uwsgi --http :8000 --chdir /home/web/djsite/ --module django_wsgi --static-map=/static=static此时，页面样式就正常了。
5.3. 高级启动1、新建uwsgi.ini，与manage.py在同一级目录。
[uwsgi]http = :8000chdir = /home/web/djsite/wsgi-file = django_wsgi.pystatic-map = /static=static

2、启动uwsgiuwsgi uwsgi.ini
3、测试访问http://ip:8000/blog/index
6. 使用supervisor管理6.1. 守护uwsgi1、在/etc/supervisor中新建djsite.conf文件：
[program:djsite]command=/root/.pyenv/versions/3.6.1/bin/uwsgi --http :8000 --chdir /home/web/djsite/ --module django_wsgi --static-map=/static=staticdirectory=/home/web/djsite/startsecs=0stopwaitsecs=0autostart=trueautorestart=true

2、重启supervisor
ps aux | grep supervisordsystemctl stop supervisordsystemctl start supervisord

附：重启djsite命令
supervisorctl -c /etc/supervisord.conf restart djsite

3、测试访问http://ip:8000/blog/index页面显示正常，至此守护进程配置成功。
4、djsite.conf可以精简修改为：
[program:djsite]command=/root/.pyenv/versions/3.6.1/bin/uwsgi --ini uwsgi.inidirectory=/home/web/djsite/startsecs=0stopwaitsecs=0autostart=trueautorestart=true

6.2. 静态资源问题（可忽略）假设，uwsgi.ini为：
[uwsgi]http = :8000chdir = /home/web/djsite/wsgi-file = django_wsgi.py

静态资源就无法访问了。在不添加static-map的情况下，需要修改两个文件：（1）修改djsite/djsite/settings.py文件，添加：
STATIC_ROOT = &#x27;/home/web/djsite/static/&#x27;
（2）修改djsite/djsite/settings.py文件为：
from django.conf.urls import url,includefrom django.contrib import adminfrom django.conf.urls.static import staticfrom django.conf import settingsurlpatterns = [    url(r&#x27;^admin/&#x27;, admin.site.urls),    url(r&#x27;^blog/&#x27;, include(&#x27;blog.urls&#x27;, namespace=&#x27;blog&#x27;)),] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

6.3. admin静态资源问题如果以python manage.py runserver启动django，那么静态资源没有问题。
如果以uwsgi启动django，静态资源看起来没有问题，但是，如果访问 http://ip:8000/admin ，就会发现这个页面的静态资源无法获取。
一个Django应用，一般有两类静态文件。一是应用内的静态文件，二是Django自带的静态文件。应用内的静态文件在djsite/static目录下。此外，在INSTALLED_APPS中配置了django.contrib.admin， 则还会有另外一组静态文件，在Django安装位置里。
例如，一个root在Python 3.6版本安装的Django，admin的静态文件在： /usr/local/lib/python3.6/site-packages/django/contrib/admin/static/admin/。
最终，在STATIC_URL里，会有两类静态文件， /static/* 与 /static/admin/* 。
了解原理，原因就很显然了。python manage.py runserver知道静态文件的位置，而uWSGI不知道静态文件在什么位置。
解决办法如下：（1）修改djsite/djsite/settings.py文件：
SITE_ROOT = os.path.dirname(os.path.abspath(__file__))SITE_ROOT = os.path.abspath(os.path.join(SITE_ROOT, &#x27;../&#x27;))STATIC_ROOT = os.path.join(SITE_ROOT, &#x27;collectedstatic&#x27;)

（2）收集所有静态文件到collectedstatic目录python manage.py collectstatic
（3）修改uwsgi.ini配置
[uwsgi]http = :8000chdir = /home/web/djsite/wsgi-file = django_wsgi.pystatic-map = /static=collectedstatic

7. nginx+uwsgi以上，我们的djsite项目已经通过uwsgi方式启动起来，并且可以保持后台运行。nginx配置不改变的情况下，我们可以正常访问 http://django.voidking.com/blog/index 。此时，nginx作为反向代理，和uwsgi间通过http交互。
接下来，就配置下nginx和uwsgi通过socket结合的方式。原理：用户发送http请求到nginx，nginx通过socket把请求交给uwsgi，uwsgi拿到django的处理结果，通过socket返还给nginx，nginx通过http返回结果给用户。
1、因为nginx和uwsgi通过socket方式交互，我们需要修改uwsgi.ini的配置为：
[uwsgi]socket = :8000chdir = /home/web/djsite/wsgi-file = django_wsgi.pystatic-map = /static=collectedstaticmaster = trueprocesses = 2enable-threads = truedaemonize = /home/web/djsite/uwsgi.log

2、重启supervisorsystemctl stop supervisordsystemctl start supervisord
3、修改nginx配置djsite.voidking.com.conf：
server &#123;    listen      80;    server_name djsite.voidking.com;    charset     utf-8;    location / &#123;        uwsgi_pass     127.0.0.1:8000;        include        uwsgi_params;    &#125;&#125;

5、重启nginx./nginx -s reload
6、测试访问此时，访问 http://ip:8000/blog/index 失败，访问 http://django.voidking.com/blog/index 正常。因为8000端口不再提供http服务，而是一个和nginx连接的socket。
8. 加速静态资源1、修改nginx配置djsite.voidking.com.conf：
server &#123;    listen      80;    server_name djsite.voidking.com;    charset     utf-8;    location / &#123;        uwsgi_pass     127.0.0.1:8000;        include        uwsgi_params;    &#125;    location /static &#123;        alias /root/djsite/collectedstatic;    &#125;&#125;

2、修改nginx.conf
user root;

3、重启nginx./nginx -s reload
9. 小结至此，django部署完毕，我们实现了三种部署方法：

nginx + django（http方式）
nginx + uwsgi（http方式）
nginx + uwsgi（socket方式）

10. 书签使用uWSGI提供静态文件 (更新至1.9)
解决uWSGI里的Django静态文件丢失
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>python</tag>
        <tag>django</tag>
        <tag>supervisor</tag>
        <tag>好好学Python</tag>
        <tag>uwsgi</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：在CentOS7上安装Hadoop和Hive</title>
    <url>/dev-hadoop-hive-on-centos/</url>
    <content><![CDATA[1. 前言本文中，我们会在CentOS7系统中安装 Hadoop1.2.1 和 Hive1.2.2 。
参考文档：

hive2.1.1安装部署
Hive安装与配置
Hive集成mysql数据库
Hive安装配置指北（含Hive Metastore详解）



2. 安装Hadoop参考文档：《在Ubuntu16.04上安装Hadoop》
3. 配置Mysql1、假设已经安装好了mysql，版本为5.6.29。
2、新建hive数据库，用来保存hive的元数据
create database hive;alter database hive character set latin1;

3、将hive数据库下的所有表的所有权限赋给hadoop用户，并配置mysql为hive-site.xml中的连接密码，然后刷新系统权限关系表。
create user &#x27;hadoop&#x27;@&#x27;%&#x27; identified by &#x27;mysql&#x27;;grant all privileges on  *.* to &#x27;hadoop&#x27;@&#x27;%&#x27; with grant option;flush privileges;

4. 安装Hive1、下载解压hive
cd /optwget http://mirror.bit.edu.cn/apache/hive/hive-1.2.2/apache-hive-1.2.2-bin.tar.gztar -zxvf apache-hive-1.2.2-bin.tar.gz

2、配置HIVE_HOME，vim /etc/profile，在最后添加
export HIVE_HOME=/opt/apache-hive-1.2.2-binexport PATH=$HIVE_HOME/bin:$PATH
立即生效，source /etc/profile
3、修改hive-site.xml文件
cd /opt/apache-hive-1.2.2-bin/conf/cp hive-default.xml.template hive-site.xml  vim hive-site.xml

找到ConnectionURL，修改为：
&lt;property&gt;  &lt;name&gt;javax.jdo.option.ConnectionURL&lt;/name&gt;  &lt;value&gt;jdbc:mysql://localhost:3306/hive?createDatabaseIfNotExist=true&lt;/value&gt;  &lt;description&gt;JDBC connect string for a JDBC metastore&lt;/description&gt;&lt;/property&gt;&lt;property&gt;    &lt;name&gt;javax.jdo.option.ConnectionDriverName&lt;/name&gt;    &lt;value&gt;com.mysql.jdbc.Driver&lt;/value&gt;  &lt;/property&gt;&lt;property&gt;    &lt;name&gt;javax.jdo.option.ConnectionUserName&lt;/name&gt;    &lt;value&gt;hadoop&lt;/value&gt;  &lt;/property&gt;     &lt;property&gt;    &lt;name&gt;javax.jdo.option.ConnectionPassword&lt;/name&gt;    &lt;value&gt;mysql&lt;/value&gt;  &lt;/property&gt;&lt;property&gt;  &lt;name&gt;hive.support.sql11.reserved.keywords&lt;/name&gt;  &lt;value&gt;false&lt;/value&gt;  &lt;description&gt;    This flag should be set to true to enable support for SQL2011 reserved keywords.    The default value is true.  &lt;/description&gt;&lt;/property&gt;

4、下载配置mysql-connector-java
wget http://www.java2s.com/Code/JarDownload/mysql/mysql-connector-java-5.1.22-bin.jar.zipunzip mysql-connector-java-5.1.22-bin.jar.zipmv mysql-connector-java-5.1.22-bin.jar /opt/apache-hive-1.2.2-bin/lib

5、修改hive-env.sh文件
cp hive-env.sh.template  hive-env.shvi  hive-env.sh

修改为：
# Set HADOOP_HOME to point to a specific hadoop install directoryHADOOP_HOME=/opt/hadoop-1.2.1

6、启动hadoop,cd /opt/hadoop-1.2.1/bin，start-all.sh。
7、启动metastore，nohup hive --service metastore &gt; metastore.log 2&gt;&amp;1 &amp;
8、启动hive，hive报错：The root scratch dir: /tmp/hive on HDFS should be writable.
mkdir /tmp/hivechmod -R 777 /tmp/hivehadoop fs -chmod -R 777 /tmp/hive

再次启动，报错：Relative path in absolute URI: ${system:java.io.tmpdir%7D/$%7Bsystem:user.name%7D新建tmpdir文件夹
mkdir /tmp/tmpdirchmod -R 777 /tmp/tmpdirhadoop fs -mkdir /tmp/tmpdirhadoop fs -chmod -R 777 /tmp/tmpdir

在hive-site.xml中，查找所有的$&#123;system:java.io.tmpdir&#125; 和 $&#123;system:java.io.tmpdir&#125;/$&#123;system:user.name&#125;，替换为/tmp/tmpdir
再次启动，成功！
5. hive问题记录问题描述：hive表中的列名，无法使用点.和冒号:。解决办法：重新创建表。因为创建表时可以使用点和冒号，修改表时不能使用。详情参考Cannot use a “.” in a Hive table column name
]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>大数据</tag>
        <tag>hadoop</tag>
        <tag>hive</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django部署到线上</title>
    <url>/dev-django-deploy/</url>
    <content><![CDATA[1. 前言《Django开发简单Blog系统》系列中，我们已经完成了一个迷你Web项目。那么，怎么把这个项目发布到线上呢？怎样给它一个域名呢？
思路：nginx + uwsgi


2. 环境准备2.1. 服务器阿里云服务器，centos7系统。
2.2. python升级python到3.6.1，统一线上和本地python环境。
1、下载python3.6.1源码wget  https://www.python.org/ftp/python/3.6.1/Python-3.6.1.tar.xz
2、解压源码
xz -d Python-3.6.1.tar.xztar -xvf Python-3.6.1.tar

3、编译源码
mkdir /usr/local/python3cd Python-3.6.1./configure --prefix=/usr/local/python3 --enable-optimizationsmake &amp;&amp; make install

如果编译失败，需要先更新编译环境：
gcc -v g++ -vyum install gccyum install gcc-c++

注：我的环境版本为 gcc version 4.8.5 20150623 (Red Hat 4.8.5-11) (GCC) 。
4、替换python
cd /usr/binmv python python.bakln -s /usr/local/python3/bin/python3.6 /usr/bin/pythonll python*python -V

5、解决遗留问题所有python相关的应用，如果使用/usr/bin/python开头的脚本，替换为/usr/bin/python2.7。比如：
vim /usr/bin/yumvim /usr/libexec/urlgrabber-ext-down

2.3. uwsgipip install uwsgi
编写测试：
# test.pydef application(env, start_response):    start_response(&#x27;200 OK&#x27;, [(&#x27;Content-Type&#x27;,&#x27;text/html&#x27;)])    return [b&quot;Hello World&quot;]

启动测试：uwsgi --http :8001 --wsgi-file test.py
报错：uwsgi: command not found，看来我们需要把python3/bin加入到path。vim /etc/profile，在文件最底部找到PATH，添加：
:/usr/local/python3/bin

使配置生效：source /etc/profile
访问 http://ip:8001 ，即可看到Hello World 。
2.4. nginx和mysql参考《在CentOS7上配置PHP运行环境》，安装好了nginx和mysql。
3. 项目部署3.1. 代码准备1、克隆项目到服务器git clone https://github.com/voidking/djsite.git
2、安装djangopip install django
3、安装pymysqlpip install pymysql
3.2. 数据库准备1、创建数据库
# mysql -uroot -pmysql&gt; create database `djsite` default character set utf8 collate utf8_general_ci; mysql&gt; exit;

2、修改djsite/djsite/settings.py中的数据库配置vim djsite/djsite/settings.py
3、创建表结构
python manage.py makemigrationspython manage.py migrate

报错：
django.db.utils.InternalError: (1665, &#x27;Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED.&#x27;)

修改mysql的binlog格式为混合模式：
# mysql -uroot -pmysql&gt; set global binlog_format=mixed;

删除数据库djsite中的所有表，然后再次执行：
python manage.py migrate

4. 启动项目4.1. 数据库问题cd djsitepython manage.py runserver

报错：
File &quot;/usr/local/python3/lib/python3.6/site-packages/django/db/backends/mysql/base.py&quot;, line 36, in &lt;module&gt;    raise ImproperlyConfigured(&quot;mysqlclient 1.3.3 or newer is required; you have %s&quot; % Database.__version__)django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.3 or newer is required; you have 0.7.11.None

解决办法：
vim /usr/local/python3/lib/python3.6/site-packages/django/db/backends/mysql/base.py

进入vim命令模式，输入/version，按N查找下一个，找到：
if version &lt; (1, 3, 3):    raise ImproperlyConfigured(&quot;mysqlclient 1.3.3 or newer is required; you have %s&quot; % Database.__version__)
注释掉它，问题解决。
4.2. url问题cd djsitepython manage.py runserver

再次报错：
File &quot;/root/djsite/djsite/urls.py&quot;, line 21, in &lt;module&gt;    url(r&#x27;^blog/&#x27;, include(&#x27;blog.urls&#x27;, namespace=&#x27;blog&#x27;)),  File &quot;/usr/local/python3/lib/python3.6/site-packages/django/urls/conf.py&quot;, line 39, in include    &#x27;Specifying a namespace in include() without providing an app_name &#x27;django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported. Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.

解决办法：
vim /usr/local/python3/lib/python3.6/site-packages/django/urls/conf.py

找到：
if namespace and not app_name:    raise ImproperlyConfigured(        &#x27;Specifying a namespace in include() without providing an app_name &#x27;        &#x27;is not supported. Set the app_name attribute in the included &#x27;        &#x27;module, or pass a 2-tuple containing the list of patterns and &#x27;        &#x27;app_name instead.&#x27;,    )
注释掉它，问题解决。
4.3. 查看效果cd djsitepython manage.py runserver

启动成功，在服务器上测试访问：curl localhost:8000/blog/index
使用浏览器查看 http://ip:8000/blog/index ，却无法访问。这是因为在settings.py中，ALLOWED_HOSTS的配置为：
ALLOWED_HOSTS = []

官方文档说：

When DEBUG is True and ALLOWED_HOSTS is empty, the host is validated against [‘localhost’, ‘127.0.0.1’, ‘[::1]’].

修改ALLOWED_HOSTS的配置为：
ALLOWED_HOSTS = [&#x27;*&#x27;]

然后启动命令改为：python manage.py runserver 0.0.0.0:8000，此时即可在浏览器看到部署好的项目。
如果还是不能访问，尝试先关闭防火墙：systemctl stop firewalld
5. nginx配置1、首先，在万网上配置域名解析，添加A记录，解析到阿里云服务器IP。假设解析好的域名为django.voidking.com。
2、在nginx的vhost中，添加django.voidking.com.conf，内容为：
server &#123;    listen 80;    server_name django.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        client_max_body_size       1024m;        client_body_buffer_size    128k;        client_body_temp_path      data/client_body_temp;        proxy_connect_timeout      90;        proxy_send_timeout         90;        proxy_read_timeout         90;        proxy_buffer_size          4k;        proxy_buffers              4 32k;        proxy_busy_buffers_size    64k;        proxy_temp_file_write_size 64k;        proxy_temp_path            data/proxy_temp;        proxy_pass http://127.0.0.1:8000;    &#125;&#125;

3、重启nginx，./nginx -s reload
4、测试访问服务器：curl django.voidking.com/blog/index本地浏览器：http://django.voidking.com/blog/index
至此，django项目已经部署成功，没有用到uwsgi。如果给django添加守护进程，那么我们的部署就接近完美了。那么，uwsgi又能干什么呢，我们继续研究。
6. uwsgi6.1. 一般启动1、编写wsgi.py文件编写django_wsgi.py文件，将其放在与文件manage.py同一个目录下。
#!/usr/bin/env python# coding: utf-8import os,djangofrom django.core.handlers.wsgi import WSGIHandleros.environ.setdefault(&quot;DJANGO_SETTINGS_MODULE&quot;, &quot;djsite.settings&quot;)django.setup()application = WSGIHandler()

2、启动项目uwsgi --http :8000 --chdir ~/djsite/ --module django_wsgi
3、查看启动结果lsof -i :8000，ps aux | grep uwsgi
4、测试访问http://ip:8000/blog/index此时，页面是没有样式的，也就是说静态资源加载失败。
5、配置静态资源uwsgi --http :8000 --chdir ~/djsite/ --module django_wsgi --static-map=/static=static此时，页面样式就正常了。
6.2. 高级启动1、新建uwsgi.ini，与manage.py在同一级目录。
[uwsgi]http = :8000chdir = /root/djsite/wsgi-file = django_wsgi.pystatic-map = &#x27;/static=static&#x27;

2、启动uwsgiuwsgi uwsgi.ini
3、测试访问http://ip:8000/blog/index
7. 守护进程7.1. 安装supervisor关闭shell后，uwsgi服务就很快关闭了。为了让它后台运行，需要让它变成守护进程。参考《CentOS安装配置Supervisor》，安装配置好supervisor。
7.2. 守护uwsgi1、在/etc/supervisor中新建djsite.conf文件：
[program:djsite]command=/usr/local/python3/bin/uwsgi --http :8000 --chdir /root/djsite/ --module django_wsgi --static-map=/static=staticdirectory=/root/djsite/startsecs=0stopwaitsecs=0autostart=trueautorestart=true

2、重启supervisor
systemctl stop supervisordsystemctl start supervisord

附：重启djsite命令
supervisorctl -c /etc/supervisord.conf restart djsite

3、测试访问http://ip:8000/blog/index页面显示正常，至此守护进程配置成功。
4、退出supervisor环境source deactivate，守护进程并没有受到影响。
8. nginx+uwsgi以上，我们的djsite项目已经通过uwsgi方式启动起来，并且可以保持后台运行。nginx配置不改变的情况下，我们可以正常访问 http://django.voidking.com/blog/index 。此时，nginx作为反向代理，和uwsgi间通过http交互。
接下来，就配置下nginx和uwsgi通过socket结合的方式。原理：用户发送http请求到nginx，nginx通过socket把请求交给uwsgi，uwsgi拿到django的处理结果，通过socket返还给nginx，nginx通过http返回结果给用户。
1、因为nginx和uwsgi通过socket方式交互，我们需要修改uwsgi.ini的配置为：
[uwsgi]socket = :8000chdir = /root/djsite/wsgi-file = django_wsgi.pystatic-map = &#x27;/static=static&#x27;master = trueprocesses = 2enable-threads = true# daemonize = /root/djsite/uwsgi.log

2、/etc/supervisor/djsite.conf，修改为
[program:djsite]command=/usr/local/python3/bin/uwsgi uwsgi.inidirectory=/root/djsite/startsecs=0stopwaitsecs=0autostart=trueautorestart=true

3、重启supervisorsystemctl stop supervisordsystemctl start supervisord
4、修改nginx配置djsite.voidking.com.conf：
server &#123;    listen      80;    server_name djsite.voidking.com;    charset     utf-8;    location / &#123;        uwsgi_pass     127.0.0.1:8000;        include        uwsgi_params;    &#125;    location /static &#123;        alias /root/djsite/static;    &#125;&#125;

5、重启nginx./nginx -s reload
6、测试访问此时，访问 http://ip:8000/blog/index 失败，访问 http://django.voidking.com/blog/index 正常。因为8000端口不再提供http服务，而是一个和nginx连接的socket。
7、static请问，此时的静态资源，是通过uwsgi获取的？还是通过nginx直接获取的？做一个测试即可，修改uwsgi为：
[uwsgi]socket = :8000chdir = /root/djsite/wsgi-file = django_wsgi.py# static-map = &#x27;/static=static&#x27;master = trueprocesses = 2enable-threads = true# daemonize = /root/djsite/uwsgi.log
此时，uwsgi不再提供静态资源。重启supervisor，页面样式正常，可见，静态资源是通过nginx获取的。之所以可以获取到，是因为我们之前在djsite/settings.py中配置了：
STATICFILES_DIRS = (    os.path.join(BASE_DIR, &quot;static&quot;),)


9. 小结至此，django部署完毕，我们实现了三种部署方法：

nginx + django（http方式）
nginx + uwsgi（http方式）
nginx + uwsgi（socket方式）

在此过程中，解决了一些奇怪的bug，学习了升级python的方法，学习了使用pyenv安装多版本python的方法（类似的还有anaconda），学习了给django或者uwsgi添加守护进程的方法，收获颇丰。
10. 书签Python Web部署方式总结
Python网络框架——Web服务器
Django在生产环境中的部署
Django 部署(Nginx)
使用Supervisor管理SpiderKeeper和Scrapyd
使用uWSGI提供静态文件 (更新至1.9)
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>python</tag>
        <tag>django</tag>
        <tag>supervisor</tag>
        <tag>好好学Python</tag>
        <tag>uwsgi</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django开发简单Blog系统——下</title>
    <url>/dev-django-blog-2/</url>
    <content><![CDATA[1. 前言博客的基本功能完成了，接下来，我们来看一下还有哪些知识点可以加入我们的系统。


2. 超链接template中可以使用：
&#123;% url &#x27;app_name: url_name&#x27; param %&#125;


app_name：应用命名空间的名称
url_name：链接名
param：参数

app_name和url_name都在urls.py中配置。
djsite/djsite/urls.py中
url(r&#x27;^blog/&#x27;, include(&#x27;blog.urls&#x27;, namespace=&#x27;blog&#x27;)),
其中namespace=&#39;blog&#39;代表的是app_name。
djsite/blog/urls.py中
url(r&#x27;^(?P&lt;article_id&gt;[0-9]+)$&#x27;, views.detail, name=&#x27;detail&#x27;),
其中name=&#39;detail&#39;代表的是url_name。
比如，我们想要访问views.detail方法，那么在页面中，可以使用：
&#123;% url &#x27;blog:detail&#x27; article.id %&#125;

实际上，这种写法并没有我们上一篇文章中的写法方便，因为ajax得到数据后无法使用这种写法，建议弃用。
3. django shelldjango shell和python shell的不同，在于django shell里加入了项目的环境，这就方便了我们的调试。比如，我们在django shell输入：
from blog.models import ArticleArticle.objects.all()Article.objects.all().values()
即可打印出所有的文章。
4. Admin进阶在之前的后台管理系统中，点击进入Articles，发现里面有很多Article Object，因为我们在blog/models.py中添加了__str__方法，所以显示的名称是文章标题。
那么，怎样把标题、内容、发布时间都显示出来呢？答案是修改blog/admin.py：
from django.contrib import adminfrom . import models# Register your models here.class ArticleAdmin(admin.ModelAdmin):    # 显示在admin控制台中的列名    list_display = (&#x27;title&#x27;, &#x27;content&#x27;, &#x27;pub_time&#x27;)    # 时间过滤器    list_filter = (&#x27;pub_time&#x27;,)admin.site.register(models.Article, ArticleAdmin)

5. 改用mysqlsqlite，做一些测试还可以，但是在实际开发中，我们通常使用mysql。
1、假设我们的数据库用户名为root，密码为空，数据库名为djsite。
2、安装pymysqlpip install pymysql
3、修改djsite/djsite/settings.py：
# Database# https://docs.djangoproject.com/en/1.11/ref/settings/#databases# DATABASES = &#123;#     &#x27;default&#x27;: &#123;#         &#x27;ENGINE&#x27;: &#x27;django.db.backends.sqlite3&#x27;,#         &#x27;NAME&#x27;: os.path.join(BASE_DIR, &#x27;db.sqlite3&#x27;),#     &#125;# &#125;DATABASES = &#123;    &#x27;default&#x27;: &#123;        &#x27;ENGINE&#x27;: &#x27;django.db.backends.mysql&#x27;,        &#x27;HOST&#x27;: &#x27;127.0.0.1&#x27;,        &#x27;PORT&#x27;: &#x27;3306&#x27;,        &#x27;NAME&#x27;: &#x27;djsite&#x27;,        &#x27;USER&#x27;: &#x27;root&#x27;,        &#x27;PASSWORD&#x27;: &#x27;&#x27;,        &#x27;OPTIONS&#x27;: &#123;            &#x27;init_command&#x27;: &quot;SET sql_mode=&#x27;STRICT_TRANS_TABLES&#x27;&quot;,        &#125;,    &#125;&#125;

4、修改djsite/djsite/__init__.py：
import pymysqlpymysql.install_as_MySQLdb()

5、数据迁移python manage.py makemigrations
python manage.py migrate
6、创建超级用户python manage.py createsuperuser
6. 源码分享https://github.com/voidking/djsite/releases/tag/v0.3.0
7. 书签django入门与实践
Django 过滤器
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>django</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django开发简单Blog系统——中</title>
    <url>/dev-django-blog-1/</url>
    <content><![CDATA[1. 系统功能1、博客列表展示2、新增博客、修改博客、删除博客、搜索博客3、后台管理


2. 后台管理django自带后台管理系统Admin，被授权的用户可以直接在后台管理系统中操作数据库。同时，我们可以按照需求对Admin进行定制。
1、创建超级用户python manage.py createsuperuser根据提示，输入用户名、邮箱、密码。
2、测试访问访问地址 http://localhost:8000/admin/ ，输入用户名密码，登录后台管理系统。
3、改成中文后台管理系统，默认是英文的，修改settings.py：
# LANGUAGE_CODE = &#x27;en-us&#x27;LANGUAGE_CODE = &#x27;zh-hans&#x27;
刷新后台管理页面，发现变成了中文。
4、注册model此时，我们在后台管理中看不到article等表数据，要想显示数据，需要在blog/admin.py中注册model。
from django.contrib import adminfrom . import models# Register your models here.admin.site.register(models.Article)
刷新后台管理页面，发现article表已经出现。
5、点击进入Articles，发现里面有很多Article Object，因为我们在blog/models.py中添加了__str__方法，所以显示的名称是文章标题。
def __str__(self):    return self.title

3. 页面开发3.1. 主页面1、在blog/urls.py中配置路由
url(r&#x27;^$&#x27;, views.index, name=&#x27;index&#x27;),url(r&#x27;^index$&#x27;, views.index, name=&#x27;index&#x27;),

2、在blog/views.py中修改index方法
def index(request):    articles = models.Article.objects.all()    return render(request, &#x27;blog/index.html&#x27;, &#123;&#x27;articles&#x27;: articles&#125;)

3、修改blog/templates/blog/index.html
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;首页&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;博客列表&lt;/h2&gt;    &lt;div class=&quot;menu&quot;&gt;        &lt;input class=&quot;search-input&quot; type=&quot;text&quot;&gt;         &lt;span class=&quot;search&quot;&gt;搜索&lt;/span&gt;         &lt;a class=&quot;add&quot; href=&quot;/blog/toadd&quot;&gt;添加&lt;/a&gt;    &lt;/div&gt;    &lt;table&gt;        &lt;thead&gt;            &lt;th&gt;博客标题&lt;/th&gt;            &lt;th&gt;发布时间&lt;/th&gt;            &lt;th&gt;操作&lt;/th&gt;        &lt;/thead&gt;        &lt;tbody&gt;            &#123;% for article in articles %&#125;            &lt;tr data-id=&quot;&#123;&#123;article.id&#125;&#125;&quot;&gt;                &lt;td&gt;&lt;a href=&quot;/blog/&#123;&#123;article.id&#125;&#125;&quot;&gt;&#123;&#123;article.title&#125;&#125;&lt;/a&gt;&lt;/td&gt;                &lt;td&gt;&#123;&#123;article.pub_time&#125;&#125;&lt;/td&gt;                &lt;td&gt;                    &lt;span class=&quot;del&quot;&gt;删除&lt;/span&gt;                &lt;/td&gt;            &lt;/tr&gt;            &#123;% endfor %&#125;        &lt;/tbody&gt;    &lt;/table&gt;&lt;/body&gt;&lt;/html&gt;

4、测试访问访问地址 http://localhost:8000/blog 或 http://localhost:8000/blog/index
3.2. 静态资源blog/index.html中，没有添加样式。接下来，我们修改一下项目结构，把样式表放在静态资源目录中。参考DJango 1.8 配置静态资源文件可访问 和 Django 静态文件。
1、新建djsite/static/css层级目录，在css下新建index.css，内容参见源码分享。
2、在settings.py中添加
# 设置STATIC_URL为存储静态文件的路径（基于根目录）STATIC_URL = &#x27;/static/&#x27; # 配置存储静态文件的路径映射值，这个值用于模版引用路径的转换 STATICFILES_DIRS = (      os.path.join(BASE_DIR, &quot;static&quot;),  )


3、修改blog/index.html为：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &#123;% load staticfiles %&#125;    &lt;link rel=&quot;stylesheet&quot; href=&quot;&#123;%static &#x27;css/index.css&#x27;%&#125;&quot;&gt;    &lt;title&gt;首页&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;!--不变--&gt;&lt;/body&gt;&lt;/html&gt;

3.3. 查看博客页面1、在blog/urls.py中添加：
url(r&#x27;^(?P&lt;article_id&gt;[0-9]+)$&#x27;, views.detail, name=&#x27;detail&#x27;),

2、在blog/views.py中添加方法：
def detail(request,article_id):    article = models.Article.objects.get(pk=article_id)    return render(request, &#x27;blog/article.html&#x27;,                  &#123;&#x27;article&#x27;: article&#125;)

3、在blog/templates/blog中添加article.html：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &#123;% load staticfiles %&#125;    &lt;link rel=&quot;stylesheet&quot; href=&quot;&#123;%static &#x27;css/article.css&#x27;%&#125;&quot;&gt;    &lt;title&gt;&#123;&#123;article.title&#125;&#125;&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;&#123;&#123;article.title&#125;&#125;&lt;/h2&gt;    &lt;div&gt;        &lt;p&gt;&#123;&#123;article.content&#125;&#125;&lt;/p&gt;    &lt;/div&gt;    &lt;div&gt;        &lt;a class=&quot;edit&quot; href=&quot;/blog/toedit/&#123;&#123;article.id&#125;&#125;&quot;&gt;编辑&lt;/a&gt;    &lt;/div&gt;&lt;/body&gt;&lt;/html&gt;

4、测试访问访问地址 http://localhost:8000/blog/1
3.4. 添加博客页面1、在blog/urls.py中添加：
url(r&#x27;^toadd$&#x27;, views.toadd, name=&#x27;toadd&#x27;),

2、在blog/views.py添加方法：
def toadd(request):    return render(request, &#x27;blog/add.html&#x27;)

3、在blog/templates/blog中添加add.html：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;添加博客&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;form action=&quot;/blog/add&quot; method=&quot;post&quot;&gt;        &lt;p&gt;            &lt;label for=&quot;title&quot;&gt;标题&lt;/label&gt;            &lt;input id=&quot;title&quot; name=&quot;title&quot; type=&quot;text&quot;&gt;        &lt;/p&gt;        &lt;p&gt;            &lt;label for=&quot;content&quot;&gt;内容&lt;/label&gt;            &lt;textarea id=&quot;content&quot; name=&quot;content&quot; id=&quot;&quot; cols=&quot;30&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;        &lt;/p&gt;        &lt;p&gt;            &lt;input type=&quot;submit&quot; value=&quot;确定&quot;&gt;        &lt;/p&gt;    &lt;/form&gt;&lt;/body&gt;&lt;/html&gt;

4、修改blog/views.py中的add方法为：
@csrf_exemptdef add(request):    title = request.POST.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.POST.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    pub_time = utc2local(timezone.now())    LOCAL_FORMAT = &quot;%Y-%m-%d %H:%M:%S&quot;    pub_time = pub_time.strftime(LOCAL_FORMAT)    models.Article.objects.create(title=title, content=content, pub_time=pub_time)    articles = models.Article.objects.all()    return render(request, &#x27;blog/index.html&#x27;,&#123;&#x27;articles&#x27;: articles&#125;)

5、测试访问访问地址 http://localhost:8000/blog/toadd
3.5. 修改博客页面1、在blog/urls.py中添加：
url(r&#x27;^toedit/(?P&lt;article_id&gt;[0-9]+)$&#x27;, views.toedit, name=&#x27;toedit&#x27;),

2、在blog/views.py添加方法：
def toedit(request, article_id):    article = models.Article.objects.get(pk=article_id)    return render(request, &#x27;blog/edit.html&#x27;, &#123;&#x27;article&#x27;: article&#125;)

3、在blog/templates/blog中添加edit.html：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;修改博客&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;form action=&quot;/blog/edit&quot; method=&quot;post&quot;&gt;        &lt;p style=&quot;display: none;&quot;&gt;            &lt;input name=&quot;id&quot; type=&quot;text&quot; value=&quot;&#123;&#123;article.id&#125;&#125;&quot;&gt;        &lt;/p&gt;        &lt;p&gt;            &lt;label for=&quot;title&quot;&gt;标题&lt;/label&gt;            &lt;input id=&quot;title&quot; name=&quot;title&quot; type=&quot;text&quot; value=&quot;&#123;&#123;article.title&#125;&#125;&quot;&gt;        &lt;/p&gt;        &lt;p&gt;            &lt;label for=&quot;content&quot;&gt;内容&lt;/label&gt;            &lt;textarea id=&quot;content&quot; name=&quot;content&quot; id=&quot;&quot; cols=&quot;30&quot; rows=&quot;10&quot;&gt;&#123;&#123;article.content&#125;&#125;&lt;/textarea&gt;        &lt;/p&gt;        &lt;p&gt;            &lt;input type=&quot;submit&quot; value=&quot;确定&quot;&gt;        &lt;/p&gt;    &lt;/form&gt;&lt;/body&gt;&lt;/html&gt;

4、修改blog/views.py中的edit方法：
@csrf_exemptdef edit(request):    article_id = request.POST.get(&#x27;id&#x27;, 0)    title = request.POST.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.POST.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    pub_time = utc2local(timezone.now())    LOCAL_FORMAT = &quot;%Y-%m-%d %H:%M:%S&quot;    pub_time = pub_time.strftime(LOCAL_FORMAT)    article = models.Article.objects.get(pk=article_id)    article.title = title    article.content = content    article.pub_time = pub_time    article.save()    articles = models.Article.objects.all()    return render(request, &#x27;blog/index.html&#x27;, &#123;&#x27;articles&#x27;: articles&#125;)

3.6. 删除博客1、修改blog/templates/blog/index.html，添加js引入：
&lt;script src=&quot;&#123;% static &#x27;lib/jquery/jquery.min.js&#x27;%&#125;&quot;&gt;&lt;/script&gt;&lt;script src=&quot;&#123;% static &#x27;lib/layer/layer.js&#x27;%&#125;&quot;&gt;&lt;/script&gt;&lt;script src=&quot;&#123;% static &#x27;lib/art-template/dist/template-native.js&#x27;%&#125;&quot;&gt;&lt;/script&gt;&lt;script src=&quot;&#123;% static &#x27;js/index.js&#x27;%&#125;&quot;&gt;&lt;/script&gt;

2、创建static/js目录，js目录中新建index.js：
$(function()&#123;    $(&#x27;table&#x27;).on(&#x27;click&#x27;,&#x27;.del&#x27;,function()&#123;        $that = $(this);        layer.confirm(&#x27;确认删除？&#x27;, &#123;            btn: [&#x27;确认&#x27;,&#x27;取消&#x27;] //按钮        &#125;, function()&#123;            var $tr = $that.parents(&#x27;tr&#x27;);            var id = $tr.attr(&#x27;data-id&#x27;);            $.ajax(&#123;                url: &#x27;/blog/delete&#x27;,                type: &#x27;POST&#x27;,                dataType: &#x27;json&#x27;,                data: &#123;id: id&#125;,                success: function(data)&#123;                    console.log(data);                    if(data.code == &#x27;0&#x27;)&#123;                        $tr.remove();                        layer.msg(&#x27;删除成功&#x27;);                    &#125;                &#125;,                error: function(xhr)&#123;                    console.log(xhr);                &#125;            &#125;);        &#125;, function()&#123;            // nothing        &#125;);            &#125;);&#125;);

3、修改blog/views.py中的delete方法：
@csrf_exemptdef delete(request):    article_id = request.POST.get(&#x27;id&#x27;, 0)    models.Article.objects.get(pk=article_id).delete()    result = &#123;&#x27;code&#x27;: 0, &#x27;ext&#x27;: &#x27;success&#x27;&#125;    return HttpResponse(json.dumps(result, ensure_ascii=False))

4、测试访问访问地址 http://localhost:8000/blog/ ，点击文章后面的删除按钮即可。
3.7. 查找博客1、在blog/urls.py中添加：
url(r&#x27;^search$&#x27;, views.search, name=&#x27;search&#x27;)

2、在blog/views.py中添加search方法：
from django.db.models import Q@csrf_exemptdef search(request):    key = request.POST.get(&#x27;key&#x27;)    articles = models.Article.objects.filter(Q(title__contains=key) | Q(content__contains=key))    json_data = serializers.serialize(&quot;json&quot;, articles)    dict_data = json.loads(json_data)    result = &#123;        &#x27;code&#x27;: 0,        &#x27;ext&#x27;: &#x27;success&#x27;,        &#x27;articles&#x27;: dict_data&#125;    return HttpResponse(json.dumps(result, ensure_ascii=False))


3、在blog/templates/blog/index.html中添加：
&lt;script type=&quot;text/template&quot; id=&quot;tr_template&quot;&gt;    &lt;% for(var i = 0 ; i &lt; articles.length ; i ++)&#123; %&gt;    &lt;% var article = articles[i]; %&gt;    &lt;tr data-id=&quot;&lt;%=article.pk%&gt;&quot;&gt;        &lt;td&gt;&lt;a href=&quot;/blog/&lt;%=article.pk%&gt;&quot;&gt;&lt;%=article.fields.title%&gt;&lt;/a&gt;&lt;/td&gt;        &lt;td&gt;&lt;%=article.fields.pub_time%&gt;&lt;/td&gt;        &lt;td&gt;            &lt;span class=&quot;del&quot;&gt;删除&lt;/span&gt;        &lt;/td&gt;    &lt;/tr&gt;    &lt;% &#125; %&gt;&lt;/script&gt;

4、在index.js中添加：
$(&#x27;.search&#x27;).on(&#x27;click&#x27;,function()&#123;    var key = $(&#x27;.search-input&#x27;).val();    $.ajax(&#123;        url: &#x27;/blog/search&#x27;,        type: &#x27;POST&#x27;,        dataType: &#x27;json&#x27;,        data: &#123;key: key&#125;,        success: function(data)&#123;            console.log(data);            if(data.code == &#x27;0&#x27;)&#123;                var html = template(&#x27;tr_template&#x27;,&#123;articles: data.articles&#125;);                $(&#x27;tbody&#x27;).html(html);            &#125;        &#125;,        error: function(xhr)&#123;            console.log(xhr);        &#125;    &#125;);&#125;);$(&#x27;.search-input&#x27;).keypress(function(event) &#123;    var key = event.which;    // console.log(key);    if(key == 13)&#123;        //do something        $(&#x27;.search&#x27;).trigger(&#x27;click&#x27;);    &#125;&#125;);

4. 效果演示
5. 源码分享https://github.com/voidking/djsite/releases/tag/v0.2.0
6. 书签django入门与实践
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>django</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django开发简单Blog系统——上</title>
    <url>/dev-django-blog-0/</url>
    <content><![CDATA[1. 前言承接《Django入门》，本文参照慕课网《django入门与实践》课程，开发一个简单的博客系统。按照国际惯例，我们先学习一下django的基础知识。


2. 模板引擎Django默认使用DTL（Django Template Language）作为模板引擎，如果想要修改为其他模板引擎，直接在djsite/djsite/settings.py中修改TEMPLATES即可。详情可以参考The Django template language: for Python programmers。
2.1. first template1、在blog目录下创建templates目录。
2、在templates目录中创建index.html文件。
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Index&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    first template!&lt;/body&gt;&lt;/html&gt;

3、修改blog/urls.py为：
from django.conf.urls import urlfrom . import viewsurlpatterns = [    url(r&#x27;^$&#x27;, views.index, name=&#x27;index&#x27;),    url(r&#x27;helloworld&#x27;, views.hello, name=&#x27;hello&#x27;)]

4、在views.py中添加方法：
def index(request):    return render(request, &#x27;index.html&#x27;)

5、测试访问启动django，访问 http://localhost:8000/blog/ ，即可看到渲染好的页面。
2.2. DTL1、修改index方法为：
def index(request):    return render(request, &#x27;index.html&#x27;,&#123;&#x27;title&#x27;: &#x27;DTL&#x27;&#125;)

2、修改index.html为：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Index&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;&#123;&#123;title&#125;&#125;&lt;/h2&gt;    &lt;p&gt;first template!&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

3、测试访问启动django，访问 http://localhost:8000/blog/ ，即可看到渲染好的页面。

不同应用下的templates目录会发生冲突，django按照INSTALLED_APP中的顺序查找templates。为了解决这个问题，我们需要在templates目录中加一层目录，以应用名为名。而模板，都放到这一层目录中。

4、在templates目录下，新建blog文件夹，把index.html移动到blog文件夹中。同时，修改index函数为：
def index(request):    return render(request, &#x27;blog/index.html&#x27;,&#123;&#x27;title&#x27;: &#x27;DTL&#x27;&#125;)

3. 增删查改django默认使用db.sqlite3数据库，我们暂时不进行修改。
3.1. Model1、在blog/models.py中添加一个类Article：
class Article(models.Model):    title = models.CharField(max_length=32, default=&#x27;Title&#x27;)    content = models.TextField(null=True)    # 参数 auto_now=True 表示自动添加隐藏的时间    pub_time = models.DateTimeField(null=True, auto_now=True)    def __str__(self):        return self.title

关于属性的配置，参考Model field reference。
2、生成数据表python manage.py makemigrations blog，创建model，生成的文件在blog/migrations目录下
python manage.py migrate，根据model生成数据库表
3、查看sql语句python manage.py sqlmigrate blog 0001
4、下载安装SQLite Expert Personal，双击db.sqlite3文件即可查看编辑数据库。
5、使用SQLiteExpert，在blog_article表中添加数据。
3.2. 查找数据1、在blog/views.py中添加方法：
from . import modelsdef list(request):    articles = models.Article.objects.all()    article = models.Article.objects.get(pk=1)    return render(request, &#x27;blog/list.html&#x27;,&#123;&#x27;articles&#x27;:articles,&#x27;article&#x27;:article&#125;)

2、在migrations/blog中添加list.html文件
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;List&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;第一篇文章&lt;/h2&gt;    &lt;h3&gt;标题：&#123;&#123;article.title&#125;&#125;&lt;/h3&gt;    &lt;p&gt;内容&#123;&#123;article.content&#125;&#125;&lt;/p&gt;    &lt;hr&gt;    &lt;h2&gt;文章列表&lt;/h2&gt;    &lt;table&gt;        &lt;thead&gt;            &lt;th&gt;标题&lt;/th&gt;            &lt;th&gt;内容&lt;/th&gt;        &lt;/thead&gt;        &lt;tbody&gt;            &#123;% for article in articles %&#125;            &lt;tr&gt;                &lt;td&gt;&#123;&#123;article.title&#125;&#125;&lt;/td&gt;                &lt;td&gt;&#123;&#123;article.content&#125;&#125;&lt;/td&gt;            &lt;/tr&gt;            &#123;% endfor %&#125;        &lt;/tbody&gt;    &lt;/table&gt;&lt;/body&gt;&lt;/html&gt;

3、修改blog/urls.py为：
from django.conf.urls import urlfrom . import viewsurlpatterns = [    url(r&#x27;^$&#x27;, views.index, name=&#x27;index&#x27;),    url(r&#x27;^index$&#x27;, views.index, name=&#x27;index&#x27;),    url(r&#x27;^helloworld$&#x27;, views.hello, name=&#x27;hello&#x27;),    url(r&#x27;^list$&#x27;,views.list, name=&#x27;list&#x27;)]

4、测试访问访问地址 http://localhost:8000/blog/list ，即可看到渲染后的效果。
3.3. 增加数据1、在blog/urls.py中添加：
url(r&#x27;^add$&#x27;,views.add, name=&#x27;add&#x27;),

2、在blog/views.py中添加方法：
import jsondef add(request):    title = request.GET.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.GET.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    article = models.Article.objects.create(title=title, content=content)    result = &#123;&#x27;code&#x27;: 0, &#x27;ext&#x27;: &#x27;success&#x27;, &#x27;article_id&#x27;: article.id&#125;    return HttpResponse(json.dumps(result,ensure_ascii=False))

3、测试访问访问地址 http://localhost:8000/blog/add?title=test&amp;content=test ，即可看到添加成功的提示。
3.4. 修改数据1、在blog/urls.py中添加：
url(r&#x27;^edit$&#x27;,views.edit, name=&#x27;edit&#x27;),

2、在blog/views.py中添加方法：
def edit(request):    article_id = request.GET.get(&#x27;id&#x27;, 0)    title = request.GET.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.GET.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    article = models.Article.objects.get(pk=article_id)    article.title = title    article.content = content    article.save()    result = &#123;&#x27;code&#x27;: 0, &#x27;ext&#x27;: &#x27;success&#x27;, &#x27;article_id&#x27;: article.id&#125;    return HttpResponse(json.dumps(result,ensure_ascii=False))

3、测试访问访问地址 http://localhost:8000/blog/edit?id=1&amp;title=test&amp;content=test222 ，即可看到修改成功的提示。
PS：修改数据和增加数据可以合成为一个接口，例如：
def edit(request):    article_id = request.GET.get(&#x27;id&#x27;, &#x27;0&#x27;)    title = request.GET.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.GET.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    if article_id == &#x27;0&#x27;:        article = models.Article.objects.create(title=title, content=content)        result = &#123;&#x27;code&#x27;: 0, &#x27;ext&#x27;: &#x27;success&#x27;, &#x27;article_id&#x27;: article.id&#125;        return HttpResponse(json.dumps(result,ensure_ascii=False))    article = models.Article.objects.get(pk=article_id)    article.title = title    article.content = content    article.save()    result = &#123;&#x27;code&#x27;: 0, &#x27;ext&#x27;: &#x27;success&#x27;, &#x27;article_id&#x27;: article.id&#125;    return HttpResponse(json.dumps(result,ensure_ascii=False))

3.5. 删除数据1、在blog/urls.py中添加：
url(r&#x27;^delete$&#x27;,views.delete, name=&#x27;delete&#x27;),

2、在blog/views.py中添加方法：
def delete(request):    article_id = request.GET.get(&#x27;id&#x27;, 0)    models.Article.objects.get(pk=article_id).delete()    result = &#123;&#x27;code&#x27;: 0, &#x27;ext&#x27;: &#x27;success&#x27;&#125;    return HttpResponse(json.dumps(result,ensure_ascii=False))

3、测试访问访问地址 http://localhost:8000/blog/delete?id=1 ，即可看到删除成功的提示。
3.6. Model转JSON
要想最终得到一个json数据，前提是我们要拥有一个dict，所以Model转JSON问题就归结为怎样组装出一个dict。

示例一：在add方法中，我们返回的结果是json格式。如果想要把article（Model）也放进结果中，该怎么处理？参考Python JSON和django的model对象转化成dict，修改代码如下：
from django.forms.models import model_to_dictdef add(request):    title = request.GET.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.GET.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    article = models.Article.objects.create(title=title, content=content)    article = model_to_dict(article)    result = &#123;&#x27;code&#x27;: 0, &#x27;ext&#x27;: &#x27;success&#x27;,&#x27;article&#x27;: article&#125;    return HttpResponse(json.dumps(result,ensure_ascii=False))

示例二：如果想要把articles（Models）也放进结果中，该怎么处理？参考django 返回json数据。首先，把Models序列化为json格式数据；然后，使用json.loads转换为dict格式数据；最后，把转换后的dict和其他dict格式数据组装到一起。
from django.core import serializersdef add(request):    title = request.GET.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.GET.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    article = models.Article.objects.create(title=title, content=content)    article = model_to_dict(article)    articles = models.Article.objects.all()    json_data = serializers.serialize(&quot;json&quot;, articles)    dict_data = json.loads(json_data)    result = &#123;        &#x27;code&#x27;: 0,        &#x27;ext&#x27;: &#x27;success&#x27;,        &#x27;article&#x27;: article,        &#x27;articles&#x27;: dict_data&#125;    return HttpResponse(json.dumps(result, ensure_ascii=False))

3.7. sqlite清空表命令delete from &#39;blog_article&#39;;update sqlite_sequence set seq = 0 where name = &#39;blog_article&#39;;
4. POST问题修改add方法为：
def add(request):    title = request.POST.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.POST.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    article = models.Article.objects.create(title=title, content=content)    article = model_to_dict(article)    articles = models.Article.objects.all()    json_data = serializers.serialize(&quot;json&quot;, articles)    dict_data = json.loads(json_data)    result = &#123;        &#x27;code&#x27;: 0,        &#x27;ext&#x27;: &#x27;success&#x27;,        &#x27;article&#x27;: article,        &#x27;articles&#x27;: dict_data&#125;    return HttpResponse(json.dumps(result, ensure_ascii=False))

使用postman发送post请求时遇到如下错误：
CSRF verification failed. Request aborted.

解决办法，使用csrf_exempt装饰器：
from django.views.decorators.csrf import csrf_exempt@csrf_exemptdef add(request):    title = request.POST.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.POST.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    article = models.Article.objects.create(title=title, content=content)    article = model_to_dict(article)    articles = models.Article.objects.all()    json_data = serializers.serialize(&quot;json&quot;, articles)    dict_data = json.loads(json_data)    result = &#123;        &#x27;code&#x27;: 0,        &#x27;ext&#x27;: &#x27;success&#x27;,        &#x27;article&#x27;: article,        &#x27;articles&#x27;: dict_data&#125;    return HttpResponse(json.dumps(result, ensure_ascii=False))

5. 时间处理5.1. 修改时区查看db.sqlite3数据库，可以看到通过接口添加的数据时间不对。参考django时间的时区问题，修改settings.py：
USE_TZ = TrueTIME_ZONE = &#x27;Asia/Shanghai&#x27;

设置了USE_TZ=True，则存储到数据库中的时间永远是UTC时间。设置了TIME_ZONE = ‘Asia/Shanghai’，能保证证模板时间的正确显示。
这时如果TIME_ZONE = ‘UTC’，用datetime.datetime.now()获取时间，django会把这个时间当成UTC时间存储到数据库中去。如果修改设置为TIME_ZONE = ‘Asia/Shanghai’，用datetime.datetime.now()获取时间，django会把这个时间当成Asia/Shanghai时间，即东八区时间，然后django会把这个时间转成带时区UTC时间存储到数据库中去，而读的时候直接按UTC时间读出来，这就是很多人遇到的存储到数据库中的时间比本地时间会小8个小时的原因。
如果要获取当前时区时间，则使用django.utils.timezone.now()。
5.2. Model参考django：DateTimeField如何自动设置为当前时间并且能被修改，我们来修改一下blog/models.py。
创建django的model时，有DateTimeField、DateField和TimeField三种类型可以用来创建日期字段，其值分别对应着datetime()、date()、time()三中对象。这三个field有着相同的参数auto_now和auto_now_add，表面上看起来很easy，但实际使用中很容易出错，下面是一些注意点。
DateTimeField.auto_now
这个参数的默认值为false，设置为true时，能够在保存该字段时，将其值设置为当前时间，并且每次修改model，都会自动更新。因此这个参数在需要存储“最后修改时间”的场景下，十分方便。需要注意的是，设置该参数为true时，并不简单地意味着字段的默认值为当前时间，而是指字段会被“强制”更新到当前时间，你无法程序中手动为字段赋值；如果使用django再带的admin管理器，那么该字段在admin中是只读的。
DateTimeField.auto_now_add
这个参数的默认值也为False，设置为True时，会在model对象第一次被创建时，将字段的值设置为创建时的时间，以后修改对象时，字段的值不会再更新。该属性通常被用在存储“创建时间”的场景下。与auto_now类似，auto_now_add也具有强制性，一旦被设置为True，就无法在程序中手动为字段赋值，在admin中字段也会成为只读的。
如何将创建时间设置为“默认当前”并且可修改
那么问题来了。实际场景中，往往既希望在对象的创建时间默认被设置为当前值，又希望能在日后修改它。怎么实现这种需求呢？
django中所有的model字段都拥有一个default参数，用来给字段设置默认值。可以用default=timezone.now来替换auto_now=True或auto_now_add=True。timezone.now对应着django.utils.timezone.now()，因此需要写成类似下面的形式：
from django.db import modelsimport django.utils.timezone as timezoneclass Article(models.Model):    title = models.CharField(max_length=32, default=&#x27;Title&#x27;)    content = models.TextField(null=True)    pub_time = models.DateTimeField(&#x27;发布日期&#x27;, default=timezone.now)    def __str__(self):        return self.title

5.3. DateEncoder经过上面的修改，时间是可以修改了，但是同时引入了另外一个问题，在add接口中，json.dumps()函数会报错：
TypeError: Object of type &#x27;datetime&#x27; is not JSON serializable

这是因为json.dumps()函数无法解析datetime格式的数据。问题来了，auto_now=True时，json.dumps()却可以解析，莫非此时不是datetime格式？且不管它，我们先解决datetime转json问题。
import json  import datetimeclass DateEncoder(json.JSONEncoder):    def default(self, obj):        if isinstance(obj, datetime.datetime):            return obj.strftime(&#x27;%Y-%m-%d %H:%M:%S&#x27;)        elif isinstance(obj, date):            return obj.strftime(&#x27;%Y-%m-%d&#x27;)        else:            return json.JSONEncoder.default(self, obj)

在使用json.dumps()函数时，添加cls参数：
json.dumps(result, cls=DateEncoder, ensure_ascii=False)

5.4. 页面渲染设置好时区后，在页面渲染时，会自动转化成当前时区时间，例如Nov. 29, 2017, 1:49 p.m.
但是，这种格式不符合我们的阅读习惯，我们可以在渲染时改成自己喜欢的格式：
&#123;&#123;article.pub_time|date:&quot;Y-m-d H:i:s&quot;&#125;&#125;

此时，输出到页面的格式就变成了2017-11-29 13:49:44
5.5. json UTC处理以add接口为例，从数据库中查询出的数据时间是UTC格式的，例如2017-11-29T05:49:44.092Z
思路一：直接返回UTC格式数据给前端，前端来完成格式化，参考js格式化json传来的UTC格式的时间，或者使用支持UTC格式化的模板引擎。
思路二：参考遍历QuerySet，给每一个pub_time转换格式：
import datetimeimport timedef utc2local(utc_st):    # UTC时间转本地时间（+8:00）    now_stamp = time.time()    local_time = datetime.datetime.fromtimestamp(now_stamp)    utc_time = datetime.datetime.utcfromtimestamp(now_stamp)    offset = local_time - utc_time    local_st = utc_st + offset    return local_st

for item in articles:    # print(item.pub_time)    local_time = utc2local(item.pub_time)    # UTC_FORMAT = &quot;%Y-%m-%dT%H:%M:%S.%fZ&quot;    LOCAL_FORMAT = &quot;%Y-%m-%d %H:%M:%S&quot;    # print(local_time.strftime(LOCAL_FORMAT))    local_time_str = local_time.strftime(LOCAL_FORMAT)    item.pub_time = datetime.datetime.strptime(local_time_str, LOCAL_FORMAT)

这种方法返回的时间，格式为2017-11-29T13:49:44，还是有问题，多了个T。我们为什么不把local_time_str赋值给item.pub_time呢？因为item.put_time限制数据类型为datetime。
思路三：存储时，直接存储字符串格式的时间。修改blog/models.py如下：
from django.db import models# Create your models here.class Article(models.Model):    title = models.CharField(max_length=32, default=&#x27;Title&#x27;)    content = models.TextField(null=True)    # pub_time = models.DateTimeField(&#x27;发布日期&#x27;, default=timezone.now)    pub_time = models.CharField(max_length=64, default=&#x27;&#x27;)    def __str__(self):        return self.title

修改add和edit接口为：
import jsonfrom django.forms.models import model_to_dictfrom django.views.decorators.csrf import csrf_exemptimport datetimeimport timefrom django.utils import timezone@csrf_exemptdef add(request):    title = request.POST.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.POST.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)        pub_time = utc2local(timezone.now())    LOCAL_FORMAT = &quot;%Y-%m-%d %H:%M:%S&quot;    pub_time = pub_time.strftime(LOCAL_FORMAT)    article = models.Article.objects.create(title=title, content=content, pub_time=pub_time)    article = model_to_dict(article)    result = &#123;        &#x27;code&#x27;: 0,        &#x27;ext&#x27;: &#x27;success&#x27;,        &#x27;article&#x27;: article&#125;    return HttpResponse(json.dumps(result, ensure_ascii=False))@csrf_exemptdef edit(request):    article_id = request.POST.get(&#x27;id&#x27;, 0)    title = request.POST.get(&#x27;title&#x27;, &#x27;defaultTitle&#x27;)    content = request.POST.get(&#x27;content&#x27;, &#x27;defaultContent&#x27;)    pub_time = utc2local(timezone.now())    LOCAL_FORMAT = &quot;%Y-%m-%d %H:%M:%S&quot;    pub_time = pub_time.strftime(LOCAL_FORMAT)    article = models.Article.objects.get(pk=article_id)    article.title = title    article.content = content    article.pub_time = pub_time    article.save()    article = model_to_dict(article)    result = &#123;        &#x27;code&#x27;: 0,        &#x27;ext&#x27;: &#x27;success&#x27;,        &#x27;article&#x27;: article&#125;    return HttpResponse(json.dumps(result, ensure_ascii=False))def utc2local(utc_st):    # UTC时间转本地时间（+8:00）    now_stamp = time.time()    local_time = datetime.datetime.fromtimestamp(now_stamp)    utc_time = datetime.datetime.utcfromtimestamp(now_stamp)    offset = local_time - utc_time    local_st = utc_st + offset    return local_st

6. 源码分享https://github.com/voidking/djsite/releases/tag/v0.1.0
7. 小结至此，涉猎了django开发blog所需要的基本知识。下文中，将会在实战中学习django更高级的用法。
8. 书签django入门与实践
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>django</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Django入门篇</title>
    <url>/dev-django-start/</url>
    <content><![CDATA[1. Django简介Django是一个开放源代码的Web应用框架，由Python写成。采用了MVC的框架模式，即模型M，视图V和控制器C。它最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的，即是CMS（内容管理系统）软件。并于2005年7月在BSD许可证下发布。


2. 安装Django安装方法一：pip install django
安装方法二：
git clone https://github.com/django/django.gitcd djangopip setup.py installpython -m django --version

3. 创建项目1、创建django项目，命名为djsitedjango-admin startproject djsite
2、运行djsitecd djsite，python manage.py runserver
3、测试访问浏览器访问 http://localhost:8000/ ，即可看到第一个django页面。
4、运行djsite进阶python manage.py runserver 0.0.0.0:8080 ，此时，允许任意ip可以访问，端口为8080。
4. 目录结构djsite│  db.sqlite3│  manage.py│└─djsite    │  settings.py    │  urls.py    │  wsgi.py    └─__init__.py


__init__.py，表示djsite是一个模块
manage.py，管理命令文件
settings.py，全局配置文件
urls.py，路由配置
wsgi.py，配置nginx、apache对接

django中的各种命令，都是在manage.py中完成的。

启动服务器，python manage.py runserver
创建应用，python manage.py startapp blog
进入shell，python manage.py shell
创建model，python manage.py makemigrations
根据model更新数据库表，python manage.py migrate

5. 创建应用django中以应用来分割功能，使结构清晰，方便复用。
1、创建blog应用python manage.py startapp blog
2、添加blog应用到settings.py
# Application definitionINSTALLED_APPS = [    &#x27;django.contrib.admin&#x27;,    &#x27;django.contrib.auth&#x27;,    &#x27;django.contrib.contenttypes&#x27;,    &#x27;django.contrib.sessions&#x27;,    &#x27;django.contrib.messages&#x27;,    &#x27;django.contrib.staticfiles&#x27;,    &#x27;blog&#x27;,]

3、blog的目录结构如下
blog│  admin.py│  apps.py│  models.py│  tests.py│  views.py│  __init__.py│└─migrations        __init__.py


admin.py，当前应用后台管理配置
apps.py，当前应用配置
models.py，定义数据库表
tests.py，测试相关
views.py，执行响应
migrations，数据迁移模块

4、helloworld编辑views.py，修改为：
from django.shortcuts import renderfrom django.http import HttpResponse# Create your views here.def hello(request):    return HttpResponse(&#x27;helloworld&#x27;)

5、为了访问到views.py，我们要配置下路由，修改urls.py为：
from blog.views import hellourlpatterns = [    url(r&#x27;^admin/&#x27;, admin.site.urls),    url(r&#x27;helloworld&#x27;, hello),]

6、测试访问启动django，访问 http://localhost:8000/helloworld ，即可看到helloworld。
6. 路由进阶1、修改djsite/urls.py为：
from django.conf.urls import url,includefrom django.contrib import adminurlpatterns = [    url(r&#x27;^admin/&#x27;, admin.site.urls),    url(r&#x27;^blog/&#x27;, include(&#x27;blog.urls&#x27;, namespace=&quot;blog&quot;)),]

2、在blog中添加urls.py为：
from django.conf.urls import urlfrom . import viewsurlpatterns = [    # url(r&#x27;^$&#x27;, views.index, name=&#x27;index&#x27;),    url(r&#x27;helloworld&#x27;, views.hello, name=&#x27;hello&#x27;)]

3、测试访问启动django，访问 http://localhost:8000/blog/helloworld ，即可看到helloworld。
7. 源码分享https://github.com/voidking/djsite/releases/tag/v0.0.0
8. 书签django初体检
pycharm 教程（一）安装和首次使用
Django URL管理
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>django</tag>
        <tag>好好学Python</tag>
        <tag>pycharm</tag>
      </tags>
  </entry>
  <entry>
    <title>Dubbo入门篇</title>
    <url>/dev-dubbo-start/</url>
    <content><![CDATA[1. Dubbo简介Dubbo是阿里巴巴公司开源的一个高性能的分布式服务框架，使得应用可通过高性能的RPC实现服务的输出和输入功能，可以和Spring框架无缝集成。
RPC（Remote Procedure Call）——远程过程调用，它是一种通过网络从远程计算机程序上请求服务，而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在，如TCP或UDP，为通信程序之间携带信息数据。在OSI网络通信模型中，RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
参考文档：

Dubbo官网
Dubbo官方文档
基于Dubbo框架构建分布式服务
Dubbo架构设计详解
Dubbo入门—搭建一个最简单的Demo框架
Dubbo源码学习（一）之dubbo-demo
pom.xml 缺少commons-pool依赖，导致编译失败
基于Dubbo的Hessian协议实现远程调用
Dubbo实现RPC调用使用入门
基于dubbo框架下的RPC通讯协议性能测试



2. 服务调用流程
1、服务容器负责启动，加载，运行服务提供者。2、服务提供者在启动时，向注册中心注册自己提供的服务。3、服务消费者在启动时，向注册中心订阅自己所需的服务。4、注册中心返回服务提供者地址列表给消费者，如果有变更，注册中心将基于长连接推送变更数据给消费者。5、服务消费者，从提供者地址列表中，基于软负载均衡算法，选一台提供者进行调用，如果调用失败，再选另一台调用。6、服务消费者和提供者，在内存中累计调用次数和调用时间，定时每分钟发送一次统计数据到监控中心
3. Dubbo注册中心Dubbo提供的注册中心有如下几种类型可供选择：

Multicast注册中心
Zookeeper注册中心
Redis注册中心
Simple注册中心

本文中，我们选择使用Zookeeper作为注册中心，Zookeeper安装配置方法参考文档《Zookeeper入门篇》。
4. dubbo-admindubbo的使用，其实只需要有注册中心、提供者、消费者这三个就可以使用了，但是并不能看到有哪些消费者和提供者，为了更好的调试，发现问题，解决问题，因此引入dubbo-admin。通过dubbo-admin可以对消费者和提供者进行管理。
1、源码下载
git clone https://github.com/alibaba/dubbo.git

这份源码，不止包含dubbo-admin，还有很多其他代码，比如dubbo-demo、dubbo-config等。
2、编译dubbo-admin进入dubbo-admin目录，打开命令行，执行：
mvn package -Dmaven.skip.test=true

即可在当前目录下生成target文件夹，文件夹中的war包就是我们需要的文件。
3、部署dubbo-admin把war包放到tomcat的webapps目录下，重命名为dubbo-admin.war。
4、启动tomcat进入tomcat的bin目录，然后双击startup.bat。
5、测试访问tomcat会自动解压war包，这时访问 http://localhost:8080/dubbo-admin ，即可看到部署后的效果。
6、用户名密码页面提示输入用户名密码，默认用户名密码都是root。用户名密码配置路径：tomcat/webapps/dubbo-admin/WEB-INF/dubbo.properties
5. dubbo-demo在上面下载的源码中，包含dubbo-demo。
1、打开dubbo文件夹，构建源码，生成IDEA支持的项目文件
mvn clean install -Dmaven.test.skipmvn idea:idea

编译时，报错：Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project dubbo-demo-provider: Compilation failure -&gt; [Help 1]
改maven配置，失败。更换maven版本，失败。怒删C:\Users\voidking\.m2\repository中的所有文件，成功。
2、IDEA打开生成的dubbo-parent.ipr文件，就可以看到整个dubbo的工程了。
3、运行Provider.java报错：Error:Maven Resources Compiler: Maven project configuration required for module ‘hessian-lite’ isn’t available. Compilation of Maven projects is supported only if external build is started from an IDE.
打开窗口右侧Maven Project，点击加号选择dubbo根目录下的pom.xml，再次运行Provider.java。
如果没有报错，则说明Provider.java运行成功，它会保持在运行状态。
4、运行Consumer.java运行成功的话，可以看到Consumer控制台打印出 Hello world, response form provider: 169.254.36.242:20880，同时，Provider控制台打印出Hello world, request from consumer: /169.254.36.242:54924。
5、打开dubbo-admin，却看不到提供者。这是因为我们在运行Provider的时候，并没有指定Zookeeper作为注册中心。
6、修改dubbo-demo-provider.xml和dubbo-demo-consumer.xml中的registry。
&lt;!-- 使用multicast广播注册中心暴露服务地址 --&gt;&lt;!--&lt;dubbo:registry address=&quot;multicast://224.5.6.7:1234&quot;/&gt;--&gt;&lt;dubbo:registry address=&quot;zookeeper://localhost:2181&quot;/&gt;

7、重新运行Provider.java，即可看到提供者。重新运行Consumer.java，却看不到消费者，这是因为消费者消费后把socket关闭了。修改Consumer.java，添加System.in.read();并且抛出异常，重新运行即可在dubbo-admin中看到消费者。
6. 小结至此，dubbo的最简单demo运行成功。dubbo支持多种协议，通过协议，可以实现不同语言间的相互调用。接下来，需要研究python调用dubbo服务。待续。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>zookeeper</tag>
        <tag>dubbo</tag>
      </tags>
  </entry>
  <entry>
    <title>Zookeeper入门篇</title>
    <url>/dev-zookeeper-start/</url>
    <content><![CDATA[1. Zookeeper简介
Official Introduction: ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services. All of these kinds of services are used in some form or another by distributed applications. Each time they are implemented there is a lot of work that goes into fixing the bugs and race conditions that are inevitable. Because of the difficulty of implementing these kinds of services, applications initially usually skimp on them, which make them brittle in the presence of change and difficult to manage. Even when done correctly, different implementations of these services lead to management complexity when the applications are deployed.

ZooKeeper是一个分布式应用程序协调服务，它提供了分布式锁、配置管理、命名服务、集群管理等功能。它的设计目标是提供一个高度可靠的分布式协调服务，使得分布式应用程序可以在不同节点之间共享信息和协同工作。ZooKeeper最初由雅虎研究院开发，并在Apache许可证下发布，现在已经成为一个Apache软件基金会的开源项目。
ZooKeeper的核心是一个具有高可用性的分布式数据存储系统，它可以存储和检索分布式应用程序的元数据、配置信息、状态等。ZooKeeper的数据存储采用内存映射文件方式，支持快速读写和高并发性。
参考文档：

What is ZooKeeper?
zookeeper介绍及运维实践



2. ZooKeeper的基本概念节点：ZooKeeper中的数据存储单元，可以是文件或目录。每个节点都有一个路径和一个数据内容，可以用来存储元数据、配置信息等。
版本：每个节点都有一个版本号，用于检测数据变更。
会话：客户端与ZooKeeper服务器之间的连接，用于实现数据的读写和协调工作。会话可以设置超时时间，用于控制会话的生命周期。
观察者：客户端可以注册一个观察器，用于监听节点的变化，并在节点变化时得到通知。
3. ZooKeeper的应用场景分布式锁：ZooKeeper可以提供分布式锁的实现，使得多个应用程序可以在不同节点之间协同工作。
配置管理：ZooKeeper可以用来存储和检索分布式应用程序的配置信息，使得多个应用程序可以共享配置信息。
命名服务：ZooKeeper可以用来存储和检索分布式应用程序的命名空间，使得多个应用程序可以共享命名空间。
集群管理：ZooKeeper可以用来管理分布式应用程序的集群，包括节点的加入、退出、状态检测等。
4. 安装配置Zookeeper4.1. 安装1、访问zookeeper官网，下载一个喜欢的zookeeper版本，这里郝同学下载的是zookeeper-3.4.10.tar.gz。
2、解压zookeeper-3.4.10.tar.gz到D:\Server目录。
4.2. 配置1、zookeeper 的配置文件在 conf 目录下，有 zoo_sample.cfg 和 log4j.properties。
2、将zoo_sample.cfg复制一份，重命名成zoo.cfg，因为zookeeper在启动时会找这个文件作为默认配置文件。下面介绍zoo.cfg文件里面几个配置的意义：

tickTime：这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔，也就是每个 tickTime 时间就会发送一个心跳。 
dataDir：顾名思义就是 Zookeeper 保存数据的目录，默认情况下，Zookeeper 将写数据的日志文件也保存在这个目录里。 
clientPort：这个端口就是客户端连接 Zookeeper 服务器的端口，Zookeeper 会监听这个端口，接受客户端的访问请求。

3、在zookeeper的根目录下，新建文件夹data。然后修改配置文件dataDir为：
dataDir=D:\\Server\\zookeeper-3.4.10\\data

4.3. 启动1、zookeeper-3.4.10/bin下存在zkServer.cmd，双击它，即可启动zookeeper。
2、打开命令行，输入jsp，如果看到如下提示，说明启动成功。
10336 RemoteMavenServer13488 Jps12268 QuorumPeerMain13052

5. 使用Zookeeper5.1. PrettyZoo使用PrettyZoo可以图形化连接Zookeeper，直观操作Zookeeper。
参考文档：vran-dev/PrettyZoo
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>chatgpt</tag>
        <tag>java</tag>
        <tag>zookeeper</tag>
      </tags>
  </entry>
  <entry>
    <title>Android终端模拟器Termux</title>
    <url>/dev-termux/</url>
    <content><![CDATA[1. termux简介termux是个非常强大的终端模拟器，能够在android上模拟linux终端。
本文中，郝同学会在自己的Honor9上尝试安装使用termux。
相关文档：

github - termux/termux-app
F-Droid Termux
The Termux Wiki
Touch Keyboard
Run an SSH server on your Android with Termux
Termux 設定 SSH Server
神器Termux的使用日常



2. 安装termux2.1. 从google play安装（已废弃）1、安装google play2、google play中搜索 termux3、下载安装termux
2.2. 从github安装1、访问termux-app/releases2、选择apk安装包，这里选择 github-debug_x86_64.apk 版本3、下载安装即可
3. termux更换软件源新版本termux（v0.118.3+）：
termux-change-repo

老版本termux：
sed -i &#x27;s@^\(deb.*stable main\)$@#\1\ndeb https://mirrors.tuna.tsinghua.edu.cn/termux/termux-packages-24 stable main@&#x27; $PREFIX/etc/apt/sources.listsed -i &#x27;s@^\(deb.*games stable\)$@#\1\ndeb https://mirrors.tuna.tsinghua.edu.cn/termux/game-packages-24 games stable@&#x27; $PREFIX/etc/apt/sources.list.d/game.listsed -i &#x27;s@^\(deb.*science stable\)$@#\1\ndeb https://mirrors.tuna.tsinghua.edu.cn/termux/science-packages-24 science stable@&#x27; $PREFIX/etc/apt/sources.list.d/science.listpkg update

4. 主机ssh访问手机4.1. termux安装sshdtermux默认不包含sshd和ssh，需要自行安装。
1、安装openssh
pkg install openssh

2、启动sshd
sshd

sshd 默认端口为 8022。
PS：关闭sshd
pkill sshdkillall sshd

3、保持sshd运行下拉手机顶部状态栏，看到termux，点击“ACQUIRE WAKELOCK”，即可看到1 session(wake lock help)。此时，termux就可以保持后台运行，sshd也不会关闭。
4.2. ssh远程访问手机1、查看手机ip
ifconfig

2、查看用户名，修改密码
whoamipasswd

3、ssh远程访问手机假设手机IP为 192.168.56.102，用户名为 u0_a285
ssh u0_a285@192.168.56.102 -p 8022

5. 手机ssh访问主机手机上ssh访问主机，与主机上ssh访问主机并没有差别，具体用法参考文档《好好学Linux：ssh命令》。
6. 挂载手机存储打开termux终端，默认路径为 /data/data/com.termux/files/home ，但是这个路径并不是安卓系统的真实路径。直接cd到根目录，也没有权限查看文件。如果想要访问安卓系统中的文件，必须先对安卓系统的磁盘进行挂载。
挂载方法为执行命令 termux-setup-storage ，执行之后会提示授权termux访问文件系统的权限，选择同意，然后home目录下会出现storage目录，storage目录中就是安卓系统的文件了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
        <tag>termux</tag>
      </tags>
  </entry>
  <entry>
    <title>英中机器文本翻译-第2章</title>
    <url>/dev-en-to-zh-translation-2/</url>
    <content><![CDATA[1. 前言tensorflow/nmt项目，并不能直接在ucloud的AI训练服务上进行训练，我们需要对代码进行一些修改。


2. 差异分析2.1. 路径上文中，在AI训练服务上运行的mnist，它的输入输入都是到UFile上。而nmt项目，在运行时指定了训练文件的路径。
2.2. tensorflowmnist项目，使用的是tensorflow1.1.0。nmt项目，使用的是tensorflow1.3.0。
3. 路径对于路径差异，需要修改nmt的运行命令和代码。
3.1. 运行命令nmt项目的运行时的命令为：
python -m nmt.nmt --src=vi --tgt=en --vocab_prefix=./tmp/nmt_data/vocab  --train_prefix=./tmp/nmt_data/train --dev_prefix=./tmp/nmt_data/tst2012  --test_prefix=./tmp/nmt_data/tst2013 --out_dir=./tmp/nmt_model --num_train_steps=12000 --steps_per_stats=100 --num_layers=2 --num_units=128 --dropout=0.2 --metrics=bleu

其中，关于路径的参数有：–vocab_prefix–train_prefix–dev_prefix–test_prefix–out_dir
前四个路径是输入；最后一个路径是输出。
最后的命令要改成：
python -m nmt.nmt --src=vi --tgt=en --vocab_prefix=vocab  --train_prefix=train --dev_prefix=tst2012  --test_prefix=tst2013 --num_train_steps=12000 --steps_per_stats=100 --num_layers=2 --num_units=128 --dropout=0.2 --metrics=bleu

对于输入路径来说，去掉了./tmp/nmt_data/，默认为FLAGS.data_dir，也就是创建AI训练服务时输入的数据输入路径。
对于输出路径来说，去掉了./tmp/mnt_model，默认为FLAGS.output_dir，也就是创建AI训练服务时输入的数据输出路径。
3.2. UFile1、移动nmt_data目录到/data/nmt。
mkdir -p /data/nmtmv ~/tmp/nmt_data /data/nmt

2、上传/data/nmt到UFile。./filemgr --action mput --dir /data/nmt --bucket vk-ucloud --prefix nmt --trimpath /data/nmt
train.en文件地址如下，其他文件类似：CDN加速地址：http://vk-ucloud.ufile.ucloud.com.cn/nmt/nmt_data/train.en源站地址：http://vk-ucloud.cn-bj.ufileos.com/nmt/nmt_data/train.en
3、python tf_tool.py pack时，指定本地测试数据地址如下。
--test_data_path=/data/nmt/nmt_data --test_output_path=/data/nmt/nmt_model

3.3. 代码4. tensorflow5. 后记未完。。。不再续。。。
6. 书签全球AI挑战赛-英中机器文本翻译
谷歌开放GNMT教程：如何使用TensorFlow构建自己的神经机器翻译系统
TensorFlow 训练镜像自定义包安装
MNIST开发案例
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>机器学习</tag>
        <tag>tensorflow</tag>
      </tags>
  </entry>
  <entry>
    <title>英中机器文本翻译-第1章</title>
    <url>/dev-en-to-zh-translation-1/</url>
    <content><![CDATA[1. UCloudUCloud是基础云计算服务提供商，与全球AI挑战赛合作，给参赛者提供免费使用GPU的时间。本以为和阿里云类似，新建个主机，搭建环境，然后跑代码。没想到平台限制不能创建主机，只能使用AI训练服务。很麻烦的样子，花点时间研究下。


2. 环境准备
阿里云centos7.2
python2.7.5（默认安装版本）
ucloud实名认证通过

3. Uhub1、登录ucloud，左侧导航栏点击公共镜像库，进入uhub产品页面。
2、创建镜像库（Docker Registry），郝同学这里命名为vk_ucloud。
3、docker登录uhubdocker login uhub.ucloud.cn
4. UAI Train工具包UAI-Train为用户提供了镜像打包工具，用户只需将所需代码文件放在某一路径下，执行打包命令即可以生成UAI-Train所需的镜像。该打包工具将在本地docker中生成两个镜像以及运行镜像的指令说明文件uaitrain_cmd.txt。生成的镜像包括cpu和gpu两个版本，其中gpu版本的镜像会自动上传至用户的Uhub镜像仓库。两个版本的镜像均可以用于本地测试，测试命令可在uaitrain_cmd.txt中查询。
4.1. UIA Train基础镜像UCloud 提供了两个UAI Train系统使用的基础镜像（范例基于tensorflow-1.1.0版本，如果想要使用 tensorflow-1.2.0，可以直接将 1.1.0 替换成1.2.0）
1、下载gpu版本docker pull uhub.ucloud.cn/uaishare/gpu_uaitrain_ubuntu-14.04_python-2.7.6_tensorflow-1.1.0:v1.0
2、下载cpu版本docker pull uhub.ucloud.cn/uaishare/cpu_uaitrain_ubuntu-14.04_python-2.7.6_tensorflow-1.1.0:v1.0
4.2. UAI SDK安装最新版本的UAI SDK和docker支持，安装UAI SDK的方法如下：
git clone https://github.com/ucloud/uai-sdkcd uai-sdksudo python setup.py install
安装Docker的方法请参见：Docker使用指南
4.3. 工具目录找到UAI-Train TensorFlow操作工具所在目录
$ls ~/uai-sdk/uaitrain_tool/tftf_tool.py

4.4. 统一路径将AI训练任务所需的代码放在统一路径下，打包时将其相对路径作为参数code_path上传
例如，我们要将~/uai-sdk/examples/tensorflow/train/mnist_summary_1.1下面的训练代码进行打包，该文件路径结构如下：
$ cd ~/uai-sdk/examples/tensorflow/train/mnist_summary_1.1$ lsdata  code
我们需要做如下准备工作：
1、准备好训练的代码，案例中训练代码在mnist_summary_1.1/code下，名为mnist_summary.py
2、将tf_tool.py 工具放入和训练代码目录同级的目录下，即mnist_summary_1.1/ 目录下
cd uai-sdkcp ./uaitrain_tool/tf/tf_tool.py ./examples/tensorflow/train/mnist_summary_1.1/

3、Ready To Pack
4.5. 打包1、进入mnist_summary_1.1目录
cd ~/uai-sdk/examples/tensorflow/train/mnist_summary_1.1/

2、打包命令
sudo python tf_tool.py pack [-h] --public_key PUBLIC_KEY                         --private_key PRIVATE_KEY                         [--project_id PROJECT_ID]                         --code_path CODE_PATH                         --mainfile_path MAINFILE_PATH                        --uhub_username UHUB_USERNAME                        --uhub_password UHUB_PASSWORD                         --uhub_registry UHUB_REGISTRY                        --uhub_imagename UHUB_IMAGENAME                        [--uhub_imagetag UHUB_IMAGETAG]                        [--internal_uhub False/True]                        --ai_arch_v AI_ARCH_V                        --test_data_path TEST_DATA_PATH                        --test_output_path TEST_OUTPUT_PATH                        --train_params TRAIN_PARAMS                        [--python_version PYTHON_VERSION]                        [--os OS_VERSION]

每个参数的具体解释，访问TensorFlow训练镜像打包查看。
3、打包样例使用mnist_summary_1.1中的训练程序为案例。test_data_path和test_data_path不要求一定在训练代码路径下，如我们可以在/data/test目录下创建了两个子目录：

/data/test/data 用于存放训练数据，此时test_data_path值为/data/test/data
/data/test/output 用于存放训练输出数据，此时test_output_path为/data/test/output

train_params为训练代码中使用到的任意训练参数，本例中为”–max_step=2000”使用命令时，需要使用sudo，保证docker镜像打包命令有足够权限。
注：我们可以将~/uai-sdk/examples/tensorflow/train/mnist_summary_1.1/data/下的测试数据放入/data/test/data/目录下。
sudo python tf_tool.py pack \                        --public_key=&lt;YOUR_PUBLIC_KEY&gt; \            --private_key=&lt;YOUR_PRIVATE_KEY&gt; \            --code_path=./code/ \            --mainfile_path=mnist_summary.py \            --uhub_username=&lt;YOUR_UHUB_USER_NAME&gt; \            --uhub_password=&lt;YOUR_UHUB_PASSWORD&gt; \            --uhub_registry=&lt;YOUR_UHUB_REFDISTRY&gt; \            --uhub_imagename=&lt;YOUR_UHUB_IMAGENAME&gt; \                        --internal_uhub=True \            --ai_arch_v=tensorflow-1.1.0 \            --test_data_path=/data/test/data \            --test_output_path=/data/test/output \            --train_params=&quot;--max_step=2000&quot; \


UCloud API的公钥和私钥，可以在左上角“产品与服务”，“API密钥 UAPI”中找到。
uhub_username和uhub_password是UCloud的账号和密码。
uhub_registry填入vk_ucloud。
ubub_imagename填入mnist_test。

具体命令为：
mkdir -p /data/test/datacp -r ~/uai-sdk/examples/tensorflow/train/mnist_summary_1.1/data/ /data/test/cd ~/uai-sdk/examples/tensorflow/train/mnist_summary_1.1

sudo python tf_tool.py pack --public_key=****** --private_key=****** --code_path=./code/ --mainfile_path=mnist_summary.py --uhub_username=voidking@qq.com --uhub_password=****** --uhub_registry=vk_ucloud --uhub_imagename=mnist_test --internal_uhub=True --ai_arch_v=tensorflow-1.1.0 --test_data_path=/data/test/data --test_output_path=/data/test/output --train_params=&quot;--max_step=2000&quot;

如果执行上诉命令时报错：
Docker login on uhub.service.ucloud.cnError response from daemon: Get https://uhub.service.ucloud.cn/v2/: x509: certificate has expired or is not yet valid

那么请删除掉--internal_uhub=True这一句，因为我们没有使用UCloud内网，而是使用的阿里云。切记，是删除，而不是改成--internal_uhub=False。这是解决错误失败，请教UCloud客服才得到的结论。在此感谢下客服小哥，很耐心帮忙排查问题。
4.6. 输出说明成功执行后，界面显示样例如下，会给出部署时所需的CMD命令以及本地测试的cmd命令:
CMD Used for deploying: /data/mnist_summary.py --max_step=2000CMD for CPU local test: sudo docker run -it -v /data/test/data:/data/data -v /data/test/output:/data/output mnist_test-cpu:uaitrain /bin/bash -c &quot;cd /data &amp;&amp; /usr/bin/python /data/mnist_summary.py --max_step=2000 --work_dir=/data --data_dir=/data/data --output_dir=/data/output --log_dir=/data/output&quot;CMD for GPU local test: sudo nvidia-docker run -it -v /data/test/data:/data/data -v /data/test/output:/data/output uhub.ucloud.cn/vk_ucloud/mnist_test:uaitrain /bin/bash -c &quot;cd /data &amp;&amp; /usr/bin/python /data/mnist_summary.py --max_step=2000 --num_gpus=1 --work_dir=/data --data_dir=/data/data --output_dir=/data/output --log_dir=/data/output&quot;You can check these cmd later in file: uaitrain_cmd.txt


CMD Used for deploying: 该输出的内容为创建训练任务时，训练启动命令框中需要填写的内容(参见创建训练任务)。可以直接复制黏贴到命令框中。
CMD for CPU local test: 该输出的内容为本地通过CPU来测试训练能否正常执行。在本地没有GPU的情况下可以使用该命令测试训练代码能否正常执行。
CMD for GPU local test：该输出的内容为本地通过GPU来测试训练能否正常执行。在本地有GPU的情况下可以使用该命令测试训练代码能否正常执行。（注：在使用前请确认GPU驱动已经安装，并已经安装了nvidia-docker，详细安装方法请参见Docker使用指南）

本地文件夹下生成了uaitrain_cmd.txt、uaiservice.log、uaitrain-cpu.Dockerfile和uaitrain.Dockerfile，其中uaitrain_cmd.txt内容和标准输出的内容一致，防止用户丢失屏幕输出内容。
docker images，在本地镜像仓库可以看到生成了两个docker镜像，分别为cpu版本和gpu版本。如下：
uhub.ucloud.cn/vk_ucloud/mnist_testmnist_test-cpu

其中gpu版本，会自动上传的到ucloud的uhub。
4.7. 自定义软件包安装如果训练代码依赖特殊的软件包，例如nltk 等，可以通过Docker 打包的形式将软件包和相关数据打包入训练的Docker镜像，详细方法参见TensorFlow 训练镜像自定义包安装。
5. UFile5.1. 创建存储空间1、登录ucloud，左侧导航栏点击对象存储，进入ufile产品页面。
2、创建存储空间，郝同学这里存储空间域名为vk-ucloud，空间类型为公有空间。创建成功会提示：我们为您同步创建了CDN加速域名，稍后您可在列表查看域名信息并进行文件访问操作。接下来您可通过以下三种方式进行空间内容管理，包括文件上传下载等。API/SDK，详细使用说明；客户端管理工具，详细使用说明。
3、之后的文件上传等操作，可以直接在网页完成，也可以使用ucloud提供的空间管理器和文件管理器。
5.2. 准备管理器1、下载空间管理器和文件管理器
mkdir -p /opt/ufilecd /opt/ufilewget http://tools.ufile.ucloud.com.cn/bucketmgr-linux64.tar.gzwget http://tools.ufile.ucloud.com.cn/filemgr-linux64.tar.gztar -xzvf bucketmgr-linux64.tar.gz tar -xzvf filemgr-linux64.tar.gzrm -rf bucketmgr-linux64.tar.gzrm -rf filemgr-linux64.tar.gzmv bucketmgr_linux64.elf/* ./ mv filemgr-linux64.elf/* ./rm -rf bucketmgr_linux64.elfrm -rf filemgr-linux64.elfmv bucketmgr-linux64 bucketmgrmv filemgr-linux64 filemgr

2、配置vim config.cfg
&#123;    &quot;public_key&quot; : &quot;paste your public key here&quot;,    &quot;private_key&quot; : &quot;paste your private key here&quot;,    &quot;proxy_host&quot; : &quot;www.cn-bj.ufileos.com&quot;,    &quot;api_host&quot; : &quot;api.spark.ucloud.cn&quot;&#125;

其中，public_key和private_key可以在“API密钥”中获得。不同地域的proxy_host是不一样的，具体如下:
北京外网：www.cn-bj.ufileos.com B机房内网：www.ufile.cn-north-02.ucloud.cnC机房内网: www.ufile.cn-north-03.ucloud.cnD机房内网: www.ufile.cn-north-04.ucloud.cn上海二外网: www.cn-sh2.ufileos.com内网: www.internal-cn-sh2-01.ufileos.com香港外网：www.hk.ufileos.com内网：www.internal-hk-01.ufileos.com广东外网：www.cn-gd.ufileos.com内网：www.internal-cn-gd-02.ufileos.com美国外网：www.us-ca.ufileos.com内网：www.internal-us-ca-01.ufileos.com

5.3. 空间管理命令5.3.1. 创建存储空间创建存储空间建议使用控制台。./bucketmgr --action CreateBucket --bucket bucketname --type public
参数说明:–bucket: 需要创建的存储空间域名–type: 需要创建的空间类型，公开空间（public）或私有空间（private）
5.3.2. 删除存储空间删除存储空间建议使用控制台。
./bucketmgr --action DeleteBucket --bucket bucketname
参数说明:–bucket: 需要删除的存储空间域名
5.3.3. 获取空间信息./bucketmgr --action DescribeBucket参数说明:–bucket: 需要查询的存储空间域名
5.3.4. 获取文件列表./bucketmgr --action GetFileList --bucket bucketname参数说明:–bucket: 需要拉取列表的存储空间域名–offset: 查询起始的文件编号（默认为0）–limit : 需要查询的记录数（默认为100）
5.4. 文件管理器文件管理工具可帮助用户进行存储空间内文件的管理操作，包括：

上传单个文件：普通上传（PUT），秒传（UPLOAD-HIT），分片上传（MPUT），表单方式上传。

上传文件夹：普通上传（PUT），分片上传（MPUT），增量上传（SYNC）。

下载文件/文件夹

删除文件/文件夹


5.4.1. 上传文件(单个文件)./filemgr --action put --bucket bucketname --key key --file filename
参数说明:–bucket: 需要上传至的存储空间–key : 上传至存储空间中的文件名–file : 需要上传的本地文件路径
示例:把一个本地文件 ucloud.jpg 上传至 bucket 名称为 pics 的存储空间中，并命名为 logo.jpg
./filemgr --action put --bucket pics --key logo.jpg --file /home/yours/pictures/ucloud.jpg
5.4.2. 上传文件夹./filemgr --action put --bucket bucketname --dir dirname
参数说明:–bucket: 需要上传至的存储空间域名–dir : 需要上传的本地文件夹–prefix: 生成文件的Key时使用的前缀，指定该参数时生成的Key是 prefix+base（filename）

用文件夹方式上传的文件默认会使用文件夹内文件所在的绝对路径来命名Key，如果想指定特殊的前缀请使用–prefix参数。

示例1:
将 ~/files 这个文件夹下的所有文件上传至名为 demobucket 的存储空间中，并且 key 的名称使用 demo/ 作为前缀，如果该文件夹下有一个名为1.jpg 的文件并且空间属性为public，则上传完成后可以通过 http://demobucket.ufile.ucloud.com.cn/demo/1.jpg 访问该文件
./filemgr --action put --dir ~/files --bucket demobucket --prefix demo/
如果您不希望使用绝对路径来作为Key，可以使用–trimpath截掉部分路径名。
例如：./filemgr --action put --dir ~/files --bucket demobucket --prefix demo/ --trimpath /root/test
示例2:假设有个目录名为 /some/dir/cutoff/files/，下面有 a.jpg、b.txt两个文件，则以下的处理会使得保存在 UFILE 的文件名为 files/a.jpg、files/b.txt。./filemgr --action put --dir ~/files --bucket demobucket --trimpath /some/dir/cutoff/
5.4.3. 分片上传分片上传允许在某个分片失败情况下进行续传，适合较大文件的场景。
./filemgr --action mput --bucket demobucket --key key --file filename
参数说明:–bucket : 需要上传至的 bucket 名称–key : 上传至 bucket 中的文件名称–file : 需要上传的本地文件路径–part : 需要进行重传的分片索引(注意重传的分片必须是之前上传失败的分片，已经成功的分片不能重传，否则最终 finish 会失败)–uploadid: 分片上传初始化返回的上传 ID–etags : 续传时需要把之前已经成功上传的分片的 ETag 以”,”分隔传递给服务端
示例:以分片上传方式上传一个本地文件 hello.avi 至名为 demobucket 的存储空间中并且命名为 world.avi
./filemgr --action mput --bucket demobucket --key world.avi --file /opt/video/hello.avi
上传失败的话，重新执行命令即可继续上传。
5.4.4. 下载文件./filemgr --action download --bucket demobucket --key key --file filename参数说明:–bucket : 需要下载的文件所在的 bucket 名称–key : 需要下载的文件 key 名称–file : 需要保存在本地的文件路径–showurl: 仅获取下载 URL，不下载实际数据–expires: 获取下载 URL 时指定过期时间–queryauth: 在 URL 的 query 中携带签名信息
示例:获取一个名为 demobucket 的存储空间中一个名为 QQ.pkg 的文件在5分钟内的有效下载URL。
./filemgr --action download --showurl --queryauth --expires 300 --bucket demobucket --key QQ.pkg
获取一个名为 demobucket 的存储空间中一个名为 QQ.pkg 的文件并且保存为/opt/data/QQ.pkg./filemgr --action download --bucket demobucket --key QQ.pkg --file /opt/data/QQ.pkg
5.4.5. 删除文件./filemgr --action delete --bucket demobucket --key key参数说明:–bucket: 需要删除的文件所在的 bucket 名称–key : 需要删除的文件在 bucket 中的名称
示例:删除一个名为 demobucket 的存储空间中名为 20140201.blog 的文件
./filemgr --action delete --bucket demobucket --key 20140201.blog
5.4.6. 秒传文件./filemgr --action upload-hit --bucket demobucket --key key --file filename参数说明:–bucket: 需要上传至的 bucket 名称–key : 上传至 bucket 中的文件名称–file : 需要上传的本地文件路径
示例:尝试秒传本地文件 falcon.avi 至存储空间 nfl2014 命名为 2014-superbow.avi
./filemgr --action upload-hit --bucket demobucket --key 2014-superbow.avi --file falcon.avi
5.4.7. 计算文件ETag该选项用于计算使用ufile特殊算法得到的文件哈希值
./filemgr --action etag --file filename参数说明:–file: 需要计算哈希的本地文件路径
示例:计算本地文件 sniff-the-rose.pdf 的 ETag./filemgr --action etag --file /opt/tiger/sniff-the-rose.pdf
5.4.8. 增量上传增量上传仅上传新增或有发生修改的整个文件。
./filemgr --action sync --bucket demobucket --dir syncdir [--speedlimit speedlimit] [--prefix prefix] [--excludeptn]
参数说明:–bucket : 需要同步至远端的 bucket 名称–dir    : 需要同步的本地文件夹–speedlimit : 上传限速(byte/s)–prefix : 生成文件的 key 时使用的前缀–excludeptn: 需要排除上传的文件模式，支持 POSIX 正则表达式
示例:把本地文件夹~/go 同步到名为 demobucket 的存储空间中
./filemgr --action sync --bucket demobucket --dir ~/go
继续检查是否还有文件需要更新./filemgr --action sync --bucket demobucket --dir ~/go
5.5. 上传mnist数据集1、上传mnist数据集./filemgr --action mput --dir /data/test/data --bucket vk-ucloud --prefix mnist/data --trimpath /data/test/data
2、查看上传的文件
./bucketmgr --action GetFileList --bucket vk-ucloud例如，看到的文件为：
&#123;    BucketName:   vk-ucloud    Key:          mnist/data/t10k-images-idx3-ubyte.gz    Hash:         AQAAAMOiWvH1La1_cmzOjKyxOGVLdg1I@session_0ade634b-8be9-4537-8f73-b900155aae20    MimeType:     application/x-gzip    Size:         1648877    Created:      1508560491    Modified:     1508560491&#125;

那么，CDN加速地址为：http://vk-ucloud.ufile.ucloud.com.cn/mnist/data/t10k-images-idx3-ubyte.gz ，源站地址为： http://vk-ucloud.cn-bj.ufileos.com/mnist/data/t10k-images-idx3-ubyte.gz
6. AI训练服务配置1、访问UCloud控制台，在左侧导航栏中找到AI训练服务，然后点击“创建AI训练任务”。
2、按照提示，填入需要的信息。UCloud API的公钥和私钥，可以在左上角“产品与服务”，“API密钥 UAPI”中找到。代码镜像库选择用户镜像库，镜像选择我们自己创建的mnist_test:uaitrain。数据输入路径为：http://vk-ucloud.cn-bj.ufileos.com/mnist/data/数据输出路径为：http://vk-ucloud.cn-bj.ufileos.com/mnist/output/训练时长为：6
3、点击右侧确定，创建任务完成。
4、选中任务，点击开始，即可开始训练。点击详情，可以查看到训练过程中的日志。点击tenserboard，看到训练过程中的精度变化等。
5、在tensorboard看到精度到达1后，即可停止训练。这时，http://vk-ucloud.cn-bj.ufileos.com/mnist/output/ 文件夹下，会出现训练结果。
可以使用命令行查看，也可以在web面板查看。
7. 书签全球AI挑战赛-英中机器文本翻译
谷歌开放GNMT教程：如何使用TensorFlow构建自己的神经机器翻译系统
UCloud文档中心
全球AI挑战赛UCloud参考资料
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>机器学习</tag>
        <tag>tensorflow</tag>
      </tags>
  </entry>
  <entry>
    <title>英中机器文本翻译-第0章</title>
    <url>/dev-en-to-zh-translation-0/</url>
    <content><![CDATA[1. 机器翻译机器翻译，即跨语言间的自动翻译，是机器学习社区最活跃的研究领域。在机器翻译的众多方法中，序列到序列（sequence-to-sequence，seq2seq）模型近期获得巨大成功。由于其可以使用深度神经网络获取句义，该模型成为谷歌翻译等多数商业翻译系统事实上的标准模型。
小生以参加《全球AI挑战赛-英中机器文本翻译》为导向，学习各种相关知识。


2. 赛题简介英中文本机器翻译竞赛的目标是评测英中文本机器翻译的能力。本次文本机器翻译语言方向为英文到中文。参赛队伍需要根据评测方提供的数据，训练机器翻译系统，并且自由地选择机器翻译技术。例如，基于规则的翻译技术、基于实例的翻译技术、统计机器翻译及神经网络机器翻译技术等。
本次竞赛将利用机器翻译的客观考核指标（BLEU、NIST score、TER）进行评分，BLEU得分会作为主要的机器评价指标。组委会将通过客观指标，并结合答辩表现，综合评估参赛者的算法模型。
3. 环境准备3.1. 软件安装
win10
python3.6
tensorflow1.3.0

3.2. 源码安装配置git clone https://github.com/tensorflow/nmt/
因为tensorflow的更新很快，nmt项目的更新也很快，所以，我们必须使用和tensorflow版本对应的nmt版本。git reset --hard 77e6c55
4. 入门demo让我们开始训练第一个 NMT 模型，将越南语翻译为英语。代码的入口是nmt.py。
4.1. 数据集我们将使用小规模的 Ted 演讲并行语料库（133k 的训练样本）进行训练。所有的数据都可从以下链接找到：https://nlp.stanford.edu/projects/nmt/ 。
我们将使用tst2012作为开发数据集，tst2013作为测试数据集。在linux下运行以下命令行下载数据：mkdir -p /tmp/nmt_data
nmt/scripts/download_iwslt15.sh /tmp/nmt_data
linux下载好数据集后，把数据集下载到win10中nmt目录中，路径为nmt/tmp/nmt_data。
4.2. 训练model在根目录（nmt目录）下运行：
python -m nmt.nmt --src=vi --tgt=en --vocab_prefix=./tmp/nmt_data/vocab  --train_prefix=./tmp/nmt_data/train --dev_prefix=./tmp/nmt_data/tst2012  --test_prefix=./tmp/nmt_data/tst2013 --out_dir=./tmp/nmt_model --num_train_steps=12000 --steps_per_stats=100 --num_layers=2 --num_units=128 --dropout=0.2 --metrics=bleu

郝同学i5-3337U &#67;&#x50;&#x55;&#x40;&#x31;&#x2e;&#x38;&#x30;&#x47;&#72;&#x7a;，8G内存，训练了将近两个小时才训练完成。下文我们要学习使用一下UCloud提供的免费GPU。
我们可以使用 Tensorboard 在训练过程中查看模型的总结：
tensorboard --port 22222 --logdir ./tmp/nmt_model/
通过以下简单的变化，就能逆向完成英语到越南语的翻译。
--src=en --tgt=vi
4.3. 使用model1、从mnt/tmp/nmt_data/tst2013.vi中复制几句话，新建nmt/tmp/my_infer_file.vi，粘贴到里面。
2、运行model
python -m nmt.nmt --model_dir=./tmp/nmt_model --inference_input_file=./tmp/my_infer_file.vi --inference_output_file=./tmp/nmt_model/output_infer

3、查看结果打开nmt/tmp/nmt_model/output_infer文件，即可看到翻译结果。
5. 书签全球AI挑战赛-英中机器文本翻译
谷歌开放GNMT教程：如何使用TensorFlow构建自己的神经机器翻译系统
Neural Machine Translation (seq2seq) Tutorial
用CNN做机器翻译？Facebook相关论文的PyTorch代码发布
从零开始：深度学习软件环境安装指南
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>pip</tag>
        <tag>机器学习</tag>
        <tag>tensorflow</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：使用Docker安装配置PyPI Server</title>
    <url>/dev-docker-pypi-server/</url>
    <content><![CDATA[1. PyPI Server简介PyPI Server是一个Python包索引（Python Package Index）的服务器实现，可以用来搭建自己的Python包管理中心。这样，我们就可以上传并分发我们自己的Python包，或者为我们的团队、组织内部提供一个单独、私有、远程的Python包服务器。
本文中，我们使用Docker安装配置一个基于 devpi 的PyPI Server。至于Python包打包上传到PyPI Server的方法，请参考《Python软件仓库PyPI》。
相关文档：

Wikipedia - Python Package Index
PyPI官网
利用Docker快速构建基于devpi的企业级私有PyPI Server
devpi - PyPI
devpi/devpi-ldap
devpi-auth-gitlab



2. 准备配置文件准备配置文件：

config.yaml：devpi的配置文件
Dockerfile：用于打包devpi-server的镜像
devpi-ldap.yaml：devpi支持ldap的配置

完整配置文件参考hexo-storage/python-pypi
3. 准备PyPI Server镜像docker build -t voidking/devpi-server:v1.0 .

4. 安装PyPI Server4.1. master节点mkdir -p /opt/pypi/data/mkdir -p /opt/pypi/certs/openssl genrsa -out /opt/pypi/certs/key.pem 2048chmod 600 /opt/pypi/certs/key.pemdocker run --rm \-v /opt/pypi/data:/app/data \-v /opt/pypi/certs/key.pem:/app/certs/key.pem \-p 3141:3141 \voidking/devpi-server:v1.0 \devpi-init --configfile config.yaml \--role masterdocker run -it --name devpi-master -d \--restart=always \-v /opt/pypi/data:/app/data \-v /opt/pypi/certs/key.pem:/app/certs/key.pem \-p 3141:3141 \voidking/devpi-server:v1.0 \devpi-server \--configfile config.yaml \--ldap-config devpi-ldap.yaml \--gitlab-registry-url https://gitlab.voidking.com \--role master \--secretfile /app/certs/key.pem \--port=3141

4.2. worker节点docker run -it --rm \-v /opt/pypi/data:/app/data \-v /opt/pypi/certs/key.pem:/app/certs/key.pem \-p 3141:3141 \voidking/devpi-server:v1.0 \devpi-init \--configfile config.yaml \--role replica \--master=http://192.168.56.101:3141docker run -it --name devpi-worker1 -d \--restart=always \-v /opt/pypi/data:/app/data \-v /opt/pypi/certs/key.pem:/app/certs/key.pem \-p 3141:3141 \voidking/devpi-server:v1.0 \devpi-server \--configfile config.yaml \--role replica \--master=http://192.168.56.101:3141 \--secretfile /app/certs/key.pem \--port=3141

5. PyPI Server初始化5.1. 登录PyPI仓库1、下载安装devpi client
pip install devpi-client

2、指定PyPI Server
devpi use http://192.168.56.101:3141

3、登录PyPI Server
devpi login root --password=&#x27;&#x27;

4、修改密码
devpi passwd # 修改密码devpi user -m root password=xxx # 修改密码方法二

5、测试登录登出
devpi login root --password=xxxdevpi logoffdevpi login root --password=xxx

5.2. 设置镜像仓库devpi index root/pypi mirror_url=&quot;https://pypi.douban.com/simple&quot;
当本地PyPI仓库中不存在某个包时，从镜像仓库中拉取。
5.3. 创建索引devpi index -c dev bases=root/pypidevpi use root/dev




]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>docker</tag>
        <tag>pip</tag>
        <tag>好好学Python</tag>
        <tag>pypi</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python软件仓库PyPI</title>
    <url>/dev-python-pypi/</url>
    <content><![CDATA[1. PyPI简介
The Python Package Index, abbreviated as PyPI (/ˌpaɪpiˈaɪ/) and also known as the Cheese Shop, is the official third-party software repository for Python.

PyPI是Python软件仓库，我们执行pip install时就是从PyPI仓库中拉取的软件包。
相关文档：

Wikipedia - Python Package Index
PyPI官网
《使用Docker安装配置PyPI Server》



2. 替换国内PyPI仓库2.1. PyPI镜像源国内使用python下载包，经常会遇到下载不了的情况，这时可以替换PyPI仓库。
目前国内靠谱的 PyPI 镜像源有：

清华：https://pypi.tuna.tsinghua.edu.cn/simple/
阿里：https://mirrors.aliyun.com/pypi/simple/
豆瓣：https://pypi.douban.com/simple/

参考文档：PyPI 镜像使用帮助。
2.2. 临时使用比如更新tensorflow，可以使用
pip install tensorflow --upgrade -i https://pypi.tuna.tsinghua.edu.cn/simplepip install tensorflow --upgrade --trusted-host -i https://pypi.tuna.tsinghua.edu.cn/simple
两者的差别，在于使用的协议是http还是https。在最新的pip版本(&gt;=7)中，使用镜像源时，会提示源地址不受信任或不安全。
2.3. Windows配置PyPI1、创建目录和文件
%appdata%\pip\pip.ini# c:\user\username\pip\pip.ini

2、pip.ini内容为：
[global]index-url = https://pypi.tuna.tsinghua.edu.cn/simple/[install]trusted-host = https://pypi.tuna.tsinghua.edu.cn/simple/

2.4. Linux配置PyPI1、创建目录和文件
vi ~/.pip/pip.conf

2、pip.conf内容为：
[global]index-url = https://pypi.tuna.tsinghua.edu.cn/simple/[install]trusted-host = https://pypi.tuna.tsinghua.edu.cn/simple/

2.5. Anaconda配置PyPIconda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/conda config --set show_channel_urls yes

虽然修改了软件源，但是pip search命令还是不能使用的，因为搜索软件使用的协议与安装软件不同。pip search基于xmlrpclib实现，pip install基于urllib2实现。
3. 打包Python软件包3.1. 打包软件包1、准备python项目
.├── example│   └── main.py└── setup.py

其中main.py内容为：
if __name__ == &#x27;__main__&#x27;:  print(&quot;hello&quot;)

2、准备setup.py
from setuptools import find_packagesfrom setuptools import setup setup(    name=&quot;example&quot;,    version=&quot;1.0.1&quot;,    author=&quot;voidking&quot;,    author_email=&quot;voidking@qq.com&quot;,    description=&quot;desc&quot;,    long_description=&quot;long desc&quot;,    license=&#x27;Apache2.0&#x27;,    # packages=[&#x27;example&#x27;]  # python项目文件夹的名字    packages=find_packages())

3、执行打包
pip install setuptoolspip install wheelpython setup.py bdist_wheel sdist

打包说明：python setup.py bdist_wheel sdist 是Python中用来打包源代码的命令，生成的发布包保存在项目目录下的 dist 目录中。
这个命令一次生成两种包：

bdist_wheel：创建一个二进制分发版的 “wheel” 归档文件。这是一种Python二进制模块分发格式，它比 egg 格式更加简单，更方便。这种分发包含已经编译好的Python源码，一般用于Python3.5以上的版本。
sdist：创建一个源码分发版的 “source distribution” 归档文件（通常是.tar.gz或.zip）。这种包含Python的原始代码，下载后需要使用者自己编译。

3.2. 测试wheel包pip install dist/example-1.0.1-py3-none-any.whlpython -m example.main

4. 上传Python软件包到 devpi4.1. 安装配置PyPI Server首先，我们要有一个基于 devpi 的PyPI Server，安装配置方法参考文档：《使用Docker安装配置PyPI Server》
4.2. 上传软件包1、下载安装devpi client
pip install devpi-client

2、指定PyPI Server
devpi use http://192.168.56.101:3141devpi use root/dev

或者：
devpi use http://192.168.56.101:3141/root/dev

3、登录PyPI Server
devpi login root --password=xxx

4、上传Python软件包
devpi upload --wheel

想要带文档上传的话，参考文档Sphinx配置conf.py文件
pip install sphinxdevpi upload --wheel --with-docs

4.3. 删除软件包devpi remove exampledevpi remove example&gt;=1.0.1

4.4. 推送包到另外的索引devpi push 命令是将包从一个索引推送到另外一个索引，例如将包example推送到root/dev
devpi push example==1.0.1 root/dev

5. 上传Python软件包到 pypi.org5.1. 准备 pypi.org token1、访问pypi.org，注册一个账号
2、在Account settings页面，Add API token
3、创建 pypi 配置文件 $HOME/.pypirc
[pypi]  username = __token__  password = pypi-xxx

5.2. 上传软件包1、安装 twine
pip install twine

2、上传Python软件包
twine upload dist/*

参考文档：

Packaging and distributing projects
Publishing package distribution releases using GitHub Actions CI/CD workflows

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>pip</tag>
        <tag>好好学Python</tag>
        <tag>pypi</tag>
      </tags>
  </entry>
  <entry>
    <title>tensorflow进阶</title>
    <url>/dev-tensorflow-advance/</url>
    <content><![CDATA[1. 分类学习分类和回归的区别在于输出变量的类型上。通俗理解定量输出是回归，或者说是连续变量预测；定性输出是分类，或者说是离散变量预测。如预测房价这是一个回归任务；把东西分成几类, 比如猫狗猪牛，就是一个分类任务。


2. MNISTMNIST是一个入门级的计算机视觉数据集，它包含各种手写数字图片，它也包含每一张图片对应的标签，告诉我们这个是数字几。接下来，我们将训练一个机器学习模型用于预测图片里面的数字。
2.1. 下载数据集访问THE MNIST DATABASE，下载：

train-images-idx3-ubyte.gz
train-labels-idx1-ubyte.gz
t10k-images-idx3-ubyte.gz
t10k-labels-idx1-ubyte.gz

把这四个文件放到Tensorflow-Tutorial/tutorial-contents/mnist目录下。
数据中包含55000张训练图片，每张图片的分辨率是28×28，所以我们的训练网络输入应该是28×28=784个像素数据。
2.2. 编码from __future__ import print_functionimport tensorflow as tffrom tensorflow.examples.tutorials.mnist import input_data# number 1 to 10 datamnist = input_data.read_data_sets(&#x27;./mnist&#x27;, one_hot=True)def add_layer(inputs, in_size, out_size, activation_function=None,):    # add one more layer and return the output of this layer    Weights = tf.Variable(tf.random_normal([in_size, out_size]))    biases = tf.Variable(tf.zeros([1, out_size]) + 0.1,)    Wx_plus_b = tf.matmul(inputs, Weights) + biases    if activation_function is None:        outputs = Wx_plus_b    else:        outputs = activation_function(Wx_plus_b,)    return outputsdef compute_accuracy(v_xs, v_ys):    global prediction    y_pre = sess.run(prediction, feed_dict=&#123;xs: v_xs&#125;)    correct_prediction = tf.equal(tf.argmax(y_pre,1), tf.argmax(v_ys,1))    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))    result = sess.run(accuracy, feed_dict=&#123;xs: v_xs, ys: v_ys&#125;)    return result# define placeholder for inputs to networkxs = tf.placeholder(tf.float32, [None, 784]) # 28x28ys = tf.placeholder(tf.float32, [None, 10])# add output layerprediction = add_layer(xs, 784, 10,  activation_function=tf.nn.softmax)# the error between prediction and real datacross_entropy = tf.reduce_mean(-tf.reduce_sum(ys * tf.log(prediction),                                              reduction_indices=[1]))       # losstrain_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)sess = tf.Session()# important step# tf.initialize_all_variables() no long valid from# 2017-03-02 if using tensorflow &gt;= 0.12if int((tf.__version__).split(&#x27;.&#x27;)[1]) &lt; 12 and int((tf.__version__).split(&#x27;.&#x27;)[0]) &lt; 1:    init = tf.initialize_all_variables()else:    init = tf.global_variables_initializer()sess.run(init)for i in range(1000):    batch_xs, batch_ys = mnist.train.next_batch(100)    sess.run(train_step, feed_dict=&#123;xs: batch_xs, ys: batch_ys&#125;)    if i % 50 == 0:        print(compute_accuracy(            mnist.test.images, mnist.test.labels))

3. 过拟合过拟合的模型，泛化能力差，不能成功的表达除了训练数据以外的其他数据。
3.1. 数据量增加数据量，大部分过拟合产生的原因是因为数据量太少了。
3.2. 正规化运用正规化，L1、l2 regularization等等，这些方法适用于大多数的机器学习，包括神经网络。主要思想是把W（权重）加入到cost，W变得太大，就让cost随之变大，成为一种惩罚机制。
3.3. dropout还有一种专门用在神经网络的正规化的方法，叫作 dropout。在训练的时候，我们随机忽略掉一些神经元和神经联结，使这个神经网络变得“不完整”。用一个不完整的神经网络训练一次。到第二次再随机忽略另一些，变成另一个不完整的神经网络。有了这些随机 drop 掉的规则，我们可以想象其实每次训练的时候，我们都让每一次预测结果都不会依赖于其中某部分特定的神经元。
conda install scikit-learn
# View more python learning tutorial on my Youtube and Youku channel!!!# Youtube video tutorial: https://www.youtube.com/channel/UCdyjiB5H8Pu7aDTNVXTTpcg# Youku video tutorial: http://i.youku.com/pythontutorial&quot;&quot;&quot;Please note, this code is only for python 3+. If you are using python 2+, please modify the code accordingly.&quot;&quot;&quot;from __future__ import print_functionimport tensorflow as tffrom sklearn.datasets import load_digitsfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import LabelBinarizer# load datadigits = load_digits()X = digits.datay = digits.targety = LabelBinarizer().fit_transform(y)X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.3)def add_layer(inputs, in_size, out_size, layer_name, activation_function=None, ):    # add one more layer and return the output of this layer    Weights = tf.Variable(tf.random_normal([in_size, out_size]))    biases = tf.Variable(tf.zeros([1, out_size]) + 0.1, )    Wx_plus_b = tf.matmul(inputs, Weights) + biases    # here to dropout    Wx_plus_b = tf.nn.dropout(Wx_plus_b, keep_prob)    if activation_function is None:        outputs = Wx_plus_b    else:        outputs = activation_function(Wx_plus_b, )    tf.summary.histogram(layer_name + &#x27;/outputs&#x27;, outputs)    return outputs# define placeholder for inputs to networkkeep_prob = tf.placeholder(tf.float32)xs = tf.placeholder(tf.float32, [None, 64])  # 8x8ys = tf.placeholder(tf.float32, [None, 10])# add output layerl1 = add_layer(xs, 64, 50, &#x27;l1&#x27;, activation_function=tf.nn.tanh)prediction = add_layer(l1, 50, 10, &#x27;l2&#x27;, activation_function=tf.nn.softmax)# the loss between prediction and real datacross_entropy = tf.reduce_mean(-tf.reduce_sum(ys * tf.log(prediction),                                              reduction_indices=[1]))  # losstf.summary.scalar(&#x27;loss&#x27;, cross_entropy)train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)sess = tf.Session()merged = tf.summary.merge_all()# summary writer goes in heretrain_writer = tf.summary.FileWriter(&quot;log/train&quot;, sess.graph)test_writer = tf.summary.FileWriter(&quot;log/test&quot;, sess.graph)# tf.initialize_all_variables() no long valid from# 2017-03-02 if using tensorflow &gt;= 0.12if int((tf.__version__).split(&#x27;.&#x27;)[1]) &lt; 12 and int((tf.__version__).split(&#x27;.&#x27;)[0]) &lt; 1:    init = tf.initialize_all_variables()else:    init = tf.global_variables_initializer()sess.run(init)for i in range(500):    # here to determine the keeping probability    sess.run(train_step, feed_dict=&#123;xs: X_train, ys: y_train, keep_prob: 0.5&#125;)    if i % 50 == 0:        # record loss        train_result = sess.run(merged, feed_dict=&#123;xs: X_train, ys: y_train, keep_prob: 1&#125;)        test_result = sess.run(merged, feed_dict=&#123;xs: X_test, ys: y_test, keep_prob: 1&#125;)        train_writer.add_summary(train_result, i)        test_writer.add_summary(test_result, i)

3.4. 参考资料
Classification 分类学习
MNIST For ML Beginners
Deep MNIST for Experts
MNIST机器学习入门
MNIST机器学习入门
Softmax函数的特点和作用是什么？

4. 源码分享https://github.com/voidking/Tensorflow-Tutorial.git
5. 书签TensorFlow官网
Tensorflow游乐场
莫烦Tensorflow教程系列
TensorFlow 官方文档中文版
TensorFlow中文社区
youtube CS 20SI: Tensorflow for Deep Learning Research
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>机器学习</tag>
        <tag>tensorflow</tag>
      </tags>
  </entry>
  <entry>
    <title>tensorboard基础</title>
    <url>/dev-tensorboard/</url>
    <content><![CDATA[1. 前言《tenserflow入门》中，我们学习了tensorflow的基本概念和搭建神经网络的方法。接下来，我们学习一下tensorboard的使用。


2. Tensorboard为了更方便 TensorFlow 程序的理解、调试与优化，Google发布了一套叫做 TensorBoard 的可视化工具。你可以用 TensorBoard 来展现你的 TensorFlow 图像，绘制图像生成的定量指标图以及附加数据。
2.1. 可视化构建过程《tenserflow入门》中，我们建立了一个神经网络。这里，我们进行一下修改，使之用Tensorboard显示出结构。
import tensorflow as tfimport numpy as nptf.set_random_seed(1)np.random.seed(1)# fake datax = np.linspace(-1, 1, 100)[:, np.newaxis]          # shape (100, 1)noise = np.random.normal(0, 0.1, size=x.shape)y = np.power(x, 2) + noise                          # shape (100, 1) + some noisewith tf.variable_scope(&#x27;Inputs&#x27;):    tf_x = tf.placeholder(tf.float32, x.shape, name=&#x27;x&#x27;)    tf_y = tf.placeholder(tf.float32, y.shape, name=&#x27;y&#x27;)with tf.variable_scope(&#x27;Net&#x27;):    l1 = tf.layers.dense(tf_x, 10, tf.nn.relu, name=&#x27;hidden_layer&#x27;)    output = tf.layers.dense(l1, 1, name=&#x27;output_layer&#x27;)    # add to histogram summary    tf.summary.histogram(&#x27;h_out&#x27;, l1)    tf.summary.histogram(&#x27;pred&#x27;, output)loss = tf.losses.mean_squared_error(tf_y, output, scope=&#x27;loss&#x27;)train_op = tf.train.GradientDescentOptimizer(learning_rate=0.5).minimize(loss)tf.summary.scalar(&#x27;loss&#x27;, loss)     # add loss to scalar summarysess = tf.Session()sess.run(tf.global_variables_initializer())writer = tf.summary.FileWriter(&#x27;./log&#x27;, sess.graph)     # write to filemerge_op = tf.summary.merge_all()                       # operation to merge all summaryfor step in range(100):    # train and net output    _, result = sess.run([train_op, merge_op], &#123;tf_x: x, tf_y: y&#125;)    writer.add_summary(result, step)

1、首先运行程序，python 305_tensorboard.py，这时，会在当前文件夹下生成log目录和 event.out.tfevent.* log文件。
2、执行tensorboard --logdir ./log，此时，会启动tensorboard web服务，默认端口为6006。访问 http://localhost:6006 ，看到tensorboard页面。
3、因为我们只定义了graph，所以点击导航栏的GRAPHS，即可看到我们定义的graph。
2.2. 可视化训练过程from __future__ import print_functionimport tensorflow as tfimport numpy as npdef add_layer(inputs, in_size, out_size, n_layer, activation_function=None):    # add one more layer and return the output of this layer    layer_name = &#x27;layer%s&#x27; % n_layer    with tf.name_scope(layer_name):        with tf.name_scope(&#x27;weights&#x27;):            Weights = tf.Variable(tf.random_normal([in_size, out_size]), name=&#x27;W&#x27;)            tf.summary.histogram(layer_name + &#x27;/weights&#x27;, Weights)        with tf.name_scope(&#x27;biases&#x27;):            biases = tf.Variable(tf.zeros([1, out_size]) + 0.1, name=&#x27;b&#x27;)            tf.summary.histogram(layer_name + &#x27;/biases&#x27;, biases)        with tf.name_scope(&#x27;Wx_plus_b&#x27;):            Wx_plus_b = tf.add(tf.matmul(inputs, Weights), biases)        if activation_function is None:            outputs = Wx_plus_b        else:            outputs = activation_function(Wx_plus_b, )        tf.summary.histogram(layer_name + &#x27;/outputs&#x27;, outputs)    return outputs# Make up some real datax_data = np.linspace(-1, 1, 300)[:, np.newaxis]noise = np.random.normal(0, 0.05, x_data.shape)y_data = np.square(x_data) - 0.5 + noise# define placeholder for inputs to networkwith tf.name_scope(&#x27;inputs&#x27;):    xs = tf.placeholder(tf.float32, [None, 1], name=&#x27;x_input&#x27;)    ys = tf.placeholder(tf.float32, [None, 1], name=&#x27;y_input&#x27;)# add hidden layerl1 = add_layer(xs, 1, 10, n_layer=1, activation_function=tf.nn.relu)# add output layerprediction = add_layer(l1, 10, 1, n_layer=2, activation_function=None)# the error between prediciton and real datawith tf.name_scope(&#x27;loss&#x27;):    loss = tf.reduce_mean(tf.reduce_sum(tf.square(ys - prediction),                                        reduction_indices=[1]))    tf.summary.scalar(&#x27;loss&#x27;, loss)with tf.name_scope(&#x27;train&#x27;):    train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)sess = tf.Session()merged = tf.summary.merge_all()writer = tf.summary.FileWriter(&quot;log/&quot;, sess.graph)init = tf.global_variables_initializer()sess.run(init)for i in range(1000):    sess.run(train_step, feed_dict=&#123;xs: x_data, ys: y_data&#125;)    if i % 50 == 0:        result = sess.run(merged,                          feed_dict=&#123;xs: x_data, ys: y_data&#125;)        writer.add_summary(result, i)

打开tensorboard，点击导航栏SCALARS，即可看到loss的变化过程。点击导航栏DISTRIBUTIONS，即可看到weights、biases和putputs的变化过程。
3. 源码分享https://github.com/voidking/Tensorflow-Tutorial.git
4. 书签TensorFlow官网
Tensorflow游乐场
莫烦Tensorflow教程系列
TensorFlow 官方文档中文版
TensorFlow中文社区
youtube CS 20SI: Tensorflow for Deep Learning Research
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>机器学习</tag>
        <tag>tensorflow</tag>
        <tag>tensorboard</tag>
      </tags>
  </entry>
  <entry>
    <title>tensorflow入门</title>
    <url>/dev-tensorflow-start/</url>
    <content><![CDATA[1. tensorflow简介TensorFlow 是一个用于人工智能的开源神器。TensorFlow 是一个采用数据流图（data flow graphs），用于数值计算的开源软件库。节点（Nodes）在图中表示数学操作，图中的线（edges）则表示在节点间相互联系的多维数据数组，即张量（tensor）。它灵活的架构让你可以在多种平台上展开计算，例如台式计算机中的一个或多个CPU（或GPU），服务器，移动设备等等。TensorFlow 最初由Google大脑小组（隶属于Google机器智能研究机构）的研究员和工程师们开发出来，用于机器学习和深度神经网络方面的研究，但这个系统的通用性使其也可广泛用于其他计算领域。
本文主要参考莫烦同学的教程，进行了少量修改。


2. 安装tensorflow1、参考Installing TensorFlow或者下载与安装，安装TensorFlow。
2、假设我们的环境是anaconda，首先我们切换到3.6的环境，然后pip安装即可。
activate py3pip install --upgrade tensorflow

3、测试
import osos.environ[&#x27;TF_CPP_MIN_LOG_LEVEL&#x27;]=&#x27;2&#x27;import tensorflow as tfhello = tf.constant(&#x27;Hello, TensorFlow!&#x27;)sess = tf.Session()print(sess.run(hello))

如果屏幕打印出“Hello, TensorFlow!”，则证明安装成功！
3. tensorflow基础3.1. 处理结构TensorFlow 让我们可以先绘制计算结构图，也可以称是一系列可人机交互的计算操作， 然后把编辑好的Python文件转换成更高效的 C++，并在后端进行计算。

TensorFlow 首先要定义神经网络的结构，然后再把数据放入结构当中去运算和training

下面动图展示了 TensorFlow 数据处理流程：
因为TensorFlow是采用 数据流图（data flow graphs）来计算， 所以首先我们得创建一个数据流图，然后再将我们的数据（数据以 张量(tensor) 的形式存在）放到数据流图中计算。
图中的 节点（Nodes）一般用来表示施加的数学操作，但也可以表示数据输入（feed in）的起点/输出（push out）的终点，或者是读取/写入持久变量（persistent variable）的终点；线（edges）则表示在节点间相互联系的多维数据数组，即 张量（tensor），训练模型时，tensor 会不断的从数据流图中的一个节点 flow 到另一节点，这就是 TensorFlow 名字的由来。一旦输入端的所有张量准备好，节点将被分配到各种计算设备完成异步并行地执行运算。
它灵活的架构让你可以在多种平台上展开计算，例如台式计算机中的一个或多个CPU（或GPU），服务器，移动设备等等。

使用图 (graph) 来表示计算任务
在被称之为 会话 (Session) 的上下文 (context) 中执行图
使用 tensor 表示数据
通过 变量 (Variable) 维护状态
使用 feed 和 fetch 可以为任意的操作(arbitrary operation) 赋值或者从其中获取数据

3.2. 线性预测demo&quot;&quot;&quot;Please note, this code is only for python 3+. If you are using python 2+, please modify the code accordingly.&quot;&quot;&quot;import osos.environ[&#x27;TF_CPP_MIN_LOG_LEVEL&#x27;]=&#x27;2&#x27;import tensorflow as tfimport numpy as np# create datax_data = np.random.rand(100).astype(np.float32)y_data = x_data*0.1 + 0.3### create tensorflow structure start ###Weights = tf.Variable(tf.random_uniform([1], -1.0, 1.0))biases = tf.Variable(tf.zeros([1]))y = Weights*x_data + biasesloss = tf.reduce_mean(tf.square(y-y_data))optimizer = tf.train.GradientDescentOptimizer(0.5)train = optimizer.minimize(loss)print(tf.__version__)if int((tf.__version__).split(&#x27;.&#x27;)[1]) &lt; 12 and int((tf.__version__).split(&#x27;.&#x27;)[0]) &lt; 1:    init = tf.initialize_all_variables()else:    init = tf.global_variables_initializer()### create tensorflow structure end ###sess = tf.Session()sess.run(init)for step in range(201):    sess.run(train)    if step % 20 == 0:        print(step, sess.run(Weights), sess.run(biases))

x_data和y_data是真实数据，y是预测出的数据，loss代表预测误差，optimizer代表调优方法，train代表最小化误差，sess.run(train)代表执行一次调优。
3.3. Sessionimport tensorflow as tfm1 = tf.constant([[2, 2]])m2 = tf.constant([[3],                  [3]])dot_operation = tf.matmul(m1, m2)print(dot_operation)  # wrong! no result# method1 use sessionsess = tf.Session()result = sess.run(dot_operation)print(result)sess.close()# method2 use sessionwith tf.Session() as sess:    result_ = sess.run(dot_operation)    print(result_)

从上面代码的执行结果可以看出，以tf开头的那些语句，并没有立即执行，而是在sess.run()的时候才会执行。
3.4. Variableimport tensorflow as tfvar = tf.Variable(0)    # our first variable in the &quot;global_variable&quot; setadd_operation = tf.add(var, 1)update_operation = tf.assign(var, add_operation)init = tf.global_variables_initializer()with tf.Session() as sess:    # once define variables, you have to initialize them by doing this    sess.run(init)    for _ in range(3):        sess.run(update_operation)        print(sess.run(var))

如果定义了变量，一定要global_variables_initializer并且run。
3.5. placeholderimport tensorflow as tfx1 = tf.placeholder(dtype=tf.float32, shape=None)y1 = tf.placeholder(dtype=tf.float32, shape=None)z1 = x1 + y1x2 = tf.placeholder(dtype=tf.float32, shape=[2, 1])y2 = tf.placeholder(dtype=tf.float32, shape=[1, 2])z2 = tf.matmul(x2, y2)with tf.Session() as sess:    # when only one operation to run    z1_value = sess.run(z1, feed_dict=&#123;x1: 1, y1: 2&#125;)    # when run multiple operations    z1_value, z2_value = sess.run(        [z1, z2],       # run them together        feed_dict=&#123;            x1: 1, y1: 2,            x2: [[2], [2]], y2: [[3, 3]]        &#125;)    print(z1_value)    print(z2_value)

在计算时，给x1赋值1，给y1赋值2。
3.6. 激励函数激励函数是用来激活神经元的函数。当你的神经网络层只有两三层，不是很多的时候， 对于隐藏层，使用任意的激励函数；在多层神经网络中，则要有所选择。在卷积神经网络中，一般使用relu；在循环神经网络中，一般使用relu或者tanh。
4. 神经网络4.1. 添加层import tensorflow as tfdef add_layer(inputs, in_size, out_size, activation_function=None):    # add one more layer and return the output of this layer    Weights = tf.Variable(tf.random_normal([in_size, out_size]))    biases = tf.Variable(tf.zeros([1, out_size]) + 0.1)    Wx_plus_b = tf.matmul(inputs, Weights) + biases    if activation_function is None:        outputs = Wx_plus_b    else:        outputs = activation_function(Wx_plus_b)    return outputs

4.2. 建造神经网络import tensorflow as tfimport numpy as npdef add_layer(inputs, in_size, out_size, activation_function=None):    # add one more layer and return the output of this layer    Weights = tf.Variable(tf.random_normal([in_size, out_size]))    biases = tf.Variable(tf.zeros([1, out_size]) + 0.1)    Wx_plus_b = tf.matmul(inputs, Weights) + biases    if activation_function is None:        outputs = Wx_plus_b    else:        outputs = activation_function(Wx_plus_b)    return outputs# Make up some real datax_data = np.linspace(-1,1,300)[:, np.newaxis]noise = np.random.normal(0, 0.05, x_data.shape)y_data = np.square(x_data) - 0.5 + noise# define placeholder for inputs to networkxs = tf.placeholder(tf.float32, [None, 1])ys = tf.placeholder(tf.float32, [None, 1])# add hidden layerl1 = add_layer(xs, 1, 10, activation_function=tf.nn.relu)# add output layerprediction = add_layer(l1, 10, 1, activation_function=None)# the error between prediction and real dataloss = tf.reduce_mean(tf.reduce_sum(tf.square(ys - prediction),                     reduction_indices=[1]))train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)# important stepinit = tf.global_variables_initializer()sess = tf.Session()sess.run(init)for i in range(1000):    # training    sess.run(train_step, feed_dict=&#123;xs: x_data, ys: y_data&#125;)    if i % 50 == 0:        # to see the step improvement        print(sess.run(loss, feed_dict=&#123;xs: x_data, ys: y_data&#125;))

4.3. 结果可视化import tensorflow as tfimport numpy as npimport matplotlib.pyplot as pltdef add_layer(inputs, in_size, out_size, activation_function=None):    # add one more layer and return the output of this layer    Weights = tf.Variable(tf.random_normal([in_size, out_size]))    biases = tf.Variable(tf.zeros([1, out_size]) + 0.1)    Wx_plus_b = tf.matmul(inputs, Weights) + biases    if activation_function is None:        outputs = Wx_plus_b    else:        outputs = activation_function(Wx_plus_b)    return outputs# Make up some real datax_data = np.linspace(-1,1,300)[:, np.newaxis]noise = np.random.normal(0, 0.05, x_data.shape)y_data = np.square(x_data) - 0.5 + noise# define placeholder for inputs to networkxs = tf.placeholder(tf.float32, [None, 1])ys = tf.placeholder(tf.float32, [None, 1])# add hidden layerl1 = add_layer(xs, 1, 10, activation_function=tf.nn.relu)# add output layerprediction = add_layer(l1, 10, 1, activation_function=None)# the error between prediction and real dataloss = tf.reduce_mean(tf.reduce_sum(tf.square(ys - prediction),                     reduction_indices=[1]))train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)# important stepinit = tf.global_variables_initializer()sess = tf.Session()sess.run(init)fig = plt.figure()ax = fig.add_subplot(1,1,1)ax.scatter(x_data, y_data)plt.ion()plt.show()# plt.show(block=False)for i in range(1000):    # training    sess.run(train_step, feed_dict=&#123;xs: x_data, ys: y_data&#125;)    if i % 50 == 0:        # to see the step improvement        # print(sess.run(loss, feed_dict=&#123;xs: x_data, ys: y_data&#125;))        try:            ax.lines.remove(lines[0])        except Exception:            pass        prediction_value = sess.run(prediction,feed_dict=&#123;xs: x_data&#125;)        lines = ax.plot(x_data, prediction_value, &#x27;r-&#x27;,lw=5)               plt.pause(0.1)

4.4. 加速神经网络训练加速神经网络训练的方法:

Stochastic Gradient Descent (SGD)
Momentum
AdaGrad
RMSProp
Adam

具体参考《加速神经网络训练 (Speed Up Training)》
4.5. optimizer相关资料：

TensorFlow可用的optimizer 链接
各种Optimizer 的对比 链接

一般使用GradientDescentOptimizer就够了，往后学习的话，可以使用MomentumOptimizer、AdamOptimizer、RMSPropOptimizer。
5. 源码分享https://github.com/voidking/Tensorflow-Tutorial.git
6. 书签TensorFlow官网
Tensorflow游乐场
莫烦Tensorflow教程系列
TensorFlow 官方文档中文版
TensorFlow中文社区
TensorFlow入门
youtube CS 20SI: Tensorflow for Deep Learning Research
Tensorflow 的处理结构及基本使用
2016年不可错过的21个深度学习视频、教程和课程
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>机器学习</tag>
        <tag>tensorflow</tag>
      </tags>
  </entry>
  <entry>
    <title>Numpy和Matplotlib</title>
    <url>/dev-numpy-matplotlib/</url>
    <content><![CDATA[1. 前言在进行机器学习时，经常用到两个库，numpy和matplotlib。
numpy(Numerical Python extensions)是一个第三方的Python包，用于科学计算。这个库的前身是1995年就开始开发的一个用于数组运算的库。经过了长时间的发展，基本上成了绝大部分Python科学计算的基础包，当然也包括所有提供Python接口的深度学习框架。
Matplotlib是Python中最常用的可视化工具之一，可以非常方便地创建海量类型地2D图表和一些基本的3D图表。Matplotlib最早是为了可视化癫痫病人的脑皮层电图相关的信号而研发，因为在函数的设计上参考了MATLAB，所以叫做Matplotlib。


2. 准备pip install numpypip install matplotlib

3. numpy3.1. 基本类型（array）import numpy as npa = [1, 2, 5, 3, 4]b = np.array(a)print(&#x27;a=&#x27;,a)print(&#x27;b=&#x27;,b)print(&#x27;type(a):&#x27;,type(a))print(&#x27;type(b)&#x27;,type(b))  print(&#x27;b.dtype&#x27;,b.dtype)b2 = b.astype(np.float32)print(&#x27;b2.dtype&#x27;,b2.dtype)                   print(&#x27;b.shape:&#x27;, b.shape)                    print(&#x27;b.argmax():&#x27;,b.argmax())                  print(&#x27;b.max():&#x27;,b.max())                     print(&#x27;b.mean():&#x27;,b.mean())            c = [[1, 2], [3, 4]]    d = np.array(c)print(&#x27;c=&#x27;,c)print(&#x27;d=&#x27;,d)print(&#x27;d.shape:&#x27;,d.shape) print(&#x27;d.size:&#x27;,d.size)print(&#x27;d.max(axis=0):&#x27;,d.max(axis=0)) # 找列最大值print(&#x27;d.max(axis=1):&#x27;,d.max(axis=1)) # 找行最大值print(&#x27;d.mean(axis=0):&#x27;,d.mean(axis=0))  # 求列平均数      print(&#x27;d.flatten():&#x27;,d.flatten()) # 展开为一维数组print(&#x27;np.ravel(c)&#x27;,np.ravel(c)) # 展开为一维数组# 3x3的浮点型2维数组，并且初始化所有元素值为1e = np.ones((3, 3), dtype=np.float32)print(&#x27;e=&#x27;,e)# 创建一个一维数组，元素值是把3重复4次，array([3, 3, 3, 3])f = np.repeat(3, 4)print(&#x27;f=&#x27;,f)# 2x2x3的无符号8位整型3维数组，并且初始化所有元素值为0g = np.zeros((2, 2, 3), dtype=np.uint8)print(&#x27;g=&#x27;,g)print(&#x27;g.shape&#x27;,g.shape)h = g.astype(np.float)  # 用另一种类型表示print(&#x27;h=&#x27;,h)l = np.arange(10)       # 类似range，array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])print(&#x27;l=&#x27;,l)m = np.linspace(0, 6, 5)# 等差数列，0到6之间5个取值，array([ 0., 1.5, 3., 4.5, 6.])print(&#x27;m&#x27;,m)p = np.array(    [[1, 2, 3, 4],     [5, 6, 7, 8]])np.save(&#x27;p.npy&#x27;, p)     # 保存到文件q = np.load(&#x27;p.npy&#x27;)    # 从文件读取

3.2. array相关操作import numpy as np&#x27;&#x27;&#x27;array([[[ 0,  1,  2,  3],        [ 4,  5,  6,  7],        [ 8,  9, 10, 11]],       [[12, 13, 14, 15],        [16, 17, 18, 19],        [20, 21, 22, 23]]])&#x27;&#x27;&#x27;a = np.arange(24).reshape((2, 3, 4))print(&#x27;a=&#x27;,a)b = a[1][1][1]print(&#x27;a[1][1][1]=&#x27;,b)&#x27;&#x27;&#x27;array([[ 8,  9, 10, 11],       [20, 21, 22, 23]])&#x27;&#x27;&#x27;c = a[:, 2, :]print(&#x27;a[:, 2, :]=&#x27;,c)&#x27;&#x27;&#x27; 用:表示当前维度上所有下标array([[ 1,  5,  9],       [13, 17, 21]])&#x27;&#x27;&#x27;d = a[:, :, 1]print(&#x27;a[:, :, 1]=&#x27;,d)&#x27;&#x27;&#x27; 用...表示没有明确指出的维度array([[ 1,  5,  9],       [13, 17, 21]])&#x27;&#x27;&#x27;e = a[..., 1]print(&#x27;a[..., 1]=&#x27;,e)&#x27;&#x27;&#x27;array([[[ 5,  6],        [ 9, 10]],       [[17, 18],        [21, 22]]])&#x27;&#x27;&#x27;f = a[:, 1:, 1:-1]print(&#x27;a[:, 1:, 1:-1]=&#x27;,f)&#x27;&#x27;&#x27;平均分成3份[array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8])]&#x27;&#x27;&#x27;g = np.split(np.arange(9), 3)print(&#x27;g=&#x27;,g)&#x27;&#x27;&#x27;按照下标位置进行划分[array([0, 1]), array([2, 3, 4, 5]), array([6, 7, 8])]&#x27;&#x27;&#x27;h = np.split(np.arange(9), [2, -3])print(&#x27;h=&#x27;,h)l_0 = np.arange(6).reshape((2, 3))l_1 = np.arange(6, 12).reshape(2, 3)print(&#x27;l_0=&#x27;,l_0)print(&#x27;l_1=&#x27;,l_1)&#x27;&#x27;&#x27;vstack是指沿着纵轴拼接两个array，verticalhstack是指沿着横轴拼接两个array，horizontal更广义的拼接用concatenate实现，horizontal后的两句依次等效于vstack和hstackstack不是拼接而是在输入array的基础上增加一个新的维度&#x27;&#x27;&#x27;m = np.vstack((l_0, l_1))p = np.hstack((l_0, l_1))q = np.concatenate((l_0, l_1))r = np.concatenate((l_0, l_1), axis=-1)s = np.stack((l_0, l_1))print(&#x27;np.vstack((l_0, l_1))=&#x27;,m)print(&#x27;np.hstack((l_0, l_1))=&#x27;,p)print(&#x27;np.concatenate((l_0, l_1))=&#x27;,q)print(&#x27;np.concatenate((l_0, l_1), axis=-1) = &#x27;,r)print(&#x27;np.stack((l_0, l_1)) = &#x27;,s)&#x27;&#x27;&#x27;按指定轴进行转置array([[[ 0,  3],        [ 6,  9]],       [[ 1,  4],        [ 7, 10]],       [[ 2,  5],        [ 8, 11]]])&#x27;&#x27;&#x27;t = s.transpose((2, 0, 1))print(&#x27;s.transpose((2, 0, 1)) = &#x27;,t)&#x27;&#x27;&#x27;默认转置将维度倒序，对于2维就是横纵轴互换array([[ 0,  4,  8],       [ 1,  5,  9],       [ 2,  6, 10],       [ 3,  7, 11]])&#x27;&#x27;&#x27;u = a[0].transpose()    # 或者u=a[0].T也是获得转置print(&#x27;u = a[0].transpose() = &#x27;,u)&#x27;&#x27;&#x27;逆时针旋转90度，第二个参数是旋转次数array([[ 3,  2,  1,  0],       [ 7,  6,  5,  4],       [11, 10,  9,  8]])&#x27;&#x27;&#x27;v = np.rot90(u, 3)print(&#x27;np.rot90(u, 3) = &#x27;,v)&#x27;&#x27;&#x27;沿纵轴左右翻转array([[ 8,  4,  0],       [ 9,  5,  1],       [10,  6,  2],       [11,  7,  3]])&#x27;&#x27;&#x27;w = np.fliplr(u)print(&#x27;np.fliplr(u) = &#x27;,w)&#x27;&#x27;&#x27;沿水平轴上下翻转array([[ 3,  7, 11],       [ 2,  6, 10],       [ 1,  5,  9],       [ 0,  4,  8]])&#x27;&#x27;&#x27;x = np.flipud(u)print(&#x27;np.flipud(u) = &#x27;,x)&#x27;&#x27;&#x27;按照一维顺序滚动位移array([[11,  0,  4],       [ 8,  1,  5],       [ 9,  2,  6],       [10,  3,  7]])&#x27;&#x27;&#x27;y = np.roll(u, 1)print(&#x27;np.roll(u, 1)&#x27;, y)&#x27;&#x27;&#x27;按照指定轴滚动位移array([[ 8,  0,  4],       [ 9,  1,  5],       [10,  2,  6],       [11,  3,  7]])&#x27;&#x27;&#x27;z = np.roll(u, 1, axis=1)print(&#x27;np.roll(u, 1, axis=1)&#x27;,z)

3.3. 数学运算import numpy as np# 绝对值，1a = np.abs(-1)# sin函数，1.0b = np.sin(np.pi/2)# tanh逆函数，0.50000107157840523c = np.arctanh(0.462118)# e为底的指数函数，20.085536923187668d = np.exp(3)# 2的3次方，8f = np.power(2, 3)# 点积，1*3+2*4=11g = np.dot([1, 2], [3, 4])# 开方，5h = np.sqrt(25)# 求和，10l = np.sum([1, 2, 3, 4])# 平均值，5.5m = np.mean([4, 5, 6, 7])# 标准差，0.96824583655185426p = np.std([1, 2, 3, 2, 1, 3, 2, 0])

3.4. 对位运算import numpy as npa = np.array([    [1, 2, 3],    [4, 5, 6]])b = np.array([    [1, 2, 3],    [1, 2, 3]])&#x27;&#x27;&#x27;维度一样的array，对位计算array([[2, 4, 6],       [5, 7, 9]])&#x27;&#x27;&#x27;a + b&#x27;&#x27;&#x27;array([[0, 0, 0],       [3, 3, 3]])&#x27;&#x27;&#x27;a - b&#x27;&#x27;&#x27;array([[ 1,  4,  9],       [ 4, 10, 18]])&#x27;&#x27;&#x27;a * b&#x27;&#x27;&#x27;array([[1, 1, 1],       [4, 2, 2]])&#x27;&#x27;&#x27;a / b&#x27;&#x27;&#x27;array([[ 1,  4,  9],       [16, 25, 36]])&#x27;&#x27;&#x27;a ** 2&#x27;&#x27;&#x27;array([[  1,   4,  27],       [  4,  25, 216]])&#x27;&#x27;&#x27;a ** bc = np.array([    [1, 2, 3],    [4, 5, 6],    [7, 8, 9],    [10, 11, 12]])d = np.array([2, 2, 2])&#x27;&#x27;&#x27;广播机制让计算的表达式保持简洁d和c的每一行分别进行运算array([[ 3,  4,  5],       [ 6,  7,  8],       [ 9, 10, 11],       [12, 13, 14]])&#x27;&#x27;&#x27;c + d&#x27;&#x27;&#x27;array([[ 2,  4,  6],       [ 8, 10, 12],       [14, 16, 18],       [20, 22, 24]])&#x27;&#x27;&#x27;c * d&#x27;&#x27;&#x27;1和c的每个元素分别进行运算array([[ 0,  1,  2],       [ 3,  4,  5],       [ 6,  7,  8],       [ 9, 10, 11]])&#x27;&#x27;&#x27;c - 1

3.5. 线性代数模块（linalg）在深度学习相关的数据处理和运算中，线性代数模块（linalg）是最常用的之一。结合numpy提供的基本函数，可以对向量，矩阵，或是说多维张量进行一些基本的运算。
import numpy as npa = np.array([3, 4])print(&#x27;a=&#x27;,a)# 求范数，默认二范数，平方和开平方print(&#x27;np.linalg.norm(a) = &#x27;,np.linalg.norm(a))# 求范数，一范数，绝对值求和print(&#x27;np.linalg.norm(a,ord=1) = &#x27;,np.linalg.norm(a,ord=1))# 求范数，无穷范数，绝对值中的最大者print(&#x27;np.linalg.norm(a,ord=np.inf) = &#x27;,np.linalg.norm(a,ord=np.inf))b = np.array([    [1, 2, 3],    [4, 5, 6],    [7, 8, 9]])c = np.array([1, 0, 1])# 矩阵和向量之间的乘法np.dot(b, c)                    # array([ 4, 10, 16])np.dot(c, b.T)                  # array([ 4, 10, 16])np.trace(b)                     # 求矩阵的迹，15np.linalg.det(b)                # 求矩阵的行列式值，0np.linalg.matrix_rank(b)    # 求矩阵的秩，2，不满秩，因为行与行之间等差d = np.array([    [2, 1],    [1, 2]])&#x27;&#x27;&#x27;对正定矩阵求本征值和本征向量本征值为u，array([ 3.,  1.])本征向量构成的二维array为v，array([[ 0.70710678, -0.70710678],       [ 0.70710678,  0.70710678]])是沿着45°方向eig()是一般情况的本征值分解，对于更常见的对称实数矩阵，eigh()更快且更稳定，不过输出的值的顺序和eig()是相反的&#x27;&#x27;&#x27;u, v = np.linalg.eig(d)# Cholesky分解并重建l = np.linalg.cholesky(d)&#x27;&#x27;&#x27;array([[ 2.,  1.],       [ 1.,  2.]])&#x27;&#x27;&#x27;np.dot(l, l.T)e = np.array([    [1, 2],    [3, 4]])# 对不镇定矩阵，进行SVD分解并重建U, s, V = np.linalg.svd(e)S = np.array([    [s[0], 0],    [0, s[1]]])&#x27;&#x27;&#x27;array([[ 1.,  2.],       [ 3.,  4.]])&#x27;&#x27;&#x27;np.dot(U, np.dot(S, V))

3.6. 随机模块（random）随机模块包含了随机数产生和统计分布相关的基本函数，Python本身也有随机模块random，不过功能更丰富。
import numpy as npimport numpy.random as random# 设置随机数种子，随机数确定random.seed(42)print(&#x27;Random number with seed 42: &#x27;, random.random())# 产生一个1x3，[0,1)之间的浮点型随机数# array([[ 0.37454012,  0.95071431,  0.73199394]])# 后面的例子就不在注释中给出具体结果了print(&#x27;random.rand(1, 3) = &#x27;,random.rand(1, 3))# 产生一个[0,1)之间的浮点型随机数print(&#x27;Random number with no seed: &#x27;, random.random())# 下边4个没有区别，都是按照指定大小产生[0,1)之间的浮点型随机数array，不Pythonic…print(&#x27;random.random((3, 3))=&#x27;,random.random((3, 3)))print(&#x27;random.sample((3, 3))=&#x27;,random.sample((3, 3)))print(&#x27;random.random_sample((3, 3))=&#x27;,random.random_sample((3, 3)))print(&#x27;random.ranf((3, 3))=&#x27;,random.ranf((3, 3)))# 产生10个[1,6)之间的浮点型随机数5*random.random(10) + 1random.uniform(1, 6, 10)# 产生10个[1,6)之间的整型随机数random.randint(1, 6, 10)# 产生2x5的标准正态分布样本result = random.normal(size=(5, 2))print(&#x27;random.normal(size=(5, 2))=&#x27;, result)# 产生5个，n=5，p=0.5的二项分布样本random.binomial(n=5, p=0.5, size=5)a = np.arange(10)# 从a中有回放的随机采样7个random.choice(a, 7)# 从a中无回放的随机采样7个random.choice(a, 7, replace=False)# 对a进行乱序并返回一个新的arrayb = random.permutation(a)# 对a进行in-place乱序random.shuffle(a)# 生成一个长度为9的随机bytes序列并作为str返回# &#x27;\x96\x9d\xd1?\xe6\x18\xbb\x9a\xec&#x27;random.bytes(9)

4. Matplotlib4.1. 2D图表Matplotlib中最基础的模块是pyplot。先从最简单的点图和线图开始，比如我们有一组数据，还有一个拟合模型，通过下面的代码图来可视化：
import numpy as npimport matplotlib as mplimport matplotlib.pyplot as plt# 通过rcParams设置全局横纵轴字体大小mpl.rcParams[&#x27;xtick.labelsize&#x27;] = 24mpl.rcParams[&#x27;ytick.labelsize&#x27;] = 24np.random.seed(42)# x轴的采样点x = np.linspace(0, 5, 100)# 通过下面曲线加上噪声生成数据，所以拟合模型就用y了……y = 2*np.sin(x) + 0.3*x**2y_data = y + np.random.normal(scale=0.3, size=100)# figure()指定图表名称plt.figure(&#x27;data&#x27;)# &#x27;.&#x27;标明画散点图，每个散点的形状是个圆plt.plot(x, y_data, &#x27;.&#x27;)# 画模型的图，plot函数默认画连线图plt.figure(&#x27;model&#x27;)plt.plot(x, y)# 两个图画一起plt.figure(&#x27;data &amp; model&#x27;)# 通过&#x27;k&#x27;指定线的颜色，lw指定线的宽度# 第三个参数除了颜色也可以指定线形，比如&#x27;r--&#x27;表示红色虚线# 更多属性可以参考官网：http://matplotlib.org/api/pyplot_api.htmlplt.plot(x, y, &#x27;k&#x27;, lw=3)# scatter可以更容易地生成散点图plt.scatter(x, y_data)# 将当前figure的图保存到文件result.pngplt.savefig(&#x27;result.png&#x27;)# 一定要加上这句才能让画好的图显示在屏幕上plt.show()

4.2. 对比图表点和线图表只是最基本的用法，有的时候我们获取了分组数据要做对比，柱状或饼状类型的图或许更合适。
import numpy as npimport matplotlib as mplimport matplotlib.pyplot as pltmpl.rcParams[&#x27;axes.titlesize&#x27;] = 20mpl.rcParams[&#x27;xtick.labelsize&#x27;] = 16mpl.rcParams[&#x27;ytick.labelsize&#x27;] = 16mpl.rcParams[&#x27;axes.labelsize&#x27;] = 16mpl.rcParams[&#x27;xtick.major.size&#x27;] = 0mpl.rcParams[&#x27;ytick.major.size&#x27;] = 0# 包含了狗，猫和猎豹的最高奔跑速度，还有对应的可视化颜色speed_map = &#123;    &#x27;dog&#x27;: (48, &#x27;#7199cf&#x27;),    &#x27;cat&#x27;: (45, &#x27;#4fc4aa&#x27;),    &#x27;cheetah&#x27;: (120, &#x27;#e1a7a2&#x27;)&#125;# 整体图的标题fig = plt.figure(&#x27;Bar chart &amp; Pie chart&#x27;)# 在整张图上加入一个子图，121的意思是在一个1行2列的子图中的第一张ax = fig.add_subplot(121)ax.set_title(&#x27;Running speed - bar chart&#x27;)# 生成x轴每个元素的位置xticks = np.arange(3)# 定义柱状图每个柱的宽度bar_width = 0.5# 动物名称animals = speed_map.keys()# 奔跑速度speeds = [x[0] for x in speed_map.values()]# 对应颜色colors = [x[1] for x in speed_map.values()]# 画柱状图，横轴是动物标签的位置，纵轴是速度，定义柱的宽度，同时设置柱的边缘为透明bars = ax.bar(xticks, speeds, width=bar_width, edgecolor=&#x27;none&#x27;)# 设置y轴的标题ax.set_ylabel(&#x27;Speed(km/h)&#x27;)# x轴每个标签的具体位置，设置为每个柱的中央ax.set_xticks(xticks+bar_width/2)# 设置每个标签的名字ax.set_xticklabels(animals)# 设置x轴的范围ax.set_xlim([bar_width/2-0.5, 3-bar_width/2])# 设置y轴的范围ax.set_ylim([0, 125])# 给每个bar分配指定的颜色for bar, color in zip(bars, colors):    bar.set_color(color)# 在122位置加入新的图ax = fig.add_subplot(122)ax.set_title(&#x27;Running speed - pie chart&#x27;)# 生成同时包含名称和速度的标签labels = [&#x27;&#123;&#125;\n&#123;&#125; km/h&#x27;.format(animal, speed) for animal, speed in zip(animals, speeds)]# 画饼状图，并指定标签和对应颜色ax.pie(speeds, labels=labels, colors=colors)plt.show()

在Matplotlib中，画图时有两个常用概念，一个是平时画图蹦出的一个窗口，这叫一个figure。Figure相当于一个大的画布，在每个figure中，又可以存在多个子图，这种子图叫做axes。顾名思义，有了横纵轴就是一幅简单的图表。在上面代码中，先把figure定义成了一个一行两列的大画布，然后通过fig.add_subplot()加入两个新的子图。subplot的定义格式很有趣，数字的前两位分别定义行数和列数，最后一位定义新加入子图的所处顺序，当然想写明确些也没问题，用逗号分开即可。
4.3. 3D图表Matplotlib中也能支持一些基础的3D图表，比如曲面图，散点图和柱状图。这些3D图表需要使用mpl_toolkits模块。1、访问 Unofficial Windows Binaries for Python Extension Packages，下载两个文件：

pyproj-1.9.5.1-cp36-cp36m-win_amd64.whl
basemap-1.1.0-cp36-cp36m-win_amd64.whl 

2、假设下载到了D盘下，那么在D盘下打开命令行，执行
pip install pyproj-1.9.5.1-cp36-cp36m-win_amd64.whlpip install basemap-1.1.0-cp36-cp36m-win_amd64.whl 

ps：相关pip命令pip list，查看安装列表pip uninstall package_name，删除包
3、示例代码
import matplotlib.pyplot as pltimport numpy as np# 3D图标必须的模块，project=&#x27;3d&#x27;的定义from mpl_toolkits.mplot3d import Axes3D     np.random.seed(42)n_grids = 51            # x-y平面的格点数 c = n_grids / 2         # 中心位置nf = 2                  # 低频成分的个数# 生成格点x = np.linspace(0, 1, n_grids)y = np.linspace(0, 1, n_grids)# x和y是长度为n_grids的array# meshgrid会把x和y组合成n_grids*n_grids的array，X和Y对应位置就是所有格点的坐标X, Y = np.meshgrid(x, y)# 生成一个0值的傅里叶谱spectrum = np.zeros((n_grids, n_grids), dtype=np.complex)# 生成一段噪音，长度是(2*nf+1)**2/2noise = [np.complex(x, y) for x, y in np.random.uniform(-1,1,((2*nf+1)**2/2, 2))]# 傅里叶频谱的每一项和其共轭关于中心对称noisy_block = np.concatenate((noise, [0j], np.conjugate(noise[::-1])))# 将生成的频谱作为低频成分spectrum[c-nf:c+nf+1, c-nf:c+nf+1] = noisy_block.reshape((2*nf+1, 2*nf+1))# 进行反傅里叶变换Z = np.real(np.fft.ifft2(np.fft.ifftshift(spectrum)))# 创建图表fig = plt.figure(&#x27;3D surface &amp; wire&#x27;)# 第一个子图，surface图ax = fig.add_subplot(1, 2, 1, projection=&#x27;3d&#x27;)# alpha定义透明度，cmap是color map# rstride和cstride是两个方向上的采样，越小越精细，lw是线宽ax.plot_surface(X, Y, Z, alpha=0.7, cmap=&#x27;jet&#x27;, rstride=1, cstride=1, lw=0)# 第二个子图，网线图ax = fig.add_subplot(1, 2, 2, projection=&#x27;3d&#x27;)ax.plot_wireframe(X, Y, Z, rstride=3, cstride=3, lw=0.5)plt.show()

4.4. 3D散点图3D的散点图也是常常用来查看空间样本分布的一种手段，并且画起来比表面图和网线图更加简单。
import matplotlib.pyplot as pltimport numpy as npfrom mpl_toolkits.mplot3d import Axes3Dnp.random.seed(42)# 采样个数500n_samples = 500dim = 3# 先生成一组3维正态分布数据，数据方向完全随机samples = np.random.multivariate_normal(    np.zeros(dim),    np.eye(dim),    n_samples)# 通过把每个样本到原点距离和均匀分布吻合得到球体内均匀分布的样本for i in range(samples.shape[0]):    r = np.power(np.random.random(), 1.0/3.0)    samples[i] *= r / np.linalg.norm(samples[i])upper_samples = []lower_samples = []for x, y, z in samples:    # 3x+2y-z=1作为判别平面    if z &gt; 3*x + 2*y - 1:        upper_samples.append((x, y, z))    else:        lower_samples.append((x, y, z))fig = plt.figure(&#x27;3D scatter plot&#x27;)ax = fig.add_subplot(111, projection=&#x27;3d&#x27;)uppers = np.array(upper_samples)lowers = np.array(lower_samples)# 用不同颜色不同形状的图标表示平面上下的样本# 判别平面上半部分为红色圆点，下半部分为绿色三角ax.scatter(uppers[:, 0], uppers[:, 1], uppers[:, 2], c=&#x27;r&#x27;, marker=&#x27;o&#x27;)ax.scatter(lowers[:, 0], lowers[:, 1], lowers[:, 2], c=&#x27;g&#x27;, marker=&#x27;^&#x27;)plt.show()


4.5. 图像显示Matplotlib也支持图像的存取和显示，并且和OpenCV一类的接口比起来，对于一般的二维矩阵的可视化要方便很多。
import matplotlib.pyplot as plt# 读取图片并显示plt.figure(&#x27;A image&#x27;)result_img = plt.imread(&#x27;result.png&#x27;)plt.imshow(result_img)# Z是上小节生成的随机图案，img0就是Z，img1是Z做了个简单的变换img0 = Zimg1 = 3*Z + 4# cmap指定为&#x27;gray&#x27;用来显示灰度图fig = plt.figure(&#x27;Auto Normalized Visualization&#x27;)ax0 = fig.add_subplot(121)ax0.imshow(img0, cmap=&#x27;gray&#x27;)ax1 = fig.add_subplot(122)ax1.imshow(img1, cmap=&#x27;gray&#x27;)plt.show()

这段代码中第一个例子是读取一个本地图片并显示，第二个例子中直接把上小节中反傅里叶变换生成的矩阵作为图像拿过来，原图和经过乘以3再加4变换的图直接绘制了两个形状一样，但是值的范围不一样的图案。显示的时候imshow会自动进行归一化，把最亮的值显示为纯白，最暗的值显示为纯黑。这是一种非常方便的设定，尤其是查看深度学习中某个卷积层的响应图时。
5. 源码分享https://github.com/voidking/udacity-python/tree/master/numpy
https://github.com/voidking/udacity-python/tree/master/matplotlib
6. 书签给深度学习入门者的Python快速教程 - 基础篇给深度学习入门者的Python快速教程 - numpy和Matplotlib篇机器学习中的数学(5)-强大的矩阵奇异值分解(SVD)及其应用
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>numpy</tag>
        <tag>matplotlib</tag>
      </tags>
  </entry>
  <entry>
    <title>APP第三方登录QQ和微博配置</title>
    <url>/dev-app-login-qq-sina/</url>
    <content><![CDATA[1. 需求使用hbuilder修改东北高师就业联盟网的app，完成了qq和微博第三方登录，真机调试正常。但是打包发布后，问题来了。使用qq登录时，会提示错误“该应用非官方正版应用，请去应用宝下载正版后进行qq登陆。（错误码：100044）”。使用微博登录时，会提示错误“21338:sso package or sign error”。
因为就业联盟网在开放平台上是一个web应用，但是我们的app打包后是一个android应用和一个ios应用。所以，要解决这个问题，我们要在开放平台上添加android应用和ios应用。


2. android打包签名在配置开放平台之前，我们先把android app打包发布，具体流程如下：
2.1. 安装java环境参考《全平台安装JDK》。
2.2. 生成keystorekeytool -genkey -alias dsjyw.keystore -keyalg RSA -validity 1000 -keystore dsjyw.keystore
说明：keytool -genkey -alias 别名.keystore -keyalg RSA -validity 有效期限(天数) -keystore 别名.keystore
2.3. dsjyw.store参数输入密钥库口令: voidking再次输入新口令: voidking您的名字与姓氏是什么?  [Unknown]:  VoidKing您的组织单位名称是什么?  [Unknown]:  Nenu您的组织名称是什么?  [Unknown]:  Nenu您所在的城市或区域名称是什么?  [Unknown]:  ChangChun您所在的省/市/自治区名称是什么?  [Unknown]:  JiLin该单位的双字母国家/地区代码是什么?  [Unknown]:  CNCN=VoidKing, OU=Nenu, O=Nenu, L=ChangChun, ST=JiLin, C=CN是否正确?  [否]:  y输入 &lt;dsjyw.keystore&gt; 的密钥口令        (如果和密钥库口令相同, 按回车):

2.4. 打包1、打开Hbuilder，发行，发行为原生安装包。
2、使用自有证书；Android包名是自动生成的，不需要修改，也可以修改为自己喜欢的包名；证书别名和私钥密码是生成keystore时配置的；证书文件选择生成的keystore文件。
3、打包，之后会自动上传源码，打包好后下载到app存放目录中。
2.5. 签名1、复制打包下载好的apk文件到dsjyw.keystore相同的目录，重命名apk文件为android-release-unsigned.apk。
2、打开cmd，执行签名命令jarsigner -verbose -keystore dsjyw.keystore -signedjar dsjyw.apk android-release-unsigned.apk dsjyw.keystore
说明：jarsigner -verbose -keystore 别名.keystore(密钥库位置) -signedjar 签名后产生的文件.apk要签名的文件.apk 别名.keystore(密钥库)
2.6. 查看签名keytool -v -list -keystore dsjyw.keystore
找到MD5: 70:1D:81:1E:D3:A6:8B:28:66:82:BA:D8:EB:79:DC:63
新浪创建移动应用时要求的是32位数字小写字母，把前面的这串数字中的冒号去掉，字母小写即可。
3. 腾讯开放平台3.1. 创建安卓应用1、登录腾讯开放平台
2、安卓应用，创建应用。
3、关联QQ互联，输入联盟网web应用的appid和appkey，确定。之所以关联QQ互联，是为了统一appid，否则，web登录和app登录获得的是不同的openid。
如果关联QQ互联后，创建应用失败，提示“对不起，您不是应用的创建者或协作者(1500401)”。
可能一：不是协作者。请先使用联盟网web应用所在账号登录，在管理中心页面，滑动鼠标到右上角用户名，单击权限权限管理，添加协作者。
可能二：账号冲突。之前的web应用在QQ互联上，此时，不要在浏览器中同时打开腾讯开放平台和QQ互联，也不要登录其他的QQ邮箱，有可能账号冲突。
可能三：开发者资料未提交审核。
4、选择移动应用 安卓，创建应用，选择应用类型，确定。此时创建的安卓应用，它的appid和appkey就和联盟网web应用相同了。
5、按照提示，填写需要的字段即可。需要注意的是，上传安装包时，需要上传签名后的apk文件。
6、填写好信息，保存即可，不需要提交审核。如果需要提交审核的话，请先根据提示加固应用。
3.2. 创建ios应用1、在安卓应用中，可以看到我们创建的联盟网app，单击它。
2、鼠标滑到右上角平台信息，单击IOS应用，即可创建IOS应用。该IOS应用的appid和appkey就和联盟网web应用相同，也和安卓应用相同。
警告，创建ios应用后，此时安卓应用和ios应用实际上是同一个应用。删除任意一个，两个应用就都没有了。
3、按照提示填写各字段即可。其中比较难的是URL scheme、AppStore ID和Bundle ID，这些信息可以到Apple Developer和itunes connect上查看到。
4. 新浪开放平台1、登录新浪开放平台
2、微连接，移动应用，立即接入。勾选Iphone和Android两个平台，创建。
3、填写应用基本信息。其中比较难的是Android签名，不必使用新浪提供的签名工具，参考本文中的“查看签名”小节，填入得到的32位字符串。
4、保存信息，提交审核。审核不通过会有详细提示，参考提示修改后再次提交审核。
5、单击左侧导航栏应用信息，高级信息。OAuth2.0 授权设置中点击编辑，授权回调页和取消授权回调页都填入https://api.weibo.com/oauth2/default.html。
5. 书签HBuilder 打包流程
腾讯开放平台创建应用的坑，安卓和iOS端如何共用同一个Appid？
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
        <tag>hbuilder</tag>
        <tag>ios</tag>
      </tags>
  </entry>
  <entry>
    <title>使用OpenCV实现实时视频目标检测</title>
    <url>/dev-object-detection/</url>
    <content><![CDATA[1. 前言老板与时俱进，开始带领大家搞机器学习。新任务：利用深度学习+python+opencv，实现实时视频目标检测。看教程，很简单的样子，搞一下。
参考文档：

《深度学习 + OpenCV，Python实现实时视频目标检测》
Real-time object detection with deep learning and OpenCV
Faster video file FPS with cv2.VideoCapture and OpenCV
用 Python 和 OpenCV 检测和跟踪运动对象
用树莓派 + Python + OpenCV 实现家庭监控和移动目标探测（下）
Python 3.x 安装opencv+opencv_contrib



2. 环境准备2.1. python1、参考《Anaconda》，安装Anaconda。
Anaconda常用命令：

查看环境，conda env list
进入环境，activate my_env
退出环境，deactivate
查看python版本，python --version
添加环境，conda create -n py3.6 python=3.6
删除环境，conda env remove -n py3.6
查看环境中的包，conda list
更新当前环境下的所有包，conda upgrade --all
安装包，conda install package_name

2、安装python3.6环境，以便使用对应版本的opencv。
conda create -n py3.6 python=3.6activate py3.6conda upgrade --all


PS：以下命令都是在python3.6环境中执行。
3、安装numpy、imutils
pip install numpypip install imutils

2.2. opencv1、访问Python Extension Packages for Windows，下载python对应版本的opencv。
比如郝同学下载的是opencv_python-3.3.0+contrib-cp36-cp36m-win_amd64.whl，cp36表示Python是3.6版本，win_amd64是表示安装的python是64bit的，+contrib表示包括contrib包。
2、下载好后，把它放到C盘中，执行安装命令：pip install C:\opencv_python-3.3.0+contrib-cp36-cp36m-win_amd64.whl
3. 运行代码3.1. 修改Adrian Rosebrock同学的原版代码适用于直接调用摄像头来获取视频流，尴尬的是，小生的电脑摄像头坏了，无法使用。于是，不得不修改代码，主要是把视频流的获取方式改为从本地获取。
# vs = VideoStream(src=0).start()# vs =cv2.VideoCapture(&#x27;C:\\Users\\voidking\\Desktop\\real-time-object-detection\\test_video.flv&#x27;)vs =cv2.VideoCapture(&#x27;./test_video.flv&#x27;)

# grab the frame from the threaded video stream and resize it# to have a maximum width of 400 pixels# frame = vs.read()# frame = imutils.resize(frame, width=400)# grab the frame from the threaded video file stream(grabbed,frame) = vs.read()# if the frame was not grabbed, then we have reached the end# of the streamif not grabbed:    breakframe = imutils.resize(frame, width=800)


3.2. 运行推荐使用命令：
python real_time_object_detection.py -p ./MobileNetSSD_deploy.prototxt.txt -m ./MobileNetSSD_deploy.caffemodel

或者，指定绝对路径，假设项目目录为C:\Users\voidking\Desktop\real-time-object-detection\，那么命令如下：
python real_time_object_detection.py -p &quot;C:\Users\voidking\Desktop\real-time-object-detection\MobileNetSSD_deploy.prototxt.txt&quot; -m &quot;C:\Users\voidking\Desktop\real-time-object-detection\MobileNetSSD_deploy.caffemodel&quot;

3.3. 进阶修改我们看到，prototxt和model都是指定的，那我们的视频文件也用这种方式指定，就更加友好一点。
# construct the argument parse and parse the argumentsap = argparse.ArgumentParser()ap.add_argument(&quot;-p&quot;, &quot;--prototxt&quot;, required=True,    help=&quot;path to Caffe &#x27;deploy&#x27; prototxt file&quot;)ap.add_argument(&quot;-m&quot;, &quot;--model&quot;, required=True,    help=&quot;path to Caffe pre-trained model&quot;)ap.add_argument(&quot;-c&quot;, &quot;--confidence&quot;, type=float, default=0.2,    help=&quot;minimum probability to filter weak detections&quot;)args = vars(ap.parse_args())

我们插入一行：
ap.add_argument(&quot;-v&quot;, &quot;--video&quot;, required=True,    help=&quot;path to Caffe video file&quot;)

然后在初始化视频流时，修改为：
vs =cv2.VideoCapture(args[&quot;video&quot;])

运行命令修改为：
python real_time_object_detection.py -p ./MobileNetSSD_deploy.prototxt.txt -m ./MobileNetSSD_deploy.caffemodel -v ./test_video.flv

4. 运行效果
5. 源码分享https://github.com/voidking/object-detection.git
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>opencv</tag>
        <tag>深度学习</tag>
        <tag>anaconda</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python获取命令行参数</title>
    <url>/dev-python-command-arg/</url>
    <content><![CDATA[1. Python获取命令行参数概述在执行Python脚本时，很多时候需要从命令行获取用户输入的参数。
Pthon获取命令行参数的方式有多种，具体包括：

sys.argv
getopt
argparse
click

个人感觉其中最简单的是sys.argv，既简单又优雅的是argparse。本文，我们就来学习一下argparse的使用。
参考文档：

Python 命令行参数
Python 命令行参数的3种传入方式



2. argparse示例1、编写 test.py，内容如下
#!/usr/bin/env python# -*- coding: utf-8 -*-import argparse# construct the argument parse and parse the argumentsap = argparse.ArgumentParser()ap.add_argument(&quot;-i&quot;, &quot;--input&quot;, required=True, help=&quot;input filename&quot;)ap.add_argument(&quot;-o&quot;, &quot;--output&quot;, default=&#x27;output.txt&#x27;, help=&quot;output filename&quot;)print(ap.parse_args())print(vars(ap.parse_args()))args = ap.parse_args()print(f&#x27;input: &#123;args.input&#125;&#x27;)print(f&#x27;output: &#123;args.output&#125;&#x27;)

2、测试执行
python test.pypython test.py -i input.txt -o output.txtpython test.py -i input.txt


]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>从零开始打造自己的JavaScript框架——第0章</title>
    <url>/dev-vkjs-0/</url>
    <content><![CDATA[1. 概念简析库、插件、框架、加载项、扩展和控件都是组件。
组件（Component）是一个含义很大的概念，一般是指软件系统的一部分，承担了特定的职责，可以独立于整个系统进行开发和测试，一个良好设计的组件应该可以在不同的软件系统中被使用（可复用）。例如V8引擎是Chrome浏览器的一部分，负责运行javascript代码，这里V8引擎就可以视为一个组件。V8引擎同时也是node.js的javascript解释器，这体现了组件的可复用性。
库（Library）是一系列预先定义好的数据结构和函数（对于面向对象语言来说，是类）的集合，程序员通过使用这些数据结构和函数实现功能。例如Moment.js是一个javascript库，提供了处理时间的一些函数。在js中，插件和库的含义相同，我们也可以说Moment.js是一个插件。
框架（Framework）也是一系列预先定义好的数据结构和函数，一般用于作为一个软件的骨架，但程序真正的功能还需要由开发者实现。框架和库的最大区别在于“控制反转”，当你使用一个库，你会调用库中的代码，而当你使用一个框架，框架会调用你的代码。框架和库是一个有交叉的概念，很多框架都是以库的形式发布的，例如Java的Spring MVC框架，其发布的jar包本身就是一个库。下图来自Library vs. Framework? ，从调用的角度说明了框架和库的关系：
来自知乎龚世伟的回答


2. 目标说到js框架和插件，我们可以想到jquery、zepto、requirejs、seajs、art-template、page.js、angularjs、vue等等。这些框架和插件有什么区别？来个表格比较。

    js框架和插件比较
    
        框架名称
        选择器
        DOM操作
        事件处理
        AJAX
        异步处理
        动画模块
        模块化管理依赖
        模板引擎
        路由管理
    
    
&lt;tr&gt;
    &lt;th&gt;jquery&lt;/th&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
    &lt;th&gt;zepto&lt;/th&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
    &lt;th&gt;requirejs&lt;/th&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
    &lt;th&gt;seajs&lt;/th&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
    &lt;th&gt;art-template&lt;/th&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
    &lt;th&gt;page.js&lt;/th&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;×&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
    &lt;th&gt;angularjs&lt;/th&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
    &lt;th&gt;vue&lt;/th&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
    &lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;



接下来，我们要开发一个js框架，类似于requirejs和seajs，实现模块化管理依赖的功能。
最早的时候，所有Javascript代码都写在一个文件里面，只要加载这一个文件就够了。后来，代码越来越多，一个文件不够了，必须分成多个文件，依次加载。下面的网页代码，相信很多人都见过。
&lt;script src=&quot;1.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;2.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;3.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;4.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;5.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;6.js&quot;&gt;&lt;/script&gt;

这段代码依次加载多个js文件，这样的写法有很大的缺点。首先，加载的时候，浏览器会停止网页渲染，加载文件越多，网页失去响应的时间就会越长；其次，由于js文件之间存在依赖关系，因此必须严格保证加载顺序（比如上例的1.js要在2.js的前面），依赖性最大的模块一定要放到最后加载，当依赖关系很复杂的时候，代码的编写和维护都会变得困难。
详情请参考Javascript模块化编程（三）：require.js的用法。
我们要编写的vkjs，就是为了解决这两个问题：（1）实现js文件的异步加载，避免网页失去响应；（2）管理模块之间的依赖性，便于代码的编写和维护。
3. 环境准备1、安装node，参考nvm项目。2、安装puer，参考超简单工具puer。
4. 异步加载首先，我们来看怎样实现异步加载。js的异步很容易实现，使用回调函数即可。
4.1. 引入jsjs的引入，不是写在页面中，那么，肯定是写在js中。参考JAVASCRIPT 装载和执行和Preload Javascript，我们可以写出如下代码：
;(function () &#123;    var vk_config = &#123;        root: &#x27;/&#x27;,        path: &#123;            &#x27;jquery&#x27;: &#x27;lib/jquery/jquery.min.js&#x27;        &#125;    &#125;;    var vk = &#123;        loadjs: function(script_filename,callback) &#123;            script_doc = document.getElementById(script_filename);            if(script_doc)&#123;                return;            &#125;            var script = document.createElement(&#x27;script&#x27;);            script.setAttribute(&#x27;id&#x27;, script_filename);            script.setAttribute(&#x27;type&#x27;, &#x27;text/javascript&#x27;);            script.setAttribute(&#x27;src&#x27;, vk_config.root + vk_config.path[script_filename]);            document.getElementsByTagName(&#x27;body&#x27;)[0].appendChild(script);            console.log(&#x27;loading:&#x27;+script_filename);            script.onload = script.onreadystatechange = function()&#123;                console.log(&#x27;loaded:&#x27;+script_filename);                callback();            &#125;        &#125;    &#125;;    window.vk = vk;&#125;)();

在页面中使用的时候，调用loadjs方法即可。
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;vkjs&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;vkjs测试页面&lt;/h2&gt;&lt;script src=&quot;../src/vk.js&quot;&gt;&lt;/script&gt;&lt;script&gt;    vk.loadjs(&#x27;jquery&#x27;,function()&#123;        console.log($(&#x27;h2&#x27;).html());    &#125;);     console.log(&#x27;页面加载完毕&#x27;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

查看控制台，我们可以看到执行顺序：
loading:jquery页面加载完毕loaded:jqueryvkjs测试页面

引入jquery的时候，该script是插入到页面最底部的，但是我们在回调函数中依然可以使用$，因为jquery加载完毕后我们才调用回调函数，这时就和页面位置无关了。
4.2. 引入多个js引入单个js，是比较容易的。而在开发时，常常需要引入多个js，如果多次调用loadjs方法的话，非常不友好。那么，怎样引入多个js文件？同样的，我们需要先加载js文件，等到所有js文件加载完成，调用回调函数即可。
;(function () &#123;    var vk_config = &#123;        root: &#x27;/&#x27;,        path: &#123;            &#x27;jquery&#x27;: &#x27;lib/jquery/jquery.min.js&#x27;,            &#x27;layer&#x27;: &#x27;lib/layer/layer.js&#x27;,            &#x27;template&#x27;: &#x27;lib/art-template/dist/template.js&#x27;        &#125;    &#125;;    var vk = &#123;        loadjs: function(script_filename,callback) &#123;            script_doc = document.getElementById(script_filename);            if(script_doc)&#123;                return;            &#125;            var script = document.createElement(&#x27;script&#x27;);            script.setAttribute(&#x27;id&#x27;, script_filename);            script.setAttribute(&#x27;type&#x27;, &#x27;text/javascript&#x27;);            script.setAttribute(&#x27;src&#x27;, vk_config.root + vk_config.path[script_filename]);            document.getElementsByTagName(&#x27;body&#x27;)[0].appendChild(script);            console.log(&#x27;loading:&#x27;+script_filename);            script.onload = script.onreadystatechange = function()&#123;                console.log(&#x27;loaded:&#x27;+script_filename);                callback();            &#125;        &#125;,        count_js: 0,        use: function(ids,callback)&#123;            var that = this;            if (!Array.isArray(ids)) &#123;                ids = [ids];            &#125;            that.count_js = ids.length;            for(var i=0;i&lt;ids.length;i++)&#123;                (function(i)&#123;                    script_doc = document.getElementById(ids[i]);                    if(script_doc)&#123;                        return;                    &#125;                    script = document.createElement(&#x27;script&#x27;);                    script.setAttribute(&#x27;id&#x27;, ids[i]);                    script.setAttribute(&#x27;type&#x27;, &#x27;text/javascript&#x27;);                    script.setAttribute(&#x27;src&#x27;, vk_config.root + vk_config.path[ids[i]]);                    document.getElementsByTagName(&#x27;body&#x27;)[0].appendChild(script);                    console.log(&#x27;loading:&#x27;+ids[i]);                    script.onload = script.onreadystatechange = function()&#123;                        console.log(&#x27;loaded:&#x27;+ids[i]);                         that.count_js--;                        if(that.count_js == 0)&#123;                            callback();                        &#125;                    &#125;                &#125;)(i);            &#125;        &#125;    &#125;;    window.vk = vk;&#125;)();

在页面使用的时候，调用use方法：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;vkjs&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;vkjs测试页面&lt;/h2&gt;&lt;script src=&quot;../src/vk.js&quot;&gt;&lt;/script&gt;&lt;script&gt;    vk.use([&#x27;jquery&#x27;,&#x27;template&#x27;],function()&#123;        console.log($(&#x27;h2&#x27;).html());    &#125;);     console.log(&#x27;页面加载完毕&#x27;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

查看控制台，我们可以看到执行顺序：
loading:jqueryloading:template页面加载完毕loaded:templateloaded:jqueryvkjs测试页面

4.3. 有序引入js很多时候，我们不止要引入多个js，而且要有顺序地引入js。为了实现有序加载js，又不想使用js回调重重嵌套，所以郝同学选择使用Promise。具体用法参考Javascript异步编程的4种方法、JavaScript Promise 告别异步乱嵌套、大白话讲解Promise（一）。
未完待续。。。
4.4. 管理依赖5. 源码分享https://github.com/voidking/vkjs.git
6. 书签从零开始编写自己的JavaScript框架（一）
从零开始编写自己的JavaScript框架（二）
jQuery源码解析（架构与依赖模块）
Query源码解析（架构与依赖模块）对应源码
Sea.js是如何工作的？
sea.js源码（Module.js核心代码）
JS模块加载器加载原理是怎么样的？
如何构建一个微型的CMD模块化加载器
如何实现一个 CMD 模块加载器
Grunt 实例之构建seajs项目
漫谈js自定义事件、DOM/伪DOM自定义事件
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>js</tag>
        <tag>框架</tag>
        <tag>puer</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：从零开始打造自己的PHP框架——第7章</title>
    <url>/dev-vkphp-7/</url>
    <content><![CDATA[1. 前言《thinkphp部署到nginx服务器》一文中，小生提到过，nginx默认情况下不支持pathinfo模式，从而不能支持ThinkPHP。能访问的，只有首页，其他函数的路径，都无法访问。
这是因为，PHP中的全局变量$_SERVER[&#39;PATH_INFO&#39;]，常常被用来优化url路径格式，比如thinkphp，而nginx是不支持pathinfo的。
同样的，小生开发的vkphp，虽然没有使用$_SERVER[&#39;PATH_INFO&#39;]，但是也使用到了$_SERVER，所以，也需要配置nginx支持pathinfo。


2. 完整demo1、首先，查看nginx配置文件的位置，ps aux | grep nginx
2、进入conf/vhost目录，添加enroll.voidking.com.conf文件。
server &#123;    listen 80;    server_name enroll.voidking.com;    root /home/wwwroot/enroll;    index index.html index.htm index.php;    error_page 404 /404.html;    location = /404.html &#123;        return 404 &#x27;Sorry, File not Found!&#x27;;    &#125;    error_page 500 502 503 504 /50x.html;    location = /50x.html &#123;        root /usr/local/nginx/html; # windows用户替换这个目录    &#125;    location / &#123;        try_files $uri @rewrite;    &#125;    location @rewrite &#123;        set $static 0;        if  ($uri ~ \.(css|js|jpg|jpeg|png|gif|ico|woff|eot|svg|css\.map|min\.map)$) &#123;            set $static 1;        &#125;        if ($static = 0) &#123;            rewrite ^/(.*)$ /index.php?s=/$1;        &#125;    &#125;    location ~ /Uploads/.*\.php$ &#123;        deny all;    &#125;    location ~ \.php/ &#123;       if ($request_uri ~ ^(.+\.php)(/.+?)($|\?)) &#123; &#125;       fastcgi_pass 127.0.0.1:9000;       include fastcgi_params;       fastcgi_param SCRIPT_NAME     $1;       fastcgi_param PATH_INFO       $2;       fastcgi_param SCRIPT_FILENAME $document_root$1;    &#125;    location ~ \.php$ &#123;        fastcgi_pass 127.0.0.1:9000;        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;        include fastcgi_params;    &#125;    location ~ /\.ht &#123;        deny  all;    &#125;&#125;

3、最后，重启nginx，./nginx -s reload
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>pathinfo</tag>
        <tag>框架</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：从零开始打造自己的PHP框架——第6章</title>
    <url>/dev-vkphp-6/</url>
    <content><![CDATA[1. 前言第0到5章，我们完成了一个属于自己的php框架，麻雀虽小五脏俱全。接下来，我们使用vkphp框架开发一个迷你项目——书签收藏。在开发过程中，也许会遇到很多问题，这时候我们就可以对框架进行调整，使之更加完善。
系统描述：用户登录系统，能够看到自己收藏的书签，并且可以对书签进行增删改查。
系统功能：1、注册登录。2、书签展示。3、增删查改。


2. 项目结构vkphp├─app│  ├─ctrl│  │  └─bookmark│  ├─model│  └─smarty│     ├─templates│     │  └─bookmark│     └─templates_c ├─assets├─config├─core├─log└─vendor


app内包含控制器、model、smarty模板。
config内包含全局配置文件。
core内包含框架的核心文件。
log内存放日志文件。
assets包含静态资源文件。
vendor内包含第三方库。

我们编写业务代码的位置，主要在app、assets两个目录。
3. 注册登录3.1. 表结构修改vk_user表结构为：
DROP TABLE IF EXISTS `vk_user`;CREATE TABLE `vk_user` (  `id` int(10) NOT NULL AUTO_INCREMENT,  `username` varchar(20) NOT NULL,  `password` varchar(32) NOT NULL COMMENT &#x27;md5加密&#x27;,  `salt` varchar(16) NOT NULL,  `screen_name` varchar(20) DEFAULT &#x27;低调的用户&#x27;,  `email` tinytext,  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3.2. modelapp/model目录下，已经有了user.php这个model类。
&lt;?phpnamespace app\model;class user extends \core\model&#123;    public function __construct()&#123;        parent::__construct(__CLASS__);    &#125;&#125;

3.3. 控制器app/ctrl/bookmark目录下，新建user_ctrl.php。
&lt;?phpnamespace app\ctrl\bookmark;class user_ctrl extends \core\render&#123;    public function index()&#123;        echo &#x27;user ctrl&#x27;;    &#125;    public function captcha()&#123;        $captcha = new \core\util\captcha();        $captcha-&gt;create(300,80,40);    &#125;    public function check()&#123;        if(!(isset($_REQUEST[&#x27;code&#x27;]) &amp;&amp; $_REQUEST[&#x27;code&#x27;] !== &#x27;&#x27;))&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-4&#x27;,                &#x27;ext&#x27;=&gt;&#x27;验证码不能为空&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);            return;        &#125;        $code = $_REQUEST[&#x27;code&#x27;];        $captcha = new \core\util\captcha();        $ret = $captcha-&gt;check($code);        if($ret == 0)&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;0&#x27;,                &#x27;ext&#x27;=&gt;&#x27;验证成功&#x27;            );        &#125;else if($ret == -1)&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-1&#x27;,                &#x27;ext&#x27;=&gt;&#x27;请先获取验证码&#x27;            );        &#125;else if($ret == -2)&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-2&#x27;,                &#x27;ext&#x27;=&gt;&#x27;验证码超时&#x27;            );        &#125;else if($ret == -3)&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-3&#x27;,                &#x27;ext&#x27;=&gt;&#x27;验证码错误&#x27;            );        &#125;        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;    public function reg()&#123;        if(!(isset($_POST[&#x27;username&#x27;])             &amp;&amp; isset($_POST[&#x27;password&#x27;])             &amp;&amp; isset($_POST[&#x27;password2&#x27;])            &amp;&amp; $_POST[&#x27;username&#x27;] !== &#x27;&#x27;            &amp;&amp; $_POST[&#x27;password&#x27;] !== &#x27;&#x27;            &amp;&amp; $_POST[&#x27;password2&#x27;] !== &#x27;&#x27;)        )&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-1&#x27;,                &#x27;ext&#x27;=&gt;&#x27;参数不能为空&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);            return;        &#125;        $username = $_POST[&#x27;username&#x27;];        $password = $_POST[&#x27;password&#x27;];        $password2 = $_POST[&#x27;password2&#x27;];        if($password !== $password2)&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-2&#x27;,                &#x27;ext&#x27;=&gt;&#x27;两次密码不同&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);            return;        &#125;        $salt = \core\util\random::get_random_string(16);        $password = md5($password.$salt);        $data = array(            &#x27;username&#x27;=&gt;$username,            &#x27;password&#x27;=&gt;$password,            &#x27;salt&#x27;=&gt;$salt        );        $user = new \app\model\user();        $ret = $user-&gt;find_by_condition([&#x27;username&#x27;=&gt;$username]);        if($ret)&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-2&#x27;,                &#x27;ext&#x27;=&gt;&#x27;用户名已存在&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);            return;        &#125;        $user_id = $user-&gt;add($data);        if($ret &gt;= 1)&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;0&#x27;,                &#x27;user_id&#x27;=&gt;$user_id,                &#x27;ext&#x27;=&gt;&#x27;注册成功&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;    &#125;    public function login()&#123;        if(!(isset($_POST[&#x27;username&#x27;])             &amp;&amp; isset($_POST[&#x27;password&#x27;])            &amp;&amp; $_POST[&#x27;username&#x27;] !== &#x27;&#x27;            &amp;&amp; $_POST[&#x27;password&#x27;] !== &#x27;&#x27;)        )&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-1&#x27;,                &#x27;ext&#x27;=&gt;&#x27;参数不能为空&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);            return;        &#125;        $username = $_POST[&#x27;username&#x27;];        $password = $_POST[&#x27;password&#x27;];        $user = new \app\model\user();        $ret = $user-&gt;find_by_condition([&#x27;username&#x27;=&gt;$username]);        if($ret)&#123;            $salt = $ret[&#x27;salt&#x27;];            $req_password = md5($password.$salt);            $real_password = $ret[&#x27;password&#x27;];            if($req_password == $real_password)&#123;                session_start();                $_SESSION[&#x27;user_id&#x27;] = $ret[&#x27;id&#x27;];                $result = array(                    &#x27;code&#x27;=&gt;&#x27;0&#x27;,                    &#x27;ext&#x27;=&gt;&#x27;登录成功&#x27;                );                echo json_encode($result,JSON_UNESCAPED_UNICODE);            &#125;        &#125;else&#123;            $result = array(                &#x27;code&#x27;=&gt;&#x27;-2&#x27;,                &#x27;ext&#x27;=&gt;&#x27;用户不存在&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;    &#125;    public function logout()&#123;        session_start();        if(isset($_SESSION[&#x27;user_id&#x27;]))&#123;            unset($_SESSION[&#x27;user_id&#x27;]);            $result = array(                &#x27;code&#x27;=&gt;&#x27;0&#x27;,                &#x27;ext&#x27;=&gt;&#x27;下线成功&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;                &#125;    public function to_login()&#123;        $this-&gt;smarty-&gt;assign(&#x27;basepath&#x27;,$this-&gt;basepath);        $this-&gt;smarty-&gt;assign(&#x27;assets&#x27;,$this-&gt;assets);        $this-&gt;smarty-&gt;display(&#x27;bookmark/user/login.html&#x27;);    &#125;&#125;

3.4. 前端如果需要详细代码，请到源码中查看，下面只描述思路。1、实现静态页面的最终效果，包括html、css和js。2、把静态页面改写为smarty页面。3、使用postman测试写好的注册登录接口。4、打通前后端。
访问地址： http://vkphp.dev/bookmark/user/to_login ，即可看到注册登录页面。
4. 源码分享https://github.com/voidking/vkphp/releases/tag/v1.6.0
5. 书签从零开始打造自己的PHP框架
session与PHP之session_start()
PHP session详解
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>框架</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：从零开始打造自己的PHP框架——第5章</title>
    <url>/dev-vkphp-5/</url>
    <content><![CDATA[1. 目标使用medoo插件来连接和操作数据库。


2. medoo基础2.1. 安装medoo1、在composer中添加依赖：
&quot;catfan/medoo&quot;: &quot;*&quot;

2、更新依赖composer update
2.2. 使用medoo1、config目录添加medoo_config.php为：
&lt;?phpreturn array(    &#x27;database_type&#x27; =&gt; &#x27;mysql&#x27;,    &#x27;database_name&#x27; =&gt; &#x27;vkphp&#x27;,    &#x27;server&#x27; =&gt; &#x27;localhost&#x27;,    &#x27;username&#x27; =&gt; &#x27;root&#x27;,    &#x27;password&#x27; =&gt; &#x27;&#x27;,    &#x27;charset&#x27; =&gt; &#x27;utf8&#x27;,    &#x27;port&#x27; =&gt; 3306,// 可选参数    &#x27;prefix&#x27; =&gt; &#x27;vk_&#x27;,// 可选，定义表的前缀);

2、core目录添加medoo.php为：
&lt;?phpnamespace core;class medoo extends \Medoo\Medoo&#123;    public function __construct()&#123;        $conf = \core\conf::all(&#x27;medoo_config&#x27;);        parent::__construct($conf);    &#125;&#125;

3、app/ctrl/indexCtrl.php中添加函数：
public function medoo()&#123;    $medoo = new \core\medoo();    dump($medoo);&#125;

访问 http://vkphp.dev/index/medoo ，即可看到medoo的相关信息。
2.3. 增删查改2.3.1. 查找$ret = $medoo-&gt;select(&#x27;user&#x27;,&#x27;*&#x27;,[&#x27;username&#x27;=&gt;&#x27;voidking&#x27;]);dump($ret);

2.4. 插入$data = array(    &#x27;username&#x27;=&gt;&#x27;voidking1&#x27;,    &#x27;password&#x27;=&gt;&#x27;voidking1&#x27;);$ret = $medoo-&gt;insert(&#x27;user&#x27;,$data);dump($ret);
因为medoo的版本问题（1.4.5），此时并没有打印出这条记录的id，而是PDOStatement object。此时，要想获取记录id，有两个方案：
方案一：从medoo中获取
dump($medoo-&gt;id());

方案二：找到vender/catfan/medoo/src/Medoo.php的第1173行，修改为
$this-&gt;exec(&#x27;INSERT INTO &#x27; . $this-&gt;tableQuote($table) . &#x27; (&#x27; . implode(&#x27;, &#x27;, $fields) . &#x27;) VALUES &#x27; . implode(&#x27;, &#x27;, $stack), $map);return $this-&gt;pdo-&gt;lastInsertId();

2.5. 删除$ret = $medoo-&gt;delete(&#x27;user&#x27;,[&#x27;username&#x27;=&gt;&#x27;voidking2&#x27;]);dump($ret-&gt;rowCount()); // 受影响的行数

2.6. 修改$ret = $medoo-&gt;update(&#x27;user&#x27;,[&#x27;username&#x27;=&gt;&#x27;voidking2&#x27;],[&#x27;username&#x27;=&gt;&#x27;voidking1&#x27;]);dump($ret-&gt;rowCount());

3. medoo进阶3.1. 回顾yii框架，对于每个表，我们都要创建一个model类，继承ActiveRecord类。比如：
&lt;?phpnamespace app\models;use yii\db\ActiveRecord;// Project.phpclass Project extends ActiveRecord&#123;    public static function model($className=__CLASS__)    &#123;        return parent::model($className);    &#125;    public function getTenProject()&#123;        // 具体实现    &#125;&#125;

在使用的时候，新建一个model对象，调用其中的方法即可：
$project = new Project();

thinkphp框架，对于每个表，我们不用创建model类。在使用的时候，直接新建一个默认model对象，传入表名，调用其中的方法即可：
$project = M(&#x27;project&#x27;);

当然，thinkphp也可以自定义model类，继承Model类。比如：
&lt;?php// ProjectModel.class.phpclass ProjectModel extends Model&#123;    public function getTenProject()&#123;        // 具体实现    &#125;&#125;

使用D方法新建自定义model对象，找不到定义类的情况下会调用M方法：
$project = D(&#x27;project&#x27;);

yii和thinkphp的两种实现方案，异曲同工，同样都包含了默认model类和自定义model类。yii中，默认model类为ActiveRecord，如果要自定义model类，继承它即可。thinkphp中，默认model类为Model，如果要自定义model类，继承它即可。
3.2. model.php鉴于yii的实现方式，我们也来新建一个model基类，其他的model类都继承它。
1、core目录下，新建model.php，内容如下：
&lt;?phpnamespace core;class model extends \core\medoo&#123;    public $table_name = &#x27;&#x27;;    public function __construct($table_name)&#123;        $name_arr = explode(&#x27;\\&#x27;,$table_name);        $this-&gt;table_name = strtolower(end($name_arr));        parent::__construct();        //dump($this-&gt;table_name);    &#125;    public function list_all()&#123;        $ret = $this-&gt;select($this-&gt;table_name,&#x27;*&#x27;);        return $ret;    &#125;    public function find_by_id($id)&#123;        $ret = $this-&gt;select($this-&gt;table_name,&#x27;*&#x27;,[&#x27;id&#x27;=&gt;$id]);        return $ret;    &#125;    public function find_by_condition($condition)&#123;        $ret = $this-&gt;select($this-&gt;table_name,&#x27;*&#x27;,$condition);        return $ret;    &#125;    public function add($data)&#123;        $ret = $this-&gt;insert($this-&gt;table_name,$data);        return $this-&gt;id();    &#125;    public function del($condition)&#123;        $ret = $this-&gt;delete($this-&gt;table_name,$condition);        return $ret-&gt;rowCount(); // 受影响的行数    &#125;    public function edit($data,$condition)&#123;        $ret = $this-&gt;update($this-&gt;table_name,$data,$condition);        return $ret-&gt;rowCount();    &#125;&#125;

2、在app/model目录中新建user.php，内容如下：
&lt;?phpnamespace app\model;class user extends \core\model&#123;    public function __construct()&#123;        parent::__construct(__CLASS__);    &#125;&#125;

3、在app/ctrl/indexCtrl.php中添加函数：
$user = new \app\model\user();dump($user-&gt;list_all());dump($user-&gt;find_by_id(1));dump($user-&gt;find_by_condition([&#x27;username&#x27;=&gt;&#x27;voidking&#x27;]));dump($user-&gt;add([&#x27;username&#x27;=&gt;&#x27;voidking1&#x27;,&#x27;password&#x27;=&gt;&#x27;voidking1&#x27;]));dump($user-&gt;edit([&#x27;username&#x27;=&gt;&#x27;voidking2&#x27;],[&#x27;username&#x27;=&gt;&#x27;voidking1&#x27;]));dump($user-&gt;del([&#x27;username&#x27;=&gt;&#x27;voidking2&#x27;]));

访问 http://vkphp.dev/index/model ，可以看到操作结果：
操作完成后，也许会感觉数据库表比较乱。这时可以清空表，并且重新从1开始自增id。truncate vk_user;
4. 小结至此，整个框架已经基本完成。接下来，我们会开发一个迷你项目，在开发的过程中，测试并改进我们的框架。
5. 源码分享https://github.com/voidking/vkphp/releases/tag/v1.5.0
6. 书签从零开始打造自己的PHP框架
Medoo官方文档
Medoo中文文档
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>框架</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：从零开始打造自己的PHP框架——第4章</title>
    <url>/dev-vkphp-4/</url>
    <content><![CDATA[1. ComposerComposer 是 PHP5.3以上 的一个依赖管理工具。它允许你申明项目所依赖的代码库，它会在你的项目中为你安装他们。Composer 不是一个包管理器。是的，它涉及 “packages” 和 “libraries”，但它在每个项目的基础上进行管理，在你项目的某个目录中（例如 vendor）进行安装。默认情况下它不会在全局安装任何东西。因此，这仅仅是一个依赖管理。
本篇，我们就在自己的项目中使用composer来进行依赖管理。


2. 安装Composer1、访问Composer下载地址，下载安装对应平台的composer，安装方法参照官方文档。
2、打开命令行，输入composer -V，测试是否安装成功。
3. 使用Composer3.1. 初次使用1、在项目根目录下新建composer.json，内容如下：
&#123;    &quot;name&quot;: &quot;VKPHP&quot;,    &quot;description&quot;: &quot;A PHP Framework for php developer&quot;,    &quot;type&quot;: &quot;Framework&quot;,    &quot;keywords&quot;: [        &quot;PHP&quot;,&quot;PHP Framework&quot;,&quot;VKPHP&quot;    ],    &quot;homepage&quot;: &quot;http://www.voidking.com&quot;,    &quot;time&quot;: &quot;2017-09-14&quot;,    &quot;license&quot;: &quot;MIT&quot;,    &quot;authors&quot;: [&#123;        &quot;name&quot;: &quot;VoidKing&quot;,        &quot;email&quot;: &quot;voidking@qq.com&quot;,        &quot;homepage&quot;: &quot;http://www.voidking.com&quot;,        &quot;role&quot;: &quot;Student&quot;    &#125;],    &quot;require&quot;: &#123;        &quot;PHP&quot;: &quot;&gt;=5.3.0&quot;,        &quot;smarty/smarty&quot;: &quot;~3.1&quot;    &#125;&#125;

需要解释的，是require字段，要求PHP大于等于5.3，smarty大于等于3.1且小于等于4.0。
版本号选择参照下表：
2、安装依赖在根目录打开命令行，输入composer，如果composer.json文件有错误会出现提示，没有错误则提示可选参数和功能。输入composer install，即可安装依赖到vendor目录中，并且自动生成composer.lock和vendor/autoload.php。
3.2. 更新依赖1、在composer.json中添加依赖，whoops任意版本，var-dumper任意版本。
&quot;filp/whoops&quot;: &quot;*&quot;,&quot;symfony/var-dumper&quot;: &quot;*&quot;


2、安装依赖composer install，提示
Loading composer repositories with package informationInstalling dependencies (including require-dev) from lock fileNothing to install or updateGenerating autoload files

从提示可以看出，composer install读取的是composer.lock中的依赖，而不是最新的composer.json。
所以，我们要使用命令composer update，这样就可以下载新写入的依赖，并且更新composer.lock。
3.3. 更换Composer源如果在composer install或composer update时非常慢，那么我们可以更换Composer源。
访问Composer中国镜像站，参照文档，在composer.json所在目录中执行：composer config repo.packagist composer https://packagist.phpcomposer.com
或者，直接在composer.json中添加：
&quot;repositories&quot;: &#123;    &quot;packagist&quot;: &#123;        &quot;type&quot;: &quot;composer&quot;,        &quot;url&quot;: &quot;https://packagist.phpcomposer.com&quot;    &#125;&#125;

4. 使用依赖4.1. smarty1、当前，lib目录和vendor目录中都包含了smarty，使用verdor中的smarty会更有格调，遂弃用lib目录，改用vendor作为第三方库目录。
2、根目录下index.php修改为：
// define(&#x27;LIB&#x27;,VKPHP.&#x27;/lib&#x27;); //第三方库所在目录define(&#x27;LIB&#x27;,VKPHP.&#x27;/vendor&#x27;); //第三方库所在目录include LIB.&#x27;/autoload.php&#x27;; //自动加载第三方库

3、core目录下render.php修改为：
// require_once(LIB.&#x27;/smarty/libs/Smarty.class.php&#x27;);
因为index.php中自动加载了所有第三方库，所以我们不再需要自己引入。
访问 http://vkphp.dev/index/render2 ，smarty正常工作。
4.2. whoops根目录index.php修改为：
if(DEBUG)&#123;    ini_set(&#x27;display_errors&#x27;, &#x27;On&#x27;);    $whoops = new \Whoops\Run;    $whoops-&gt;pushHandler(new \Whoops\Handler\PrettyPageHandler);    $whoops-&gt;register();&#125;else&#123;    ini_set(&#x27;display_errors&#x27;, &#x27;Off&#x27;);&#125;

访问 http://vkphp.dev/index/render3 ，即可看到炫酷的报错页面，没错，whoops就是一个错误显示插件。
4.3. var-dumper修改app/ctrl/indexCtrl.php中的data函数为：
public function data()&#123;    $db = new \core\db();    $sql = &#x27;select * from vk_user&#x27;;    $result = $db-&gt;query($sql);    dump($result);    dump($result-&gt;fetchAll());&#125;

访问 http://vkphp.dev/index/data ，即可看到炫酷的变量输出页面，没错，var-dumper是一个变量显示插件。
5. 源码分享https://github.com/voidking/vkphp/releases/tag/v1.4.0
6. 书签从零开始打造自己的PHP框架
Composer中文网
Composer中文文档
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>框架</tag>
      </tags>
  </entry>
  <entry>
    <title>使用lanproxy进行内网穿透</title>
    <url>/dev-lanproxy/</url>
    <content><![CDATA[1. 内网穿透《微信本地调试》一文中，郝同学提到了使用ngrok、natapp和花生壳进行内网穿透。但是，想要使用自定义域名，都是要收费的。
本文中，我们要搭建一个免费的内网穿透服务器。内网穿透服务器，可选的软件有lanproxy、frp、n2n等等，今天我们选择的是lanproxy。

lanproxy源码地址
业余草推荐一款局域网（内网）穿透工具lanproxy
frp源码地址
frp中文文档
使用frp实现内网穿透
n2n源码地址
n2n内网穿透神器(一条命令实现穿透)
n2n内网穿透神器



2. 主机资源1、一台公网服务器（运行proxy-server）。2、一台内网pc或服务器（运行proxy-client）。
3. 安装lanproxy服务端3.1. 安装java参考《全平台安装JDK》。
3.2. 安装lanproxy1、下载lanproxy项目访问lanproxy下载地址，下载proxy-server-0.1.zip，上传到公网服务器。
或者，直接在服务器上下载
wget https://github.com/ffay/lanproxy/files/1274739/proxy-server-0.1.zipcurl -C - -O -L https://github.com/ffay/lanproxy/files/1274739/proxy-server-0.1.zip

2、解压安装
unzip proxy-server-0.1.zipmv proxy-server-0.1 /usr/local/

3、修改配置文件
vim /usr/local/proxy-server-0.1/conf/config.properties
修改管理员的用户名和密码。
4、启动服务
cd /usr/local/proxy-server-0.1/binchmod +x startup.sh./startup.sh

5、访问 http://host_ip:8090 ，即可看到登录界面。
3.3. nginx反向代理1、添加域名解析 local 到公网服务器ip。
2、在nginx虚拟主机配置目录中，添加local.voidking.com.conf，内容如下：
server &#123;    listen 80;    server_name local.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        client_max_body_size       1024m;        client_body_buffer_size    128k;        client_body_temp_path      data/client_body_temp;        proxy_connect_timeout      90;        proxy_send_timeout         90;        proxy_read_timeout         90;        proxy_buffer_size          4k;        proxy_buffers              4 32k;        proxy_busy_buffers_size    64k;        proxy_temp_file_write_size 64k;        proxy_temp_path            data/proxy_temp;                proxy_pass http://127.0.0.1:8090;    &#125;&#125;

3、重启nginx
mkdir -p /usr/local/nginx/data/client_body_tempmkdir -p /usr/local/nginx/data/proxy_tempnginx -tnginx -s reload

4、测试访问访问 http://local.voidking.com/ ，即可看到登录界面。
4. 使用lanproxy4.1. 服务端配置1、登录lanproxy，添加客户端，输入客户端备注名称，生成随机密钥，提交添加。
2、客户端列表中，配置管理中，都会出现新添加的客户端。
3、单击配置管理中的客户端，添加配置（每个客户端可以添加多个配置）。

代理名称，推荐输入客户端要代理出去的端口，或者是客户端想要发布到公网的项目名称。
公网端口，填入一个服务器空闲端口，用来转发请求给客户端。
代理IP端口，填入客户端端口，公网会转发请求给该客户端端口。

4.2. 客户端配置1、下载lanproxy项目访问lanproxy下载地址，下载proxy-client-0.1.zip，解压到喜欢的目录。
2、修改配置进入proxy-client-0.1/conf目录，修改config.properties为：
#与在proxy-server配置后台创建客户端时填写的秘钥保持一致；没有服务器可以登录 https://lanproxy.org/ 创建客户端获取秘钥client.key=7533f855416741d88732954991668715ssl.enable=truessl.jksPath=test.jksssl.keyStorePassword=123456#这里填写实际的proxy-server地址；没有服务器默认即可，自己有服务器的更换为自己的proxy-server（IP）地址server.host=local.voidking.com#proxy-server ssl默认端口4993，默认普通端口4900#ssl.enable=true时这里填写ssl端口，ssl.enable=false时这里填写普通端口server.port=4993

3、启动lanproxy客户端进入proxy-client-0.1/bin目录，双击startup.bat
如果启动失败，一般是因为jdk没有安装配置成功，参考《IDEA的常用配置》中的安装jdk，安装配置jdk后再次启动即可。
4、测试访问访问地址 http://local.voidking.com:50000/ ，即可看到本地访问客户端80端口相同的页面。
至此，代理成功！
5. 进阶配置5.1. 一个端口一个项目假设，我们本地的4000端口开启了node服务。那么，怎么把这个服务优雅地提供给整个互联网？
1、服务端添加配置
2、启动本地node服务
3、已经启动lanproxy客户端，访问 http://local.voidking.com:50001/
此时，整个互联网都能访问到这个node项目，但是，带着端口号很不友好。那么，我们就给这个项目添加一个单独的域名。
1、添加域名解析 node.local 到公网ip。
2、在nginx虚拟主机配置目录中，添加node.local.voidking.com.conf，内容如下：
server &#123;    listen 80;    server_name node.local.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        client_max_body_size       1024m;        client_body_buffer_size    128k;        client_body_temp_path      data/client_body_temp;        proxy_connect_timeout      90;        proxy_send_timeout         90;        proxy_read_timeout         90;        proxy_buffer_size          4k;        proxy_buffers              4 32k;        proxy_busy_buffers_size    64k;        proxy_temp_file_write_size 64k;        proxy_temp_path            data/proxy_temp;                proxy_pass http://127.0.0.1:50001;    &#125;&#125;

3、重启nginx
nginx -tnginx -s reload

4、测试访问访问地址 http://node.local.voidking.com/ ，即可看到本地node服务。
5.2. 一个端口多个项目1、通过我们开放出的80端口，可以访问web根目录下的很多项目，比如在其他文章中提到过的basic项目和vkphp项目，下文以vkphp项目为例。
2、当前，vkphp项目首页是简单的文字显示。
3、通过外网访问的地址为 http://local.voidking.com:50000/vkphp
此时，整个互联网都能访问到这个vkphp项目，但是，带着端口号和项目名，感觉像是个欺诈网站。那么，我们能否给这个项目添加一个单独的域名呢？当然也是可以的。
1、添加域名解析 vkphp.local 到公网ip。
2、在nginx虚拟主机配置目录中，添加vkphp.local.voidking.com.conf，内容如下：
server &#123;    listen 80;    server_name vkphp.local.voidking.com;    charset utf-8;    location /&#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        client_max_body_size       1024m;        client_body_buffer_size    128k;        client_body_temp_path      data/client_body_temp;        proxy_connect_timeout      90;        proxy_send_timeout         90;        proxy_read_timeout         90;        proxy_buffer_size          4k;        proxy_buffers              4 32k;        proxy_busy_buffers_size    64k;        proxy_temp_file_write_size 64k;        proxy_temp_path            data/proxy_temp;                proxy_pass http://127.0.0.1:50000;    &#125;&#125;

3、重启nginx
./nginx -s reload

4、打开本地apache的http-vhosts.conf，添加配置：
&lt;VirtualHost *:80&gt; #laragon magic!    DocumentRoot &quot;C:/laragon/www/vkphp/&quot;    ServerName vkphp.local.voidking.com    ServerAlias vkphp.local.voidking.com&lt;/VirtualHost&gt;

5、重启本地apache
6、测试访问访问地址 http://vkphp.local.voidking.com/ ，即可看到本地vkphp项目。
访问时该地址会自动在后面加上vkphp，成为 http://vkphp.local.voidking.com/vkphp/
6. 结语需要说明的是，在使用centos7作为lanproxy外网服务器的时候，无法代理内网服务器的22端口。所有的ssh连接请求都被centos7当做恶意请求自己处理了，并不会转发到内网。而使用ubuntu16作为lanproxy外网服务器，就没有这个问题。很神奇，猜测是个bug，暂时没有找到原因。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>nginx</tag>
        <tag>apache</tag>
        <tag>内网穿透</tag>
        <tag>代理</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：从零开始打造自己的PHP框架——第3章</title>
    <url>/dev-vkphp-3/</url>
    <content><![CDATA[1. 目标本篇，我们的目标有三个：

调整项目结构
加载配置类
添加日志类



2. 调整项目结构vkphp├─app│  ├─ctrl│  ├─model│  ├─smarty│  └─view├─config├─core├─lib├─log└─public


app内包含控制器、数据库model、smarty模板、普通渲染模板。
config内包含全局配置文件。
core内包含框架的核心文件。
lib内包含第三方库。
log内存放日志文件。
public包含静态资源文件。

3. 加载配置类我们之前的路由配置、数据库配置都是写在程序中的，修改起来不方便，下面我们把这些配置都放在配置文件中。
3.1. conf.php在core目录中，新建conf.php，内容如下：
&lt;?phpnamespace core;class conf&#123;    public function get($name,$file)&#123;        /**         * 1、判断文件是否存在         * 2、判断配置是否存在         */        $file_path = CONFIG.&#x27;/&#x27;.$file.&#x27;.php&#x27;;        if(is_file($file_path))&#123;            $conf = include $file_path;            if(isset($conf[$name]))&#123;                    return $conf[$name];            &#125;else&#123;                throw new \Exception(&#x27;没有配置项&#x27;.$name);            &#125;        &#125;else&#123;            throw new \Exception(&#x27;找不到配置文件&#x27;.$file);        &#125;    &#125;    public function all($file)&#123;        $file_path = CONFIG.&#x27;/&#x27;.$file.&#x27;.php&#x27;;        if(is_file($file_path))&#123;            $conf = include $file_path;            return $conf;        &#125;else&#123;            throw new \Exception(&#x27;找不到配置文件&#x27;.$file);        &#125;    &#125;&#125;

3.2. 配置类在config目录中，新建配置文件route_config.php和db_config.php。
&lt;?phpreturn array(    &#x27;CTRL&#x27;=&gt;&#x27;index&#x27;,    &#x27;ACTION&#x27;=&gt;&#x27;index&#x27;);

&lt;?phpreturn array(    &#x27;DSN&#x27;=&gt;&#x27;mysql:host=localhost;dbname=vkphp&#x27;,    &#x27;USER&#x27;=&gt;&#x27;root&#x27;,    &#x27;PASSWD&#x27;=&gt;&#x27;&#x27;);

3.3. 使用修改core目录中的route.php和db.php。
&lt;?php/** * 路由控制 */namespace core;class route&#123;    public $ctrl=&#x27;index&#x27;;    public $action=&#x27;index&#x27;;    public $params=array();    public function __construct()&#123;        //echo &#x27;route is ready!&#x27;;        /**         * 1、隐藏index.php         * 2、获取URL中的控制器和方法         * 3、获取URL中的参数         */        $this-&gt;ctrl = \core\conf::get(&#x27;CTRL&#x27;,&#x27;route_config&#x27;);        $this-&gt;action = \core\conf::get(&#x27;ACTION&#x27;,&#x27;route_config&#x27;);        // 其他不变&#125;

&lt;?phpnamespace core;class db extends \PDO&#123;    public function __construct()&#123;        $conf = \core\conf::all(&#x27;db_config&#x27;);        $dsn = $conf[&#x27;DSN&#x27;];        $user = $conf[&#x27;USER&#x27;];        $passwd = $conf[&#x27;PASSWD&#x27;];        try&#123;            parent::__construct($dsn,$user,$passwd);            // echo &#x27;database connect success&#x27;;        &#125;catch (\Exception $e)&#123;            echo $e-&gt;getMessage();        &#125;    &#125;&#125;

4. 添加日志类4.1. log.php在core目录，添加log.php文件，内容如下：
&lt;?phpnamespace core;class log&#123;    public function log($message,$file_name)&#123;        $log_path = LOG.&#x27;/&#x27;.$file_name.&#x27;-&#x27;.date(&#x27;YmdHis&#x27;).&#x27;.log&#x27;;        $message = date(&#x27;Y-m-d H:i:s&#x27;).&#x27; &#x27;.$message;        file_put_contents($log_path,json_encode($message));    &#125;&#125;

4.2. 使用在indexCtrl.php中，添加：
public function log()&#123;    $log = new \core\log();    $log-&gt;log(&#x27;this is log&#x27;,&#x27;log_test&#x27;);    echo &#x27;成功写入日志&#x27;;&#125;

访问 http://vkphp.dev/index/log ，即可在log目录下生成日志文件。如果时间不正确，就在php.ini中搜索 date.timezone ，然后修改时区为：
data.timezone=&quot;Asia/Shanghai&quot;

或者，直接在程序代码中使用函数ini_set(&#39;date.timezone&#39;,&#39;Asia/Shanghai&#39;); ，或者date_default_timezone_set(‘Asia/Shanghai&#39;); 。
5. 源码分享https://github.com/voidking/vkphp/releases/tag/v1.3.0
6. 书签从零开始打造自己的PHP框架
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>框架</tag>
      </tags>
  </entry>
  <entry>
    <title>HBuilder Android真机调试</title>
    <url>/dev-hbuilder-android-debug/</url>
    <content><![CDATA[1. 关于调试难者不会，会者不难。对于调试，这句话尤其合适。无论是前端调试、Java调试、PHP调试、Python调试，还是我们这次要研究的HBuilder Android真机调试，都是一个道理。欲速则不达，磨刀不误砍柴工。花点时间，磨一磨调试这把刀。


2. 环境准备1、安装好Hbuilder。
2、安装好chrome。
3、电脑安装好360手机助手。
4、Android手机和连接线。
3. 启动调试模式以魅族pro6s和荣耀6为例。
3.1. 魅族pro6s1、设置，关于手机，在“版本号”的项目栏里，连续点击7次，就会提示开启开发人员选项。
2、回到设置界面，辅助功能，点进去即可看到“开发人员选项”，进入“开发人员选项”就可以打开“USB调试”了。
3、在开发人员选项界面中，下拉到最后，性能优化，高级日志输出，选择全部允许。
至此，魅族pro6s调试模式启动成功。然而，郝同学在调试时，可以启动应用，但是无法使用console.log()。
在拨号页，输入*#*#3646633#*#*，进入工程模式，侧滑到Log and Debugging，Debug Utils，调试等级调成Engineer Mode。然而，并没有什么用处。
备份HBuilder\tools\adbs中的adb.exe、AdbWinApi.dll、AdbWinUsbApi.dll，然后复制HBuilder\tools\adbs\1.0.31中的所有文件，粘贴到HBuilder\tools\adbs目录中。然而，也没有什么用处。
好吧，我的魅族pro6s有个坑，遂弃用。。。
3.2. 荣耀61、设置，关于手机，在“版本号”的项目栏里，连续点击7次，就会提示进入开发者模式；
2、回到设置界面，下拉即可看到“开发人员选项”。
3、点击开发人员选项界面里，点击开发人员选项后面的滑块，即可开启开发人员模式。
4、开发人员选项界面里，下拉打开“USB调试”。
可选操作：1、在拨号页，输入*#*#2846579#*#*，进入工程菜单。
2、后台设置，Log设置，点击勾选需要的日志。
4. 调试流程1、手机连接到电脑上。
2、启动360手机助手，连接手机，然后启动360演示。
3、启动HBuilder，ctrl+R，在手机上运行应用。
4、启动chrome，在地址栏输入 chrome://inspect ，打开设备检查页面。
file:///storage/emulated/0/Android/data/io.dcloud.HBuilder/.HBuilder/apps/HBuilder/www/pages/center/… 代表的是该文件在Android手机中的位置。
5、点击“inspect”，即可打开该页面的调试。如果启动了一个白屏界面，说明要翻墙才能使用。
6、之后的调试，与前端调试相同。
7、样式调整，打断点，在页面选择元素（在调试器中选择，或者在360演示中选择均可）。。。
至此，就可以愉快地进行Android真机调试了。
5. 书签HBuilder之Chrome调试Android手机Chrome调试Android应用（Debug）
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
        <tag>hbuilder</tag>
        <tag>chrome</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Nginx：Nginx URL大小写转换</title>
    <url>/dev-nginx-url-to-lowercase/</url>
    <content><![CDATA[1. url大小写问题将Windows下的项目（如：php）迁移到Linux下，由于Windows操作系统中，文件名是不区分大小写的；而Linux系统是大小写敏感，会导致有些网页出现404情况。那么，怎样Nginx实现url请求不区分大小写呢？运维笔记提供了四种解决方案，郝同学采用安装lua模块的方式。lua-nginx-module来自大牛agentzh的开源项目，在Nginx中嵌入Lua语言，使之可以支持强大Lua语法。


1.1. 安装lua1、下载luawget http://luajit.org/download/LuaJIT-2.0.2.tar.gz
2、安装luatar -xvzf LuaJIT-2.0.2.tar.gz
cd LuaJIT-2.0.2
make &amp;&amp; make install
3、配置lua在当前用户的home目录下，编辑.bashrc，添加：
export LUAJIT_LIB=&quot;/usr/local/lib&quot;export LUAJIT_INC=&quot;/usr/local/include/luajit-2.0&quot;

使配置立即生效，source .bashrc
1.2. 下载nginx1、查看nginx编译安装时安装了哪些模块/usr/local/nginx/sbin/nginx -V
结果为：
nginx version: nginx/1.10.0built by gcc 4.4.7 20120313 (Red Hat 4.4.7-17) (GCC) built with OpenSSL 1.0.1e-fips 11 Feb 2013TLS SNI support enabledconfigure arguments: --user=www --group=www --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_v2_module --with-http_gzip_static_module --with-ipv6 --with-http_sub_module

2、下载对应版本的nginxwget http://nginx.org/download/nginx-1.10.0.tar.gz
3、解压tar -xvzf nginx-1.10.0.tar.gz
1.3. 下载nginx-lua1、在opt目录下，下载nginx-lua模块wget https://github.com/simpl/ngx_devel_kit/archive/v0.2.19.tar.gz
wget https://github.com/chaoslawful/lua-nginx-module/archive/v0.10.2.tar.gz
2、解压模块tar -xvzf ngx_devel_kit-0.2.19.tar.gz
tar -xvzf lua-nginx-module-0.10.2.tar.gz
1.4. 编译安装nginx-lua1、进入nginx源码目录cd cd nginx-1.10.0
2、参考nginx -V中的参数，添加add-module编译参数：./configure --user=www --group=www --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_v2_module --with-http_gzip_static_module --with-ipv6 --with-http_sub_module --add-module=/opt/ngx_devel_kit-0.2.19 --add-module=/opt/lua-nginx-module-0.10.2
3、编译make
注意，一定不要make install
4、替换nginx二进制文件
cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
service nginx stop
cp ./objs/nginx /usr/local/nginx/sbin/
5、重新读取库文件ldconfig
6、启动nginx/usr/local/nginx/sbin/nginx -t
service nginx start
1.5. nginx-lua使用修改test.voidking.com.conf文件：
location / &#123;      if ($uri ~ [A-Z])&#123;          rewrite_by_lua &#x27;return ngx.redirect(string.lower(ngx.var.uri),ngx.HTTP_MOVED_PERMANENTLY)&#x27;;      &#125;  &#125;  

2. 书签Nginx实现url请求不区分大小写
已安装nginx动态添加模块
 error while loading shared libraries: libluajit-5.1.so.2
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>好好学Nginx</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：MySQL常用命令</title>
    <url>/dev-mysql-command/</url>
    <content><![CDATA[1. 前言mysql是郝同学使用最多的数据库，但是每次要手敲命令，还是得谷歌百度。好记性不如烂笔头，本文对常用mysql命令进行一些整理，方便再次拾起。


2. mysql中SQL语句的大小写规则mysql中，数据库名、表名、列名、索引名、SQL关键字、函数名都不区分大小写详情参考MySQL中SQL语句的大小写规则
3. 权限相关参考《MySQL重置密码》。
4. 数据库相关4.1. 创建/删除数据库-- 创建数据库create database vkdb default character set utf8 collate utf8_general_ci;-- 删除数据库drop database vkdb;

4.2. 创建/删除用户-- 创建用户create user &#x27;vkuser&#x27;@&#x27;%&#x27; identified by &#x27;vkpassword&#x27;;-- orinsert into mysql.user(Host,User,Password) values(&#x27;%&#x27;,&#x27;vkuser&#x27;,&#x27;vkpassword&#x27;);-- 删除用户drop user &#x27;vkuser&#x27;;drop user &#x27;vkuser&#x27;@&#x27;192.168.56.101&#x27;;

4.3. 授权数据库给用户-- 授权数据库给用户grant all privileges on vkdb.* to &#x27;vkuser&#x27;@&#x27;%&#x27; with grant option;-- orinsert into mysql.user(HOST,Db,USER,Select_priv,Insert_priv,Update_priv,Delete_priv,Create_priv,Drop_priv)    VALUES(&#x27;%&#x27;,&#x27;vkdb&#x27;,&#x27;vkuser&#x27;,&#x27;Y&#x27;,&#x27;Y&#x27;,&#x27;Y&#x27;,&#x27;Y&#x27;,&#x27;Y&#x27;,&#x27;Y&#x27;);-- 授权数据库给用户，同时指定密码-- 危险！这条命令会同时修改vkuser的密码，不区分数据库！grant all privileges on vkdb.* to &#x27;vkuser&#x27;@&#x27;%&#x27; identified by &#x27;vkpassword2&#x27; with grant option; -- 授权数据库给用户，同时指定主机和密码-- 只有通过192.168.56.101访问数据库时，使用密码vkpassword2，其他主机访问数据库时还是使用vkpasswordgrant all privileges on vkdb.* to &#x27;vkuser&#x27;@&#x27;192.168.56.101&#x27; identified by &#x27;vkpassword2&#x27; with grant option;-- 取消授权revoke all on vkdb.* from &#x27;vkuser&#x27;@&#x27;%&#x27;;-- 授权用户只允许登录权限grant usage on *.* to &#x27;vkuser&#x27;@&#x27;%&#x27;;-- 查看用户和授权show grants for &#x27;vkuser&#x27;@&#x27;%&#x27;;select User,Host from mysql.user;

5. 数据表相关5.1. 创建/删除数据表-- 创建数据表create table if not exists `user` (  `id` int(8) not null auto_increment,  `name` varchar(32) not null,  `password` varchar(32) not null default &#x27;&#x27;,  primary key (id)) engine=innodb default charset=utf8;-- 查看数据表describe table user;-- 删除数据表drop table `user`;-- 删除所有数据表select concat(&#x27;drop table &#x27;,table_name,&#x27;;&#x27;) FROM information_schema.`TABLES` WHERE table_schema=&#x27;vkdb&#x27;;


5.2. 添加/修改/删除column-- 添加columnalter table `user` add column `deleted1` bool not null default false;-- 修改columnalter table `user` change `deleted1` `deleted` bool not null default false;alter table `user` modify `deleted` int(1) not null default 0;alter table `user` modify column `deleted` int(1) not null default 0;-- 删除columnalter table `user` drop `deleted`;alter table `user` drop column `deleted`;

5.3. 设置unique-- 设置uniquealter table `user` modify `name` varchar(32) not null unique;alter table `user` modify column `name` varchar(32) not null unique;

5.4. 插入/修改/删除数据-- 插入数据insert into `user` (`name`,`password`) values(&#x27;haojin&#x27;,&#x27;voidking&#x27;);-- 修改数据update `user` set password=&#x27;haojin&#x27; where name=&#x27;haojin&#x27;;-- 删除数据delete from `user` where name=&#x27;haojin&#x27;;

5.5. 查询数据-- 查询数据select * from `user` where name=&#x27;haojin&#x27; and password=&#x27;voidking&#x27;;select * from `user` where name=&#x27;haojin&#x27; or password=&#x27;haojin&#x27;;select * from `user` where name&lt;&gt;&#x27;haojin0&#x27;;select * from `user` where name like &#x27;%ao%&#x27;;select * from `user` order by id asc;select * from `user` order by id desc;select * from `user` order by id desc limit 2;select * from `user` where unix_timestamp(update_time) &lt; unix_timestamp(&#x27;2020-03-01 10:14:51&#x27;);

5.6. 查看表是否被锁-- 查看表是否被锁SHOW OPEN TABLES;SHOW OPEN TABLES WHERE In_use &gt; 0;SHOW PROCESSLIST;SHOW TABLE STATUS LIKE &#x27;your_table_name&#x27;;

6. 导入导出数据6.1. 导出数据库数据mysqldump -uroot -ppassword vkdb &gt; /root/vkdb.sql

6.2. 导出数据库所有表结构mysqldump -uroot -ppassword -d vkdb &gt; /root/vkdb_structure.sql

6.3. 导出数据库user表数据mysqldump -uroot -ppassword vkdb user &gt; /root/vkdb_user.sql

6.4. 导出数据库user表结构mysqldump -uroot -ppassword -d vkdb user &gt; /root/user_structure.sql

CREATE DATABASE 语句之前加一个 DROP DATABASE
mysqldump -uroot -ppassword -d --add-drop-table vkdb user &gt; /root/user_structure.sql 

6.5. 导入数据库数据通过mysql命令导入
mysql -uroot -ppassword vkdb &lt; /root/vkdb.sqlmysql -uroot -ppassword -f vkdb &lt; /root/vkdb.sql

或者登录到mysql后执行导入
source /root/vkdb.sql;

6.6. csv数据导入使用dbeaver导入csv数据，可以自动创建表结构。
7. 从binlog导出sql语句首先确保mysql server开启了binlog
show variables like &#x27;%log_bin%&#x27;;

本地导出：
mysqlbinlog --base64-output=decode-rows -v --result-file=output.sql binlog.000001

binlog文件可能比较多，选择最近修改的那个导出。
远程导出：
mysqlbinlog --read-from-remote-server --host=host_name --user=user_name --password=password --base64-output=decode-rows -v --result-file=output.sql -v binlog.000001

8. shell执行sql8.1. shell中直接执行sql示例mysql -h 192.168.56.100 -uroot -pvoidking -D vkdb -e &quot;select * from user;&quot;

8.2. 获取数据写入到txt需求：mysql数据库，获取数据库vk中的app表中所有记录的name，写入到 name.txt 文件。
脚本：
#!/bin/bashsql=&quot;select name from app where app.deleted=0;&quot;data=$(mysql -h192.168.56.100 -uroot -pmypassword -s -e &quot;use vk;$&#123;sql&#125;&quot;)echo &quot;$&#123;data&#125;&quot; &gt; name.txt

8.3. 获取数据写入到csv需求：mysql数据库，获取数据库vk中的app表中所有记录的name和summary，写入到 app.csv 文件。脚本：
#!/bin/bashsql=&quot;select name,summary from app where app.deleted=0;&quot;data=$(mysql -h192.168.56.100 -uroot -pmypassword -s -e &quot;use vk;$&#123;sql&#125;&quot; | sed &#x27;s/\t/,/g&#x27;)echo &quot;$&#123;data&#125;&quot; &gt; app.csv

注意：假设name或者summary字段本身就包含制表符\t，那么最终导出的数据是会有问题的。这种情形建议使用python执行sql获取数据。
9. python执行sql需求：mysql数据库，获取数据库vk中的app表中所有记录的name和summary（summary字段中可能包含\t），写入到 app.xlsx 文件。
安装依赖：
pip install pandas pymysql openpyxl

脚本：
import pandas as pdimport pymysqlsql=&quot;select name,summary from app where app.deleted=0&quot;conn = pymysql.connect(host=&#x27;192.168.56.100&#x27;, user=&#x27;root&#x27;, password=&#x27;mypassword&#x27;, db=&#x27;vk&#x27;)df = pd.read_sql(sql, conn)df.to_excel(&quot;app.xlsx&quot;, index=False)conn.close()

10. 高级命令10.1. 联合查询left join（左联接）：返回左表中的所有记录以及和右表中的联接字段相等的记录。right join（右联接）：返回右表中的所有记录以及和左表中的联接字段相等的记录。inner join（等值联接）：只返回两个表中联接字段相等的记录。
例子：查询用户表（user）中的用户名（name）和用户详细表（user_detail）中的用户手机号码（telephone）。
select user.name, user_detail.telephonefrom user inner join user_detailon user.id=user_detail.user_idwhere user.id=1;-- orselect U1.name, U2.telephonefrom user as U1 inner join user_detail as U2on U1.id=U2.user_idwhere U1.id=1;

10.2. 联合查询3张表例子：查询用户表（user）中的用户名（name）、用户角色表（role）中的角色名（name）、用户详细表（user_detail）中的用户手机号码（telephone）。
select user.name, user_detail.telephone, role.namefrom user left join roleon user.role_id=role.idleft join user_detailon user.id=user_detail.user_idwhere user.id=1;

10.3. 查询重复记录需求：查询user表中的重名记录
select * from user where name in (select name from user group by name having count(name) &gt; 1);

10.4. 替换字符串需求：用户表（user）的个性签名字段（info）是字符串，字符串中都包含“my name is haojin”，现在需要把“my name is haojin”都替换成“my name is voidking”。
update user set info=replace(info,&quot;my name is haojin&quot;,&quot;my name is voidking&quot;) where info like &quot;%my name is haojin%&quot;;

10.5. 拼接命令例子：根据手机号17625160000查询用户，给该用户添加另外一个手机号17625160001，手机号用逗号分隔。
select name,mobiles from user where mobiles like &quot;%17625160000%&quot;;-- 查询出name=&quot;haojin&quot;,mobiles=&quot;17625160000&quot;update user set mobiles=&quot;17625160000,17625160001&quot; where name=&quot;haojin&quot;;

更简单的方法是使用concat拼接：
select CONCAT(&#x27;update user set mobiles=&quot;&#x27;,mobiles,&#x27;,17625161201&quot; where name=&quot;&#x27;,name,&#x27;&quot;;&#x27;) from user where mobiles like &quot;%17625160000%&quot;;

假设有很多用户需要另外添加手机号，使用拼接的方式也很麻烦，此时需要借助shell脚本。已有文本mobiles.txt，内容为：
17625160000 1762516000117625160002 17625160003

编写shell脚本为：
#!/bin/bash grep -v &quot;^$&quot; mobiles.txt | while read linedo  old=`echo $line | awk &#x27;&#123;print $1&#125;&#x27;`  new=`echo $line | awk &#x27;&#123;print $2&#125;&#x27;`  echo &quot;select CONCAT(&#x27;update user set mobiles=\&quot;&#x27;,mobiles,&#x27;,$&#123;new&#125;\&quot; where name=\&quot;&#x27;,name,&#x27;\&quot;;&#x27;) from user where mobiles like \&quot;%$&#123;old&#125;%\&quot;;&quot;done

10.6. 定时备份需求：每天自动备份数据库。1、测试命令
/usr/bin/mysqldump -h 127.0.0.1 -uroot -pvoidking vkdb &gt; /root/vkdb-`date +%Y%m%d-%H%M%S`.sql

2、测试定时任务
crontab -e

写入每分钟执行一次的定时任务：
# backup vkdbdb* * * * * /usr/bin/mysqldump -h 127.0.0.1 -uroot -pvoidking vkdb &gt; /root/vkdb-`date +\%Y\%m\%d-\%H\%M\%S`.sql

查看执行结果是否正常，如果不正常，通过crontab日志排查：
tail -f /var/log/cron

3、确认定时任务
# backup vkdbdb0 0 * * * /usr/bin/mysqldump -h 127.0.0.1 -uroot -pvoidking vkdb &gt; /root/vkdb-`date +\%Y\%m\%d-\%H\%M\%S`.sql

]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>mysql</tag>
        <tag>数据库</tag>
        <tag>好好学MySQL</tag>
      </tags>
  </entry>
  <entry>
    <title>使用HBuilder开发移动APP</title>
    <url>/dev-hbuilder-app/</url>
    <content><![CDATA[1. 前言HBuilder是DCloud（数字天堂）推出的一款支持HTML5的Web开发IDE。HBuilder的编写用到了Java、C、Web和Ruby。HBuilder本身主体是由Java编写，它基于Eclipse，所以顺其自然地兼容了Eclipse的插件。快，是HBuilder的最大优势，通过完整的语法提示和代码输入法、代码块等，大幅提升HTML、js、css的开发效率。
内置MUI框架，真正彻底的跨平台开发，不是简单的跨iOS和Android。基于mui，一套HTML5工程，通过前端构建工具（如grunt）条件编译，可同时发行到iOS Appstore、安卓各大应用商店、普通手机浏览器、微信App和流应用。并且在每个平台上，都能调用该平台的专有API达到原生体验。


2. 环境准备2.1. java参考《全平台安装JDK》。
2.2. Android模拟器2.2.1. 下载SDK1、访问AndroidDevTools，下载android-sdk。记住，不要去安卓官网下载，不要去安卓官网下载，不要去安卓官网下载。因为安卓官网下载的，解压后只有tools文件夹，没有AVD Manager和SDK Manager。
2、启动SDK Manager，下载如下文件。（1）Tools目录（必须的工具）：

Android SDK Tools（必须，只需下载一个版本，一般选最新版本）：基础工具包，版本号带rc字样的是预览版。
Android SDK Platform-tools（必须，只需下载一个版本，一般选最新版本）：从android2.3开始划出此目录，存放公用开发工具，比如adb、sqlite3等，被划分到了这里。
Android SDK Build-tools（必须，可以安装多个版本）：Android项目构建工具。

（2）Android xxx（API xx）目录（可选的各平台开发工具）：

Documentation for Android Sdk（可选）：安卓开发者官网的一些离线文档，不过下载下来打开也很慢，后面会提供另外一个离线版。
SDK Platform（必须）：对应平台的开发工具，需要在哪个版本的平台下开发就下载哪个。
Samples for SDK（可选，此项在高版本tools中已不提供，需要在IDE里通过Import Sample引入，当然也可以下载离线版）：内置的安卓示例程序，推荐安装。
Sources for Android SDK（可选）：安卓API的源代码，推荐安装。
xxxxxxxx  Image（可选）：各个以Image结尾的东西是支持相应平台的模拟器，一般选择Intel x86 Atom_64 System Image。

（3）Extras目录（可选的扩展）：

Android Support Libraries（需要，高版本tools中已不见了，应该是集成到了别的地方）：在低版本平台实现高版本平台控件效果时提供支持。
Android Support Repository（需要）：主要是方便在gradle中使用Android Support Libraries，因为Google并没有把这些库发布到maven center或者jcenter去，而是使用了Google自己的maven仓库。
Intel x86 Emulator Accelerator(HAXM installer)（可选，但非常需要，需要CPU支持虚拟化技术支持）：windows平台的Intel x86模拟器加速工具，配合Intel x86 atom/atom_64 System Image使用可加快模拟器的运行速度。

2.2.2. 创建虚拟机1、启动AVD Manager，创建一个安卓虚拟机。
2、启动创建的虚拟机。
也许会启动失败，报错如下：
Starting emulator for AVD &#x27;android7.0&#x27;emulator: ERROR: x86_64 emulation currently requires hardware acceleration!Please ensure Intel HAXM is properly installed and usable.CPU acceleration status: HAXM is not installed on this machine

3、再次打开SDK Manager，发现Intel x86 Emulator Accelerator（HAXM installer）的状态是Not Compatible with Windows。
4、搜索“Hardware_Accelerated_Execution_Manager”，找到Intel官方下载地址，下载haxm-windows_v6_2_0.zip。
5、解压haxm-windows_v6_2_0.zip，放到SDK/extras/intel/目录下，然后双击silent_install.bat。
6、再次启动虚拟机，成功！
2.3. HBuilder1、访问Hbuilder官网，下载HBuilder。
2、解压HBuilder，放在一个喜欢的目录。
3. helloworld1、打开HBuilder，文件，新建，移动App。
2、应用名称填入helloworld，模板选择Hello mui，完成。
3、单击工具栏的三角号，在手机设备上运行/停止移动设备。
4、如果已经启动了安卓虚拟机，理论上会出现虚拟机的选项。但是，如果没有出现的话，直接ctrl+R也可以在虚拟机上启动应用。
4. 书签使用HBuilder开发移动APP：开发环境准备
DCloud文档
Android离线打包
IOS离线打包
如何安装配置手机模拟器
7个最佳的Android模拟器
Android SDK Manager和AVD Manager使用
Android官网
Android Studio &amp;&amp; SDK下载地址
sdkmanager使用说明
在 Android Emulator 上运行应用
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
        <tag>hbuilder</tag>
        <tag>ios</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Nginx：Nginx配置黑白名单</title>
    <url>/dev-nginx-allow-deny/</url>
    <content><![CDATA[1. 前言在现代的Web服务中，控制访问权限是保障服务器安全和资源合理利用的重要手段。Nginx作为一款高性能的Web服务器和反向代理服务器，提供了灵活的访问控制机制，可以通过配置黑白名单来实现对访问请求的精细化管理。本文将学习如何在Nginx中配置黑白名单，掌握相关配置技巧。


2. Nginx访问控制的基本原理Nginx通过allow和deny指令来实现访问控制。这两个指令可以在http块、server块或location块中使用，用于指定哪些IP地址或IP段可以访问特定的资源。访问控制的规则是按照配置文件中指令的顺序依次检查的，一旦匹配到规则，就会立即执行相应的操作（允许或拒绝访问）。除了使用allow和deny指令，还可以使用geo模块。
3. 配置白名单白名单是指允许特定的IP地址或IP段访问服务器资源，而拒绝其他所有IP访问的。以下是配置白名单的步骤和示例。
3.1. 使用allow和deny指令配置白名单这是最直接的方法，适用于白名单规则较少的情况。
示例配置：
server &#123;    listen 80;    server_name example.com;    location / &#123;        # 允许特定IP或IP段访问        allow 192.168.1.100;  # 允许单个IP        allow 192.168.1.0/24; # 允许一个IP段        # 如果没有匹配到allow列表，则返回403 Forbidden        deny all;        # 其他配置        root /var/www/html;        index index.html;    &#125;&#125;

3.2. 使用外部文件配置白名单使用外部配置文件可以方便白名单的复用。对于管理大量 IP 地址，创建单独的文件用于列出白名单IP，并在 Nginx 配置中包含它们会更有效。
1、创建配置文件 whiteip.conf
2、将相应的 deny 和 allow 指令添加到配置文件配置示例：
allow 192.168.1.100;  # 允许单个IPallow 192.168.1.0/24; # 允许一个IP段deny all;             # # 如果没有匹配到allow列表，则返回403 Forbidden

3、在 Nginx 配置文件中包含配置文件
include /path/to/whiteip.conf;

3.3. 使用geo模块配置白名单当白名单规则较多时，使用geo模块可以更方便地管理。示例配置：
http &#123;    geo $allow_ip &#123;        default 0;  # 默认不允许访问        192.168.1.100 1;  # 允许单个IP        192.168.1.0/24 1; # 允许一个IP段    &#125;    server &#123;        listen 80;        server_name example.com;        location / &#123;            if ($allow_ip = 0) &#123;                return 403;  # 如果IP不在白名单中，返回403 Forbidden            &#125;            # 其他配置            root /var/www/html;            index index.html;        &#125;    &#125;&#125;

4. 配置黑名单黑名单是指拒绝特定的IP地址或IP段访问服务器资源，而允许其他所有IP的访问。以下是配置黑名单的步骤和示例。
4.1. 使用allow和deny指令配置黑名单这是最直接的方法，适用于黑名单规则较少的情况。示例配置：
server &#123;    listen 80;    server_name example.com;    location / &#123;        # 拒绝特定IP或IP段访问        deny 192.168.1.200;  # 拒绝单个IP        deny 192.168.2.0/24; # 拒绝一个IP段        allow all; # 允许所有，可以省略        # 其他配置        root /var/www/html;        index index.html;    &#125;&#125;

4.2. 使用外部文件配置黑名单使用外部配置文件可以方便黑名单的复用。对于管理大量 IP 地址，创建单独的文件用于列出黑名单IP，并在 Nginx 配置中包含它们会更有效。
1、创建配置文件 blockip.conf
2、将相应的 deny 和 allow 指令添加到配置文件
配置示例：
deny 192.168.1.200;  # 拒绝单个IPdeny 192.168.2.0/24; # 拒绝一个IP段allow all; # 允许所有，可以省略

3、在 Nginx 配置文件中包含配置文件
include /path/to/blockip.conf;

4.3. 使用geo模块配置黑名单当黑名单规则较多时，使用geo模块可以更方便地管理。示例配置：
http &#123;    geo $deny_ip &#123;        default 0;  # 默认允许访问        192.168.1.200 1;  # 拒绝单个IP        192.168.2.0/24 1; # 拒绝一个IP段    &#125;    server &#123;        listen 80;        server_name example.com;        location / &#123;            if ($deny_ip = 1) &#123;                return 403;  # 如果IP在黑名单中，返回403 Forbidden            &#125;            # 其他配置            root /var/www/html;            index index.html;        &#125;    &#125;&#125;

5. 注意事项顺序很重要：Nginx会按照配置文件中指令的顺序依次检查allow和deny指令。因此，通常建议将deny all或allow all放在最后，以确保所有未明确允许或拒绝的IP都能被正确处理。
测试配置：配置完成后，建议使用nginx -t命令检查配置文件的语法是否正确。如果语法正确，可以使用nginx -s reload命令重新加载配置文件。
查看日志：如果控制访问没有按预期工作，可以通过查看Nginx的错误日志（通常在/var/log/nginx/error.log）来排查问题。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>好好学Nginx</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Nginx：Nginx问题记录</title>
    <url>/dev-nginx-problem/</url>
    <content><![CDATA[1. 前言本文记录使用Nginx过程中遇到的问题，备忘。
相关文档：

《Nginx入门篇》
《Hexo启用HTTPS加密连接》
《七牛对象存储添加HTTPS代理》



2. 重定向问题通过chrome浏览器访问Nginx公网主机IP http://8.136.13.58 ，发现报错 403
仔细看了下地址栏，原来是 http 自动转到了 https。本地curl验证一下是否是chrome的问题：
curl http://8.136.13.58curl http://8.136.13.58 -L

由上图可以看出，确实是nginx对请求进行了重定向。
那么，为什么通过ip访问会进行重定向？在 default.conf 的配置中，没有进行修改，没有配置任何重定向！这样看来，问题只能出在 www.voidking.com.conf 和 cdn.voidking.com.conf 这两个文件。
删除 www.voidking.com.conf ，重启nginx，问题依然存在。删除 cdn.voidking.com.conf ，重启nginx，问题解决！那么，问题就肯定出在 cdn.voidking.com.conf 这个文件上。从配置上看，没有任何问题，但是为什么会导致IP访问nginx被重定向呢？找了很多资料，没有找到答案。后来灵光一现：cdn.voidking.com.conf 比 default.conf 优先级更高！因为c开头的配置文件要比d开头的配置文件先加载，而 default.conf 并没有特殊的优先级配置。所以，最终结果就是 cdn.voidking.com.conf 才是真正意义上的 default ！
解决方案：重命名 default.conf 为 0.default.conf 。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>问题排查</tag>
        <tag>好好学Nginx</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Nginx：Nginx入门篇</title>
    <url>/dev-nginx-start/</url>
    <content><![CDATA[1. Nginx是什么？Nginx (engine x) 是一个高性能的HTTP静态页面服务器，更是一个常用的反向代理服务器，同时还可以作为IMAP/POP3/SMTP代理服务器。
本文中，我们学习安装Nginx，并使用Nginx配置一个 hello world 静态站点，为Nginx进阶做好准备。


2. 安装Nginxlinux中安装nginx，参考文档《CentOS7设置Nginx开机自启动》
docker安装nginx，参考文档《使用Docker安装配置Nginx》
3. Nginx目录结构因为nginx目录是可以指定的，所以真实使用的目录结构请以命令查看。
ps aux | grep nginx

3.1. 程序目录可能的程序目录：
/usr/sbin/nginx

3.2. 配置目录可能的默认配置目录：
/etc/nginx//etc/nginx/conf

nginx.conf一般放在这两个目录中，nginx.conf中会写清楚子配置目录，比如：
http &#123;    include /etc/nginx/conf.d/*.conf;&#125;
那么，子配置文件就放在 /etc/nginx/conf.d/ 目录中，并且子配置文件必须以 .conf 结尾。一般情况下，我们比较少修改 nginx.conf，大部分时候都是修改子配置文件。
3.3. 根目录可能的默认Web根目录：
/usr/share/nginx/html
根目录下一般会有nginx自带的index.html和50x.html。
curl $&#123;nginx_server&#125;时看到内容，就是index.html文件中的内容。
我们自己的静态页面项目，最好不要放到默认根目录下，因为默认根目录可以通过ip和路径访问到，这往往是不符合预期的。推荐的静态页面项目目录为：/usr/share/nginx/work
3.4. 日志目录可能的默认日志目录：
/var/log/nginx
日志文件中一般有两个日志文件，error.log和access.log。
4. hello world1、创建一个html文件
echo &quot;hello world&quot; &gt; /usr/share/nginx/html/hello.html 

2、创建一个nginx配置文件 conf.d/hello.conf
server &#123;    listen 80;    server_name hello.voidking.com;    charset utf-8;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        root /usr/share/nginx/html/;        index hello.html;    &#125;&#125;

3、测试配置
nginx -t

4、重新加载配置
nginx -s reload

5、本地 /etc/hosts 中添加一条解析
192.x.x.x hello.voidking.com
其中 192.x.x.x 是nginx主机的IP地址。
6、浏览器访问 http://hello.voidking.com可以在页面中看到 hello world
5. 重启Nginx方法方法一：使用systemctl重启nginx
systemctl restart nginx

方法二：先kill，再通过bin文件启动nginx
ps -ef | grep nginxkill -9 xxx/usr/sbin/nginx -c /etc/nginx/conf/nginx.conf

6. Nginx配置详解Nginx配置详解，请参阅文档：《好好学Nginx：Nginx配置详解》
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>好好学Nginx</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Nginx：Nginx配置详解</title>
    <url>/dev-nginx-config/</url>
    <content><![CDATA[1. 前言Nginx是一个功能强大的服务器软件，通过合理的配置可以实现多种网络服务。本文系统学习Nginx配置方法，备忘。


2. 配置文件目录可能的默认配置目录：
/etc/nginx//etc/nginx/conf

nginx.conf一般放在这两个目录中，nginx.conf中会写清楚子配置目录，比如：
http &#123;    include /etc/nginx/conf.d/*.conf;&#125;
那么，子配置文件就放在 /etc/nginx/conf.d/ 目录中，并且子配置文件必须以 .conf 结尾。一般情况下，我们比较少修改 nginx.conf，大部分时候都是修改子配置文件。
3. 配置文件结构配置文件由指令和指令块组成，指令块通常以大括号{}包围。
基本指令：

worker_processes：设置工作进程数。
events：配置事件模块。
http：配置HTTP模块。
stream：配置STREAM模块。

指令块：

server：定义一个服务器块，可以包含多个location块。
location：定义请求的处理规则。

4. 配置文件优先级conf.d中的配置文件，是按照名称顺序有优先级的，第一个匹配到的配置文件会作为default。如果一个域名指向了nginx，但是nginx中没有这个域名的配置，那么就会走default配置。详情参考《使用Docker安装配置Nginx》文中的【重定向问题排查】一节。
5. 常用配置5.1. 静态页面服务器配置server &#123;    listen 80;    server_name www.voidking.com;    charset utf-8;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        root /usr/share/nginx/html/;        index index.html;    &#125;&#125;
这个配置，是一个标准的静态页面服务器配置。使用以上配置，如果已经配置好了域名解析，那么访问 www.voidking.com 时，就会看到nginx首页，也就是 index.html。
5.2. 反向代理到其他域名server &#123;    listen 80;    server_name www.voidking.com;    charset utf-8;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        proxy_pass http://voidking.coding.me;    &#125;&#125;
使用以上配置，访问 www.voidking.com 时，实际上看到的是 voidking.coding.me 这个域名返回的内容。
5.3. 反向代理到一个服务server &#123;    listen 80;    server_name www.voidking.com;    charset utf-8;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        proxy_pass http://192.168.56.101:8080;    &#125;&#125;
使用以上配置，访问 www.voidking.com 时，实际上看到的是 192.168.56.101:8080 这个服务返回的内容。
5.4. 反向代理到绑定了HOST的服务有些服务在启动时必须绑定HOST，比如cvat在启动时通过 CVAT_HOST 变量指定HOST，后续访问cvat服务时，只能通过绑定的这个HOST访问，其他域名和IP访问都会报错404。
绑定了HOST的服务，在配置反向代理时，需要在 proxy_set_header 中指定服务绑定的HOST。假设CVAT_HOST=192.168.56.102，那么反向代理配置示例如下：
server &#123;    listen 80;    server_name cvat.voidking.com;    charset utf-8;    location / &#123;        proxy_set_header   Host             192.168.56.102;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        proxy_pass http://192.168.56.102:8888;    &#125;&#125;

5.5. 反向代理到upstreamupstream www_voidking_com&#123;    server 192.168.56.101:8080 weight=5;    server 192.168.56.102:8081 weight=2;    server 192.168.56.103:8000;&#125;server &#123;    listen 80;    server_name www.voidking.com;    charset utf-8;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        proxy_pass http://www_voidking_com;    &#125;&#125;
使用以上配置，访问 www.voidking.com 时，实际上看到的是upstream里的服务返回的内容。但是问题来了，upstream里有三个服务，到底是哪个服务返回的内容呢？答：不一定，这就要用到传说中的加权轮询算法了。三个服务的权值分别为 5、2、1（默认），从权值来看，最大可能打到第一个服务。有没有想到什么知识点？没错，负载均衡，使用了加权轮询算法的负载均衡。
5.6. 反向代理到websocket服务Connection值硬编码：
http &#123;    server &#123;        listen 80;        server_name www.voidking.com;        charset utf-8;        location ^~ /ws &#123;            proxy_set_header   Host             $host;            proxy_set_header   X-Real-IP        $remote_addr;            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;            # websocket support            proxy_http_version 1.1;            proxy_set_header Upgrade $http_upgrade;            proxy_set_header Connection &quot;upgrade&quot;;            proxy_pass http://192.168.56.101:8080;        &#125;        location / &#123;            proxy_set_header   Host             $host;            proxy_set_header   X-Real-IP        $remote_addr;            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;            proxy_pass http://192.168.56.101:8080;        &#125;    &#125;&#125;
这种写法中，Connection的值直接被硬编码为upgrade。
动态设置Connection值：
http &#123;    map $http_upgrade $connection_upgrade &#123;        default upgrade;        &#x27;&#x27; close;    &#125;    server &#123;        listen 80;        server_name www.voidking.com;        charset utf-8;        location ^~ /ws &#123;            proxy_set_header   Host             $host;            proxy_set_header   X-Real-IP        $remote_addr;            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;            # websocket support            proxy_http_version 1.1;            proxy_set_header Upgrade $http_upgrade;            proxy_set_header Connection $connection_upgrade;            proxy_pass http://192.168.56.101:8080;        &#125;        location / &#123;            proxy_set_header   Host             $host;            proxy_set_header   X-Real-IP        $remote_addr;            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;            proxy_pass http://192.168.56.101:8080;        &#125;    &#125;&#125;

这种写法中，Connection的值是变量$connection_upgrade。该变量的值是根据请求头中的$http_upgrade来决定的。如果$http_upgrade的值是默认值（即空），那么Connection的值会被设置为close，否则会被设置为upgrade。
更多内容参考一篇带给你Nginx代理WebSocket方法和NGINX as a WebSocket Proxy
5.7. 配置域名证书server &#123;    listen 80;    listen 443 ssl;    server_name www.voidking.com;    charset utf-8;    ssl_certificate /etc/nginx/ssl/1_www.voidking.com_bundle.crt;    ssl_certificate_key  /etc/nginx/ssl/2_www.voidking.com.key;    ssl_session_timeout  5m;    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;    ssl_ciphers  HIGH:!ADH:!EXPORT56:RC4+RSA:+MEDIUM;    ssl_prefer_server_ciphers on;    if ($ssl_protocol = &quot;&quot;) &#123;        return 301 https://$host$request_uri;    &#125;    location / &#123;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;        root /usr/share/nginx/work/voidking/;        index index.html;    &#125;    error_page 404 /404.html;    location = /404.html &#123;        root /usr/share/nginx/work/voidking/;        index 404.html;    &#125;    location ~ /\.git &#123;        return 404;    &#125;&#125;

5.8. 四层代理5.8.1. 加载stream模块nginx四层代理依赖ngx_stream_core_module模块（下面简称stream模块）。
nginx -V &amp;&gt; nginx_version.txtgrep &quot;stream&quot; nginx_version.txt
如果看到--with-stream，表明已经安装并开启stream模块。
nginx1.22.0以上版本默认支持并开启stream模块，一般配置路径为：/usr/share/nginx/modules/ ，在 nginx.conf 中include该路径。
如果是低版本的nginx，需要自行编译安装stream模块，或者使用包管理器安装stream模块。
yum install -y nginx-mod-stream

安装完成后，nginx.conf中加载ngx_stream_module.so
#load_module /usr/lib/nginx/modules/ngx_stream_module.so;load_module /usr/lib64/nginx/modules/ngx_stream_module.so;

5.8.2. 四层代理配置一个四层代理的实例：
stream &#123;    upstream remote_mysql &#123;        server 192.168.1.1:3306;    &#125;    server &#123;        listen 3306 so_keepalive=on; # 开始TCP存活探测        proxy_connect_timeout 10s;   # 连接超时时间        proxy_timeout 300s;          # 端口保持时间        proxy_pass remote_mysql;    &#125;&#125;

注意：stream是和http同一级的，不要配置到nginx/conf.d目录中。最好新建一个nginx/stream.d目录，专门放置四层代理配置。
1、修改 nginx.conf ，添加
stream &#123;    include /etc/nginx/stream.d/*.conf;&#125;

2、添加一个 server 配置 stream.d/mysql.conf
upstream remote_mysql &#123;    server 192.168.1.1:3306;&#125;server &#123;    listen 3306 so_keepalive=on; # 开始TCP存活探测    proxy_connect_timeout 10s;   # 连接超时时间    proxy_timeout 300s;          # 端口保持时间    proxy_pass remote_mysql;&#125;

3、重新加载配置
nginx -tnginx -s reload

如果报错：nginx: [emerg] unknown directive “stream” in /etc/nginx/nginx.conf:17那么需要安装 nginx-mod-stream
yum install nginx-mod-stream

5.9. 请求body大小Syntax:     client_max_body_size size;Default:    client_max_body_size 1m;Context:    http, server, location


Sets the maximum allowed size of the client request body. If the size in a request exceeds the configured value, the 413 (Request Entity Too Large) error is returned to the client. Please be aware that browsers cannot correctly display this error. Setting size to 0 disables checking of client request body size.

参考文档ngx_http_core_module - client_max_body_size
5.10. 允许跨域允许某个域名跨域：
server &#123;    listen 80;    server_name www.voidking.com;    add_header Access-Control-Allow-Origin &#x27;test.voidking.com&#x27;;    add_header Access-Control-Allow-Headers &#x27;*&#x27;;    add_header Access-Control-Allow-Credentials true;    add_header Access-Control-Allow-Methods &quot;GET, POST, PUT, OPTIONS&quot;;    ...&#125;

允许所有域名跨域：
server &#123;    listen 80;    server_name www.voidking.com;    add_header Access-Control-Allow-Origin &#x27;*&#x27;;    add_header Access-Control-Allow-Headers &#x27;*&#x27;;    add_header Access-Control-Allow-Credentials true;    add_header Access-Control-Allow-Methods &quot;GET, POST, PUT, OPTIONS&quot;;    ...&#125;

指定多个域名允许跨域：
server &#123;    listen 80;    server_name cdn.voidking.com;    location /search.xml &#123;        if ($http_origin ~* (https?://(.+\.)?(voidking.com|www.voidking.com)$)) &#123;            set $allowed_origin $http_origin;        &#125;        add_header &#x27;Access-Control-Allow-Origin&#x27; $allowed_origin always;        add_header &#x27;Access-Control-Allow-Methods&#x27; &#x27;GET, POST, OPTIONS&#x27; always;        add_header &#x27;Access-Control-Allow-Headers&#x27; &#x27;DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range&#x27; always;        add_header &#x27;Access-Control-Expose-Headers&#x27; &#x27;Content-Length,Content-Range&#x27; always;        proxy_set_header   Host             $host;        proxy_set_header   X-Real-IP        $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;        root /usr/share/nginx/work/voidking/;        index search.xml;    &#125;    ...&#125;

5.11. 支持SNI转发SNI（Server Name Indication）是一个TLS的扩展字段，对应nginx内置变量$ssl_preread_server_name，可用于访问不同域名跳转到不同的后端地址，实现根据域名四层代理。
此外，还有两个常用的TLS扩展字段：

Protocol Version：对应nginx内置变量是 $ssl_preread_protocol，可用于客户端不同TLS协议版本跳转到不同的后端地址，实现根据TLS协议版本四层代理。
ALPN（Application-Layer Protocol Negotiation），对应nginx内置变量是 $ssl_preread_alpn_protocols，可用于根据客户端不同应用层协议版本跳转到不同后端地址，实现根据应用层协议的四层代理。

示例：
stream &#123;    map $ssl_preread_server_name $name &#123;        ssltest.voidking.com     backend;        default                  backend2;    &#125;    upstream backend &#123;        server 127.0.0.1:8888;    &#125;    upstream backend2 &#123;        server 127.0.0.1:8889;    &#125;    server &#123;        ssl_preread on;        listen      443;        proxy_pass  $name;    &#125;&#125;

参考文档：

SSL/TLS协议运行机制的概述
Nginx支持SNI转发
利用nginx实现https和ssh共用端口绕过防火墙限制ssh登录的方法
nginx 分发 ssh

5.12. 404重定向到首页需求：访问URL test.voidking.com/x/y/z 时，如果路径不存在出现404，那么自定义404页面或者跳转到首页。
要将Nginx的404错误重定向到首页，我们可以使用error_page指令来捕获404错误，并使用rewrite指令或者location块来重定向到首页。
配置404重定向到首页，方法一：
server &#123;      listen 80;      server_name test.voidking.com;        root /path/to/your/webroot/;      index index.html index.htm;        location / &#123;          try_files $uri $uri/ =404;      &#125;        error_page 404 /404.html;      location = /404.html &#123;          internal;          rewrite ^ / last;      &#125;  &#125;

在这个配置中，try_files指令用于尝试按顺序提供文件或目录。如果请求的文件或目录不存在，则返回404错误。error_page指令将404错误映射到/404.html，但请注意，实际上并不需要真实的/404.html文件。然后，我们定义了一个location块来处理/404.html请求，并使用rewrite指令将其重定向到首页（/）。internal指令确保该location块只能由Nginx内部调用，不能直接通过浏览器访问。
配置404重定向到首页，方法二：
server &#123;      listen 80;      server_name test.voidking.com;        root /path/to/your/webroot/;      index index.html index.htm;        location / &#123;          try_files $uri $uri/ =404;      &#125;        error_page 404 /index.html;      location = /index.html &#123;          internal;      &#125;  &#125;

在这个配置中，我们直接将404错误重定向到/index.html（即首页）。同样，我们定义了一个location块来处理/index.html请求，并使用internal指令确保它只能由Nginx内部调用。
配置自定义404页面：
server &#123;      listen 80;      server_name test.voidking.com;        root /path/to/your/webroot/;      index index.html index.htm;        location / &#123;          try_files $uri $uri/ =404;      &#125;        error_page 404 /404.html;    location = /404.html &#123;        root /path/to/your/webroot/;        index 404.html;    &#125;&#125;

5.13. 404重定向到不同路径需求：访问URL test.voidking.com/x/y/z 时，如果路径不存在出现404，那么跳转到 test.voidking.com/x。其中 x、y、z 都代表一个字符串。
配置方法：
server &#123;  listen 80;  server_name test.voidking.com;  root /usr/share/nginx/html/;  index index.html index.htm;  location / &#123;    # 尝试按照请求的URI提供文件，如果文件不存在，则尝试下一个选项    try_files $uri $uri/ @redirect;  &#125;  # 定义命名location来处理404重定向，重定向到第一级目录，实际在浏览器中看不到跳转  location @redirect &#123;    # 提取URI的第一级目录    rewrite ^/([^/]+)/(.*)$ /$1/ last;  &#125;  # 错误页面配置，适用于第一级目录是404的URL  error_page 404 /404.html;  location = /404.html &#123;    root /usr/share/nginx/html/;    index 404.html;  &#125;&#125;

5.14. 下载限速需求：配置下载限速，避免个别用户占满带宽。
配置方法：
server &#123;    listen 80;    server_name demo.voidking.com;        location /download &#123;        # 限制下载速度为每秒500K        limit_rate 500k;    &#125;&#125;

测试方法：1、创建一个大文件
cd /opt/download &amp;&amp; dd if=/dev/zero of=1gfile.img bs=1M count=1024

2、配置nginx下载限速
server &#123;    listen 80;    server_name demo.voidking.com;        location /download &#123;        alias /opt/download;        autoindex on;        autoindex_exact_size off;        # 限制下载速度为每秒500K        limit_rate 500k;    &#125;&#125;

3、验证下载限速
wget http://demo.voidking.com/download/1gfile.img

6. Nginx基础规则6.1. location规则在常见配置中，我们看到了一个叫 location 的关键字。location规则为：
location [ = | ~ | ~* | ^~ ] /uri/ &#123; ... &#125;

紧跟在location后面的，是可选的修饰符，uri是要匹配的字符串。
常见匹配：

location = /uri 精确匹配
location ^~ /uri 前缀匹配，在正则匹配之前
location ~ pattern 正则匹配，区分大小写
location ~* pattern 正则匹配，不区分大小写
location /uri 前缀匹配，在正则匹配之后
location / 通用匹配，未匹配到其它location的请求会匹配到它

详情参考location 匹配规则
6.2. 匹配规则匹配过程：1、精确匹配2、前缀匹配（^~）3、正则匹配（按配置顺序）4、前缀匹配（/xxx）5、通用匹配
其中前缀匹配如果有包含关系时，遵循最大匹配原则。
6.3. proxy_pass规则如果nginx作为反向代理使用，那么必须要搞明白的就是proxy_pass的规则，这关系到请求能否打到正确的接口。proxy_pass规则：

如果proxy_pass后面没有路径，那么转发时带上uri
如果proxy_pass后面有路径，那么转发时使用该路径替换匹配到的uri

6.3.1. 保留urilocation  /test &#123;    proxy_set_header Host   $host;    proxy_set_header X-Real-IP      $remote_addr;    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_pass http://192.168.56.101:8080;&#125;
访问 www.voidking.com/test/index 会被代理到 http://192.168.56.101:8080/test/index 这个url，/test/index 被保留转发给了后端服务。
6.3.2. 去掉urilocation  /test &#123;    proxy_set_header Host   $host;    proxy_set_header X-Real-IP      $remote_addr;    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_pass http://192.168.56.101:8080/;&#125;
访问 www.voidking.com/test/index 会被代理到 http://192.168.56.101:8080/index 这个url，/test 被去掉了。
6.3.3. 替换urilocation  /test &#123;    proxy_set_header Host   $host;    proxy_set_header X-Real-IP      $remote_addr;    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_pass http://192.168.56.101:8080/qa/;&#125;
访问 www.voidking.com/test/index 会被代理到 http://192.168.56.101:8080/qa/index 这个url，/test/index 被替换成了 /qa/index，然后转发给了后端服务。
location  /test &#123;    proxy_set_header Host   $host;    proxy_set_header X-Real-IP      $remote_addr;    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_pass http://192.168.56.101:8080/qa;&#125;

访问 www.voidking.com/test/index 会被代理到 http://192.168.56.101:8080/qaindex 这个url，/test/index 被替换成了 /qaindex，然后转发给了后端服务。
7. 配置日志格式http &#123;    ...    #log_format  main  &#x27;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#x27;    #                  &#x27;$status $body_bytes_sent &quot;$http_referer&quot; &#x27;    #                  &#x27;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#x27;;    #log_format main &#x27;$remote_addr - $remote_port [$time_local] &#x27;    #                &#x27;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot; &#x27;    #                &#x27;$host $http_host $scheme &#x27;    #                &#x27;&quot;$http_referer&quot; &quot;$request&quot; &#x27;    #                &#x27;&quot;$request_uri&quot; &quot;$request_method&quot; &#x27;    #                &#x27;$request_time $request_length &#x27;    #                &#x27;$status $bytes_sent &#x27;    #                &#x27;$body_bytes_sent &#x27;    #                &#x27;$upstream_addr $upstream_response_time&#x27;;        log_format main escape=json &#x27;&#123; &quot;@timestamp&quot;: &quot;$time_local&quot;, &#x27;        &#x27;&quot;http_user_agent&quot;: &quot;$http_user_agent&quot;,&#x27;        &#x27;&quot;http_x_forwarded_for&quot;: &quot;$http_x_forwarded_for&quot;,&#x27;        &#x27;&quot;remote_addr&quot;: &quot;$remote_addr&quot;,&#x27;        &#x27;&quot;remote_port&quot;: &quot;$remote_port&quot;,&#x27;        &#x27;&quot;host&quot;: &quot;$host&quot;,&#x27;        &#x27;&quot;http_host&quot;: &quot;$http_host&quot;,&#x27;        &#x27;&quot;scheme&quot;: &quot;$scheme&quot;,&#x27;        &#x27;&quot;http_referer&quot;: &quot;$http_referer&quot;,&#x27;        &#x27;&quot;request&quot;: &quot;$request&quot;,&#x27;        &#x27;&quot;request_uri&quot;: &quot;$request_uri&quot;,&#x27;        &#x27;&quot;request_method&quot;: &quot;$request_method&quot;,&#x27;        &#x27;&quot;request_time&quot;: &quot;$request_time&quot;,&#x27;        &#x27;&quot;request_length&quot;: &quot;$request_length&quot;,&#x27;        &#x27;&quot;response_status&quot;: &quot;$status&quot;,&#x27;        &#x27;&quot;bytes_sent&quot;: &quot;$bytes_sent&quot;,&#x27;        &#x27;&quot;body_bytes_sent&quot;: &quot;$body_bytes_sent&quot;,&#x27;        &#x27;&quot;upstream_addr&quot;: &quot;$upstream_addr&quot;,&#x27;        &#x27;&quot;upstream_response_time&quot;: &quot;$upstream_response_time&quot;&#125;&#x27;;&#125;




字段
含义
示例



body_bytes_sent
响应body字节数
3650


bytes_sent
响应总字节数
175


host
IP或域名(不包括端口)
10.10.10.14


http_host
IP或域名(包括端口)
10.10.10.14:81


http_referer
referer信息
http://10.10.10.14/


http_user_agent
UA信息
Mozilla/5.0


http_x_forwarded_for
XFF信息
192.168.1.1


remote_addr
客户端地址
10.10.10.1


remote_user
客户端认证用户名
admin


request
请求URI和协议
GET


request_body
请求的body



request_length
请求长度
571


request_method
请求方法
GET


request_time
请求处理时间
0.000


response_body
返回的body，依赖lua，需要编写lua脚本来采集



response_header_data
响应头数据，依赖headers-more-nginx-module



schema
协议
http


server_name
虚拟主机名称



server_port
服务器端口



server_protocol
服务器协议



ssl_cipher
交换数据中的算法



ssl_protocol
SSL协议版本



status
返回状态码
404


time_local
时间戳
16/Jun/2019:23:29:50


upstream_addr
后端提供服务地址



upstream_connect_time
与服务器连接所花费的时间



upstream_response_time
后端处理时间



upstream_status
upstream状态
200


参考文档：

Nginx配置日志格式
Nginx 日志

修改日志格式后，需要重启nginx，然后查看日志确认格式
tail -f /opt/nginx/log/access.log

8. 配置白名单参考文档：《Nginx配置黑白名单》
9. map指令map 的作用是创建自定义变量。
语法：
map $var1 $var2 &#123;...&#125;
map 的var1为源变量，通常是nginx的内置变量，var2 是自定义变量。 var2 的值取决于 var1 在对应表达式的匹配情况。如果一个都匹配不到则 var2 就是 default 对应的值。
例子：
map $args $foo &#123;    default 0;    debug   1;&#125;
args 是nginx内置变量，就是获取的请求 url 的参数。 如果 args 匹配到 debug 那么 foo 的值会被设为 1 ，如果 args 一个都匹配不到 foo 就是 default 定义的值，在这里就是 0
参考文档Nginx map 使用详解
10. rewrite指令rewrite指令的作用是重定向。
语法：
rewrite regex replacement [flag];

rewrite：该指令是实现URL重写的指令。
regex：用于匹配URI的正则表达式。
replacement：将regex正则匹配到的内容替换成 replacement。
flag: flag标记。

flag有如下值：

last: 本条规则匹配完成后，继续向下匹配新的location URI 规则。(不常用)
break: 本条规则匹配完成即终止，不再匹配后面的任何规则(不常用)。
redirect: 返回302临时重定向，浏览器地址会显示跳转新的URL地址。
permanent: 返回301永久重定向。浏览器地址会显示跳转新的URL地址。

例子：
rewrite ^/(.*) http://www.baidu.com/$1 permanent;

rewrite 是固定关键字，表示开始进行rewrite匹配规则。
regex 是 ^/(.*)，这是一个正则表达式，匹配完整的域名和后面的路径地址。
replacement 是 http://www.baidu.com/$1，其中$1是取regex部分()里面的内容，如果匹配成功后跳转到的URL
flag 是 permanent，代表永久重定向的含义，即跳转到 http://www.baidu.com/$1 

参考文档Nginx中的Rewrite的重定向配置与实践
11. 状态码
1xx:信息响应类，表示接收到请求并且继续处理
2xx:处理成功响应类，表示动作被成功接收、理解和接受
3xx:重定向响应类，为了完成指定的动作，必须接受进一步处理
4xx:客户端错误，客户请求包含语法错误或者是不能正确执行
5xx:服务端错误，服务器不能正确执行一个正确的请求

详情参考HTTP 状态码详解与选用
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>好好学Nginx</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：CentOS下MySQL数据库导出和导入</title>
    <url>/dev-centos-mysql-dump-source/</url>
    <content><![CDATA[1. 前言mysql数据库导出和导入，在使用navicat等图形化管理工具的情况下很方便。但是，学校服务器上3306端口禁止远程访问，所以，我们只能采用如下两种方案：

在shell中导出导入
安装phpmyadmin



2. 使用shell2.1. 导出假设我们要导出的数据库名为vkphp。
mysqldump -uroot -ppassword vkphp &gt; /root/vkphp.sql
执行该命令，则会在/root目录下导出vkphp.sql文件。
2.2. 导入1、使用xftp上传vkphp.sql到/root目录下。
2、登录mysql
mysql -u root -p
3、新建数据库CREATE DATABASE vkphp DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
PS：删除数据库DROP DATABASE vkphp;
4、导入数据use vkphp;
source /root/vkphp.sql;
3. 使用phpmyadmin3.1. 安装phpmyadmin1、到phpmyadmin官网下载phpmyadmin源码，比如phpMyAdmin-4.7.4-all-languages.tar.gz。
2、上传phpMyAdmin-4.7.4-all-languages.tar.gz到/root目录下。
3、解压安装tar -xvf phpMyAdmin-4.7.4-all-languages.tar.gz
mv phpMyAdmin-4.7.4-all-languages phpmyadmin
mv phpmyadmin [web_path]，web_path是web根目录，比如/home/wwwroot。
4、配置
cd /home/wwwroot/phpmyadmin/libraries
vim config.default.php
// localhost =&gt; 127.0.0.1$cfg[&#x27;Servers&#x27;][$i][&#x27;host&#x27;] = &#x27;127.0.0.1&#x27;;$cfg[&#x27;Servers&#x27;][$i][&#x27;user&#x27;] = &#x27;root&#x27;;$cfg[&#x27;Servers&#x27;][$i][&#x27;password&#x27;] = &#x27;password&#x27;;

3.2. nginx配置1、在phpmyadmin目录中mkdir log
touch log/phpmyadmin-nginx.log
2、查找php-cgi.sockfind / -name &#39;php-cgi.sock&#39;
看到的结果为/tmp/php-cgi.sock
3、在nginx的conf/vhost目录中，新建phpmyadmin.voidking.com.conf，内容如下：
server &#123;    listen 80;    server_name  phpmyadmin.voidking.com;    root /home/wwwroot/phpmyadmin;  access_log  /home/wwwroot/phpmyadmin/log/phpmyadmin-nginx.log  main;    set $php_upstream &#x27;unix:/tmp/php-cgi.sock&#x27;;    location / &#123;     index index.php;    &#125;      location ~ \.php$ &#123;       fastcgi_pass   $php_upstream;      fastcgi_index  index.php;      fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;      include     fastcgi_params;    &#125;      location ~ /\.ht &#123;      deny  all;    &#125;  &#125;

4、重启nginx./nginx -t
./nginx -s reload
如果报错 nginx: [emerg] unknown log format &quot;main&quot; in... ，那么最简单的方法是去掉main。
access_log  /home/wwwroot/phpmyadmin/log/phpmyadmin-nginx.log;
5、在万网添加域名解析phpmyadmin。
6、测试访问 http://phpmyadmin.voidking.com
3.3. 导出导入1、登录phpmyadmin。
2、进入vkphp数据库。
3、单击导航栏“导出”，格式选择SQL，执行。即可导出sql文件到本地。
4、单击导航栏“导入”，选择文件，执行。但是，最大文件限制为50MB。此时，我们需要修改php.ini和nginx.conf。
（1）查找php.ini和nginx.conffind / -name &#39;php.ini&#39;
find / -name &#39;nginx.conf&#39;
（2）编辑php.inivim /usr/local/php/etc/php.ini
输入/upload，查找“upload”的位置，按N键跳转下一个匹配。
修改upload_max_filesize和post_max_size为
upload_max_filesize = 500Mpost_max_size = 500M

（3）编辑nginx.confvim /usr/local/nginx/conf/nginx.conf
修改client_max_body_size为
client_max_body_size 500m

（4）重启php/etc/init.d/php-fpm restart
（5）重启nginx./nginx -t
./nginx -s reload
5、再次查看最大文件限制，就变成了500MB。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>nginx</tag>
        <tag>mysql</tag>
        <tag>好好学MySQL</tag>
        <tag>phpmyadmin</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：CentOS安装Apache和PHP环境</title>
    <url>/dev-centos-apache-php/</url>
    <content><![CDATA[1. 前言《在CentOS7上配置PHP运行环境》一文中，已经配置好了PHP环境。但是，Nginx和PHP的配合不是很好，所以，郝同学决定在原本的环境中加入Apache。



2. 安装Apache1、安装apacheyum install httpd
2、查看httpd安装位置rpm -ql httpd
3、启动apachesystemctl restart httpd.service
如果启动失败，很有可能是因为80端口被占用。此时，需要修改监听端口。cd /etc/httpd/conf， vim httpd.conf，找到 Listen 80，修改为 Listen 8080。
4、设置开机启动systemctl enable httpd.service
5、测试访问访问地址 http://host_ip:8080，即可看到“Testing 123”。
cd /var/www/html，vim index.html，输入hello。
再次访问 http://host_ip:8080，即可看到“hello”。
PS：如果无法访问，请先关闭防火墙。systemctl stop firewalld.service ，关闭防火墙。
systemctl disable firewalld.service ，禁止开机启动。
3. 安装php5.6能不能使用原来环境中的php和apache配合？理论上是能的，但是配置麻烦，郝同学决定再安装一个php。
如果直接执行yum install php，centos7默认安装php5.4版本。如果需要安装更高版本，我们需要如下操作。
1、追加epel及remi源
rpm -Uvh http://ftp.iij.ad.jp/pub/linux/fedora/epel/7/x86_64/e/epel-release-7-10.noarch.rpmrpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

2、确认安装的php版本yum list --enablerepo=remi --enablerepo=remi-php56 | grep php
如果报错，就先执行yum install epel-release
3、安装php5.6
yum install --enablerepo=remi --enablerepo=remi-php56 php php-opcache php-pecl-apcu php-devel php-mbstring php-mcrypt php-mysqlnd php-phpunit-PHPUnit php-pecl-xdebug php-pecl-xhprof php-pdo php-pear php-fpm php-cli php-xml php-bcmath php-process php-gd php-common

4、确认php版本php -v
5、查看php安装位置rpm -ql php
whereis php
which php
php配置文件的默认位置为/etc/php.ini
4. Apache使用PHP1、重启apachesystemctl stop httpd.service
systemctl start httpd.service
2、在/var/www/html，新建文件index.php，内容如下：
&lt;?php    echo &#x27;hello php5.6&#x27;;?&gt;

3、测试访问访问地址 http://host_ip:8080/index.php，即可看到“hello php5.6”。
5. 书签如何搭建lamp(CentOS7+Apache+MySQL+PHP)环境
Install Apache, PHP And MySQL On CentOS 7 (LAMP)
在CentOS7.0安装php5.6
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>apache</tag>
        <tag>php</tag>
        <tag>yum源</tag>
        <tag>好好学PHP</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：PHP使用Memcache</title>
    <url>/dev-php-memcache/</url>
    <content><![CDATA[1. memcache简介Memcached 是一个高性能的分布式内存对象缓存系统，用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数，从而提高动态、数据库驱动网站的速度。Memcached基于一个存储键/值对的hashmap。
memcache有memcache和memcached两种名称，其实memcache是这个项目的名称，而memcached是它服务器端的主程序文件名。
memcached是一种服务器，是内存缓存服务器，就像apache服务器一样。只不过apache是用来提供web服务，而memcached是用来进行内存缓存。那么memcache客户端呢？我们可能用php开发网站，也可能用java或者python，这些相对与服务器来说是客户，都在使用服务器的服务。在php的扩展中加入memcache，就可以调用服务器的服务了，就是可以调用memcached来进行内存缓存了。


2. 下载安装memcached服务端1、下载memcahed，wget http://www.memcached.org/files/memcached-1.5.1.tar.gz
2、解压源码，tar -xvf memcached-1.5.1.tar.gz
3、编译安装，cd memcached-1.5.1，./configure，make &amp;&amp; make install
3. 启动memcached服务端1、memcached的默认目录为/usr/local/bin/memcached
2、启动memcache，memcached -u root -d

-d 选项是启动一个守护进程。
-m 是分配给Memcache使用的内存数量，单位是MB，默认64MB。
-u 是运行Memcache的用户，如果当前为root 的话，需要使用此参数指定用户
-p [num]是设置Memcache的TCP监听的端口，最好是1024以上的端口。
-c 选项是最大运行的并发连接数，默认是1024。
-P [file] 是设置保存Memcache的pid文件。

或者，service memcached start
3、验证安装结果，memcached -h
4、设置开机启动，chkconfig memcached on
5、查看监听端口，yum install lsof，lsof -i tcp:11211
4. php安装memcache4.1. 安装libmemcached1、下载wget https://launchpad.net/libmemcached/1.0/1.0.18/+download/libmemcached-1.0.18.tar.gz
2、解压tar -xvf libmemcached-1.0.18.tar.gz
3、编译安装到/usr/local/libmemcachedcd libmemcached-1.0.18
./configure --prefix=/usr/local/libmemcached
make &amp;&amp; make install
4.2. 安装memcached的PHP扩展1、下载wget https://pecl.php.net/get/memcached-2.2.0.tgz
2、解压tar -xvf memcached-2.2.0.tgz
3、生成配置文件cd memcached-2.2.0
/usr/local/php/bin/phpize
4、编译安装./configure --enable-memcached --with-php-config=/usr/local/php/bin/php-config --with-libmemcached-dir=/usr/local/libmemcached --disable-memcached-sasl
make &amp;&amp; make install
5、修改php.ini，添加 extension = &quot;memcached.so&quot;
6、验证安装结果，/usr/local/php/bin/php -m | grep memcache，如果出现memcached，则证明安装成功。
7、重启php-fpm，service php-fpm restart
8、访问 http://host_ip/p.php，ctrl+f，查找memcache，如果能找到memcache，则证明成功启动。
5. 测试1、在web目录下，新建mem.php，内容如下：
&lt;?php    $mem = new Memcached;    $mem-&gt;addServer(&quot;127.0.0.1&quot;, 11211);    $mem-&gt;set(&#x27;key&#x27;, &#x27;hello memcache!&#x27;);    $val = $mem-&gt;get(&#x27;key&#x27;);    echo $val;?&gt;

2、访问 http://host_ip/mem.php，即可看到“hello memcached!”。至此，memcache安装配置成功！
6. 书签教你CentOS 7下安装操作Memcached
centos yum安装memcached及php memcache扩展
Nginx中文官方文档 Memcached
ngx_http_memcached_module
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
      </tags>
  </entry>
  <entry>
    <title>PS盖章抠图</title>
    <url>/hobby-ps-seal-cutout/</url>
    <content><![CDATA[1. 目标从图A中抠出盖章，然后盖到图B中。


2. 抠图方法2.1. 剪裁1、打开图A，双击背景，转换为图层0。
2、剪裁出盖章部分。
2.2. 整体抠图1、打开通道面板，复制蓝色通达，只显示蓝色通道副本。
2、单击蓝色通道副本，ctrl+L（图像-调整-色阶），调整输入色阶，使黑的更黑，白的更白。
3、ctrl+I（图像-调整-反向），使黑白颠倒。因为白色的是选区。
4、ctrl+单击蓝色通道副本（选择-载入选区）。
5、回到图层面板，ctrl+C，ctrl+V，只显示新图层1。至此，就把背景去掉了。
2.3. 去除多余1、新建图层2，双击背景色，选择白色，ctrl+backspace（填充）。把图层2拉到图层1下面。
2、在图层1上，新建蒙版。B键（选择画笔工具），调整画笔大小。前景色选择黑色。
3、使用画笔把黑色的多余文字抹去，文字与盖章重叠部分不处理。
2.4. 重叠处理1、选中图层1，锁定透明背景像素。
2、使用仿制图章工具。按住alt键锁定红色图章部分作为原点，然后涂抹黑色重叠部分。注意，此时焦点要放在图层1上，而不是图层1的蒙版上。
3、使用橡皮擦工具，擦掉多余像素。
4、ctrl+S，保存为png或者psd格式。
3. 使用1、打开盖章图片和图B。
2、把盖章图片，拖到图B上，成为盖章图层。
2、在图层面板，单击盖章图层，选择正片叠底，至此大功告成。
4. 书签photoshop制作印章ps抠出印章教程
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>ps</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：从零开始打造自己的PHP框架――第2章</title>
    <url>/dev-vkphp-2/</url>
    <content><![CDATA[1. 目标本篇，我们来实现加载控制器、数据查询和页面渲染。


2. 加载控制器2.1. 控制器在app目录下，新建ctrl目录，ctrl目录下新建indexCtrl.php文件，内容如下：
&lt;?phpnamespace app\ctrl;class indexCtrl&#123;    public function index()&#123;        echo &#x27;index ctrl&#x27;;    &#125;&#125;

2.2. 调用控制器在根目录下的index.php文件中，继续添加：
include CORE.&#x27;/autoload.php&#x27;;spl_autoload_register(&#x27;\core\autoload::load&#x27;);$route = new \core\route();$ctrl = $route-&gt;ctrl;$action = $route-&gt;action;$params = $route-&gt;params;$ctrl_file = APP.&#x27;/ctrl/&#x27;.$ctrl.&#x27;Ctrl.php&#x27;;$ctrl_class = &#x27;\\app\\ctrl\\&#x27;.$ctrl.&#x27;Ctrl&#x27;;if(is_file($ctrl_file))&#123;    include $ctrl_file;    $ctrl_obj = new $ctrl_class;    $ctrl_obj-&gt;$action();&#125;else &#123;    throw new \Exception(&#x27;找不到控制器&#x27;.$ctrl_file);&#125;

访问地址 http://vkphp.dev ，即可看到“index ctrl”。
3. 数据查询1、在mysql中，新建数据库vkphp。
2、在vkphp数据库中，新建表vk_user，字段包括id、username和password。
3、在common文件夹下，新建db.php，内容如下：
&lt;?phpnamespace core\common;class db extends \PDO&#123;    public function __construct()&#123;        $dsn = &#x27;mysql:host=localhost;dbname=vkphp&#x27;;        $username = &#x27;root&#x27;;        $passwd = &#x27;&#x27;;        try&#123;            parent::__construct($dsn,$username,$passwd);            // echo &#x27;database connect success&#x27;;        &#125;catch (\Exception $e)&#123;            echo $e-&gt;getMessage();        &#125;    &#125;&#125;

4、在indexCtrl.php中，添加：
public function data()&#123;    $db = new \core\common\db();    $sql = &#x27;select * from vk_user&#x27;;    $result = $db-&gt;query($sql);    p($result);    p($result-&gt;fetchAll());&#125;

访问地址 http://vkphp.dev/index/data ，即可看到从数据库中查询出的数据。
4. 页面渲染页面渲染，主要有两部分工作：赋值和显示。我们需要实现两个函数：assign和display。
1、在app目录下新建view目录，view目录下新建index目录，index目录中新建render.html，内容如下：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Render&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;p&gt;第一个视图&lt;/p&gt;    &lt;p&gt;用户名：&lt;?php echo $username; ?&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

2、在core目录中，添加render.php，内容如下：
&lt;?phpnamespace core;class render&#123;    public $params = array();    public function assign($name,$value)&#123;        $this-&gt;params[$name] = $value;    &#125;    public function display($file)&#123;        $file = APP.&#x27;/view/&#x27;.$file;        if(is_file($file))&#123;            extract($this-&gt;params); //把数组变成变量            include $file;        &#125;    &#125;&#125;

3、修改indexCtrl.php如下：
&lt;?phpnamespace app\ctrl;class indexCtrl extends \core\render&#123;    // 其他    public function render()&#123;        $this-&gt;assign(&#x27;username&#x27;,&#x27;voidking&#x27;);        $this-&gt;display(&#x27;index/render.html&#x27;);    &#125;&#125;

访问地址 http://vkphp.dev/index/render ，即可看到渲染出的页面。
5. 页面渲染进阶直接在页面echo，难以体现水平，我们来安装一个模板引擎――smarty。
5.1. 命名空间接下来smarty的使用，牵涉到命名空间这个知识点，在此学习一下。
首先声明：命名空间和文件路径没有关系，没有关系，没有关系！虽然，在使用命名空间时经常参考文件路径，但是，它们没有必然关系。
命名空间的作用：解决重名问题。不同的命名空间中，可以存在相同类名和函数名。我们在使用一个类和函数时，必须明确指出使用的是哪一个命名空间中的类和函数。
上文我们说到，在文件系统中访问一个文件有三种方式，PHP命名空间中的元素使用同样的原理。例如，类名可以通过三种方式引用：
1、非限定名称，或不包含前缀的类名称，例如 $a=new foo(); 或 foo::staticmethod(); 。如果当前命名空间是 currentnamespace，foo 将被解析为 \currentnamespace\foo ；如果当前没有指定命名空间，则foo会被解析为 \foo。2、限定名称，或包含前缀的名称，例如 $a = new subnamespace\foo(); 或 subnamespace\foo::staticmethod(); 。如果当前的命名空间是 currentnamespace，则 foo 会被解析为 \currentnamespace\subnamespace\foo ；如果当前没有指定命名空间，foo 会被解析为\subnamespace\foo。3、完全限定名称，或包含了全局前缀操作符的名称，例如，$a = new \currentnamespace\foo(); 或 \currentnamespace\foo::staticmethod();。在这种情况下，foo 总是被解析为代码中的文字名(literal name) \currentnamespace\foo。
下面举个栗子：
&lt;?phpnamespace A\B\C;class Exception extends \Exception &#123;&#125;$a = new Exception(&#x27;hi&#x27;); // $a 是类 A\B\C\Exception 的一个对象$b = new \Exception(&#x27;hi&#x27;); // $b 是类 Exception 的一个对象$c = new ArrayObject; // 致命错误, 找不到 A\B\C\ArrayObject 类?&gt;

5.2. 下载安装smarty1、访问smarty官方下载 ，下载smarty，郝同学下载的是3.1.30版本。
2、在根目录下新建lib，解压smarty到lib目录下，重命名文件夹为smarty。
5.3. 使用smarty1、在app目录下新建smarty目录，smarty目录下新建templates、template_c、configs、cache四个目录。
2、在templates目录下新建index目录，index目录中新建render2.html，内容如下：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Smarty&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;p&gt;第一个Smarty页面&lt;/p&gt;    &lt;p&gt;用户名：&#123;&#123;$username&#125;&#125;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

3、修改core目录下的render.php如下：
&lt;?phpnamespace core;class render&#123;       public $smarty;    public function __construct()&#123;        require_once(LIB.&#x27;/smarty/libs/Smarty.class.php&#x27;);        $this-&gt;smarty = new \Smarty();        $this-&gt;smarty-&gt;setTemplateDir(APP.&#x27;/smarty/templates/&#x27;);        $this-&gt;smarty-&gt;setCompileDir(APP.&#x27;/smarty/templates_c/&#x27;);        $this-&gt;smarty-&gt;setConfigDir(APP.&#x27;/smarty/configs/&#x27;);        $this-&gt;smarty-&gt;setCacheDir(APP.&#x27;/smarty/cache/&#x27;);    &#125;    public $params = array();    public function assign($name,$value)&#123;        $this-&gt;params[$name] = $value;    &#125;    public function display($file)&#123;        $file = APP.&#x27;/view/&#x27;.$file;        if(is_file($file))&#123;            extract($this-&gt;params); //把数组变成变量            include $file;        &#125;    &#125;&#125;

4、修改indexCtrl.php如下：
&lt;?phpnamespace app\ctrl;include CORE.&#x27;/render.php&#x27;;class indexCtrl extends \render&#123;    // 其他    public function render2()&#123;        $this-&gt;smarty-&gt;assign(&#x27;username&#x27;,&#x27;voidking&#x27;);        $this-&gt;smarty-&gt;display(&#x27;index/render2.html&#x27;);    &#125;&#125;

访问地址 http://vkphp.dev/index/render2 ，即可看到渲染出的页面。
6. 源码分享https://github.com/voidking/vkphp/releases/tag/v1.2.0
7. 书签从零开始打造自己的PHP框架
使用命名空间：基础
使用命名空间：后备全局函数/常量
smarty基础安装
smarty进阶安装
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>框架</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：从零开始打造自己的PHP框架——第1章</title>
    <url>/dev-vkphp-1/</url>
    <content><![CDATA[1. 目标本篇，我们来实现类库自动加载，以及路由解析。


2. 类库自动加载2.1. 常规加载常规加载一般使用include或者require，它们最根本的区别在于错误处理的方式不一样。 
include包括并运行指定文件。include一个文件存在错误的话，那么程序不会中断，而是继续执行，并显示一个警告错误。
include_once的作用和include几乎相同，唯一的差别在于导入之前会检查要导入的文件是否已经被导入过了，如果有的话就不会再次重复导入。
require会将目标文件的内容读入，并且把本身替换成这些读入的内容。require一个文件存在错误的话，那么程序就会中断执行了，并显示致命错误。
require_once的作用和require几乎相同，唯一的差别在于导入之前会检查要导入的文件是否已经被导入过了，如果有的话就不会再次重复导入。
在使用一个文件（类库）的函数之前，我们需要先使用include或者require，把该文件引入进当前文件，然后才能使用文件中的函数。
例如我们要新建一个route对象。1、core目录中，新建route.php：
&lt;?php/** * 路由控制 */namespace core;class route&#123;    public function __construct()&#123;        echo &#x27;route is ready!&#x27;;    &#125;&#125;

2、根目录下index.php中，添加：
$route = new \core\route();

会报错Fatal error: Class ‘core\route’ not found in…
需要改成：
include &#x27;\core\route.php&#x27;;$route = new \core\route();

或者：
require &#x27;\core\route.php&#x27;;$route = new \core\route();

2.2. 自动加载bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )

将函数注册到SPL __autoload函数队列中。如果该队列中的函数尚未激活，则激活它们。成功时返回 TRUE，失败时返回 FALSE。
spl_autoload_register的一般用法：
spl_autoload_register(function ($class_name) &#123;    require_once $class_name . &#x27;.php&#x27;;&#125;);$route = new \core\route();

在新建route对象时，class_name也就是\core\route会传入到spl_autoload_register函数中，该函数的参数是一个回调函数。回调函数拿到class_name，然后进行文件的引入。
也就是说，和常规加载相比，使用自动加载，我们不必对每一个类库单独进行引入。
2.3. 自动加载进阶上例中，spl_autoload_register的回调函数是一个匿名函数，而且比较简单。下面，我们来写一个更高级的回调函数。新建aotuload.php，内容如下：
&lt;?php/** * 自动加载类库 */namespace core;class autoload&#123;    public static function load($class_name)&#123;        if(file_exists($class_name.&#x27;.php&#x27;))&#123;            require_once $class_name.&#x27;.php&#x27;;            return true;        &#125;else&#123;            echo &#x27;error: unable to load &#x27;.$class_name.&#x27;.php&#x27;;            return false;        &#125;    &#125;&#125;

使用的时候，改成：
include CORE.&#x27;/autoload.php&#x27;;spl_autoload_register(&#x27;\core\autoload::load&#x27;);$route = new \core\route();

2.4. 加载机制简析在使用include的时候，会用到php文件系统。在文件系统中访问一个文件有三种方式：
1、相对文件名形式如route.php。它会被解析为 include_path/route.php，其中 include_path 表示.;C:/laragon/bin/php/php-5.6.16/PEAR 。假设当前目录是C:/laragon/www/vkphp，则该文件名依次被解析为：

C:/laragon/www/vkphp/route.php
C:/laragon/bin/php/php-5.6.16/PEAR/route.php

2、相对路径名形式如core/route.php，它会被解析为 include_path/core/route.php。假设当前目录是C:/laragon/www/vkphp，则该文件名依次被解析为：

C:/laragon/www/vkphp/core/route.php
C:/laragon/bin/php/php-5.6.16/PEAR/core/route.php

3、绝对路径名形式如/core/route.php，在linux系统中，它会被解析为/core/route.php；在windows系统中，它会被解析为 include_path/core/route.php，和相对路径一样。
绝对路径名形如C:/laragon/www/vkphp/core/route.php 或者C:\laragon\www\vkphp\core\route.php 或者 C:\\laragon\\www\\vkphp\\core\\route.php ，在windows系统中，会被解析为C:/laragon/www/vkphp/core/route.php。也就是说，windows中斜线和反斜线和双反斜线效果相同。
获取include_path和设置include_path的栗子：
echo get_include_path();ini_set(&#x27;include_path&#x27;, ini_get(&#x27;include_path&#x27;).PATH_SEPARATOR.&#x27;lib_path/libs&#x27;);echo get_include_path();

3. 路由控制3.1. 隐藏index.php1、访问地址 http://vkphp.dev/index.php ，此时，我们看到“helloworld”和“route is ready!”。
2、访问地址 http://vkphp.dev/index.php/index/index ，可以看到同样的信息。
3、访问地址 http://vkphp.dev/index/index ，则会报404错误。那么，我们怎样隐藏掉index.php呢？答案是添加.htaccess。
在项目根目录下，添加.htaccess，内容如下：
Options +FollowSymLinks  IndexIgnore */*  RewriteEngine on  # if a directory or a file exists, use it directly  RewriteCond %&#123;REQUEST_FILENAME&#125; !-f  RewriteCond %&#123;REQUEST_FILENAME&#125; !-d# otherwise forward it to index.php  RewriteRule . index.php

4、访问地址 http://vkphp.dev/index/index ，可以看到和1、2中相同的信息。
3.2. 获取URL中的控制器和方法&lt;?php/** * 路由控制 */namespace core;class route&#123;    public $ctrl;    public $action;    public function __construct()&#123;        //echo &#x27;route is ready!&#x27;;        /**         * 1、隐藏index.php         * 2、获取URL中的控制器和方法         */        if(isset($_SERVER[&#x27;REQUEST_URI&#x27;]) &amp;&amp; $_SERVER[&#x27;REQUEST_URI&#x27;] != &#x27;/&#x27;)&#123;            $path = $_SERVER[&#x27;REQUEST_URI&#x27;];            $patharr = explode(&#x27;/&#x27;,trim($path, &#x27;/&#x27;));            p($patharr);            if(isset($patharr[0]))&#123;                if($patharr[0] != &#x27;index.php&#x27;)&#123;                    // 省略了index.php                    $this-&gt;ctrl = $patharr[0];                    if(isset($patharr[1]))&#123;                        $this-&gt;action = $patharr[1];                    &#125; else&#123;                        $this-&gt;action = &#x27;index&#x27;;                    &#125;                &#125;else&#123;                    // 没省略index.php                    if(isset($patharr[1]))&#123;                        $this-&gt;ctrl = $patharr[1];                    &#125;                    if(isset($patharr[2]))&#123;                        $this-&gt;action = $patharr[2];                    &#125; else&#123;                        $this-&gt;action = &#x27;index&#x27;;                    &#125;                &#125;            &#125;else&#123;                $this-&gt;ctrl = &#x27;index&#x27;;                $this-&gt;action = &#x27;index&#x27;;            &#125;        &#125;else&#123;            $this-&gt;ctrl = &#x27;index&#x27;;            $this-&gt;action = &#x27;index&#x27;;        &#125;    &#125;&#125;

访问地址 http://vkphp.dev/index/index 或者 http://vkphp.dev/index.php/index/index ，即可看到打印出的patharr信息。
3.3. 获取URL中的参数&lt;?php/** * 路由控制 */namespace core;class route&#123;    public $ctrl;    public $action;    public $params=array();    public function __construct()&#123;        //echo &#x27;route is ready!&#x27;;        /**         * 1、隐藏index.php         * 2、获取URL中的控制器和方法         * 3、获取URL中的参数         */        if(isset($_SERVER[&#x27;REQUEST_URI&#x27;]) &amp;&amp; $_SERVER[&#x27;REQUEST_URI&#x27;] != &#x27;/&#x27;)&#123;            $path = $_SERVER[&#x27;REQUEST_URI&#x27;];            $patharr = explode(&#x27;/&#x27;,trim($path, &#x27;/&#x27;));            //p($patharr);            if(isset($patharr[0]))&#123;                if($patharr[0] != &#x27;index.php&#x27;)&#123;                    // 省略了index.php                    $this-&gt;ctrl = $patharr[0];                    if(isset($patharr[1]))&#123;                        $this-&gt;action = $patharr[1];                    &#125; else&#123;                        $this-&gt;action = &#x27;index&#x27;;                    &#125;                    $count = count($patharr);                    $i=2;                    while($i &lt; $count)&#123;                        $this-&gt;params[$patharr[$i]] = $patharr[$i+1];                        $i = $i + 2;                    &#125;                &#125;else&#123;                    // 没省略index.php                    if(isset($patharr[1]))&#123;                        $this-&gt;ctrl = $patharr[1];                    &#125;                    if(isset($patharr[2]))&#123;                        $this-&gt;action = $patharr[2];                    &#125; else&#123;                        $this-&gt;action = &#x27;index&#x27;;                    &#125;                    $count = count($patharr);                    $i=3;                    while($i &lt; $count)&#123;                        $this-&gt;params[$patharr[$i]] = $patharr[$i+1];                        $i = $i + 2;                    &#125;                &#125;            &#125;else&#123;                $this-&gt;ctrl = &#x27;index&#x27;;                $this-&gt;action = &#x27;index&#x27;;            &#125;        &#125;else &#123;            $this-&gt;ctrl = &#x27;index&#x27;;            $this-&gt;action = &#x27;index&#x27;;        &#125;        p($this-&gt;params);    &#125;&#125;

访问地址 http://vkphp.dev/index/index/id/3/name/voidking 或者 http://vkphp.dev/index.php/index/index/id/3/name/voidking ，即可看到打印出的params信息。
3.4. 支持localhost访问地址 http://localhost/vkphp/index.php/index/index/id/3/name/voidking ，无法正常获取控制器、方法和参数，修改如下：
&lt;?php/** * 路由控制 */namespace core;class route&#123;    public $ctrl=&#x27;index&#x27;;    public $action=&#x27;index&#x27;;    public $params=array();    public function __construct()&#123;        //echo &#x27;route is ready!&#x27;;        /**         * 1、隐藏index.php         * 2、获取URL中的控制器和方法         * 3、获取URL中的参数         */        if(isset($_SERVER[&#x27;REQUEST_URI&#x27;]) &amp;&amp; $_SERVER[&#x27;REQUEST_URI&#x27;] != &#x27;/&#x27; )&#123;            $path = $_SERVER[&#x27;REQUEST_URI&#x27;];            $patharr = explode(&#x27;/&#x27;,trim($path, &#x27;/&#x27;));        &#125;else&#123;            $patharr = array();        &#125;                if(isset($_SERVER[&#x27;HTTP_HOST&#x27;]) &amp;&amp; ($_SERVER[&#x27;HTTP_HOST&#x27;] == &#x27;localhost&#x27; || $_SERVER[&#x27;HTTP_HOST&#x27;] == &#x27;127.0.0.1&#x27;) )&#123;            // 去掉项目名称            $patharr = array_slice($patharr,1,count($patharr)-1);        &#125;        if(isset($patharr[0]))&#123;            if($patharr[0] == &#x27;index.php&#x27;)&#123;                // 去掉index.php                $patharr = array_slice($patharr,1,count($patharr)-1);            &#125;            if(isset($patharr[0]))&#123;                $this-&gt;ctrl = $patharr[0];            &#125;            if(isset($patharr[1]))&#123;                $this-&gt;action = $patharr[1];            &#125;                         $count = count($patharr);            $i=2;            while($i &lt; $count)&#123;                if(isset($patharr[$i+1]))&#123;                    $this-&gt;params[$patharr[$i]] = $patharr[$i+1];                &#125;                $i = $i + 2;            &#125;        &#125;                p($this-&gt;ctrl);        p($this-&gt;action);        p($this-&gt;params);    &#125;&#125;

4. 源码分享https://github.com/voidking/vkphp/releases/tag/v1.1.0
5. 书签从零开始打造自己的PHP框架
PHP 檔案引入路徑問題
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>框架</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：从零开始打造自己的PHP框架——第0章</title>
    <url>/dev-vkphp-0/</url>
    <content><![CDATA[1. 前言使用过thinkphp和yii，涉猎过shopnc。市场上已经有了这么多好的框架，为什么还要开发自己的框架？答：假装大神。为了在装神的道路上走的更远，郝同学决定开发一个自己的php框架，命名为vkphp。


2. 框架运行流程1、入口文件
2、定义常量
3、引入函数库
4、自动加载类
5、启动框架
6、路由解析
7、加载控制器
8、返回结果
3. helloworld3.1. 集成环境集成开发环境使用laragon，在www目录下新建vkphp项目目录。
3.2. 项目结构vkphp│  index.php│  README.md│├─app└─core    │  vk.php    │    └─common            function.php

3.3. index.php&lt;?php/*入口文件1、定义常量2、加载函数库3、启动框架*/define(&#x27;VKPHP&#x27;,realpath(&#x27;./&#x27;)); //获取项目所在根目录define(&#x27;CORE&#x27;,VKPHP.&#x27;/core&#x27;); //核心文件所在目录define(&#x27;APP&#x27;,VKPHP.&#x27;/app&#x27;); //应用文件所在目录define(&#x27;DEBUG&#x27;,true); //是否开启调试if(DEBUG)&#123;    ini_set(&#x27;display_errors&#x27;, &#x27;On&#x27;);&#125;else&#123;    ini_set(&#x27;display_errors&#x27;, &#x27;Off&#x27;);&#125;include CORE.&#x27;/common/function.php&#x27;;//p(VKPHP);include CORE.&#x27;/vk.php&#x27;;\core\vk::run();

3.4. function.php&lt;?phpfunction p($var)&#123;    if(is_null($var))&#123;        var_dump(NULL);    &#125;else if(is_bool($var))&#123;        var_dump($var);    &#125;else&#123;        echo &#x27;&lt;p&gt;&#x27;. print_r($var, true) .&#x27;&lt;/p&gt;&#x27;;    &#125;&#125;

3.5. vk.php&lt;?phpnamespace core;class vk&#123;    public function run()&#123;        p(&#x27;helloworld&#x27;);    &#125;&#125;

3.6. 测试访问启动laragon，在浏览器中访问 http://vkphp.dev 或者 http://vkphp.dev/index.php ，即可看到“helloworld”。
4. 源码分享https://github.com/voidking/vkphp/releases/tag/v1.0.0
5. 书签从零开始打造自己的PHP框架
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>框架</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：使用Navicat对比和同步MySQL</title>
    <url>/dev-navicat-mysqldiff/</url>
    <content><![CDATA[1. 问题线上一份数据库，测试环境一份数据库。怎样知道两个数据库之间的表结构和数据差异？
1、表结构对比：对比开发库和正式库之间的表结构，哪个表结构有字段的增加减少，或者字段类型的改变，并生成sql进行结构的同步。
2、数据对比：对比两个数据库中的某个表的数据差异，生成sql脚本进行两个库的同步。


2. 解决办法很简单，使用 navicat 。
很多开发者使用navicat创建数据库、创建表、增删查改、导出sql文件、导入sql文件等。但是，只有少数人知道，navicat还有强大的对比同步功能。
2.1. 情景描述东北高师就业联盟网，线上（阿里云）有一份数据库db_new_dsjyw，本地有一份数据库也叫db_new_dsjyw。因为在本地进行开发，所以本地的数据库表结构是最新的，而线上的数据库数据是最新的。开发完成要把本地数据库表结构同步到线上，该怎么做？
2.2. 表结构同步1、在navicat中，首先确认本地数据库连接和线上数据库连接都正常。
2、工具，结构同步。
3、结构同步窗口，常规选项卡中，源连接选择本地，源数据库选择db_new_dsjyw；目标连接选择阿里云，目标数据库选择db_new_dsjyw。
4、对比选项全部勾选，除了对比自动递增值，运行选项全部勾选。
5、单击“对比”，即可跳转到对比选项卡。
6、对比完成，右键查询修改小窗口，即可看到“运行查询”前的选项。
7、单击“全部勾选”，然后单击“已勾选脚本的详细信息”，可以看到脚本详细信息。
8、全选，复制，保存为sql脚本，用于修改线上的数据库表结构。
9、单击“运行查询”，即可修改线上数据库表结构。或者直接在线上数据库中执行第8步中得到的sql文件，也可以修改线上数据库表结构。
2.3. 表数据同步在本情景中，不需要同步表数据。如果非要同步的话，那么我们要先把线上的数据同步到本地，然后再把本地数据同步到线上。当然，更好的做法是线上新建数据库db_new_dsjyw2，然后切换到db_new_dsjyw2，万一同步除了问题，我们还可以切换回db_new_dsjyw。
下面我们尝试下线上数据同步到本地。
1、在navicat中，首先确认本地数据库连接和线上数据库连接都正常。
2、工具，数据同步。
3、数据同步窗口，常规选项卡中，源连接选择阿里云，源数据库选择db_new_dsjyw；目标连接选择本机，目标数据库选择db_new_dsjyw。
4、单击“全选”，源表出现在目标表中。
5、单击“开始”，开始数据同步。
3. 后记有同学推荐mysqldiff，mysqldiff工具是官方mysql-utilities工具集的一个脚本，可以用来对比不同数据库之间的表结构，或者同个数据库间的表结构。如果在windows下，直接下载安装mysql-utilities就可以了。
然而，上手体验不愉快，遂放弃，感兴趣的小伙伴自行百度。
4. 书签
10秒钟完成MySQL数据库结构对比
MySQL 对比数据库表结构
mysqldiff — Identify Differences Among Database Objects

]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>mysql</tag>
        <tag>数据库</tag>
        <tag>好好学MySQL</tag>
      </tags>
  </entry>
  <entry>
    <title>yii框架整合smarty使用自定义验证码</title>
    <url>/dev-yii-smarty-captcha/</url>
    <content><![CDATA[1. 前言把模板引擎换成smarty后，yii框架自带的captcha，非常不友好，使用起来不方便。干脆自己封装一个php图片验证码模块，想要移植到其他框架，简单修改即可。


2. 设计思路PHP生成验证码的原理：使用PHP的GD库，生成一张带验证码的图片，并将验证码保存在Session中。PHP生成验证码的大致流程有：
1、产生一张png的图片；
2、为图片设置背景色；
3、设置字体颜色和样式；
4、产生N位数的随机的验证码；
5、把产生的每个字符调整旋转角度和位置画到png图片上；
6、加入噪点和干扰线防止注册机器分析原图片来恶意破解验证码；
7、输出图片；
8、释放图片所占内存。
3. 项目结构1、《yii框架实战》的基础上，在controllers目录中新建util目录，util目录中新建CaptchaController.php，用来实现验证码模块。
2、在web中新建font目录，font目录中放入微软雅黑字体msyh.ttf，用来显示验证码上的字体。

4. 代码实现&lt;?phpnamespace app\controllers\util;use Yii;use yii\filters\AccessControl;use yii\web\Controller;use yii\filters\VerbFilter;class CaptchaController extends Controller&#123;    public function actionIndex()    &#123;        echo &#x27;captcha index function&#x27;;    &#125;    public function actionGetcode()&#123;        session_start();        $response = Yii::$app-&gt;getResponse();        $response-&gt;format = $response::FORMAT_RAW;        $this-&gt;createCode(300,80,30);    &#125;    public function createCode($width,$height,$fontsize)&#123;        //设置验证码图片大小的函数        $image = imagecreate($width, $height);            //设置验证码颜色 imagecolorallocate(int im, int red, int green, int blue);        $bgcolor = imagecolorallocate($image,255,255,255); //#fff        //区域填充 int imagefill(int im, int x, int y, int col) (x,y) 所在的区域着色,col 表示欲涂上的颜色        imagefill($image, 0, 0, $bgcolor);                //设置变量        $captcha_code = &quot;&quot;;        //生成随机的字母和数字        for($i=0;$i&lt;4;$i++)&#123;                          //设置字体颜色，随机颜色，0-120深颜色            $fontcolor = imagecolorallocate($image, rand(0,120),rand(0,120), rand(0,120));                 //$fontcolor = imagecolorallocate($image, 0, 0, 0);            //设置需要随机取的值，去掉容易出错的值如0和o、1和l、2和z            $data =&#x27;abcdefghigkmnpqrstuvwxy3456789&#x27;;            //取出值，字符串截取方法  strlen获取字符串长度            $fontcontent = substr($data, rand(0,strlen($data)-1),1);            //连续定义变量            $captcha_code .= $fontcontent;                            //imagestring设置字体大小，最大值为5            //$fontsize = 5;             //设置坐标            $x = ($i*$width/4)+rand(5,10);            $y = rand(($height+$fontsize)/2-5,($height+$fontsize)/2+5);            //imagestring($image,$fontsize,$x,$y,$fontcontent,$fontcolor);            imagettftext($image,$fontsize,rand(-10,10),$x,$y,$fontcolor,&#x27;font/msyh.ttf&#x27;,$fontcontent);        &#125;        //存到session        $_SESSION[&#x27;captcha_code&#x27;] = $captcha_code;        //增加干扰元素，设置雪花点        for($i=0;$i&lt;200;$i++)&#123;            //设置点的颜色，50-200颜色比数字浅，不干扰阅读            $pointcolor = imagecolorallocate($image,rand(50,200), rand(50,200), rand(50,200));                //imagesetpixel — 画一个单一像素            imagesetpixel($image, rand(1,$width-1), rand(1,$height-1), $pointcolor);        &#125;        //增加干扰元素，设置横线        for($i=0;$i&lt;2;$i++)&#123;            //设置线的颜色            $linecolor = imagecolorallocate($image,rand(80,220), rand(80,220),rand(80,220));            //设置线，两点一线            imageline($image,rand(1,$width-1), rand(1,$height-1),rand(1,$width-1), rand(1,$height-1),$linecolor);        &#125;                 //设置头部，image/png        header(&#x27;Content-Type: image/png&#x27;);        //imagepng() 建立png图形函数        imagepng($image);        //imagedestroy() 结束图形函数 销毁$image        imagedestroy($image);    &#125;    public function actionCheck($code)&#123;        session_start();        if(isset($_SESSION[&#x27;captcha_code&#x27;]))&#123;            if($_SESSION[&#x27;captcha_code&#x27;] == $code)&#123;                unset($_SESSION[&#x27;captcha_code&#x27;]);                $result = array(                    &#x27;code&#x27;=&gt; &#x27;0&#x27;,                    &#x27;ext&#x27;=&gt; &#x27;验证成功&#x27;                );            &#125;else&#123;                $result = array(                    &#x27;code&#x27;=&gt; &#x27;1&#x27;,                    &#x27;ext&#x27;=&gt; &#x27;验证码错误&#x27;                );            &#125;        &#125;else&#123;            $result = array(                &#x27;code&#x27;=&gt; &#x27;2&#x27;,                &#x27;ext&#x27;=&gt; &#x27;请先刷新验证码&#x27;            );        &#125;        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125; &#125;

5. 使用5.1. 接口获取验证码接口：http://localhost/basic/web/util/captcha/getcode
验证验证码接口：http://localhost/basic/web/util/captcha/check?code=f7nh
5.2. 显示在smarty渲染的页面中，直接使用验证码接口即可显示图片。
&lt;p&gt;&lt;img id=&quot;captcha&quot; src=&quot;http://localhost/basic/web/util/captcha/getcode&quot; alt=&quot;&quot;&gt;&lt;/p&gt;&lt;p&gt;请输入验证码：&lt;input id=&quot;code&quot; type=&quot;text&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;input id=&quot;check&quot; type=&quot;button&quot; value=&quot;确定&quot;&gt;&lt;/p&gt;

5.3. 刷新如果要刷新验证码，在js中重置src地址即可。
$(&#x27;#captcha&#x27;).click(function()&#123;    var $new_src = &#x27;http://localhost/basic/web/util/captcha/getcode?&#x27;+Math.random();    $(this).attr(&#x27;src&#x27;,$new_src);&#125;);

5.4. 验证$(&#x27;#check&#x27;).click(function()&#123;    var data = &#123;        code: $(&#x27;#code&#x27;).val()    &#125;;    $.ajax(&#123;        url: &#x27;http://localhost/basic/web/util/captcha/check&#x27;,        type: &#x27;GET&#x27;,        dataType: &#x27;json&#x27;,        data: data,        success: function(data)&#123;            alert(data.ext);        &#125;,        error: function(xhr)&#123;            console.log(xhr);        &#125;    &#125;);&#125;);

5.5. 效果演示
6. 源码分享1、下载安装：git clone https://github.com/voidking/yii-basic.git basic
2、利用navicat等工具连接到本地mysql数据库，创建数据库basic，在数据库中创建表bas_project(int id, varchar title, varchar content)。注意，编码格式选择utf8。
3、验证码测试页url：http://localhost/basic/web/util/captcha/index
7. 书签PHP生成各种验证码和Ajax验证
PHP如何实现验证码
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>yii</tag>
        <tag>验证码</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：PHP调试之Xdebug+PhpStorm</title>
    <url>/dev-php-debug-xdebug/</url>
    <content><![CDATA[1. 前言该怎么强调调试的重要性呢？我们生病的时候，要去看医生，医生会通过各种仪器对我们进行检查，定位病因，然后给我们治疗。程序也会生病，生病的时候，作为医生（开发者）的我们，就要通过各种办法定位bug，然后修改代码。定位bug并且修改代码的过程，就是调试。
最简单直接的调试办法是摘出需要调试的部分，放入 main.php 中，然后运行脚本：php main.php前提是调试环境要安装好php，检查方法：php -v
但是，这种方法并不适合项目的调试，因为项目中涉及到很多上下文，模拟起来会比较麻烦。这时候就需要调试工具出场了。
PHP的调试工具有很多，本文记录一下Xdebug的使用方法。配合使用的集成环境为laragon-1.0，IDE为PhpStorm-2016.2.1。


2. 启用Xdebug1、启动laragon后，任务栏右键laragon图标，定位到PHP，然后单击php.ini。（或者直接到laragon安装目录寻找php.ini，然后打开它）
2、找到[XDebug]，然后修改如下：
[XDebug]zend_extension=php_xdebug.dllxdebug.profiler_append=onxdebug.profiler_enable=onxdebug.profiler_enable_trigger=onxdebug.profiler_output_dir=&quot;C:/laragon/tmp/xdebug&quot;xdebug.profiler_output_name=&quot;cachegrind.out.%t-%s&quot;xdebug.remote_enable=onxdebug.remote_handler=&quot;dbgp&quot;xdebug.remote_host=&quot;127.0.0.1&quot;xdebug.remote_port=9000xdebug.trace_output_dir=&quot;C:/laragon/tmp/xdebug&quot;xdebug.idekey=PHPSTORM

3、访问http://localhost，查看phpinfo。
4、在网页中Ctrl+F，搜索“xdebug”，如果找到了xdebug的配置信息，则证明xdebug启用成功。
3. PhpStorm配置1、打开phpstorm，File，Settings。
2、搜索“debug”，可以看到PHP下面的Debug，单击Debug。
3、在xdebug栏，Debug port默认端口为9000，一般不需要修改。同时，三个选项全部打钩。
4、展开Debug，单击DBGp Proxy，填入IDE key为PHPSTORM，Host为localhost，Port为80。单击OK，退出设置。
4. Debug配置1、View，Toolbar，显示工具栏。
2、View，Tool Windows，Project，显示项目结构。
3、单击工具栏向下的小三角，Edit Configurations。
4、在新打开的Run/Debug Configurations窗口中，单击左上角加号，选择PHP Web Application。
5、右侧Server配置为本地Web服务器。
5. 开始调试1、在PhpStorm中，单击代码左侧插入断点。
2、单击导航栏的电话，然后单击导航栏中的虫子，即可打开默认浏览器进入调试。
3、网页卡住，PhpStorm自动定位到断点处。
4、此时，我们就可以根据PhpStorm提供的按钮进行需要的调试。
6. XDEBUG_SESSION_START在调试时，郝同学发现一个神奇的问题：假设默认浏览器是chrome，那么在chrome中访问接口，PhpStorm会自动定位到断点处。但是，如果这时使用firefox或postman访问同样的接口，则无法定位到断点处。
解决办法：郝同学注意到，在PhpStorm打开默认浏览器时，url中带有参数，例如：http://www.dsjyw.net/?XDEBUG_SESSION_START=10218
在firefox和postman中的接口地址后也加上XDEBUG_SESSION_START=10218，成功跳转到断点，问题解决。
7. 书签有哪些 PHP 调试技巧？
如何调试PHP程序
Xdebug文档
Xdebug下载地址
php使用Xdebug进行调试
用 Xdebug 修正 PHP 应用程序中的错误
Xdebug 配置
phpstorm+Xdebug断点调试PHP
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>调试</tag>
        <tag>xdebug</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux修改命令提示符信息(PS1)</title>
    <url>/dev-linux-ps1/</url>
    <content><![CDATA[1. 问题如何将 root@izwz94j3dpfuv8pcrn1qitz 修改成 root@voidking-ali？
2. 解决办法1、查看原PS1，echo $PS1，显示结果为：
[\u@\h \W]\$

2、编辑.bashrc，vi ~/.bashrc
3、添加
export PS1=&#x27;[\u@voidking-ali \W]\$&#x27;

或者
export PS1=&quot;[\u@voidking-ali \W]\\$&quot;

4、立即生效，source ~/.bashrc


3. 扩展一般情况下，Linux 的命令提示信息类似于以下样子，显示为[当前用户名@短主机名  当前短路径]提示符，例如 [root@voidking-ali ~]#。
不过，有时候我们可能想自定义命令提示符的显示信息，比如显示完整的主机名和完整的路径。
PS1变量中提示符各项含义:
\d ：代表日期，格式为weekday month date，例如：&quot;Mon Aug 1&quot;\H ：完整的主机名称。例如：我的机器名称为：voidking.linux，则这个名称就是voidking.linux\h ：仅取主机的第一个名字，如上例，则为voidking，.linux则被省略\t ：显示时间为24小时格式，如：HH：MM：SS\T ：显示时间为12小时格式\A ：显示时间为24小时格式：HH：MM\u ：当前用户的账号名称\v ：BASH的版本信息\w ：完整的工作目录名称。家目录会以 ~代替\W ：利用basename取得工作目录名称，所以只会列出最后一个目录\$ ：提示字符，如果是root时，提示符为：# ，普通用户则为：$


]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>leetcode刷题记录-排序算法总结</title>
    <url>/dev-leetcode-sort/</url>
    <content><![CDATA[1. 前言排序有内部排序和外部排序，内部排序是数据记录在内存中进行排序，而外部排序是因排序的数据很大，一次不能容纳全部的排序记录，在排序过程中需要访问外存。
内部排序大致分为五种：交换排序、插入排序、选择排序、归并排序和基数排序。其中交换排序包括冒泡排序和快速排序、插入排序包括直接插入排序和希尔排序，选择排序包括简单选择排序和堆排序。
当n较大时，应采用时间复杂度为O(nlog2n)的排序方法：快速排序、堆排序或归并排序。
快速排序目前是内部排序中最好的方法，当待排序的关键字是随机分布时，快速排序的平均时间最短。


2. 自拟题目使用冒泡排序、快速排序、直接插入排序、希尔排序、简单选择排序、堆排序、归并排序、基数排序共八种排序方法，给[8,7,4,2,1,3,5,9,0,6]按照升序和降序排序。示例输入：
[8,7,4,2,1,3,5,9,0,6]

示例输出：
[0,1,2,3,4,5,6,7,8,9][9,8,7,6,5,4,3,2,1,0]

3. 冒泡排序冒泡排序重复地走访过要排序的元素，依次比较相邻两个元素，如果他们的顺序错误就把他们调换过来，直到没有元素再需要交换，排序完成。这个算法的名字由来是因为越小（或越大）的元素会经由交换慢慢“浮”到数列的顶端。
以左边为水面，升序冒泡排序算法描述如下：
1、从最后一对相邻的元素开始，比较相邻的元素，如果后一个比前一个小，则调换它们的位置（小的冒泡）。2、对每一对相邻元素作同样的工作，从最后一对到第一对。3、第一个元素固定为最小元素，不参与接下来的比较。4、对越来越少的元素（除了固定元素）重复上面的步骤，直到没有任何一对元素需要比较。

// 冒泡排序public int[] bubble(int[] arr)&#123;    int sum = arr.length;    int temp = 0;    // sum个数，需要sum次冒泡，每次冒泡一个数找到位置    for(int i=0;i&lt;sum;i++)&#123;        for(int j=sum-1;j&gt;i;j--)&#123;            if(arr[j] &lt; arr[j-1])&#123;                temp = arr[j-1];                arr[j-1] = arr[j];                arr[j] = temp;            &#125;        &#125;    &#125;    return arr;&#125;

4. 快速排序快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序，它是对冒泡排序的改进。它采用了一种分治的策略，通常称其为分治法(Divide-and-ConquerMethod)。MoreWindows同学把它归结为：挖坑填数+分治法。
升序快速排序算法描述如下：
1、i = L; j = R; 将基准数挖出形成第一个坑a[i]。2、j–由后向前找比基准输小的数，找到后挖出此数填前一个坑a[i]中。3、i++由前向后找比基准数大的数，找到后也挖出此数填到前一个坑a[j]中。4、重复执行2，3步，直到i==j，将基准数填入a[i]中。5、以上，得到两个区，左边的区比基数小，右边的区比基数大。6、对于得到的两个区，分别重复1-4。

// 快速排序public int[] quick(int[] arr,int l,int r)&#123;    int temp = arr[l];    int i = l;    int j = r;    while(i &lt; j)&#123;        while(i &lt; j &amp;&amp; arr[j] &gt;= temp)               j--;         if(i &lt; j)&#123;            arr[i] = arr[j];            i++;        &#125;                while(i &lt; j &amp;&amp; arr[i] &lt; temp)&#123;            i++;        &#125;        if(i &lt; j)&#123;            arr[j] = arr[i];             j--;        &#125;    &#125;    arr[i] = temp;            if(l &lt; r)&#123;        quick(arr, l, i-1);        quick(arr, i+1, r);    &#125;    return arr;    &#125;


5. 直接插入排序插入排序是一种简单直观的排序算法，它的工作原理非常类似于我们抓扑克牌。
升序插入算法描述如下：
1、从第一个元素开始，该元素可以认为已经被排序。2、取出下一个元素，在已排序的元素序列中从后向前扫描。3、如果新元素小于已排序的元素（老元素），将老元素移到下一位置。4、重复步骤3，直到找到新元素大于或等于老元素的位置，将新元素插入到该位置。5、重复步骤2~4。

// 直接插入排序public int[] insertion(int[] arr)&#123;    int sum = arr.length;    for(int i=1;i&lt;sum;i++)&#123;        int temp = arr[i];        int j = i-1;        while(j&gt;=0 &amp;&amp; temp&lt;arr[j])&#123;            arr[j+1] = arr[j];            j--;        &#125;        arr[j+1] = temp;    &#125;    return arr;&#125;


6. 希尔排序希尔排序，也叫递减增量排序，是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的：

插入排序在对几乎已经排好序的数据操作时，效率高，即可以达到线性排序的效率
但插入排序一般来说是低效的，因为插入排序每次只能将数据移动一位

希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序，算法的最后一步就是普通的插入排序，但是到了这步，需排序的数据几乎是已排好的了（此时插入排序较快）。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n^2)的排序（冒泡排序或直接插入排序），可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据，所以小数据只需进行少数比较和交换即可到正确位置。
升序希尔算法描述如下：1、选择增量gap=length/2，把元素分成gap组，每组取值为{i,i+gap,i+2gap,…}。2、分别对每一组中的数据进行从前到后扫描插入排序。3、缩小增量gap = gap/2，这种增量选择可以用一个序列来表示，{n/2,(n/2)/2…1}，称为增量序列。4、重复步骤2~3，直到增量gap=1。

// 希尔排序public int[] shell(int[] arr)&#123;    int sum = arr.length;    int gap = sum/2;    while(gap &gt;= 1)&#123;        //分成gap组        for(int i=0;i&lt;gap;i++)&#123;            //组内排序            for(int j=i+gap;j&lt;sum;j += gap)&#123;                int temp = arr[j];                int k = j-gap;                while(k &gt;= 0 &amp;&amp; temp &lt; arr[k])&#123;                    arr[k+gap] = arr[k];                    k = k-gap;                &#125;                arr[k+gap] = temp;            &#125;        &#125;        gap = gap/2;            &#125;    return arr;&#125;


7. 简单选择排序选择排序是一种简单直观的排序算法。它的工作原理很容易理解：初始时在序列中找到最小（大）元素，放到序列的起始位置作为已排序序列；然后，再从剩余未排序元素中继续寻找最小（大）元素，放到已排序序列的末尾。以此类推，直到所有元素均排序完毕。
注意选择排序与冒泡排序的区别：冒泡排序通过依次交换相邻两个顺序不合法的元素位置，从而将当前最小（大）元素放到合适的位置；而选择排序每遍历一次都记住了当前最小（大）元素的位置，最后仅需一次交换操作即可将其放到合适的位置。
升序简单选择排序描述如下：1、未排序序列中，假设最小元素指针position指向第一个元素i。2、依次对比所有未排序元素，遇到更小的元素，则把position指向更小的元素。3、遍历未排序序列结束，交换position和i的元素，i位置排序完成。4、重复1到3，直到所有位置排序完成。
// 简单选择排序public int[] selection(int[] arr)&#123;    int sum = arr.length;    for(int i=0;i&lt;sum;i++)&#123;        int position = i;        for(int j=i+1;j&lt;sum;j++)&#123;            if(arr[j] &lt; arr[position])&#123;                position = j;            &#125;        &#125;        int tmp = arr[i];        arr[i] = arr[position];        arr[position] = tmp;    &#125;    return arr;&#125;

8. 堆排序堆排序是利用堆这种数据结构而设计的一种排序算法，堆排序是一种选择排序，它的最坏、最好，平均时间复杂度均为O(nlogn)，它也是不稳定排序。
堆是具有以下性质的完全二叉树：每个结点的值都大于或等于其左右孩子结点的值，称为大顶堆；或者每个结点的值都小于或等于其左右孩子结点的值，称为小顶堆。
堆排序的基本思想是：将待排序序列构造成一个大顶堆，此时，整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换，此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆，这样会得到n个元素的次小值。如此反复执行，便能得到一个有序序列了。
参照图解排序算法(三)之堆排序，升序堆排序描述如下：1、构造初始堆。将给定无序序列构造成一个大顶堆（一般升序采用大顶堆，降序采用小顶堆)。2、从最后一个非叶子结点开始（叶结点自然不用调整，第一个非叶子结点arr.length/2-1），从右至左，从下至上进行调整。3、交换位置后也许子根结构混乱，那么继续调整，直到构造完成大顶堆。4、将堆顶元素与末尾元素进行交换，使末尾元素最大。然后继续调整堆，构造成大顶堆。再将堆顶元素与末尾元素交换，得到第二大元素。如此反复进行交换、重建、交换。
// 堆排序public int[] heap(int[] arr)&#123;    int sum = arr.length;    //1.构建大顶堆    for(int i=arr.length/2-1;i&gt;=0;i--)&#123;        //从第一个非叶子结点从下至上，从右至左调整结构        adjustHeap(arr,i,arr.length);    &#125;    //2.调整堆结构+交换堆顶元素与末尾元素    for(int j=arr.length-1;j&gt;0;j--)&#123;        swap(arr,0,j);//将堆顶元素与末尾元素进行交换        adjustHeap(arr,0,j);//重新对堆进行调整    &#125;    return arr;&#125;//调整大顶堆（仅是调整过程，建立在大顶堆已构建的基础上）public void adjustHeap(int []arr,int i,int length)&#123;    int temp = arr[i];//先取出当前元素i    for(int k=i*2+1;k&lt;length;k=k*2+1)&#123;//从i结点的左子结点开始，也就是2i+1处开始        if(k+1&lt;length &amp;&amp; arr[k]&lt;arr[k+1])&#123;//如果左子结点小于右子结点，k指向右子结点            k++;        &#125;        if(arr[k] &gt;temp)&#123;//如果子节点大于父节点，将子节点值赋给父节点（不用进行交换）            arr[i] = arr[k];            i = k;        &#125;else&#123;            break;        &#125;    &#125;    arr[i] = temp;//将temp值放到最终的位置&#125;//交换元素public static void swap(int []arr,int a ,int b)&#123;    int temp=arr[a];    arr[a] = arr[b];    arr[b] = temp;&#125;

9. 归并排序归并排序是创建在归并操作上的一种有效的排序算法，效率为O(nlogn)，1945年由冯·诺伊曼首次提出。
归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用，我们将一个大问题分割成小问题分别解决，然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并，然后四四归并，然后是八八归并，一直下去直到归并了整个数组。
归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作，归并操作步骤如下：
1、申请空间，该空间用来存放合并后的序列2、设定两个指针，最初位置分别为两个已经排序序列的起始位置3、比较两个指针所指向的元素，选择相对小的元素放入到合并空间，并移动指针到下一位置4、重复步骤3直到某一指针到达序列尾5、将另一序列剩下的所有元素直接复制到合并序列尾
参照图解排序算法(四)之归并排序，代码如下：
// 归并排序public int[] merge(int[] arr)&#123;    int []temp = new int[arr.length];//在排序前，先建好一个长度等于原数组长度的临时数组，避免递归中频繁开辟空间    mergeSort(arr,0,arr.length-1,temp);    return arr;&#125;public void mergeSort(int[] arr,int left,int right,int []temp)&#123;    if(left&lt;right)&#123;        int mid = (left+right)/2;        mergeSort(arr,left,mid,temp);//左边归并排序，使得左子序列有序        mergeSort(arr,mid+1,right,temp);//右边归并排序，使得右子序列有序        mergeCore(arr,left,mid,right,temp);//将两个有序子数组合并操作    &#125;&#125;public void mergeCore(int[] arr,int left,int mid,int right,int[] temp)&#123;    int i = left;//左序列指针    int j = mid+1;//右序列指针    int t = 0;//临时数组指针    while (i&lt;=mid &amp;&amp; j&lt;=right)&#123;        if(arr[i]&lt;=arr[j])&#123;            temp[t++] = arr[i++];        &#125;else &#123;            temp[t++] = arr[j++];        &#125;    &#125;    while(i&lt;=mid)&#123;//将左边剩余元素填充进temp中        temp[t++] = arr[i++];    &#125;    while(j&lt;=right)&#123;//将右序列剩余元素填充进temp中        temp[t++] = arr[j++];    &#125;    t = 0;    //将temp中的元素全部拷贝到原数组中    while(left &lt;= right)&#123;        arr[left++] = temp[t++];    &#125;&#125;


10. 基数排序基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机上的贡献。它是这样实现的：将所有待比较正整数统一为同样的数位长度，数位较短的数前面补零。然后，从最低位开始进行基数为10的计数排序，一直到最高位计数排序完后，数列就变成一个有序序列（利用了计数排序的稳定性）。
基数排序步骤如下：1、初始化：构造一个10*n的二维数组，一个长度为n的数组用于存储每次位排序时每个桶子里有多少个元素。2、循环操作：从低位开始（我们采用LSD的方式），将所有元素对应该位的数字存到相应的桶子里去（对应二维数组的那一列）。然后将所有桶子里的元素按照桶子标号从小到大取出，对于同一个桶子里的元素，先放进去的先取出，后放进去的后取出（保证排序稳定性）。这样原数组就按该位排序完毕了，继续下一位操作，直到最高位排序完成。
参考基数排序详解以及java实现，代码如下：
// 基数排序public int[] radix(int[] arr, int d)&#123;    int n=1;//代表位数对应的数：1,10,100...    int k=0;//保存每一位排序后的结果用于下一位的排序输入    int length=arr.length;    int[][] bucket=new int[10][length];//排序桶用于保存每次排序后的结果，这一位上排序结果相同的数字放在同一个桶里    int[] order=new int[length];//用于保存每个桶里有多少个数字    while(n&lt;d)    &#123;        for(int num:arr) //将数组array里的每个数字放在相应的桶里        &#123;            int digit=(num/n)%10;            bucket[digit][order[digit]]=num;            order[digit]++;        &#125;        for(int i=0;i&lt;length;i++)//将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果        &#123;            if(order[i]!=0)//这个桶里有数据，从上到下遍历这个桶并将数据保存到原数组中            &#123;                for(int j=0;j&lt;order[i];j++)                &#123;                    arr[k]=bucket[i][j];                    k++;                &#125;            &#125;            order[i]=0;//将桶里计数器置0，用于下一次位排序        &#125;        n*=10;        k=0;//将k置0，用于下一轮保存位排序结果    &#125;        return arr;&#125;


11. 源码地址https://github.com/voidking/leetcode/tree/master/src/com/voidking/leetcode/sort
12. 书签常用排序算法总结(一)
常用排序算法总结(二)
八大排序算法
图解排序算法(一)之3种简单排序(选择，冒泡，直接插入)
图解排序算法(二)之希尔排序
]]></content>
      <categories>
        <category>engineering</category>
        <category>algorithm</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>算法</tag>
        <tag>leetcode</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：VirtualBox中安装OpenStack</title>
    <url>/dev-virtualbox-ubuntu-openstack/</url>
    <content><![CDATA[1. 前言OpenStack的安装是一个难点，非常难。哪怕已经安装过OpenStack，再次安装，也不敢说一定成功。安装过程中某处细微变化，也许就会出现各种奇葩错误。
总的来说，OpenStack大致分为4种安装方式：1、基于虚拟机的快速体验安装。这是最简单的方法，把搭建好环境的虚拟机运行起来即可。但是，遗憾的是，在网上没有找到OpenStack的虚拟机镜像。
2、基于DevStack快速脚本安装。这是OpenStack官方推荐的方式，安装速度较快，基于Folsom版本。但是，安装过程中一言不合就报错，困难重重。
3、使用自动化安装工具。RDO、Fuel、Puppet、Chef、Salt等，都是OpenStack的自动化安装工具。说是自动化安装工具，实际上安装配置时，也是非常繁琐。
4、手动安装。这种方式最困难，但是实际生产环境下，基本都需要手动安装。
本文，就探讨一下在VirtualBox中使用DevStack安装OpenStack的步骤。也就是说，我们要自己创建一个OpenStack的虚拟机镜像。


2. 系统准备1、VirtualBox中安装好Ubuntu16，至少需要内存4G，硬盘20G。
2、配置好Ubuntu16的网络地址为192.169.56.102。
3、安装好ssh服务，方便远程操作。
步骤2和3可以参考《VirtualBox下CentOS7和Ubuntu16.04网络配置》。
3. 环境准备1、安装gitapt-get install git
2、安装pythonapt-get install python
3、添加stack用户useradd -s /bin/bash -d /opt/stack -m stack
4、给stack用户添加sudo权限echo &quot;stack ALL=(ALL) NOPASSWD: ALL&quot; | sudo tee /etc/sudoers.d/stack
5、切换到stack用户su - stack
6、下载devstackgit clone https://git.openstack.org/openstack-dev/devstack或者git clone https://github.com/openstack-dev/devstack
推荐从github下载。速度缓慢，请耐心等待。
7、切换到stable/newton分支cd devstack
git checkout stable/newton
4. 配置devstack4.1. 创建local.conf在devstack目录下创建local.conf文件，内容如下：
[[local|localrc]]ADMIN_PASSWORD=voidkingDATABASE_PASSWORD=$ADMIN_PASSWORDRABBIT_PASSWORD=$ADMIN_PASSWORDSERVICE_PASSWORD=$ADMIN_PASSWORD# BranchesKEYSTONE_BRANCH=stable/newtonNOVA_BRANCH=stable/newtonNEUTRON_BRANCH=stable/newtonSWIFT_BRANCH=stable/newtonGLANCE_BRANCH=stable/newtonCINDER_BRANCH=stable/newton# Use mirrorGIT_BASE=http://git.trystack.cnNOVNC_REPO=http://git.trystack.cn/kanaka/noVNC.gitSPICE_REPO=http://git.trystack.cn/git/spice/spice-html5.git# Enable heat servicesenable_service h-eng h-api h-api-cfn h-api-cw# Enable heat pluginenable_plugin heat http://git.trystack.cn/openstack/heat.git stable/newtonIMAGE_URL_SITE=&quot;http://download.fedoraproject.org&quot;IMAGE_URL_PATH=&quot;/pub/fedora/linux/releases/25/CloudImages/x86_64/images/&quot;IMAGE_URL_FILE=&quot;Fedora-Cloud-Base-25-1.3.x86_64.qcow2&quot;IMAGE_URLS+=&quot;,&quot;$IMAGE_URL_SITE$IMAGE_URL_PATH$IMAGE_URL_FILE# Service/Dashboard IPFLOATING_RANGE=192.168.1.224/27HOST_IP=192.168.56.102FLAT_INTERFACE=enp0s3

4.2. local.conf说明1、密码配置
ADMIN_PASSWORD=voidkingDATABASE_PASSWORD=$ADMIN_PASSWORDRABBIT_PASSWORD=$ADMIN_PASSWORDSERVICE_PASSWORD=$ADMIN_PASSWORD

2、git仓库devstack默认会从 https://git.openstack.org 上下载openstack组件，那是相当的慢。故在local.conf必须要把仓库替换成其他源，国内推荐git.trystack.cn。注意，trystack用的是http，不是https。
GIT_BASE=http://git.trystack.cn
或者
GIT_BASE=https://github.com

3、branch保证与devstack的branch一致，避免不可预知的问题。
NOVA_BRANCH=stable/newton

4、Service/Dashboard IP与虚拟机IP保持一致。
HOST_IP=192.168.56.102

4.3. 更换pypi源（可选）devstack默认python源为 https://pypi.python.org ，国内访问非常慢。可以从 https://www.pypi-mirrors.org/ 选一个国内的镜像，把镜像地址写到pip.conf文件，并放到/root/.pip/pip.conf以及/home/stack/.pip/pip.conf。
[global]index-url = https://pypi.doubanio.com/simple

5. 安装devstack1、切换stack用户，在devstack目录下。./stack.sh
安装过程中，会三次提示输入密码，这些密码需要和local.conf中保持一致。
如果下载报错了。。。，请重新执行：./stack.sh
这一步非常久，郝同学用了六个多小时，请耐心等待。
3、安装成功，提示：
=========================Total runtime         2982run_process           104test_with_retry         6apt-get-update         22pip_install           462restart_apache_server  15wait_for_service       26apt-get                12=========================This is your host IP address: 192.168.56.102This is your host IPv6 address: ::1Horizon is now available at http://192.168.56.102/dashboardKeystone is serving at http://192.168.56.102/identity/The default users are: admin and demoThe password: voidking2017-06-15 01:01:59.770 | WARNING: 2017-06-15 01:01:59.771 | Using lib/neutron-legacy is deprecated, and it will be removed in the future2017-06-15 01:01:59.771 | stack.sh completed in 2982 seconds.


4、大功告成，这时就可以通过浏览器访问openstack服务了！访问地址：http://192.168.56.102/dashboard

PS：如果这一步无法访问，提示“Internal Server Error”，那么需要手动把ip地址加到local_settings.py里的ALLOWED_HOSTS字段。vim /opt/stack/horizon/openstack_dashboard/local/local_settings.py
# If horizon is running in production (DEBUG is False), set this# with the list of host/domain names that the application can serve.# For more information see:# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hostsALLOWED_HOSTS = [&#x27;*&#x27;]

然后重启apache2服务，service apache2 restart
5、输入用户名admin，密码voidking，进入管理面板。
6. 书签
VirtualBox + DevStack
DevStack
10分钟安装OpenStack
Fuel 30 分钟快速安装OpenStack
个人电脑上搭建OpenStack的实验室
All-In-One Single Machine
Huawei Icehouse All in One Operation Guide
OpenStack项目系列介绍（3） Devstack
DevStack环境搭建
Ubuntu14.04下载地址1
Ubuntu14.04下载地址2

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>ubuntu</tag>
        <tag>openstack</tag>
        <tag>virtualbox</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学OpenStack：OpenStack入门篇</title>
    <url>/dev-openstack-start/</url>
    <content><![CDATA[1. 云计算“云”是两个比喻：第一个比喻是互联网，计算资源不在本地，在互联网的另一端（云端）。第二个比喻是群组，天上的云是雨滴的汇聚成群组，云计算中的云是计算资源的群组。
云计算有一个愿景，就是希望把计算像水电一样提供给用户使用。然而用户并不是直接使用电，而是使用电灯电视等；同样，用户并不会直接使用计算资源，而是使用在计算资源上层开发的一些应用。 
“云”是分层的：

IaaS：Infrastructure-as-a-Service，基础设施即服务。
PaaS：Platform-as-a-Service，平台即服务。
SaaS：Software-as-a-Service，软件即服务。



2. What is OpenStack?
OpenStack is a cloud operating system that controls large pools of compute, storage, and networking resources throughout a datacenter, all managed through a dashboard that gives administrators control while empowering their users to provision resources through a web interface.

OpenStack是一个云操作系统，控制整个数据中心的计算、存储和网络资源。管理员通过面板管理OpenStack，普通用户通过一个Web界面获取资源。
OpenStack将硬件资源虚拟化出计算资源池，向上开放了一系列API，用于支持上层应用的开发，满足用户对计算资源的各种需求。
OpenStack与Docker的集成有两个方面：1、利用Docker进行OpenStack部署。2、在OpenStack中集成Docker，提供PaaS服务。
3. 书签
写在最前面 - 每天5分钟玩转 OpenStack（1）
5分钟学会OpenStack 基础知识
为何现在流行OpenStack和Docker结合？
深度解析Docker和OpenStack系统集成
Docker学习笔记 — Docker与OpenStack集成
How To Get Started With OpenStack
云服务的三层概念

]]></content>
      <categories>
        <category>engineering</category>
        <category>openstack</category>
      </categories>
      <tags>
        <tag>openstack</tag>
        <tag>好好学OpenStack</tag>
      </tags>
  </entry>
  <entry>
    <title>行为型模式之迭代器模式</title>
    <url>/dev-design-pattern-iterator/</url>
    <content><![CDATA[1. 需求场景在Java中，经常需要将一类对象放到List、Set、Map等容器中，而且通常需要对容器中的对象进行遍历访问。那么如何实现对这些容器的遍历呢？当然，每个容器本身都提供了遍历的方法。但是这些方法是不统一的，客户端的使用变得麻烦。


2. 解决办法其实，在Java中不管是采用List、Set方式存储的对象，还是采用Map方式存储对象，都可以使用迭代器进行遍历。
3. 总结迭代器模式提供一种方法，可以顺序访问一个集合中的对象，而又不需要暴露该对象的内部表示。
迭代器模式分离了集合对象的遍历行为，抽象出一个迭起器类来负责，这样既可以做到不暴露集合的内部结构，又可让外部代码透明地访问集合内部的数据。
迭代器模式主要由4部分组成：迭代器角色、具体迭代器角色、容器角色和具体容器角色。

迭代器角色负责定义访问和遍历元素的接口。
具体迭代器角色要实现迭代器接口，并要记录遍历中的当前位置。
容器角色负责提供创建具体迭代器角色的接口。
具体容器角色实现创建具体迭代器角色的接口，这个具体迭代器角色与该容器的结构无关。

4. 源码分享https://github.com/voidking/design-pattern-behavior.git
5. 参考文献《易学设计模式》《大话设计模式》接口设计六大原则软件设计六大设计原则讲解Java中如何遍历Map对象的4种方法Java容器集合类的区别用法
]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
      </categories>
      <tags>
        <tag>设计模式</tag>
      </tags>
  </entry>
  <entry>
    <title>行为型模式之策略模式</title>
    <url>/dev-design-pattern-strategy/</url>
    <content><![CDATA[1. 现实场景在销售产品时，为了促销，经常会进行打折。针对不同的时间、不同的消费群体，采用的打折策略也会不同。比如有的打八折，有的买一送一。这种动态的调整销售策略的行为，就是策略模式在现实生活中的应用。


2. 需求场景一个大公司有两个子公司，吉林子公司和河北子公司，每个子公司都要计算薪资。薪资的计算包括基本工资、社会保险和个人所得税。
一般思路，使用模板方法模式。
public abstract class SalaryTemplate&#123;    public void Compute()    &#123;        computeSalary();        computeInsurance();        computeTax();    &#125;    public abstract void computeSalary();    public abstract void computeInsurance();    public abstract void computeTax();&#125;

public class JiLinSalary extends SalaryTemplate&#123;    public void computeSalary()    &#123;        System.out.println(&quot;采用吉林算法计算基本工资&quot;);    &#125;    public void computeInsurance()    &#123;        System.out.println(&quot;采用吉林算法计算保险&quot;);    &#125;    public void computeTax()    &#123;        System.out.println(&quot;采用吉林算法计算所得税&quot;);    &#125;&#125;

public class HeBeiSalary extends SalaryTemplate&#123;    public void computeSalary()    &#123;        System.out.println(&quot;采用河北算法计算基本工资&quot;);    &#125;    public void computeInsurance()    &#123;        System.out.println(&quot;采用河北算法计算保险&quot;);    &#125;    public void computeTax()    &#123;        System.out.println(&quot;采用河北算法计算所得税&quot;);    &#125;&#125;

public class Client &#123;    public static void main(String[] args) &#123;        SalaryTemplate jilin = new JiLinSalary();        SalaryTemplate hebei = new HeBeiSalary();                jilin.computeSalary();        jilin.computeInsurance();        jilin.computeTax();                hebei.computeSalary();        hebei.computeInsurance();        hebei.computeTax();    &#125;&#125;

3. 解决办法但是，如果实现到这里，还是有问题的，那就是类的职责不清。这个模板类，既要计算基本工资，又要计算社会保险，还要计算个人所得税。违反了单一职责原则。那该如何设计呢？很明显，需要把计算基本工资、社会保险、个人所得税的功能进行拆分。
4. 总结策略模式就是定义了一系列的算法，并将每一个算法封装起来，而且使它们还可以相互替换。策略模式让算法独立于使用它的客户端而独立变化。
策略模式主要由3部分组成：抽象策略类、具体策略类、上下文场景类。
优点：使用策略模式，可以替换继承关系的办法，也可以避免使用多重条件转移语句。缺点：使用策略模式时客户端必须知道所有的策略类，并自行决定使用哪一个策略类，如果算法比较多，则会造成很多的策略类。
5. 源码分享https://github.com/voidking/design-pattern-behavior.git
6. 参考文献《易学设计模式》《大话设计模式》接口设计六大原则软件设计六大设计原则讲解
]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
      </categories>
      <tags>
        <tag>设计模式</tag>
      </tags>
  </entry>
  <entry>
    <title>行为型模式之观察者模式</title>
    <url>/dev-design-pattern-observer/</url>
    <content><![CDATA[1. 现实场景每当有最新一期的报纸出版时，送报员都会按照订阅者的名单，把最新的报纸按时送到订阅者手里。而且订阅者也可以随时退订或者订阅其他的报纸，这就是观察者在现实生活中的应用。


2. 需求场景用户可以在电脑端、安卓端查看股票行情，当股票价格和买卖数量发生变化时，要通知这两个客户端。
一般思路：
public class ComputerClient&#123;    public void updatePrice(String name)    &#123;        System.out.println(name + &quot;股票在电脑上的价格更新了&quot;);    &#125;    public void updateCount(String name)    &#123;        System.out.println(name + &quot;股票在电脑上的买卖数量更新了&quot;);    &#125;&#125;

public class AndroidClient&#123;    public void updatePrice(String name)    &#123;        System.out.println(name + &quot;股票在安卓上的价格更新了&quot;);    &#125;    public void updateCount(String name)    &#123;        System.out.println(name + &quot;股票在安卓上的买卖数量更新了&quot;);    &#125;&#125;

public class Stock&#123;    private String stockName = &quot;中信证券&quot;;    public void changeCount()    &#123;        ComputerClient computerClient = new ComputerClient();        computerClient.updateCount(stockName);                AndroidClient androidClient = new AndroidClient();        androidClient.updateCount(stockName);    &#125;    public void changePrice()    &#123;        ComputerClient computerClient = new ComputerClient();        computerClient.updatePrice(stockName);                AndroidClient androidClient = new AndroidClient();        androidClient.updatePrice(stockName);    &#125;&#125;

public class Client&#123;    public static void main(String[] args)    &#123;        Stock stock = new Stock();        stock.changePrice();        stock.changeCount();    &#125;&#125;

上面的代码没什么问题，但是，如果要增加一个苹果客户端，就要修改Stock类的代码，增加对苹果客户端的支持。而且，苹果客户端要写很多重复代码。
后来又不想支持苹果客户端了，这时又要修改Stock类的代码。
一个苹果客户端还好，如果还有更多其他的客户端呢？
3. 解决办法如果能够在股票类增加一个功能，可以动态、随意地添加或删除客户端，是不是就可以解决这个问题呢？要实现动态、随意地添加或删除客户端，就必须面向接口编程，即把各种客户端抽象处理，在股票类里针对抽象的客户端编程。
4. 总结观察者模式就是定义对象间的一种一对多的依赖关系，当一个对象的状态发生改变时，所有依赖于它的对象都将得到通知并自动更新。观察者模式主要由4个部分组成：抽象目标类、具体目标类、抽象观察者类、具体观察者类。
5. 源码分享https://github.com/voidking/design-pattern-behavior.git
6. 参考文献《易学设计模式》《大话设计模式》接口设计六大原则软件设计六大设计原则讲解
]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
      </categories>
      <tags>
        <tag>设计模式</tag>
      </tags>
  </entry>
  <entry>
    <title>行为型模式之模板方法模式</title>
    <url>/dev-design-pattern-template-method/</url>
    <content><![CDATA[1. 场景有一个报表打印程序，用户规定必须要打印表头、正文、表尾三个部分。
一般思路：
public class Report &#123;    public void print()    &#123;        printTitle();        printBody();        printTail();    &#125;        public void printTitle()    &#123;        System.out.println(&quot;采用一种方式打印表头&quot;);    &#125;    public void printBody()    &#123;        System.out.println(&quot;采用一种方式打印正文&quot;);    &#125;    public void printTail()    &#123;        System.out.println(&quot;采用一种方式打印表尾&quot;);    &#125;&#125;



客户端调用的时候，直接new一个Report对象，然后调用print方法即可。现在看起来没什么问题，可是需求是不断变化的。如果用户提出需要把表头改一下样式，怎么办？最简单的方法就是在Report类里修改打印表头的代码。
修改完毕，用户又觉得还是原来的样式好看，要求再改回原来的样式，这时我们要再修改Report。
修改完毕，用户又要求修改正文和表尾，我们接着修改Report。
修改完毕，用户要求打印两套样式的报表，第一套就使用最初的样式，第二套使用修改过表头的样式。这时，我们没法通过仅仅修改Report来完成需求了，我们要添加一个Report2。以后也许还要添加Report3、Report4等等。
我们发现，改来改去非常麻烦，而且存在大量重复代码，还违反了开闭原则。那么怎么解决这个问题呢？
2. 分析分析需求可以看出，打印表头、正文、表尾这个流程是不变的，而打印的方式是变化的。
根据开闭原则，一个软件实体如类、模块和函数应该对扩展开放，对修改关闭。也就是说，一个软件实体应该通过扩展来实现变化，而不是通过修改已有的代码来实现变化。
那么我们最好把不变的部分和变化的部分分开，不变的部分作为软件实体，变化的部分作为扩展。
3. 解决办法Report类作为抽象类，规定流程。子类继承Report类，实现不同样式。
public abstract class Report&#123;    public void print()    &#123;        printTitle();        printBody();        printTail();    &#125;    public abstract void printTitle();    public abstract void printBody();    public abstract void printTail();&#125;

public class ReportImpl extends Report&#123;    public void printTitle()    &#123;        System.out.println(&quot;采用一种方式打印表头&quot;);    &#125;    public void printBody()    &#123;        System.out.println(&quot;采用一种方式打印正文&quot;);    &#125;    public void printTail()    &#123;        System.out.println(&quot;采用一种方式打印表尾&quot;);    &#125;&#125;

4. 总结模板方法可以总结为四个字：按部就班。适用于流程是固定的，而流程的具体实现是变化的情况。
5. 应用Web开发中的HttpServlet类就是一个典型模板应用。如果没有HttpServlet，那么我们的MyServlet需要继承GenericServlet。
接下来的处理流程是确定的：1、转化ServletRequest和ServletResponse为HttpServletRequest和HttpServletResponse。2、判断请求类型是get、post或者其他类型。3、从HttpServletRequest中拿到请求参数，进行业务处理。
其中经常变化的是第3步。
而HttpServlet，规定了处理的流程。我们的MyServlet，如果继承HttpServlet，那么只要专注于第3步即可。
6. 源码分享https://github.com/voidking/design-pattern-behavior.git
7. 参考文献《易学设计模式》《大话设计模式》接口设计六大原则软件设计六大设计原则讲解
]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
      </categories>
      <tags>
        <tag>设计模式</tag>
      </tags>
  </entry>
  <entry>
    <title>设计模式概述</title>
    <url>/dev-design-pattern-summary/</url>
    <content><![CDATA[1. 设计模式简介1979年，建筑师亚历山大编写了《建筑的永恒之道》，这本书阐述了建筑与规划的新观点。作者通过对当代建筑的研究发现：优秀的建筑中，总是存在着一些相似之处。如果能够找出这些优秀建筑的特征，就能够找到这些建筑是采用了哪些方法，如何设计才变得优秀的，从而也就能够客观评价一个建筑设计的好坏。通过找出并掌握这些优秀建筑的设计方法，建筑师就可以把这些方法复制到其他的建筑设计中，从而设计出同样优秀的建筑来。
建筑行业中存在的情况和软件行业存在的情况类似。在软件行业，是否也能够从那些优秀的软件中，找到一些相同的特征、优秀的设计方法，从而使软件开发人员能够掌握这些方法，并将其应用到其他的软件开发中，从而也开发出同样优秀的软件呢？
为了探讨这个问题，很多软件开发人员开始了这方面的研究，其中影响最大的文献是《设计模式》一书，该书的四个作者被称为GoF或Gang of Four。这本书总结了人类历史上软件开发的经验，给出了描述模式的一些特征，并提炼出用于指导软件设计的23种模式和一些面向对象的设计方法。


2. 设计模式分类设计模式可以分为三大类：创建型模式（Creational Patterns）、结构型模式（Structural Patterns）、行为型模式（Behavioral Patterns）。
2.1. 创建型模式软件设计方面，分工越来越细，因此对象的创建和对象的使用分开也就成了必然趋势。因为对象的创建会消耗很多资源，所以对对象的创建进行研究，能够高效地创建对象就是创建型模式要探讨的问题。创建型模式有6个：

简单工厂模式（Simple Factory）
工厂方法模式（Factory Method）
抽象工厂模式（Abstract Factory）
创建者模式（Builder）
原型模式（Prototype）
单例模式（Singleton）

2.2. 结构型模式在解决了对象的创建问题后，对象的组成以及对象之间的依赖关系就成了开发人员关注的焦点，因为如何设计对象的结构、继承和依赖关系会影响后续程序的维护性、代码的健壮性、耦合性等。对象结构的设计很容易体现出设计人员水平的高低。结构型模式有7个：

外观模式（Facade）
适配器模式（Adapter）
代理模式（Proxy）
装饰模式（Decorator）
桥模式（Bridge）
组合模式（Composite）
享元模式（Flyweight）

2.3. 行为型模式在对象的创建和对象的结构问题都解决了之后，就剩下对象的行为问题了，如果对象的行为设计的好，那么对象的行为就会更清晰，它们之间的协作效率就会提高。行为型模式有11个：

模板方法模式（Template Method）
观察者模式（Observer）
状态模式（State）
策略模式（Strategy）
职责链模式（Chain of Responsibility）
命令模式（Command）
访问者模式（Visitor）
调停者模式（Mediator）
备忘录模式（Memento）
迭代器模式（Iterator）
解释器模式（Interpreter）

3. 类之间关系在面向对象设计模式中，类与类之间主要有6种关系，分别是：依赖、关联、聚合、组合、继承、实现，耦合度依次增强。

依赖，一个类的方法里使用了另一个类。
关联，一个类里包含另一个类作为属性（成员变量）。
聚合，强的关联，整体与个体。
组合，更强的关联，同生共死。
继承
实现

4. staruml逆向问题使用staruml逆向生成类图的时候，提示错误：“Description: Unrecoverable Parse Error”。
原因1：java文件编码为包含BOM的UTF-8，而BOM在staruml中无法处理。解决办法：使用格式转换工具，转换为不包含BOM的UTF-8。或者，转换为GB2312。
原因2：java文件中包含@Override等标签，staruml无法识别。解决办法：去掉标签。或者换用其他逆向工具，比如WhiteStarUML、BOUML、UMLet等。
5. 源码分享https://github.com/voidking/design.git
6. 参考文献《易学设计模式》《大话设计模式》接口设计六大原则软件设计六大设计原则讲解了解这23种设计模式依赖、关联、聚合和组合及其之间区别的简单理解StarUML使用说明-指导手册使用StarUML画类图UML建模之时序图（Sequence Diagram）
]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
      </categories>
      <tags>
        <tag>设计模式</tag>
        <tag>staruml</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：curl命令</title>
    <url>/dev-linux-curl/</url>
    <content><![CDATA[1. curl简介curl命令始于1998年，是一个命令行工具和库，用于通过 URL 传输数据。curl几乎支持所有协议类型的数据传输，HTTP、HTTPS、FTP、SFTP、SMB、TELNET等等等等。对于开发者而言，curl最常见的用途是代替浏览器或者postman等接口测试工具，用来请求接口并获取数据。
更多内容参考curl官网。


2. curl参数说明curl命令的参数很多，这里我们对一些常用参数进行说明。

X：指定请求类型，GET、POST、PATCH、DELETE、PUT等
s：不输出错误和进度
H：设置header内容
d：设置body内容
d@：设置body内容，从文件读取
F：请求时带文件
m：请求超时时间
k：忽略SSL证书验证
o：保存文件

3. curl实践3.1. curl不输出错误和进度# 不输出错误和进度curl -s &quot;https://www.voidking.com&quot;curl -sX GET &quot;https://www.voidking.com&quot;# 不产生任何输出curl -s -o /dev/null &quot;https://www.voidking.com&quot;
更多内容参考 curl的用法指南。
3.2. curl post请求带参数1、application/x-www-form-urlencoded请求：
id=1ip=10.0.0.1curl -X POST &quot;http://rap2api.taobao.org/app/mock/241888/updateip&quot; -d &quot;id=$&#123;id&#125;&amp;ip=$&#123;ip&#125;&quot; 

2、application/json请求：
curl -X POST &quot;http://rap2api.taobao.org/app/mock/241888/updateip&quot; -H &quot;Content-type: application/json&quot; -d &#x27;&#123;&quot;id&quot;:&quot;&#x27;$&#123;id&#125;&#x27;&quot;,&quot;ip&quot;:&quot;&#x27;$&#123;ip&#125;&#x27;&quot;&#125;&#x27; # orcurl -X POST &quot;http://rap2api.taobao.org/app/mock/241888/updateip&quot; -H &quot;Content-type: application/json&quot; -d@data.json# data.json是大文件curl -X POST &quot;http://rap2api.taobao.org/app/mock/241888/updateip&quot; -H &quot;Content-type: application/json&quot; -H &quot;Expect:&quot; -d@data.json

其中data.json中的内容为：
&#123;  &quot;id&quot;:1,  &quot;ip&quot;:&quot;10.0.0.1&quot;&#125;

3、multipart/form-data请求：
curl -X POST http://rap2api.taobao.org/app/mock/241888/updateip -F &quot;id=$&#123;id&#125;&quot; -F &quot;filename=@file.tar.gz&quot;

3.3. curl设置超时curl -m 15 -s &quot;https://www.voidking.com&quot;

3.4. curl忽略证书验证执行curl命令时，如下报错：
curl: (60) SSL certificate problem, verify that the CA cert is OK. Details:error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failedMore details here: http://curl.haxx.se/docs/sslcerts.htmlcurl performs SSL certificate verification by default, using a &quot;bundle&quot; of Certificate Authority (CA) public keys (CA certs). The default bundle is named curl-ca-bundle.crt; you can specify an alternate file using the --cacert option.If this HTTPS server uses a certificate signed by a CA represented in the bundle, the certificate verification probably failed due to a problem with the certificate (it might be expired, or the name might not match the domain name in the URL).If you&#x27;d like to turn off curl&#x27;s verification of the certificate, use the -k (or --insecure) option.

解决办法：如报错提示，使用 -k 参数
curl -k &quot;https://www.voidking.com&quot;

3.5. curl指定CA证书curl --cacert /path/to/ca.pem &quot;https://www.voidking.com&quot;

3.6. curl下载文件curl -o icon.png &quot;https://www.python.org/static/opengraph-icon-200x200.png&quot;

curl下载这种方法也可以用来解决curl报错：curl: (23) Failed writing body (4096 != xxx)
curl -o /dev/null -s -w &quot;%&#123;http_code&#125;\n&quot; -X GET http://example.com

3.7. curl指定网卡curl命令指定网卡，可以用于测试该网卡的网络是否畅通
curl --interface eth0 www.baidu.com




]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：rsync命令</title>
    <url>/dev-linux-rsync/</url>
    <content><![CDATA[1. rsync命令简介
rsync is an open source utility that provides fast incremental file transfer. rsync is freely available under the GNU General Public License and is currently being maintained by Wayne Davison.


rsync是一个开源的提供快速增量文件传输的工具。rsync使用GNU协议，当前维护者是Wayne Davison。

rsync是开源的增量传输工具，速度很快。基本原理是文件分块检查md5，相同就不再传输，不同就增量传输。rsync可以代替本地拷贝命令cp，也可以代替远程拷贝命令scp。
参考文档：

rsync官网
rsync文档
archlinux - rsync
rsync 文件同步
详解rsync算法–如何减少同步文件时的网络传输量



2. 安装rsync2.1. centosyum install -y rsync

3. rsync命令说明3.1. 命令格式#Local:    rsync [OPTION...] SRC... [DEST]#Access via remote shell:    #Pull:        rsync [OPTION...] [USER@]HOST:SRC... [DEST]    #Push:        rsync [OPTION...] SRC... [USER@]HOST:DEST#Access via rsync daemon:    #Pull:        rsync [OPTION...] [USER@]HOST::SRC... [DEST]        rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST]    #Push:        rsync [OPTION...] SRC... [USER@]HOST::DEST        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST)

特别注意：

SRC目录后面加/，表示拷贝SRC中的内容到DEST目录中
SRC目录后面不加/，表示在DEST目录中新建一个SRC目录，然后SRC中的内容到DEST/SRC
DEST目录后加不加/不做区别

3.2. 常用参数
-v：显示传输的过程，-vvvv可以获取更详细信息
–partial：保留部分传输的文件，也就是实现断点续传功能
–progress：显示进度
-P：–partial 和 –progress的集合
-r：–recursive 对子目录以递归模式处理
-t：–times，保持mtime属性。
-o：–owner，保持owner属性
-p：–perms：保持perms属性（权限）
-g：–group：保持group属性（所属组）
–device：拷贝设备文件
–specials：拷贝特殊文件
-D：–device和–specials的集合
-l：–links 拷贝软链接，而非软链接指向的对象
-L：拷贝软链接，以及软链接指向的对象
-a：–archive 归档模式，对子目录以递归模式处理，并且保持文件属性。等同于-rtopgDl
-z：传输时进行压缩提高传输效率
–exclude：排除不需要传输的文件
–exclude-from：指定一个文件，排除文件中指定的目录和文件，文件中每行写一个要排除的目录和文件
–include：指定必须要传输的文件
-b：–backup 对目标上已存在的文件做一个备份，备份的文件名默认使用~做后缀
–backup-dir：指定备份文件的保存路径
-u：–update 仅在源mtime比目标mtime新时才拷贝
–delete：以SRC为主，对DEST进行同步。多则删，少则补。
–dry-run：模拟运行rsync，而不真正传输
e：指定要使用的远程shell程序，默认是ssh
existing：只更新目标端已存在的文件，目标端不存在的文件不传输
ignore-existing：只更新目标端不存在的文件，目标端存在的文件不传输
W：–whole-file 全量传输，而不是默认的增量传输
–bwlimit：限速，单位为KB/s

特别注意：

不加-t参数，如果两个文件的md5相同，mtime不同，传输时会更改目标文件的mtime为当前时间，增量修改。
加上-t参数，如果两个文件的md5相同，mtime不同，传输时会更改目标文件的mtime和源文件相同，增量修改。

更多内容参考 rsync文档
4. 使用rsync4.1. 本地拷贝断点续传需求：本地拷贝/opt/nginx目录中的所有文件，到/opt/nginx.bak，断点续传
rsync -P -avz /opt/nginx/ /opt/nginx.bak/

4.2. 远程拷贝断点续传需求1：拷贝本地/opt/nginx目录中的所有文件，到远程服务器192.168.56.102:/opt/nginx目录中，断点续传
rsync -P -avz /opt/nginx/ 192.168.56.102:/opt/nginx/rsync -P -avz -e &#x27;ssh -p 2222&#x27; /opt/nginx/ 192.168.56.102:/opt/nginx/

注意：要提前配置好ssh免密登录，配置方法参考文档《shell命令之ssh》。
需求2：拷贝远程服务器192.168.56.102:/opt/nginx目录中的所有文件，到本地/opt/nginx目录中，断点续传
rsync -P -avz 192.168.56.102:/opt/nginx/ /opt/nginx/

注意：如果想暂时结束rsync，稍后继续，请使用ctrl+C，而不是直接kill。直接kill进程会导致断点续传失效（缓存文件来不及写入到目标文件），补救办法是把缓存文件重命名为目标文件，就可以断点续传了。
4.3. 后台运行rsync后台运行rsync，最好使用screen而不是nohup。如果使用nohup，那么在终端关闭后，rsync可能也会终止，报错：
rsync error: received SIGINT, SIGTERM, or SIGHUP (code 20) at rsync.c(638) [sender=3.1.2]rsync error: received SIGINT, SIGTERM, or SIGHUP (code 20) at io.c(504) [generator=3.1.2]
没有找到原因，因此就先避免nohup和rsync配合使用。
使用screen后台运行rsync示例：
screen rsync -P -avz --log-file=/path/to/rsync.log /opt/nginx/ /opt/nginx.bak/# ctrl+A+Dscreen -lsscreen -r 23500.ttys005.B000000349215A

screen详细说明参考文档《Linux后台运行脚本或程序》
4.4. 拷贝时限速拷贝时，限速20MB/s
rsync -P -avz --bwlimit=20480 /data1/file.tgz /data2/

4.5. 排除指定目录和文件拷贝路径为 /opt/nginx ，排除目录 /opt/nginx/log
rsync -P -avz --exclude=log /opt/nginx/ 192.168.56.102:/opt/nginx/

拷贝路径为 /opt/nginx ，排除文件 /opt/nginx/log/*
rsync -P -avz --exclude=log/* /opt/nginx/ 192.168.56.102:/opt/nginx/

注意：排除目录和文件时，使用源目录的相对目录
参考文档：How to Use rsync to Exclude Files and Directories in Data Transfer
5. rsync偶发报错问题5.1. 问题描述执行命令：
rsync -P -avz -e &#x27;ssh -o StrictHostKeyChecking=no&#x27; filename.tgz root@192.168.56.101:/opt/

偶发报错：
rsync: [sender] write error: Broken pipe (32)rsync error: error in rsync protocol data stream (code 12) at io.c(848) [sender=3.2.7]

添加了 --timeout=1200 参数后，偶发报错：
deflate on token returned 0 (487 bytes left)rsync error: error in rsync protocol data stream (code 12) at token.c(481) [sender=3.2.7]

5.2. 解决办法直接传输，不压缩
rsync -P -av -e &#x27;ssh -o StrictHostKeyChecking=no&#x27; filename.tgz root@192.168.56.101:/opt/

参考文档：error in rsync protocol data stream
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：循环读取文本</title>
    <url>/dev-shell-loop-read-file/</url>
    <content><![CDATA[1. 前言本文中，记录使用shell循环读取文本的常用方法和技巧。


2. 读取内容并拼接2.1. 需求描述已知mobile.txt为：
haojin 17625160000voidking 17625160001

需求：根据 mobile.txt 中的内容拼接成SQL，修改不同用户的手机号。例如：
update user set mobile=&quot;17625160000&quot; where name=&quot;haojin&quot;;

2.2. 脚本实现脚本：
#!/bin/bashgrep -v &quot;^$&quot; mobile.txt | while read linedo  name=`echo $line | awk &#x27;&#123;print $1&#125;&#x27;`  mobile=`echo $line | awk &#x27;&#123;print $2&#125;&#x27;`  echo &quot;update user set mobile=\&quot;$&#123;mobile&#125;\&quot; where name=\&quot;$&#123;name&#125;\&quot;;&quot;done

PS：不能使用for line in cat &#39;mobile.txt&#39;，因为这种方法会按照空格或换行切分文本。
3. 读取IP和端口并测试连通性3.1. 需求描述使用while read line循环读取文本的方法，对于单纯的拼接需求是没有问题的。但是，如果涉及到输入缓存，就会有问题了。
已知service.txt内容为：
127.0.0.1 80127.0.0.1 8080192.168.56.101 8080

需求：探测service.txt中每个服务的连通性，并记录结果。
3.2. 错误实现我们用while read line方式实现脚本：
#!/bin/bash#cat /dev/null &gt; detectresult.txt: &gt; detectresult.txtcat service.txt | while read linedo  ip=$(echo $line | awk &#x27;&#123;print $1&#125;&#x27;)  port=$(echo $line | awk &#x27;&#123;print $2&#125;&#x27;)  res=$(nc -w 2 -v $ip $port)  echo &quot;$res&quot; &gt;&gt; detectresult.txtdone

执行脚本后，我们发现结果文件中只有一条结果！这就不符合预期了。这是因为while使用重定向机制，while read line一次性将文件信息读入输入缓存，并按行赋值给变量line，直到输入缓存数据为空。而刚好nc、telnet、ssh等命令，会读取输入缓存中的所有数据，这就导致输入缓存被清空了，while循环结束。
3.3. 更正实现解决办法：使用重定向将 /dev/null （一个特殊的设备文件，会丢弃所有写入其中的数据，并在读取时立即结束文件）作为输入传递给 nc。
res=$(nc -w 2 -v $ip $port &lt; /dev/null)
实际上，这意味着 nc 会立即得到一个“文件结束”(EOF)的指示，无需等待用户输入。这样做可以让 nc 在尝试连接后立即终止，不会保持等待用户数据输入。
或者，改用 -zv 参数使得nc命令不需要输入。因为这里探讨的是 nc、telnet、ssh 这类等待输入的命令在while循环中的通用解决办法，所以不使用-zv参数。
因此，正确的脚本应该改成：
#!/bin/bashcat /dev/null &gt; detectresult.txtwhile read linedo  ip=$(echo $line | awk &#x27;&#123;print $1&#125;&#x27;)  port=$(echo $line | awk &#x27;&#123;print $2&#125;&#x27;)  res=$(nc -w 2 -v $ip $port &lt; /dev/null)  echo &quot;$res&quot; &gt;&gt; detectresult.txtdone &lt; service.txt

4. 读取内容并用户交互4.1. 需求描述使用while read line循环读取文本的方案，已经具备了较好的通用性，可以应对大多数场景。但是，如果循环读取时，还需要和用户进行交互，那么就不适用了，下面看一个例子。
已知applist.txt内容为：
app1 running hbaapp2 stop hbeapp3 running hna

需求：对applist.txt中的app进行修改，修改每个app前都需要进行确认。
4.2. 错误实现按照while read line的思路，编写 main.sh 内容为：
#!/bin/bashwhile read line;do  app_id=$(echo $line | awk &#x27;&#123;print $1&#125;&#x27;)  idc=$(echo $app_id | awk -F&#x27;.&#x27; &#x27;&#123;print $NF&#125;&#x27;)  #echo $&#123;app_id&#125;&quot; &quot;$&#123;idc&#125;  bash modify.sh $&#123;idc&#125; $&#123;app_id&#125;done &lt; applist.txt

modify.sh内容为：
#!/bin/bashidc=$1app_id=$2echo -e &quot;idc: $&#123;idc&#125;&quot;echo -e &quot;app_id: $&#123;app_id&#125;&quot;read -p &quot;确认进行修改？[Y/N]&quot; inputif [[ $input = &quot;y&quot; || $input = &quot;Y&quot; ]];then    echo -e &quot;continue...&quot;else    echo -e &quot;exit&quot;    exit 1fi# logic code

但是问题来了，最终执行效果不符合预期，modify.sh中的确认交互效果会失效！这是因为read会读取缓存中的内容，而不是等待交互。
4.3. 更正实现那么，怎么解决上面的这个问题？非要使用while read line的话，确实没有好的解决办法。但是，我们可以把while read line替换掉！新的 main.sh 如下：
#!/bin/bashfilename=&quot;applist.txt&quot;lines=$(cat $&#123;filename&#125; | sed &#x27;/^$/d&#x27;)linenum=$(echo &quot;$&#123;lines&#125;&quot; | wc -l)#echo -e &quot;$&#123;lines&#125;&quot;#echo -e &quot;$&#123;linenum&#125;&quot;index=1while [[ $&#123;index&#125; -le $&#123;linenum&#125; ]];do  #echo $&#123;index&#125;  line=$(echo &quot;$&#123;lines&#125;&quot; | sed -n &quot;$&#123;index&#125;p&quot;)  app_id=$(echo $line | awk &#x27;&#123;print $1&#125;&#x27;)  idc=$(echo $app_id | awk -F&#x27;.&#x27; &#x27;&#123;print $NF&#125;&#x27;)  bash modify.sh $&#123;idc&#125; $&#123;app_id&#125;  index=$(($&#123;index&#125;+1))done

或者使用for循环：
#!/bin/bashfilename=&quot;applist.txt&quot;lines=$(cat $&#123;filename&#125; | sed &#x27;/^$/d&#x27;)linenum=$(echo &quot;$&#123;lines&#125;&quot; | wc -l)#echo -e &quot;$&#123;lines&#125;&quot;#echo -e &quot;$&#123;linenum&#125;&quot;for index in `seq 1 $&#123;linenum&#125;`;do  #echo $&#123;index&#125;  line=$(echo &quot;$&#123;lines&#125;&quot; | sed -n &quot;$&#123;index&#125;p&quot;)  app_id=$(echo $line | awk &#x27;&#123;print $1&#125;&#x27;)  idc=$(echo $app_id | awk -F&#x27;.&#x27; &#x27;&#123;print $NF&#125;&#x27;)  bash modify.sh $&#123;idc&#125; $&#123;app_id&#125;done

5. 读取文件并分批5.1. 需求描述需求：文件中有N行文本，读取文件内容后，平均分成M份，每份都组合成一个新的string，以逗号分隔。
文件文本示例内容为：
111222333444

5.2. 脚本实现#!/bin/bash# 检查脚本是否接收到一个参数if [ &quot;$#&quot; -ne 1 ]; then    echo &quot;Usage: $0 &lt;filename&gt;&quot;    exit 1fifilename=$1M=3# 检查文件是否存在if [ ! -f &quot;$filename&quot; ]; then    echo &quot;File not found!&quot;    exit 1fi# 计算文件中总行数total_lines=$(wc -l &lt; &quot;$filename&quot;)# 计算每个部分的大概行数lines_per_part=$(( ( total_lines + M - 1 ) / M ))# 分割文件并处理每一部分for (( part=1; part&lt;=M; part++ )); do    start_line=$(( (part - 1) * lines_per_part + 1 ))    end_line=$(( part * lines_per_part ))    if [ $end_line -gt $total_lines ]; then        end_line=$total_lines    fi    # 提取当前部分文本并转化为逗号分隔的字符串    part_content=$(sed -n &quot;$&#123;start_line&#125;,$&#123;end_line&#125;p&quot; &quot;$filename&quot; | tr &#x27;\n&#x27; &#x27;,&#x27; | sed &#x27;s/,$//&#x27;)    # 输出当前部分的逗号分隔的字符串    echo &quot;part$part $part_content&quot;    # 执行分批后的逻辑处理    # 判断是否为最后一部分    if [ $end_line -eq $total_lines ]; then        break    fidone



]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>shell</tag>
        <tag>好好学Shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：失败重试</title>
    <url>/dev-shell-retry/</url>
    <content><![CDATA[1. 为什么需要重试？典型场景：程序的实现需要调用第三方的API，但是我们并不能保证第三方API一直好用，也不能保证网络一直畅通，所以在调用第三方API时需要加上错误重试。
通用场景：程序的运行不符合预期，我们知道再次调用大概率可以使之符合预期，这时就需要重试。
本文中，我们学习一下Shell脚本中的失败重试。


2. 代码示例本节来自ChatGPT。
#!/bin/bash# 最大重试次数MAX_RETRY=5# 当前重试次数retry_count=0# 循环执行命令，如果失败则重试while true; do    # 执行命令，可以将具体的命令替换为你需要执行的命令    command_result=$(some_command)    # 检查命令的返回值，如果成功则退出循环    if [ $? -eq 0 ]; then        echo &quot;Command succeeded!&quot;        break    fi    # 命令执行失败，检查是否已达到最大重试次数    if [ $retry_count -ge $MAX_RETRY ]; then        echo &quot;Command failed after $MAX_RETRY attempts.&quot;        exit 1    fi    # 命令执行失败，增加重试次数，等待一段时间后再次执行命令    echo &quot;Command failed, retrying in 10 seconds...&quot;    retry_count=$((retry_count+1))    sleep 10done

在上面的脚本中，MAX_RETRY定义了最大重试次数，retry_count用于记录当前重试次数，while循环用于不断执行命令，if语句用于检查命令的返回值，如果成功则退出循环，否则增加重试次数，并等待一段时间后再次执行命令，直到命令成功或达到最大重试次数。如果命令在最大重试次数内执行失败，则脚本退出并返回错误码1。
]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>chatgpt</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：安全删除</title>
    <url>/dev-shell-saferm/</url>
    <content><![CDATA[1. 前言很多时候，我们会误删文件，那么一个回收站就很有必要了。看了一些回收站的实现，都比较复杂，干脆自己写一个。
思路：使用mv替换rm，把想要删除的文件暂时移动到回收站。


2. 安全删除脚本1、编写安全删除脚本，saferm.sh
#!/bin/bashTRASHDIR=&quot;$&#123;HOME&#125;/.saferm/trash&quot;[[ ! -d $&#123;TRASHDIR&#125; ]] &amp;&amp; mkdir -p $&#123;TRASHDIR&#125;function usage()&#123;    echo -e &quot;    Brief: move directory and files to ~/.local/trash    Usage: alias rm=/path/to/saferm.sh            rm &lt;dir&gt;            rm &lt;file&gt;            rm &lt;file1&gt; &lt;file2&gt;    &quot;&#125;function main()&#123;    local now=$(date &quot;+%Y%m%d%H%M%S&quot;)    local destdir=&quot;$&#123;TRASHDIR&#125;/$&#123;now&#125;&quot;    mkdir $&#123;destdir&#125;    local opt    while getopts &quot;h&quot; opt    do      case $opt in        h)          usage &amp;&amp; exit 0 ;;        \?)          usage &amp;&amp; exit 0 ;;      esac    done    local files=&quot;$*&quot;    local successfiles=&quot;&quot;    for file in $&#123;files&#125;;do        mv $&#123;file&#125; $&#123;destdir&#125; &amp;&amp; successfiles=$&#123;successfiles&#125;&quot; &quot;$&#123;file&#125;    done    if [[ -z &quot;$&#123;successfiles&#125;&quot; ]];then        echo -e &quot;\033[31mNothing to do! Check the source filenames please!\033[0m&quot;    else        echo -e &quot;$&#123;successfiles&#125; have been moved to \033[31m$&#123;destdir&#125;\033[0m&quot;    fi&#125;main &quot;$@&quot;

2、使用方法编辑 .bash_profile ，添加内容
# safermalias rm=&quot;/path/to/saferm.sh&quot;
添加内容完成， source .bash_profile ，rm命令就被替换为saferm脚本了。
以上，安全删除的功能就完成了，自己偶尔手动清理下 TRASHDIR 中的文件。以下内容可选，是对该saferm脚本的补充。
3. 清理回收站脚本1、清理回收站脚本，cleartrash.sh
#!/bin/bash# Clear files in trash that been removed 3 days ago.TRASHDIR=&quot;$&#123;HOME&#125;/.saferm/trash&quot;CLEARLOG=&quot;$&#123;TRASHDIR&#125;/clear.log&quot;[[ -d $&#123;TRASHDIR&#125; ]] \&amp;&amp; find $&#123;TRASHDIR&#125; -mindepth 1 -type d -mtime +2 \| xargs rm -rfv | tee -a $&#123;CLEARLOG&#125;

2、使用方法编辑 .bash_profile ，添加内容
# safermalias rm=&quot;/path/to/saferm.sh&quot;alias cleartrash=&quot;/path/to/cleartrash.sh&quot;
添加内容完成， source .bash_profile ，然后就可以使用 cleartrash 命令清理回收站了。
3、定时任务参考《Linux设置定时任务》，添加定时任务。
crontab -e，添加定时任务，每天晚上执行一次：
5 0 * * * /bin/bash /path/to/cleartrash.sh

4. 进阶saferm.sh 脚本存在一个严重bug：无法删除文件名带空格的文件，甚至会误删文件。因为Shell默认以空格、Tab、回车作为值与值之间的分隔符，而不是做为文件名的一部分。如果文件名中带空格，就会被认为是多个文件。
修改 saferm.sh 如下：
#!/bin/bashTRASHDIR=&quot;$&#123;HOME&#125;/.saferm/trash&quot;[[ ! -d $&#123;TRASHDIR&#125; ]] &amp;&amp; mkdir -p $&#123;TRASHDIR&#125;function usage()&#123;    echo -e &quot;    Brief: Move directories or files to ~/.local/trash           You can move 1-3 files with wildcard. More is unsupported.            If file name contains space, use quotation mark please.    Usage: alias rm=/path/to/saferm.sh            rm &lt;dir&gt;            rm &lt;file&gt;            rm *.&lt;filetype&gt;            rm &lt;file1&gt; &lt;file2&gt; &lt;file3&gt;    &quot;&#125;function main()&#123;    local now=$(date &quot;+%Y%m%d%H%M%S&quot;)    local destdir=&quot;$&#123;TRASHDIR&#125;/$&#123;now&#125;&quot;    mkdir $&#123;destdir&#125;    local opt    while getopts &quot;h&quot; opt    do      case $opt in        h)          usage &amp;&amp; exit 0 ;;        \?)          usage &amp;&amp; exit 0 ;;      esac    done    local file1=&quot;$1&quot;    local file2=&quot;$2&quot;    local file3=&quot;$3&quot;    local file4=&quot;$4&quot;    local successfiles=&quot;&quot;    [[ -n &quot;$&#123;file1&#125;&quot; ]] &amp;&amp; mv &quot;$&#123;file1&#125;&quot; $&#123;destdir&#125; &amp;&amp; successfiles=$&#123;successfiles&#125;&quot; &quot;$&#123;file1&#125;    [[ -n &quot;$&#123;file2&#125;&quot; ]] &amp;&amp; mv &quot;$&#123;file2&#125;&quot; $&#123;destdir&#125; &amp;&amp; successfiles=$&#123;successfiles&#125;&quot; &quot;$&#123;file2&#125;    [[ -n &quot;$&#123;file3&#125;&quot; ]] &amp;&amp; mv &quot;$&#123;file3&#125;&quot; $&#123;destdir&#125; &amp;&amp; successfiles=$&#123;successfiles&#125;&quot; &quot;$&#123;file3&#125;    [[ -n &quot;$&#123;file4&#125;&quot; ]] &amp;&amp; echo -e &quot;\033[31mYou can move 1-3 files with wildcard. More is unsupported.\033[0m&quot;    if [[ -z &quot;$&#123;successfiles&#125;&quot; ]];then        echo -e &quot;\033[31mNothing to do! Check the source filenames please!\033[0m&quot;    else        echo -e &quot;$&#123;successfiles&#125; have been moved to \033[31m$&#123;destdir&#125;\033[0m&quot;    fi&#125;main &quot;$@&quot;

5. 别人家的saferm1、下载安装saferm.sh
git clone https://github.com/lagerspetz/linux-stuffsudo mv linux-stuff/scripts/saferm.sh /binrm -Rf linux-stuff

2、编辑.bashrc文件，末尾添加
alias rm=saferm.shalias sudo=&#x27;sudo &#x27;

3、使环境生效
source .bashrc
之后，再使用rm或者sudo rm命令，实际上执行的是mv操作，移动文件到Trash目录。
参考文档：Make “rm” Command To Move The Files To “Trash Can”
]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>shell</tag>
        <tag>好好学Shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：脚本调用子脚本</title>
    <url>/dev-shell-sub-shell/</url>
    <content><![CDATA[1. 前言Shell脚本调用脚本的方法有两种，第一种是bash调用，在子shell里执行子脚本；第二种是引用调用，在当前脚本里执行子脚本。
《Shell脚本之模块化》一文中，我们主要学习了引用调用。本文中，我们来学习一下bash调用。


2. 调用顺序控制本节内容主要来自ChatGPT。
在一个 bash 脚本中调用多个子 bash 脚本时，有两种常用的方式来确保子进程执行的顺序和状态，分别是wait和&amp;&amp;。
2.1. wait第一种是使用 wait 命令来等待所有子进程执行完毕。这种方法会阻塞当前进程，直到所有子进程都执行完毕，然后才会继续执行后面的代码。这种方法可以确保所有子进程都已经完成，但是会阻塞当前进程，不能同时执行其他代码。
例如：
#!/bin/bash./sub_script1.sh &amp;./sub_script2.sh &amp;./sub_script3.sh &amp;waitecho &quot;All sub scripts completed.&quot;

wait说明：

wait 在没有任何参数的情况下，wait命令会等待所有后台进程完成，然后再继续执行脚本。
wait $PID 在继续脚本之前等待特定进程结束。
&amp; 命令后的&amp;符号表示后台任务。
$! 获取最后一个后台进程的PID。
$? 打印上一个进程的退出状态。

再来一个示例，来自文档：Bash 教程 - 异步任务
async-parent.sh
#!/bin/bash# async-parent : Asynchronous execution demo (parent)echo &quot;Parent: starting...&quot;echo &quot;Parent: launching child script...&quot;async-child.sh &amp;pid=$!echo &quot;Parent: child (PID= $pid) launched.&quot;echo &quot;Parent: continuing...&quot;sleep 2echo &quot;Parent: pausing to wait for child to finish...&quot;wait $pidecho &quot;Parent: child is finished. Continuing...&quot;echo &quot;Parent: parent is done. Exiting.&quot;

async-child.sh
#!/bin/bash# async-child : Asynchronous execution demo (child)echo &quot;Child: child is running...&quot;sleep 5echo &quot;Child: child is done. Exiting.&quot;

在这个例子中，我们看到该子脚本是非常简单的。真正的操作通过父脚本完成。在父脚本中，子脚本被启动， 并被放置到后台运行。子脚本的进程 ID 记录在 pid 变量中，这个变量的值是 $! shell 参数的值，它总是 包含放到后台执行的最后一个任务的进程 ID 号。
父脚本继续，然后执行一个以子进程 PID 为参数的 wait 命令。这就导致父脚本暂停运行，直到子脚本退出，意味着父脚本结束。
2.2. &amp;&amp;第二种是使用 &amp;&amp; 连接多个子进程。这种方法会等待上一个子进程执行完毕之后，才会执行下一个子进程。如果有一个子进程执行失败，后面的子进程将不会被执行。这种方法可以确保所有子进程都被执行，但是如果有一个子进程执行失败，整个脚本就会停止执行。
例如：
#!/bin/bash./sub_script1.sh &amp;&amp; ./sub_script2.sh &amp;&amp; ./sub_script3.shecho &quot;All sub scripts completed.&quot;







]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>chatgpt</tag>
        <tag>shell</tag>
        <tag>好好学Shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：脚本模块化</title>
    <url>/dev-shell-modules/</url>
    <content><![CDATA[1. 脚本模块化Shell脚本调用脚本的方法有两种，第一种是bash调用，在子shell里执行子脚本；第二种是引用调用，在当前脚本里执行子脚本。
脚本的模块化，两种调用方法都能实现，这里推荐第二种方法：引用调用。
引用调用时，父脚本和子脚本里的变量是共用的。


2. 模块化设计1、创建modules目录，后续模块放入这个目录里
2、创建文件 modules/check.sh
read -p &quot;确认进行检查？[Y/N]&quot; inputif [[ $input = &quot;y&quot; || $input = &quot;Y&quot; ]];then    echo -e &quot;\n------ continue... ------\n&quot;    # check something    echo -e &quot;\n------ finished ------\n&quot;else    echo -e &quot;\n------ 不检查 ------\n&quot;fi

3、创建文件 modules/modify.sh
read -p &quot;确认进行修改？[Y/N]&quot; inputif [[ $input = &quot;y&quot; || $input = &quot;Y&quot; ]];then    echo -e &quot;\n------ continue... ------\n&quot;    # modify something    echo -e &quot;\n------ finished ------\n&quot;else    echo -e &quot;\n------ 不修改 ------\n&quot;fi

4、创建文件 main.sh 
#!/bin/bashfunction main()&#123;    # 参数提示和校验，代码省略    read -p &quot;确认参数正确，继续执行脚本？[Y/N]&quot; input    if [[ $input = &quot;y&quot; || $input = &quot;Y&quot; ]];then        echo -e &quot;\n------ continue... ------\n&quot;    else        echo -e &quot;\n------ exit ------\n&quot;        exit 1    fi    # 调用各个模块    source modules/check.sh    source modules/modify.sh&#125;main &quot;$@&quot;

其中，参数提示和校验部分代码参考 《Shell脚本之参数提示和校验》
3. 交互部分模块化交互部分模块化，可以选择是否开启交互。
1、改写main.sh，抽象出函数
#!/bin/bashINTERACTIVE=trueCHECK_ENABLE=trueMODIFY_ENABLE=truefunction run() &#123;    func=$1    info=$2    if [[ $&#123;INTERACTIVE&#125; = false ]];then        $&#123;func&#125;        return 0    fi    read -p &quot;$&#123;info&#125; [Y/N]&quot; input    if [[ $input = &quot;y&quot; || $input = &quot;Y&quot; ]];then        $&#123;func&#125;    else        echo &quot;放弃操作&quot;        return 1    fi    return 0&#125;function nothing()&#123;    :&#125;function main()&#123;    # 参数提示和校验，代码省略    run nothing &quot;确认参数正确，继续执行？&quot;    [[ $? -ne 0 ]] &amp;&amp; exit 1        # 调用各个模块    [[ $&#123;CHECK_ENABLE&#125; = true ]] &amp;&amp; source modules/check.sh    [[ $&#123;MODIFY_ENABLE&#125; = true ]] &amp;&amp; source modules/modify.sh&#125;main &quot;$@&quot;

2、改写 modules/check.sh
echo -e &quot;appname=$&#123;appname&#125;&quot;function check()&#123;    # check something&#125;run check &quot;确认进行检查？&quot;

2、改写 modules/modify.sh
echo -e &quot;appname=$&#123;appname&#125;&quot;function modify()&#123;    # modify something&#125;run modify &quot;确认进行修改？&quot;

]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>shell</tag>
        <tag>好好学Shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：脚本参数提示和校验</title>
    <url>/dev-shell-args-prompt-and-check/</url>
    <content><![CDATA[1. 需求描述有一个脚本，需要appname、ip、hostname、domain四个参数，并且都不能为空。


2. 脚本实现main.sh 脚本内容为：
#!/bin/bashfunction usage()&#123;    echo -e &quot;-a -i -H -d are requierd    -a: appName    -i: ip    -h: hostname    -d: domain    &quot;&#125;checkOpts () &#123;    local key=$1    local value=$2    [[ -z &quot;$&#123;value&#125;&quot; ]] \    &amp;&amp; echo -e &quot;\033[31mFATAL: $&#123;key&#125; should not be empty! \033[0m&quot; \    &amp;&amp; usage \    &amp;&amp; return 1    return 0&#125;function main()&#123;    while getopts &quot;a:i:H:d:&quot; opt;do        case $opt in            a) appname=$&#123;OPTARG&#125; &amp;&amp; echo &quot;appname=$&#123;appname&#125;&quot; ;;            i) ip=$&#123;OPTARG&#125; &amp;&amp; echo &quot;ip=$&#123;ip&#125;&quot; ;;            H) hostname=$&#123;OPTARG&#125; &amp;&amp; echo &quot;hostname=$&#123;hostname&#125;&quot; ;;            d) domain=$&#123;OPTARG&#125; &amp;&amp; echo &quot;domain=$&#123;domain&#125;&quot; ;;            *) usage &amp;&amp; exit 0 ;;        esac    done    read -p &quot;确认参数无误，继续执行脚本？[Y/N]&quot; input    if [[ $input = &quot;y&quot; || $input = &quot;Y&quot; ]];then        echo -e &quot;\n------ continue... ------\n&quot;    else        echo -e &quot;\n------ exit ------\n&quot;        exit 1    fi    ! checkOpts &quot;appname&quot; $&#123;appname&#125; &amp;&amp; exit 1    ! checkOpts &quot;ip&quot; $&#123;ip&#125; &amp;&amp; exit 1    ! checkOpts &quot;hostname&quot; $&#123;hostname&#125; &amp;&amp; exit 1    ! checkOpts &quot;domain&quot; $&#123;domain&#125; &amp;&amp; exit 1        # logic code&#125;main &quot;$@&quot;

问题来了，如果想要添加参数怎么办？最简单的思路就是修改getopts部分，添加新的参数。
这里提供另外一个思路：提前留出一个other变量，用来接收多出的参数，这个other变量可以拆分成多个子变量，这样就具备了更好的通用性。
]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>shell</tag>
        <tag>好好学Shell</tag>
      </tags>
  </entry>
  <entry>
    <title>iftop命令</title>
    <url>/dev-iftop/</url>
    <content><![CDATA[1. 哪个程序占用了带宽？查看一台主机的监控，有时候我们会发现网络带宽被打满了，这时候怎样定位出是哪个程序大量占用了带宽？答：使用网络数据包分析工具。比如iftop，可以监控到进出网卡的带宽用量。
参考文档：

How do I find out which process is eating up my bandwidth?
iftop: display bandwidth usage on an interface
网络流量监控工具iftop
iftop简介
从零开始学习iftop流量监控



2. 安装iftopyum install iftop

3. 使用iftop显示当前主机的流量和端口。
iftop -P

这里比较坑的是，有的统计项并不会显示端口。原因未知，解决方法未知。。。
按下n，将输出的主机信息都通过IP显示，不进行DNS解析，因为解析可能不准确而产生误导。
4. 输出内容说明iftop的输出从上到下可以分为三部分：流量刻度，详细信息和统计信息。
4.1. 流量刻度是iftop输出中最上面的一行。此行信息是流量刻度，用于显示网卡带宽流量。
4.2. 详细信息是iftop输出中最大的一个部分，此部分又分为左、中、右三列。

左边显示本机的主机名/IP和端口
中间显示正在和本机通信的主机名/IP和端口，=&gt;代表发送数据，反之&lt;=是接受数据。
右边表示2s内，10s内，40s内的一个平均流量值，显示不同时间段的流量值。

4.3. 统计信息位于iftop输出的最下面。TX表示发送流量，RX表示接收流量，TOTAL表示发送和接收的全部流量。

cum：从运行iftop到当前时刻，发送和接收的总流量。
peak：流量的峰值。
rate：在过去2s，10s，40s，平均的流量的值。

5. iftop常用参数-i 指定需要检测的网卡， 如果有多个网络接口，则需要注意网络接口的选择，如：# iftop -i eth1-B 将输出以byte为单位显示网卡流量，默认是bit-n 将输出的主机信息都通过IP显示，不进行DNS解析 -N 只显示连接端口号，不显示端口对应的服务名称-F 显示特定网段的网卡进出流量  如iftop -F 192.168.85.0/24-h 帮助，显示参数信息-p 以混杂模式运行iftop，此时iftop可以用作网络嗅探器 ;-P 显示主机以及端口信息-m 设置输出界面中最上面的流量刻度最大值，流量刻度分5个大段显示  如：# iftop -m 100M-f 使用筛选码选择数据包来计数  如iftop -f filter code-b 不显示流量图形条-c 指定可选的配置文件   如iftop  -c config file-t 使用不带ncurses的文本界面，    以下两个是只和-t一起用的：    -s num num秒后打印一次文本输出然后退出，-t -s 60组合使用，表示取60秒网络流量输出到终端    -L num 打印的行数-f 参数支持tcpdump的语法，可以使用各种过滤条件。

6. 找到最费流量的IP和端口1、进入iftop界面
iftop -i eth0 -nNB -m 10M


-i 指定网卡，
-n 代表主机通过ip显示不走DNS
-N 只显示连接端口号，不显示端口对应的服务名称(不加会显示如ssh这样的服务名称，不便于排查)
-B 指定显示单位为Kb，默认是bit，太小！
-m 设置输出界面中最上面的流量刻度最大值，流量刻度分5个大段显示

2、系列操作

按下L显示流量刻度
按下T显示总量
按下3，根据最近40s统计排序
按下t，发送和接受合成一行
多按几次B，查看最近2s、10s、40s的统计，找到的流量用得最多的IP
按下l，输入指定IP，回车
按下p，根据端口号显示

7. 根据端口号查找进程参考文档《shell命令之系统管理》
8. 后记使用iftop命令查找占用带宽最多的程序，还是比较麻烦的，后来发现了一个更好用的工具 nethogs。
nethogs安装使用方法：
yum install nethogsnethogs









]]></content>
      <categories>
        <category>engineering</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>问题排查</tag>
      </tags>
  </entry>
  <entry>
    <title>iotop命令</title>
    <url>/dev-iotop/</url>
    <content><![CDATA[1. 哪个程序占用了磁盘IO？查看一台主机的监控，有时候我们会发现磁盘IO被打满了，这时候怎样定位出是哪个程序大量占用了磁盘IO？答：使用磁盘IO分析工具。比如iotop，可以监控到读写磁盘的线程和读写速度。
参考文档：

在 Linux 中如何使用 iotop 和 iostat 监控磁盘 I/O 活动？
Linux性能监控命令系列之 iotop
Monitor Disk I/O with iotop in Linux
Iotop



2. 安装iotopyum install iotop
3. 使用iotop显示当前主机的IO情况iotop
4. 输出内容说明

Thread ID (TID).
I/O Priority class/level (PRIO).
the owner of the thread with TID (USER).
the disk read per second (DISK READ).
the disk write per second (DISK WRITE).
the percentage of time the thread spent while swapping in (SWAPIN).
the percentage of time the thread spent waiting on I/O (IO&gt;).
the command the thread is running (COMMAND).

5. 根据线程号查找进程已知线程号（例如34567），怎样查找对应的进程号？
方法一：直接查找线程号ps -efL | grep 34567
方法二：根据COMMAND关键字（例如nginx）查找进程号，然后查看进程包含的线程号，最后和要查找的线程号进行对比。
ps -ef | grep nginxps -T -p 12345











]]></content>
      <categories>
        <category>engineering</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>问题排查</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：编码格式转换</title>
    <url>/dev-linux-encoding/</url>
    <content><![CDATA[1. 编码格式修改执行sh脚本时报错：/bin/sh^M: bad interpreter: No such file or directory
1、添加执行权限chmod a+x filename
2、然后修改文件格式vi filename
3、利用如下命令查看文件格式
:set ff 或 :set fileformat
可以看到 fileformat=dos 或 fileformat=unix。
4、利用如下命令修改文件格式
:set ff=unix 或 :set fileformat=unix
5、保存文件:wq
6、最后再执行文件./filename


2. UTF-8 BOM 转 UTF-8使用macos/linux中编译运行windows下创建的java项目，报错 Error:(1, 1) java: 非法字符: ‘\ufeff’。BOM（byte order mark）是为 UTF-16 和 UTF-32 准备的，用于标记字节序（byte order）。微软在 UTF-8 中使用 BOM 是因为这样可以把 UTF-8 和 ASCII 等编码明确区分开，但这样的文件在 Windows 之外的操作系统里会带来问题。那么，怎么把utf8 bom编码文件批量转换为utf8编码文件呢？使用下面的脚本。
# 当前目录下递归所有文件grep -r -i -l $&#x27;^\xEF\xBB\xBF&#x27; . | xargs sed -i &#x27;s/^\xEF\xBB\xBF//g&#x27;# 当前目录下所有文件grep -i -l $&#x27;^\xEF\xBB\xBF&#x27; * | xargs sed -i &#x27;s/^\xEF\xBB\xBF//g&#x27;# macosbrew install gnu-sedgrep -r -i -l $&#x27;^\xEF\xBB\xBF&#x27; . | xargs gsed -i &#x27;s/^\xEF\xBB\xBF//g&#x27;grep -i -l $&#x27;^\xEF\xBB\xBF&#x27; * | xargs gsed -i &#x27;s/^\xEF\xBB\xBF//g&#x27;

3. GBK 转 UTF-81、单个文件编码转换打开一个文件，发现中文显示乱码，大概率编码问题。使用下面的命令进行转码：
iconv -f GBK -t UTF-8 inputfile &gt; outputfile

2、多个文件批量编码转换
#!/bin/bashfind . -type f -name &quot;*.py&quot; &gt; filename.txtwhile read line;do     iconv -f GBK -t UTF-8 $&#123;line&#125; &gt; $&#123;line&#125;.bak    mv $&#123;line&#125;.bak $&#123;line&#125;done &lt; filename.txtrm -f filename.txt


]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>macos</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：文件管理常用命令</title>
    <url>/dev-linux-file/</url>
    <content><![CDATA[1. 前言本文记录Linux中常用的文件管理命令，主要包括：

cp：拷贝文件
mv：移动文件
rm：删除文件
tar：解压与压缩文件
zip：解压与压缩文件
unar：解压文件
split：拆分文件



2. cp命令2.1. 纯净cpcp 命令，可能会被 alias 改写附加一些参数。因此使用 cp 前，最好先 unalias 一下。
alias | grep -w &quot;cp&quot; &amp;&amp; unalias cp

或者，使用cp的绝对路径
/usr/bin/cp

2.2. cp常用参数cp -f #强制覆盖cp -R #递归拷贝所有文件。对特殊文件（管道文件、块设备文件、字符设备文件）会进行创建而不是拷贝。cp -r #递归拷贝所有文件。所有source文件当做普通文件。

2.3. 递归拷贝cp -rf public/* voidking #递归拷贝所有文件，不包括隐藏文件cp -rf public/. voidking #递归拷贝所有文件，包括隐藏文件

2.4. 匹配排除.和..当前目录下，一定存在两个目录：.和..。有一些操作我们需要排除这两个目录，怎样匹配排除它们呢？
ls -als -a | xargsls -a | wc -lls -a | grep -vw &#x27;\.&#x27;ls -a | grep -vw &#x27;[.]&#x27;ls -a | grep -v &#x27;^.$&#x27; | grep -v &#x27;^..$&#x27;

2.5. cp排除目录需求：源目录为dirA，目标目录dirB，复制dirA下所有目录&amp;文件到dirB，排除目录log方法一：不包含隐藏文件
cd dirAls | grep -v log | xargscp -r `ls | grep -v log | xargs` pathto/dirB

方法二：包含隐藏文件
cd dirAls -a | grep -vw &#x27;\.&#x27; | grep -v log | xargscp -r `ls -a | grep -vw &#x27;\.&#x27; | grep -v log | xargs` pathto/dirB

2.6. cp特定文件需求：找到 test0 目录中第一层目录下的所有文件类型的文件，并拷贝到 test1 目录中
find test0/ -d 1 -type f -exec cp &#123;&#125; test1/ \;

3. rsync命令参考文档：rsync命令的安装使用
4. mv命令4.1. 移动非隐藏文件mv test0/* test1

4.2. 移动隐藏文件mv test0/.[^.]* test1

4.3. 移动隐藏文件和非隐藏文件mv test0/* test0/.[^.]* test1

5. rm命令5.1. rm排除目录需求：想要删除dirA目录下的所有目录和文件，除了.git目录
cd dirAls -a | grep -vw &#x27;\.&#x27; | grep -vw &#x27;\.git&#x27; | xargsrm -rf `ls -a | grep -vw &#x27;\.&#x27; | grep -vw &#x27;\.git&#x27; | xargs`

5.2. 删除特殊字符名称的文件ls -ifind . -inum 83661618# 删除文件find . -inum 83661618 -delete# 删除非空目录find . -inum 83661619 -exec rm &#123;&#125; \;

5.3. 删除10天前的日志需求：找出10天前的日志并删除。脚本（推荐）：
find /usr/local/tomcat/logs/ -type f -mtime +10 | xargs rm -rfv

或者：
find /usr/local/tomcat/logs/ -type f -mtime +10 -exec rm -rfv &#123;&#125; \;

6. tar命令6.1. 压缩与解压tar.gz文件tar -czvf filename.tar.gz filenametar -xzvf filename.tar.gz

6.2. 压缩文件时去掉路径压缩文件时，仅保留父目录
tar -czvf filename.tgz -C /path/to/dir/ subdir

压缩文件时，去掉所有上层目录
tar -czvf filename.tgz -C /path/to/dir/subdir .

6.3. 解压文件到指定路径解压文件到指定路径
mkdir filenametar -xzvf filename.tgz -C filename

不包含上层目录的压缩文件，解压文件到指定路径
tar -xzvf filename.tgz -C filename -m --no-overwrite-dir

参数说明：
-m, --touch    Don&#x27;t extract file modified time.--no-overwrite-dir    Preserve metadata of existing directories.

如果不指定 -m 和 --no-overwrite-dir 参数，可能会报错：
tar: .: Cannot utime: Operation not permittedtar: .: Cannot change mode to rwxr-xr-x: Operation not permittedtar: Exiting with failure status due to previous errors

这是因为压缩文件中包含 . 和 .. ，解压时会尝试修改父目录的属性。详情参考文档 Permissions cannot be restored for a tar
6.4. tar排除目录需求：打包目录dirA下所有目录&amp;文件，排除目录dirA/log和dirA/data
tar -czvf dirA.tar.gz dirA --exclude dirA/log --exclude dirA/data

6.5. tar文件拆分与合并使用sz传输大文件，有时候会被中断。这时我们可以把文件进行拆分，下载后再进行合并。
tar -czf - bigfile.tar.gz | split -b 50m -d - bigfile.tar.gz.cat bigfile.tar.gz.* | tar -xzf -md5sum bigfile.tar.gz

7. zip命令7.1. 压缩与解压zip文件zip filename.zip filenameunzip filename.zip

7.2. 解压zip文件到指定目录unzip -d filename filename.zip

7.3. 加密压缩和解压zip文件zip -er filename.zip filenamezip -er -P xxxxxx filename.zip filenameunzip -P xxxxxx filename.zip

7.4. 批量加密压缩和解压zip文件for i in `ls`;do zip -er -P xxxxxx $i.zip $i;donefor i in *.zip;do unzip -P xxxxxx $i;done

8. unar命令8.1. 解压中文名zip文件yum install unarunar -e GBK 中文名.zipunar -e GBK -p xxxxxx 中文名.zipfor i in *.zip;do unar -e GBK -p xxxxxx $i;done

9. split命令9.1. 按照固定行数拆分文件需求：大文件拆分成小文件，每50行拆分成一个文件。脚本：
#!/bin/bashlinenum=&quot;50&quot;bigfile=&quot;bigfile.txt&quot;datadir=&quot;files&quot;prefix=&quot;smallfile.&quot;mkdir -p $&#123;datadir&#125;split -l $&#123;linenum&#125; $&#123;bigfile&#125; -d -a 2 $&#123;datadir&#125;/$&#123;prefix&#125;

9.2. 按照行数百分比拆分文件需求：大文件拆分成小文件，按百分比拆分，例如每 34% 拆分成一个文件。脚本：
#!/bin/bashpercent=&quot;34&quot;bigfile=&quot;bigfile.txt&quot;datadir=&quot;files&quot;prefix=&quot;smallfile.&quot;rm -rf $&#123;datadir&#125;/$&#123;prefix&#125;*mkdir -p $&#123;datadir&#125;sum=$(wc -l &lt; &quot;$&#123;bigfile&#125;&quot; | tr -d &#x27;[:space:]&#x27;)linenum=$((($&#123;sum&#125;*$&#123;percent&#125;+99)/100)) # bash计算按照整数运算，这里数学模拟向上取整 (a + b - 1) / b ，相当于 ceil(a / b)split -l $&#123;linenum&#125; $&#123;bigfile&#125; -d -a 2 $&#123;datadir&#125;/$&#123;prefix&#125;

注意：wc -l 之后最好加上 tr -d &#39;[:space:]&#39; ，因为某些版本的wc命令会在输出结果之前添加空格。
10. 其他10.1. 文件软链创建文件软链
ln -s source_file link_file

强制修改文件软链
ln -sfn new_source_file link_file

10.2. 显示文件的绝对路径realpath filename

10.3. 查看文件字节查看文件的前16个字节
hexdump -n 16 -C demo.txt




]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：grep/awk/sed 文本三剑客</title>
    <url>/dev-linux-grep-awk-sed/</url>
    <content><![CDATA[1. awk注意事项awk命令格式：awk [选项参数] &#39;script&#39; var=value file(s)
这里需要特别注意的是，包裹script的是单引号，单引号，单引号！参考文档Linux awk 命令


2. 删除空行过滤hostlist.txt中的空行：
cat hostlist.txt |tr -s &#x27;\n&#x27;cat hostlist.txt |sed &#x27;/^$/d&#x27;cat hostlist.txt |awk &#x27;&#123;if($0!=&quot;&quot;)print&#125;&#x27;cat hostlist.txt |awk &#x27;&#123;if(length!=0) print $0&#125;&#x27;grep -v &quot;^$&quot; hostlist.txt# 终极方法（linux和windows空行都适用）grep -v -e &#x27;^[[:space:]]*$&#x27; hostlist.txt

3. 过滤windows换行符在windows里编辑好的文件，上传到linux后发现多了^M。以过滤hostlist.txt中的^M为例：
sed -i &#x27;s/^M//g&#x27; hostlist.txt
注意，直接复制粘贴上面的命令是无效的。^M的输入方式是 Ctrl + V ，然后 Ctrl + M 。
4. 去除空格去除test.txt文本中的空格：
cat test.txt | tr -d &#x27; &#x27;

5. 空格转换行把test.txt文本中的空格变成换行符。
cat test.txt | tr &#x27; &#x27; &#x27;\n&#x27;cat test.txt | sed &#x27;s/ /\n/g&#x27;

6. 查找并插入一行查找包含- [engineering,subject0]的文件，- [engineering,subject0]之后紧接着插入一行- [engineering,subject1]
sed -i &#x27;s/- \[engineering,subject0\]/- \[engineering,subject0\]\n- \[engineering,subject1\]/g&#x27; *sed -i &#x27;s/- \[engineering,subject0\]/- \[engineering,subject0\]\n- \[engineering,subject1\]/g&#x27; `grep -e &#x27;- \[engineering,subject0\]&#x27; -rl .`

7. 大小写字符转换# 大写转小写echo &quot;192.168.56.101 MASTER&quot; | tr &#x27;[A-Z]&#x27; &#x27;[a-z]&#x27;# 小写转大写echo &quot;192.168.56.101 master&quot; | tr &#x27;[a-z]&#x27; &#x27;[A-Z]&#x27;# 某一列大写转小写echo &quot;192.168.56.101 MASTER&quot; | awk &#x27;$3=tolower($2) &#123;print $1&quot; &quot;$2&quot; &quot;$3&#125;&#x27;# 某一列小写转大写echo &quot;192.168.56.101 master&quot; | awk &#x27;$3=toupper($2) &#123;print $1&quot; &quot;$2&quot; &quot;$3&#125;&#x27;

PS：对于/etc/hosts中的主机名和域名，只保留大写或者只保留小写就可以了，因为主机名和域名不区分大小写。
8. 读取某一列读取第一列，读取最后一列。
echo &quot;www.voidking.com&quot; | awk -F&#x27;.&#x27; &#x27;&#123;print $1&#125;&#x27;echo &quot;www.voidking.com&quot; | awk -F&#x27;.&#x27; &#x27;&#123;print $3&#125;&#x27;echo &quot;www.voidking.com&quot; | awk -F&#x27;.&#x27; &#x27;&#123;print $NF&#125;&#x27;

9. 第一列换到最后一列已知namelist.txt：
haojin 70 80voidking 90 100

需求：第一列name，在显示时放到最后一列。
cat namelist.txt | awk &#x27;&#123;for(i=2;i&lt;=NF;i++)printf(&quot;%s &quot;, $i);print $1&#125;&#x27;

10. 循环读取单列文本已知hostlist.txt为：
www.baidu.comwww.voidking.com

需求：批量查询主机名或者主机IP
脚本：
for i in `cat hostlist.txt`;do host $i;done

11. 文本比较需求：两个姓名列表，需要对比出两个文件中相同的姓名和不同的姓名。
脚本：
cat file1 &gt; file.txtcat file2 &gt;&gt; file.txtcat file.txt | sort |uniq -c | sort -n &gt; result.txt


12. 联合查询使用shell，能否实现类似于SQL的联合查询？必须可以。
已知file1的内容为：
1 realname2 nickname
file2的内容为：
voidking nicknamehaojin realnamehankin nicknamejinhao realnamevk nickname  

需求：根据file1和file2的第二列，把file1和file2合并成一个文件。
2 voidking nickname1 haojin realname2 hankin nickname1 jinhao realname2 vk nickname

这个需求使用awk命令来实现。NR，表示awk开始执行程序后所读取的数据行数。FNR，与NR功用类似，不同的是awk每打开一个新文件，FNR便从0重新累计。NR==FNR：用于在读取两个或两个以上的文件时，判断是不是在读取第一个文件。awk处理多个文件的语法：
awk -F 分隔符 &#x27;BEGIN &#123; 初始化 &#125; &#123; 循环执行部分 &#125; END &#123; 结束处理 &#125;&#x27; file_list1 file_list2
其中BEGIN和END可以省略，-F也可以使用默认，循环执行部分，是按行对文件进行处理的。
脚本：
awk -F &quot; &quot; &#x27;NR==FNR&#123;a[$2]=$0;next&#125;&#123;print a[$2]&quot; &quot;$1&#125;&#x27; file1 file2 \| awk &#x27;&#123;print $1&quot; &quot;$3&quot; &quot;$2&#125;&#x27;
由NR=FNR为真时，判断当前读入的是第一个文件file1，执行第一个花括号内的内容。把file1中每行记录都存入数组a，并使用file1的第2个字段作为下标。
由NR=FNR为假时，判断当前读入了第二个文件file2，执行第二个花括号内的内容。file2中的每行，根据file2的第2个字段打印数组a中的内容，同时打印file2中的第一列。
13. 求交集已知file1内容为：
haojinvoidkingvk

file2内容为：
haoshuai 95 93 80vk 99 99 100haojin 100 100 99baidu 100 100 100voidking 99 99 99

需求：file1第一列和file2第一列求交集，显示file2中交集的内容。
awk &#x27;&#123;if(NR==FNR)&#123;a[$1]=$1&#125;else if($1 in a)&#123;print $0&#125;&#125;&#x27; file1 file2# orawk &#x27;&#123;if(NR==FNR)a[$1]=$1;else if($1 in a)print $0&#125;&#x27; file1 file2

14. 每两行合成一行已知ip-port.txt内容为：
127.0.0.180127.0.0.18080192.168.56.1013306

需求：把ip和对应端口放在同一行。
127.0.0.1 80127.0.0.1 8080192.168.56.101 3306

脚本：
sed -n &quot;N;s/\n/ /p&quot; ip-port.txtawk &#x27;NR%2&#123;printf &quot;%s &quot;,$0;next;&#125;1&#x27; ip-port.txtawk &#x27;ORS=NR%2?FS:RS&#x27; ip-port.txtawk &#x27;&#123; ORS = (NR%2 ? FS : RS) &#125; 1&#x27; ip-port.txtawk &#x27;&#123; ORS = (NR%2 ? &quot;,&quot; : RS) &#125; 1&#x27; ip-port.txt

15. 计算一列的和已知fruit.txt内容为：
apple 10orange 7banana 0watermelon 1
第一列是水果名称，第二列是水果数量。
需求1：计算水果的总数。
awk &#x27;&#123;sum += $2&#125;;END &#123;print sum&#125;&#x27; fruit.txtcat fruit.txt | awk &#x27;&#123;sum += $2&#125;;END &#123;print sum&#125;&#x27;

需求2：计算存在多少种水果。
cat fruit.txt | awk &#x27;&#123;if ($2&gt;0) (sum += 1); else (sum += 0)&#125;;END&#123;print sum&#125;&#x27;

16. 根据时间筛选日志筛选11月10日和11月11日的14:30到18:00的日志
grep -E &#x27;1[0,1]/Nov/2022:14:[3,4,5][0-9]|1[0,1]/Nov/2022:1[5-7]|1[0,1]/Nov/2022:18:00&#x27; access.log# orgrep -E &#x27;10/Nov/2022&#x27; access.log | sed -n &#x27;/2022:14:30/,/2022:18:00/p&#x27; &gt; tmp.loggrep -E &#x27;11/Nov/2022&#x27; access.log | sed -n &#x27;/2022:14:30/,/2022:18:00/p&#x27; &gt;&gt; tmp.log
grep筛选出的日志，包含18:00:00-18:00:59内的日志。sed筛选出的日志，只包含第一个匹配上2022:18:00的日志，不包含18:00:00-18:00:59内的日志。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>linux</category>
        <category>troubleshooting</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：输入输出重定向</title>
    <url>/dev-linux-io-redirect/</url>
    <content><![CDATA[1. 重定向语法1.1. 文件描述符
0：标准输入
1：标准输出，默认指向屏幕
2：错误输出，默认指向屏幕
/dev/null： 黑洞

1.2. 重定向操作符
&gt;：将命令的标准输出重定向到指定文件
&gt;&gt;：将命令的标准输出追加到指定文件的末尾
&lt;：将命令的标准输入重定向为指定文件的内容
|：将一个命令的标准输出作为另一个命令的标准输入
&amp;&gt;：将命令的标准输出和错误输出重定向到指定文件



2. 重定向示例# 标准输出重定向到文件ls &gt;out.txtcat out.txt# 等同于ls 1&gt;out.txtcat out.txt# 错误输出重定向到文件ls 2&gt;out.txtcat out.txtls xxx 2&gt;out.txtcat out.txt# 标准输出和错误输出都重定向到文件(ls xxx || ls) &amp;&gt;out.txtcat out.txt# 等同于(ls xxx || ls) &gt;out.txt 2&gt;&amp;1cat out.txt# 标准输出和错误输出都丢弃(ls xxx || ls) &amp;&gt;/dev/null# 等同于(ls xxx; ls) &amp;&gt;/dev/null

3. 重定向应用3.1. 输出到屏幕和文件输出内容同时到屏幕和文件：
echo &quot;hello&quot; | tee test.log

3.2. 清空文件内容清空test.txt文件中的内容
echo &quot;&quot; &gt; test.txtecho -n &quot;&quot; &gt; test.txtcat /dev/null &gt; test.txt:&gt; test.txt&gt; test.txt

3.3. cat写入文件新建文件：
cat &lt;&lt;EOF &gt; test.txthello worldwelcome to python world!EOFcat &lt;&lt;EOF | tee test.txthello worldwelcome to python world!EOF

追加内容：
cat &lt;&lt;EOF &gt;&gt; test.txthello worldwelcome to python world!EOFcat &lt;&lt;EOF | tee -a test.txthello worldwelcome to python world!EOF

写入的内容有$符：
cat &lt;&lt;EOF &gt; /etc/init.wsl#!/bin/sh/etc/init.d/cron \$1/etc/init.d/ssh \$1EOF





]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux启用IPv6</title>
    <url>/dev-linux-ipv6/</url>
    <content><![CDATA[1. IPv6简介
网际协议第6版（英语：Internet Protocol version 6，缩写：IPv6）是网际协议的最新版本，用作互联网的协议。用它来取代IPv4主要是为了解决IPv4地址枯竭问题，同时它也在其他方面对于IPv4有许多改进。

本文中，我们学习Linux系统开启IPv6的方法。
参考文档：维基百科 - IPv6


2. 查看本机IPv6ifconfig

如果看到了inet6，表明本机已经开启了IPv6，后面的一串字符就是 IPv6 地址，例如：
2a02:2149:88f1:4c00:9991:9daa:b580:aee1

如果没有看到 inet6，表明本机没有开启IPv6，也可以通过sysctl命令确认。
sysctl net.ipv6.conf.all.disable_ipv6

看到输出结果为1，表明IPv6是关闭的。
3. CentOS7开启IPv61、查看主机是否已经加载ipv6模块
lsmod | grep ipv6

如果没有回显，表明没有加载，需要自行加载
modprobe ipv6

2、启用ipv6
vim /etc/sysctl.conf

添加或修改内容：
net.ipv6.conf.all.disable_ipv6 = 0net.ipv6.conf.default.disable_ipv6 = 0

3、使改动生效
sysctl -p

4、查看本机IPv6
ifconfig



]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：journalctl命令</title>
    <url>/dev-linux-journalctl/</url>
    <content><![CDATA[1. journalctl命令简介journalctl是systemd套件的一个命令，是一个系统日志管理器，它允许我们在Linux系统上检查和查看系统日志消息。它提供了一种简单而灵活的方式来查看和分析系统日志，包括内核、系统服务和应用程序日志。
参考文档：

systemd
《好好学Linux：systemctl命令》
Linux系统管理员应该知道的journalctl知识



2. journalctl基本用法2.1. 查看帮助journalctl --helpman journalctl

2.2. 查看所有日志查看系统的全部日志信息，从最早的日志条目到最新的日志条目。
journalctl

将日志条目的内容以“cat”的形式（原始文本）显示，而不是默认的格式化展示。使用原始文本格式可能会使输出看起来不太结构化，但它能够显示更详细的信息，对于某些问题的追踪会更有帮助。
journalctl -o cat

2.3. 查看最新的日志直接跳转到最新的日志条目，从日志的末尾开始浏览。
journalctl -e

实时跟踪日志消息
journalctl -f

2.4. 翻页journalctl查看日志时翻页：fn + 上下
2.5. 查看换行日志单行日志太长时，不方便查看，可以查看换行日志。
journalctl -e | less

3. journalctl高级用法3.1. 按程序（单元）过滤使用 -u 选项，后接单元名称（如服务名称），可以过滤出特定单元产生的日志。
查看特定程序日志
journalctl -xeu kubelet

逆序查看程序日志
journalctl -xeu kubelet -r

逆序查看程序日志，并进行二次过滤
journalctl -xeu kubelet -r | grep -v &quot;Search Line limits were exceeded&quot; | head -n 100

3.2. 按时间过滤使用 –since 和 –until 选项，可以限定日志显示的时间范围。
查看特定时间范围内的日志消息
journalctl --since &quot;1 hour ago&quot;journalctl --since &quot;YYYY-MM-DD HH:MM:SS&quot; --until &quot;YYYY-MM-DD HH:MM:SS&quot;

3.3. 按优先级过滤使用 -p 选项，后接优先级数字或关键词，可以过滤出指定优先级的日志。
查看所有错误日志
journalctl -p err

3.4. 按关键词过滤使用 -t 选项，后接关键词，可以过滤出包含指定关键词的日志条目。
查看包含 “error” 的日志
journalctl -t error

3.5. 查看引导日志查看引导日志
journalctl -b

查看详细引导日志
journalctl -xb

3.6. 查看内核日志查看内核日志
journalctl -k

3.7. 清理日志清理2天前的日志
journalctl --vacuum-time=2d

清理超过500M的日志
journalctl --vacuum-size=500M



]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>troubleshooting</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux服务配置开机自启动</title>
    <url>/dev-linux-service-autostart-at-boot/</url>
    <content><![CDATA[1. 服务开机自启动概述在Linux系统使用过程中，我们经常会遇到需要设置某些服务或程序在系统启动时自动启动的情况。例如，我们可能希望web服务、数据库服务或者自定义脚本在服务器启动后自动运行。在Linux中，我们可以使用不同的方法来实现这一点。
常见的方法包括：

使用systemd
使用SysVinit
使用supervisor

参考文档：

《shell命令之systemctl》



2. systemd配置服务开机自启动在现代的Linux发行版中，如Ubuntu 16.04及更高版本、CentOS 7及更高版本，已经采用了systemd来控制系统服务。
整个配置过程，只是需要创建一个systemd服务文件。
cd /etc/systemd/system/# cd /usr/lib/systemd/systemvim myservice.service

内容如下：
[Unit]Description=My Custom ScriptAfter=network.target[Service]ExecStart=/root/script.sh[Install]WantedBy=multi-user.target

systemd服务文件调用的 /root/script.sh 脚本内容为：
#!/bin/bashtail -f /dev/null

配置好systemd服务文件后，我们就可以使用以下命令来控制服务：
systemctl start myservice # 启动服务systemctl status myservice # 查看服务状态systemctl stop myservice # 停止服务systemctl enable myservice # 设置开机自启动

如果删除了 myservice.service 文件，需要重新加载systemd单元配置
systemctl daemon-reload

3. systemd服务配置文件说明3.1. [Unit]块此区块用于设定服务的说明、文档链接等信息，以及定义依赖关系。

Description：用于给出此服务的简短描述。
Documentation：指定文档的链接，通常是man手册页面或者http/https的跳转界面。
After/Before：定义了服务启动的顺序。一般来说，After=network.target表示在网络服务之后再启动。

3.2. [Service]块定义启动服务的运行模式和对应的命令。

Type：定义了服务的启动模式，一般有以下选项。
oneshot：服务启动后，在设定的ExecStart指令全部运行完后服务状态跳转为activating
simple（默认）：ExecStart指令运行后，立即跳转为activated
forking：当ExecStart指定的主程序结束时，服务跳转为activated
restart：当service状态转为deactivating，且Cause由Watchdog引发时，立即重启服务
notify：当主程序通过sd_notify通知(通过/lib/systemd/notify接口)systemd服务进入active状态时，服务跳转为activated


ExecStartPre：启动服务的前置命令，比如启动stunnel前需要创建一个目录 /usr/bin/mkdir -p /var/run/stunnel/
ExecStart：表示启动服务要运行的命令。
ExecStop：表示停止服务要运行的命令。
ExecReload：表示重新加载服务要运行的命令。
Restart：服务重启配置。
no：这是默认值。Service发生任何导致其退出的事情，systemd都不会尝试重启它。
on-success：只有当service正常退出时，即退出状态码为0，systemd才会尝试重启。像被信号杀死或段错误导致的非正常退出等情况，都不会触发重启。
on-failure：当service以非正常方式或被信号杀死且没有设置Service的”SuccessExitStatus”时，即退出状态码非零，systemd才会尝试重启。但由于指定的重启指令(ExecStop=、ExecStopPost=, 以及 TimeoutStopSec=)而退出时不会被重启。
on-abnormal：当service以任何被信号杀死、dump核心或超时状态退出时，systemd会尝试重启。
on-abort：当Service的主进程退出，且导致此次退出的信号名字为 SIGABRT，systemd才会尝试重启这个Service。
on-watchdog：当服务未发送 “keep-alive”消息至systemd造成看门狗超时时，systemd会尝试重启。
always：无论服务以何种状态退出，systemd都会尝试重新启动它（除非手动systemctl stop）。


RestartSec：尝试重启的时间间隔，比如 60s
WorkingDirectory：工作目录，相当于cd进入这个目录再执行命令
Environment：定义服务的环境变量

注意：[Service]的启动、重启、停止命令全部要求使用绝对路径
3.3. [Install]块定义了服务的安装目标，即是否要将此服务在某状态开机时就启动。

WantedBy：此服务被硬性要求在WantedBy指定的服务启动后才启动。
RequiredBy：此服务被强硬要求在RequiredBy指定的服务启动后才启动。

4. systemd配置示例Nginx开机自启动配置方法，参考文档《CentOS7中安装配置Nginx》Harbor开机自启动配置方法，参考文档《Harbor入门篇》
5. systemd服务配置服务检查并重启配置定时任务：
*/5 * * * * systemctl is-active --quiet your-service.service || systemctl start your-service.service

6. SysVinit配置服务开机自启动尽管systemd已经在许多发行版中成为了默认的服务管理工具，但也有一些发行版或旧版本的系统仍在使用SysVinit。在基于SysVinit的系统中，我们可以创建一个启动脚本并将其链接到相应的runlevel目录。
1、创建一个启动脚本 myservice
cd /etc/init.dvim myservicechmod a+x myservice

myservice内容为：
#!/bin/sh# chkconfig: 345 99 01# description: My custom startup scriptcase &quot;$1&quot; in  start)    /root/script.sh    ;;  stop)    # Stop script commands here    ;;  *)    echo &quot;Usage: $0 &#123;start|stop&#125;&quot;    exit 1esacexit 0

2、链接到runlevel目录
chkconfig --add myservicels -l /etc/rd*.d/*

可以看到，/etc/rc[0~6].d 目录中都创建了一个 myservice 的链接。
3、检查服务状态
chkconfig --list myservice
看到内容为：
myservice       0:关 1:关 2:关 3:开 4:开 5:开 6:关

这个表示服务myservice在不同的系统运行级别(runlevel)下的开启或关闭状态。在Linux系统中，总共有7个运行级别，编号为0到6，每个级别都对应着不同的系统状态，其中：

0: 关闭系统
1: 单用户模式(single-user mode)
2: 多用户模式，不含NFS(multi-user mode without NFS)
3: 完全的多用户模式(multi-user mode with networking)
4: 用户可自定义的运行级别
5: 图形界面模式(multi-user mode with GUI)
6: 重启系统

所以，myservice 0:关 1:关 2:关 3:开 4:开 5:开 6:关的意思就是在系统处于运行级别3、4、5时，myservice服务会自动启动；在运行级别0、1、2、6时，myservice服务不会启动。
4、设置所有level状态下服务都能自动启动
chkconfig --level 123456 myservice on

在包含systemd的系统中，会自动转成systemd服务文件。
7. SysVinit配置示例MySQL开机自启动配置方法，参考文档《CentOS7中安装配置MySQL》
8. rc.local配置服务开机自启动除了使用chkconfig命令，我们还可以把脚本的启动命令直接写入 /etc/rc.local 文件中，那么开机时也会自动启动。这种配置服务开启自启动的方式，也可以归类于SysVinit。
rc.local配置开机自启动示例：《使用frp进行内网穿透》
9. supervisor配置服务开机自启动supervisor安装配置方法，参考文档《Linux安装配置Supervisor》
supervisor配置开机自启动示例：《CentOS7中安装配置Tomcat》
]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：ssh命令</title>
    <url>/dev-linux-ssh/</url>
    <content><![CDATA[1. ssh简介
SSH (Secure Shell) 是一个为远程登录会话和其他网络服务提供安全性的协议。通过一个安全通道，用户可以在未加密的网络中为网络服务提供强大的身份验证和安全加密，让用户可以安全地在不安全的网络环境中执行远程命令，并将本地和远程主机之间的数据进行安全传输。

SSH协议的实现包括服务端和客户端。

SSH 服务端提供远程登录和文件传输等功能，可以被其他用户或系统通过 SSH 协议连接并访问。
SSH 客户端是用来连接远程服务端的工具。用户可以使用命令行或图形界面工具来连接到 SSH 服务端，并通过 SSH 协议进行通信。
SSH 客户端可以通过用户名和密码、公钥验证等方式进行身份验证，登录到服务端。登录成功后，用户就可以在远程服务器上执行命令、上传或下载文件等操作。

SSH协议有很多实现，其中最广泛使用的是OpenSSH，OpenSSH也是Linux、Unix等操作系统的SSH协议默认实现。
参考文档：

SSHD



2. 安装ssh serverubuntu/debian中：
apt-get updateapt-get install -y openssh-server

centos/redhat中：
yum install -y openssh-server

3. 配置ssh server3.1. 基础配置ssh server的配置文件位于 /etc/ssh/sshd_config。需要使用具有root权限的编辑器（例如：nano，vi）对其进行编辑。
在配置文件中的关键参数包括：

Port：默认端口是22，可以根据需要改变端口。
PermitRootLogin：这个参数决定是否允许使用root用户进行远程登录，默认是Yes，可改为No。
PasswordAuthentication：如设置为no，则关闭了密码登录，只能通过密钥登录。

编辑完成之后，需要重启SSH服务让更改生效：
/etc/init.d/ssh restart

3.2. 白名单配置SSH白名单配置实际上是通过TCP Wrappers实现的。TCP Wrappers为许多网络服务提供了一个基于主机的访问控制策略，包括SSH。
TCP Wrappers依赖两个配置文件：/etc/hosts.allow和/etc/hosts.deny。
在hosts.allow文件中列出的主机可以访问网络服务，而在hosts.deny文件中列出的主机则被拒绝访问。
每个文件中的条目都是一行，包含一个服务名和一列主机名或IP地址。服务名是/etc/services文件中列出的服务名或由互联网守护程序（inetd）或xinetd启动的守护进程的名称。主机名或IP地址可以是完全限定的主机名、IP地址、IPv6地址、DNS域名、网络地址或者是这些值的模式。
要为SSH配置一个白名单，需要在/etc/hosts.allow文件中指定ssh和sshd服务可以访问的主机，然后在/etc/hosts.deny文件中禁止所有其他主机访问这些服务。
假设我们希望只允许10.0.0.0/24网络中的主机和192.168.56.200通过SSH访问我们的服务器，其他所有主机都不能访问。可以按如下方式配置:
在/etc/hosts.allow文件中添加配置：
sshd:10.0.0.0/24sshd:192.168.56.200

在/etc/hosts.deny文件中添加配置：
sshd:ALL

参考文档：hosts.allow format and example on Linux
4. 使用ssh client4.1. ssh登录服务器ssh voidking@192.168.56.100ssh voidking@192.168.56.100 -p 2222 # 当远程机器ssh端口为2222时

4.2. 测试ssh连接ssh -vvv voidking@192.168.56.100 /bin/true

4.3. ssh执行远程命令执行远程命令很简单，直接ssh后面跟着命令就可以了。
ssh voidking@192.168.56.100 &quot;echo helloworld&quot;

那如果远程命令里面包含双引号怎么办？变成单引号，或者添加转义。
ssh voidking@192.168.56.100 &quot;echo &#x27;helloworld&#x27;&quot;ssh voidking@192.168.56.100 &quot;echo \&quot;helloworld\&quot;&quot;

那如果需要远程命令里使用变量怎么办？添加转义。
ssh voidking@192.168.56.100 &quot;host=\$(hostname);echo \&quot;\$&#123;host&#125;\&quot;&quot;

4.4. ssh批量执行远程命令已知主机列表hosts.txt内容为：
192.168.56.101192.168.56.102

需求：给主机列表中的机器添加开机启动命令
脚本：
for i in `cat hosts.txt`; do ssh $i &quot;echo &#x27;/home/voidking/start.sh&#x27; | sudo tee -a /etc/rc.local&quot;;done

5. 配置免密登录5.1. 生成密钥对通过ssh登录远程主机，要么使用密码，要么使用密钥对。使用密钥对的话，可以实现免密登录。
生成默认密钥对：
ssh-keygen
执行命令后，一路回车，会生成公钥~/.ssh/id_rsa.pub 和私钥 ~/.ssh/id_rsa 
指定参数生成密钥对：
ssh-keygen -C &quot;voidking@qq.com&quot; -f ~/.ssh/voidking_rsa 
执行命令后，~/.ssh/ 目录下会生成 voidking_rsa 和 voidking_rsa.pub，注释内容为 &#118;&#111;&#105;&#100;&#107;&#105;&#110;&#103;&#x40;&#113;&#113;&#46;&#99;&#x6f;&#109;
5.2. 添加公钥到远程机器假设远程机器为192.168.56.100。
添加默认公钥到远程机器：
ssh-copy-id voidking@192.168.56.100ssh-copy-id voidking@192.168.56.100 -p 2222 # 当远程机器ssh端口为2222时

根据提示输入密码，这条命令会把 ~/.ssh/id_rsa.pub 的内容追加到远程机器的 ~/.ssh/authorized_keys 中。注意：authorized_keys文件权限需要配置为600。如果配置好了sshd，但是无法使用密钥登录机器，请检查authorized_keys文件权限。
添加指定公钥到远程机器：
ssh-copy-id -i ~/.ssh/voidking_rsa voidking@192.168.56.100

执行这条命令后，~/.ssh/voidking_rsa.pub 中的内容会被拷贝到远程机器的 ~/.ssh/authorized_keys 中。
对于macos系统，需要单独单装 ssh-copy-id 命令。
sudo curl -L https://raw.githubusercontent.com/beautifulcode/ssh-copy-id-for-OSX/master/install.sh | sh

5.3. 密钥添加失败问题如果ssh-copy-id报错cannot create .ssh/authorized_keys: Operation not permitted或者手动编辑authorized_keys后无法保存，chmod也无法改变authorized_keys文件的权限。
那么可能是因为authorized_keys被加锁了，防止误删，需要先进行解锁。
chattr -i ~/.ssh/authorized_keys
参考文档chattr命令 – 更改文件隐藏属性
5.4. 免密登录远程机器使用默认私钥登录：
ssh voidking@192.168.56.100

指定私钥登录：
ssh -i ~/.ssh/id_rsa voidking@192.168.56.100 

执行ssh命令时，默认会先使用密钥进行验证，如果密钥配置正常，那么就不需要输入密码了。
6. 配置主机别名登录登录远程服务器，需要执行命令
ssh username@host -p port

如果有大量的username、host、port，是很难记忆的，因此我们需要配置主机别名登录。
主机别名登录具体配置方法如下：
1、创建 ~/.ssh/config 文件，内容为
Host jumpboxHostName jumpbox.voidking.comUser voidkingPort 2345IdentityFile ~/.ssh/voidking_rsaHost bastionhostHostName 45.68.78.134User rootPort 3456IdentityFile ~/.ssh/id_rsa

2、配置 authorized_keys
ssh-copy-id -i ~/.ssh/voidking_rsa.pub jumpboxssh-copy-id -i ~/.ssh/id_rsa.pub bastionhost

3、登录服务器 
ssh jumpboxssh bastionhost

7. 配置用户只能运行特定命令7.1. 运行命令/脚本1、authorized_keys 配置
command=&quot;/usr/bin/date&quot; ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9...

2、运行命令
ssh voidking@192.168.56.100

默认会运行 date 命令。
7.2. 运行命令/脚本 + 参数1、准备脚本 test.sh
#/bin/bashecho $1echo $2

2、authorized_keys 配置
#command=&quot;/usr/bin/echo $SSH_ORIGINAL_COMMAND&quot; ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9...command=&quot;/path/to/test.sh $SSH_ORIGINAL_COMMAND&quot; ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9...

3、运行脚本
ssh voidking@192.168.56.100 &quot;test1 test2&quot;ssh voidking@192.168.56.100 test1 test2

默认会运行 test.sh 脚本，test1 和 test2会作为两个参数传递给 test.sh。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>linux</category>
        <category>troubleshooting</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：sudo命令</title>
    <url>/dev-linux-sudo/</url>
    <content><![CDATA[1. sudo简介
在sudo于1980年前后被写出之前，一般用户利用 su 切换用户，管理系统时通常使用此命令切换为超级用户。但是使用su的缺点之一在于必须要先告知目标用户的密码。


sudo使一般用户不需要知道目标用户的密码即可获得权限。首先由超级用户将普通用户的名字、可以执行的特定命令、按照哪种用户或用户组的身份执行等信息，登记在特殊的文件中（通常是/etc/sudoers），即完成对该用户的授权（此时该用户称为sudoer）；在一般用户需要获取特定权限时，其可在命令前加上sudo，此时sudo将会询问该用户自己的密码（以确认终端前的是该用户本人），回答后系统即会将该命令的进程以目标用户的权限执行。如果没有指定目标用户，就默认以超级用户的权限执行。之后的一段时间内（默认为5分钟，可在/etc/sudoers自定义），使用sudo不需要再次输入密码。

当给用户授权sudo权限时，进行哪些限制会更加安全？本文以给 voidking 用户授权为例，学习一下授权sudo的方法。
参考文档：

维基百科 - sudo
How To Edit the Sudoers File
8 Best practices with sudo on Linux – Do’s and Dont’s of sudo
关于linux的sudo的权限分离最佳实践



2. 给用户授权sudo2.1. 方法一：直接授权给用户1、编辑 sudoers 文件
# vim /etc/sudoersvisudo

添加授权内容：
voidking   ALL=(ALL:ALL) ALL

2、测试
su voidkingsudo pwd

2.2. 方法二：添加用户到sudo组（推荐）2.2.1. 授权给sudo组授权给sudo组，仅操作一次即可。
1、添加sudo用户组
groupadd sudo

2、编辑 sudoers 文件
visudo

添加内容：
%sudo ALL=(ALL:ALL) ALL

2.2.2. 添加用户到sudo组usermod -G sudo -a voidking

3. 授权文件解释已知一条sudoers授权规则为：
voidking ALL=(ALL:ALL) ALL

这个配置的含义为：

voidking，表示这个规则是对用户 voidking 生效的
第一个ALL，表示这个规则是对所有主机生效的
第二个ALL，表示voidking可以以所有用户身份运行命令
第三个ALL，表示voidking可以以所有用户组身份运行命令
第四个ALL，表示voidking用户可以运行所有命令

如果授权规则改为：
voidking ALL=(ALL:ALL) NOPASSWD:ALL
那么表示voidking用户在运行所有命令时都不需要密码。
4. 最佳实践4.1. 开启sudo日志将所有sudo命令操作记录到系统的日志文件中，以便日后审计。
sudoers文件中，添加日志配置
Defaults        logfile=/var/log/sudo.log

4.2. 特定命令的执行权限限制用户只能执行特定的命令，可以提高系统安全性。
sudoers文件中，配置改为只能执行指定命令
voidking ALL=(ALL) /path/to/command

4.3. 锁定sudo命令路径锁定sudo可以执行的命令的路径。
Defaults secure_path=&quot;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&quot;

4.4. 切换其他用户需要密码默认情况下，用户可以使用sudo命令切换到任何其他用户的身份，我们可以限制用户切换到其他用户时需要密码。
sudoers文件中，添加切换用户需要密码的配置
Defaults        targetpw

4.5. 禁止用户切换到root（不可行）sudoers文件中，禁止用户使用sudo su切换到root
voidking ALL=(ALL) ALL,!/bin/su

但是，这个方法并不能阻止用户切换到root，比如用户还可以使用sudo -i或者sudo bash切换到root。
更好的办法是配置用户只能执行特定的命令。
4.6. 特定sudo配置使用单独配置文件对特定用户和用户组的一组配置，独立作为一个文件，结构会更加清晰。
默认 /etc/sudoers.d 目录中的文件会被include
#includedir /etc/sudoers.d

编辑独立配置文件：
visudo -f /etc/sudoers.d/ops

4.7. 不给docker命令sudo权限如果给用户授权了sudo docker权限，那么用户就可以挂载一些没有权限文件到容器中，此时就有权限进行操作了。因此最好不要给用户授权docker的sudo权限，而是应该配置普通用户也可以直接执行docker命令。
5. 权限分配5.1. sudoers默认权限分组CentOS7 sudoers中的默认权限分组，可以参考。
## Command Aliases## These are groups of related commands...## Networking# Cmnd_Alias NETWORKING = /sbin/route, /sbin/ifconfig, /bin/ping, /sbin/dhclient, /usr/bin/net, /sbin/iptables, /usr/bin/rfcomm, /usr/bin/wvdial, /sbin/iwconfig, /sbin/mii-tool## Installation and management of software# Cmnd_Alias SOFTWARE = /bin/rpm, /usr/bin/up2date, /usr/bin/yum## Services# Cmnd_Alias SERVICES = /sbin/service, /sbin/chkconfig, /usr/bin/systemctl start, /usr/bin/systemctl stop, /usr/bin/systemctl reload, /usr/bin/systemctl restart, /usr/bin/systemctl status, /usr/bin/systemctl enable, /usr/bin/systemctl disable## Updating the locate database# Cmnd_Alias LOCATE = /usr/bin/updatedb## Storage# Cmnd_Alias STORAGE = /sbin/fdisk, /sbin/sfdisk, /sbin/parted, /sbin/partprobe, /bin/mount, /bin/umount## Delegating permissions# Cmnd_Alias DELEGATING = /usr/sbin/visudo, /bin/chown, /bin/chmod, /bin/chgrp## Processes# Cmnd_Alias PROCESSES = /bin/nice, /bin/kill, /usr/bin/kill, /usr/bin/killall## Drivers# Cmnd_Alias DRIVERS = /sbin/modprobe

5.2. 权限配置示例5.2.1. 运维组1、权限规划

初级：查看文件列表和文件内容、查看系统信息、查看网络状态
高级：初级所有权限、修改网络配置、进程管理、存储管理、软件包管理
经理：超级用户所有权限

2、创建配置文件
visudo -f /etc/sudoers.d/op

注意：配置文件名不能含有.，否则不会生效，详情参考文档File in /etc/sudoers.d file not being read by sudo
3、配置内容
Cmnd_Alias JUNIOR_OP_CMD = /usr/bin/ls,/usr/bin/cat,/usr/bin/grep,/usr/bin/tail,/usr/bin/df,/usr/bin/free,/usr/bin/top,/usr/bin/hostname,/usr/bin/ping,/usr/sbin/ifconfig,/usr/bin/netstat,/usr/bin/systemctl status *,/usr/bin/journalctlCmnd_Alias SENIOR_OP_CMD = /usr/sbin/route,/usr/sbin/iptables,/usr/bin/nice,/usr/bin/kill,/usr/bin/pkill,/usr/bin/killall,/usr/sbin/fdisk,/usr/sbin/sfdisk,/usr/sbin/partprobe,/bin/mount,/usr/bin/systemctl,/usr/bin/journalctl,/usr/bin/rpm,/usr/bin/up2date,/usr/bin/yum%juniorop ALL=(ALL) NOPASSWD: JUNIOR_OP_CMD%seniorop ALL=(ALL) NOPASSWD: JUNIOR_OP_CMD, SENIOR_OP_CMD%superop ALL=(ALL) NOPASSWD: ALL

如果是ubuntu/debian系统，软件包管理命令需要改成 /usr/bin/apt,/usr/bin/apt-get另外不同的系统中，命令的路径也是不同的，需要针对不同系统进行修改。
5.2.2. 开发组1、权限规划

初级：查看文件列表和文件内容权限，对应服务查看日志的权限
高级：初级所有权限，重启对应服务
经理：项目所在服务器的ALL权限，不能修改root密码，不能修改visudo

2、创建配置文件
visudo -f /etc/sudoers.d/dev

3、配置内容
Cmnd_Alias JUNIOR_DEV_CMD = /usr/bin/ls,/usr/bin/cat,/usr/bin/grep /data/apps/logs,/usr/bin/tail /data/apps/logsCmnd_Alias SENIOR_DEV_CMD = /sbin/serviceCmnd_Alias SUPER_DEV_CMD = ALL,/usr/bin/passwd [A-Za-z]*,!/usr/bin/passwd root,!/usr/sbin/visudo%juniordev ALL=(ALL) NOPASSWD: JUNIOR_DEV_CMD%seniordev ALL=(ALL) NOPASSWD: JUNIOR_DEV_CMD, SENIOR_DEV_CMD%superdev ALL=(ALL) NOPASSWD: SUPER_DEV_CMD

5.2.3. 网络组1、权限规划

初级：普通用户的权限，不加入sudo列表
高级：所在服务器的网络管理权限

2、创建配置文件
visudo -f /etc/sudoers.d/net

3、配置内容
Cmnd_Alias SENIOR_NET_CMD = /usr/sbin/route,/usr/sbin/ifconfig,/usr/bin/ping,/usr/sbin/dhclient,/usr/include/net,/usr/sbin/iptables,/usr/sbin/mii-tool,/usr/bin/cat /var/log/*%seniornet ALL=(ALL) NOPASSWD: SENIOR_NET_CMD

5.2.4. DBA组1、权限规划

初级：普通权限，不加入sudo列表
高级：数据库服务器的ALL权限，不能修改root密码，不能修改visudo

2、创建配置文件
visudo -f /etc/sudoers.d/dba

3、配置内容
Cmnd_Alias SENIOR_DBA_CMD = ALL,/usr/bin/passwd [A-Za-z]*,!/usr/bin/passwd root,!/usr/sbin/visudo%seniordba ALL=(ALL) NOPASSWD: SENIOR_NET_CMD





]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：系统管理常用命令</title>
    <url>/dev-linux-system-management/</url>
    <content><![CDATA[1. 前言本文记录linux中系统管理时常用的命令，备忘。


2. 查看硬件配置2.1. 查看服务器型号dmidecodedmidecode | grep &quot;System Information&quot; -A16 

2.2. 查看CPU物理CPU数（physical id）：主板上实际插入的cpu数量，可以数不重复的 physical id 有几个。CPU核心数（cpu cores）：单块CPU上面能处理数据的芯片组的数量，如双核、四核等逻辑CPU数：一般情况下，逻辑CPU=物理CPU个数×每颗核数；如果开启了超线程技术，逻辑CPU=物理CPU个数×每颗核数x2 
# 查看CPU详细信息lscpucat /proc/cpuinfo# 查询物理CPU数量lscpu | grep -i &quot;Architecture&quot; | wc -lcat /proc/cpuinfo | grep &quot;physical id&quot; |sort | uniq | wc -l# 查询每颗物理CPU核数lscpu | grep &quot;per socket&quot;cat /proc/cpuinfo | grep &quot;core id&quot; | sort | uniq | wc -l# 查询系统的每颗物理CPU核心是否启用超线程技术lscpu | grep -e &quot;per socket&quot; -e &quot;per core&quot;cat /proc/cpuinfo | grep -e &quot;cpu cores&quot;  -e &quot;siblings&quot; | sort | uniq# 查询系统具有多少个逻辑CPUlscpu | grep -i &quot;CPU(s):&quot;cat /proc/cpuinfo | grep &quot;processor&quot; | wc -l

lscpu查看到的Core(s) per socket表示这颗物理CPU有多少核心，Thread(s) per core表示每个核心能同时处理多少线程。
参考文档：/proc/cpuinfo文件解读（超易理解）
2.3. 查看内存# 查看内存free -hfree -mcat /proc/meminfo

2.4. 查看磁盘lsblkdf -h

2.5. 查看显卡lspci | grep -i vga # 最前面的序号是显卡代号lspci | grep -i nvidia lspci -v -s 02:00.0 # 通过显卡序号查看显卡详情lshw -numeric -C display # 查看所有显卡详情

2.6. 查看网卡lspci | grep Ethernet # 查看网卡是百兆、千兆、还是万兆网卡

3. 查看系统信息3.1. 查看系统版本lsb_release -acat /etc/issuecat /etc/os-releasecat /etc/centos-releasehostnamectl

3.2. 查看系统位数uname -afile /bin/lsgetconf LONG_BIThostnamectl

3.3. 查看内核版本uname -ahostnamectl

3.4. 查看是否是物理机dmidecode -s system-product-namehostnamectl

4. 系统配置4.1. 修改默认shellecho $SHELLcat /etc/shellswhich bashchsh -s /bin/bash

4.2. 显示颜色vim ~/.bashrc

取消 force_color_prompt=yes 的提示。
4.3. 禁止检查邮件系统经常提示：您在 /var/spool/mail/root 中有新邮件
echo &quot;unset MAILCHECK&quot; &gt;&gt; /etc/profilesource /etc/profile

4.4. 添加用户useradd -m voidking -s /bin/bashpasswd voidking

4.5. 给用户添加sudo权限参考文档：《shell命令之sudo》
4.6. 添加用户组groupadd docker

4.7. 添加用户到用户组usermod -G docker -a gitlab-runnerusermod -G root -a gitlab-runner

4.8. 用户切换用户组如果一个用户属于多个用户组，可以切换默认用户组。
newgrp dockerid

4.9. 从用户组删除用户gpasswd -d docker gitlab-runnergroups gitlab-runner

或者直接修改 /etc/group 文件，删除对应行。
4.10. 删除用户删除用户和用户目录
userdel -r voidking

删除用户，然后删除用户目录
userdel voidkingfind /home/ -nouser | xargs rm -rfv

4.11. 切换用户切换root
sudo -i# orsudo su

切换普通用户
su voidking

4.12. 修改用户家目录usermod -d /data/voidking voidking # 切换用户家目录到 /data/voidkingusermod -md /data/voidking voidking # 切换用户家目录，同时把原来家目录中的内容移动到新的家目录

对于root用户，需要先进入单用户模式，才能进行修改家目录操作。
4.13. 添加字体查看字体
fc-list

添加字体
mkdir -p /usr/share/fonts/win/cd /usr/share/fonts/win/rz # 或者 wget/scptar -xzvf fonts.tgz

添加字体后更新缓存
fc-cache -fvfc-list

4.14. 设置临时IP添加临时IP
ip addr add 192.168.51.20/24 dev em1

删除临时IP
ip addr del 192.168.51.20/24 dev em1

4.15. 网桥从属网卡两台主机通过网线直连，并且设置了临时IP后，如果发现互ping不通，那么可以检查下桥接从属网卡配置。有的时候，一个网桥可能会添加多个从属网卡，从而导致网卡设置了IP但是不通。
从属网卡在网络中起到以下作用：

扩展网络端口：通过将多个网卡添加到网桥或交换机上，可以增加网络设备的端口数量，从而连接更多的设备或虚拟机。
实现网络聚合：从属网卡可以用于实现网络聚合，将多个物理网卡绑定在一起，提供更高的带宽和冗余。
实现冗余和容错：通过将多个从属网卡连接到网桥或交换机上，可以实现冗余路径，当一个路径发生故障时，可以切换到其他路径，提高网络的可靠性和容错性。
提供网络隔离：从属网卡可以用于隔离网络流量，将不同类型的流量或不同网络的流量分开处理，增加网络的安全性和性能。

查看网桥信息
ip addr # 可以查看到网卡从属的网桥brctl showbridge link

增加网桥从属网卡
bridge link set dev eth2 master br0

删除网桥从属网卡
ip link set dev eth2 downbrctl delif br0 eth2ip link set dev eth2 up

4.16. 确定网卡编号和网口对应关系ethtool -p eth2

执行上面的命令后，观察网口，闪烁的那个网口就是网卡编号对应的网口。
4.17. 修改网卡配置1、查看网卡配置
ethtool eth2

示例输出如下：
Settings for eth2:    Supported ports: [ TP ]    Supported link modes:   10baseT/Half 10baseT/Full                            100baseT/Half 100baseT/Full                            1000baseT/Full    Supported pause frame use: Symmetric    Supports auto-negotiation: Yes    Supported FEC modes: Not reported    Advertised link modes:  10baseT/Half 10baseT/Full                            100baseT/Half 100baseT/Full                            1000baseT/Full    Advertised pause frame use: Symmetric    Advertised auto-negotiation: Yes    Advertised FEC modes: Not reported    Speed: 100Mb/s    Duplex: Full    Auto-negotiation: on    Port: Twisted Pair    PHYAD: 1    Transceiver: internal    MDI-X: off (auto)    Supports Wake-on: pumbg    Wake-on: g        Current message level: 0x00000007 (7)                               drv probe link    Link detected: yes

从输出可以看出，网卡支持千兆，但是实际上的速度是百兆。
2、设置网卡千兆全双工
ethtool -s eth2 speed 1000 duplex full# orethtool -s eth2 speed 1000 duplex full autoneg on

如果执行完上面的命令后，主机断网，那么请检查网线是否支持千兆，大概率是网线的问题。
4.18. 修改内核参数1、查看内核参数，例如
sysctl net.core.rmem_max

2、修改内核参数，例如
sysctl -w net.core.rmem_max=2097152

3、永久生效修改内核参数
vim /etc/sysctl.conf# 写入 net.core.rmem_max=2097152sysctl -p

如果用的是其它的配置文件，那么需要指定它，例如 
sysctl -p /etc/sysctl.d/mysettings.conf

参考文档：Linux 实例常用内核参数介绍
5. 软件安装5.1. 查看命令属于哪个软件包1、centos7
yum makecache fastyum whatprovides lsb_release

2、ubuntu
apt-get install -y apt-fileapt-file updateapt-file search ifconfig

5.2. 重建软件安装源centos7
cd /etc/yum.repos.drm -f *.reposcp 192.168.56.101:/etc/yum.repos.d/* .yum clean allyum makecache fast

5.3. EPEL源EPEL（ExtraPackagesforEnterpriseLinux）是基于Fedora的一个项目，为RedHat系的操作系统提供额外的高质量软件包。
yum install epel-release

安装epel源之后，可以安装很多原本 No package xxx available 的软件，比如jq：
yum install jq

5.4. 安装指定版本软件以安装Docker为例，参考文档《好好学Docker：Docker入门篇》
6. 问题排查参考文档：《好好学Linux：Linux问题排查常用命令》
7. Uncategories7.1. 跟踪系统调用假设ls执行卡住，可以通过strace命令查看ls具体的执行情况
strace ls /mnt

7.2. 时间和时间戳# 输出时间date# 输出时间并格式化date &quot;+%Y-%m-%d %H:%M:%S&quot;# 时间转时间戳（秒）date &quot;+%s&quot;# 时间转时间戳（微秒）microsecond=$(($(date &quot;+%s%N&quot;)/1000000))# 时间戳（秒）转时间date -d @1612351314 &quot;+%Y-%m-%d %H:%M:%S&quot;

7.3. 还原history万万没想到，centos4.3的history -c命令，不止会清除当前登录的操作历史记录，还会清除 .bash_history 中的内容。
如果不小心清除了 .bash_history 中的内容，该怎么办？如果屏幕上还残留着history的历史记录，那么还有得救。拷贝屏幕上的内容，到 history.txt，假设内容为：
244486  2021-01-08 17:35:18 cd haojin244487  2021-01-08 17:36:01 vim main.py244488  2021-01-08 17:38:33 python main.py

怎么恢复成标准的 .bash_history 格式呢？使用如下脚本：
#!/bin/bashcat /dev/null &gt; bash_historywhile read linedo  time=$(echo $line | awk &#x27;&#123;print $2&quot; &quot;$3&#125;&#x27;)  #cmd=$(echo $line | awk &#x27;&#123;$1=&quot;&quot;;$2=&quot;&quot;;$3=&quot;&quot;;print $0&#125;&#x27;)  cmd=$(echo $line | awk -F &quot; &quot;  &#x27;&#123;for (i=4;i&lt;=NF;i++)printf(&quot;%s &quot;, $i);print &quot;&quot;&#125;&#x27;)  timestamp=$(date -d &quot;$time&quot; +%s)  echo &quot;#&quot;$timestamp &gt;&gt; bash_history  echo $cmd &gt;&gt; bash_historydone &lt; history.txt

执行完成，使用 bash_history 替换 .bash_history 即可。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>linux</category>
        <category>troubleshooting</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>docker</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：systemctl命令</title>
    <url>/dev-linux-systemctl/</url>
    <content><![CDATA[1. systemctl命令简介
systemd is a software suite that provides an array of system components for Linux operating systems. Its main aim is to unify service configuration and behavior across Linux distributions; Its primary component is a “system and service manager”—an init system used to bootstrap user space and manage user processes. It also provides replacements for various daemons and utilities, including device management, login management, network connection management, and event logging. 

systemd 是一个软件套件，它为 Linux 操作系统提供一系列系统组件。它的主要目标是统一 Linux 发行版中的服务配置和行为；它的主要组件是“系统和服务管理器”——一个用于引导用户空间和管理用户进程的 init 系统。它还提供各种守护程序和实用程序的替代品，包括设备管理、登录管理、网络连接管理和事件日志记录。
systemd’s core components include the following:

systemd is a system and service manager for Linux operating systems.
systemctl is a command to introspect and control the state of the systemd system and service manager. Not to be confused with sysctl.
systemd-analyze may be used to determine system boot-up performance statistics and retrieve other state and tracing information from the system and service manager.

如wiki所说，systemctl是systemd的一个核心组件，作用是作为命令客户端控制systemd。在CentOS7中，systemctl命令主要负责控制systemd系统和服务管理器。基本取代了service和chkconfig命令，虽然service和chkconfig命令依然保留，但是据说已经被阉割过。
参考文档：

systemd
《shell命令之journalctl》
CentOS7/RHEL7 systemd详解
CentOS7进程管理systemd详解
Centos7下的systemctl命令与service和chkconfig
Linux初始化init系统之Sysvinit
浅析 Linux 初始化 init 系统（1）：sysvinit
chkconfig和systemctl



2. systemd历史2.1. Linux启动过程Linux系统从启动到提供服务的过程是这样，先是机器加电，然后通过MBR或者UEFI加载GRUB，再启动内核，内核启动服务，然后开始对外服务。
SysVinit、UpStart、systemd主要是解决服务引导管理的问题。
2.2. SysVinitSysVinit是最早的解决方案，依靠划分不同的运行级别，启动不同的服务集，服务依靠脚本控制，并且是顺序执行的。在CentOS5系统中，配置文件为/etc/inittab。
SysVinit方案的优点是：原理简单，易于理解；依靠shell脚本控制，编写服务脚本门槛比较低。缺点是：服务顺序启动，启动过程比较慢；不能做到根据需要来启动服务，比如通常希望插入U盘的时候，再启动USB控制的服务，这样可以更好的节省系统资源。
获取系统运行级别之后，根据运行级别顺序的执行以下位置的启动脚本，从而将系统初始化为预设的运行级别
/etc/rc.d/rc.sysinit 重要的系统初始化服务/etc/rc.d/rc 和 /etc/rc.d/rcX.d X为运行级别/etc/rc.d/rc.local 用户个性化服务

chkconfig是一个用于维护/etc/rc[0~6].d目录的命令行工具。

等级0：表示关机
等级1：表示单用户模式
等级2：表示无网络连接的多用户命令行模式
等级3：表示有网络连接的多用户命令行模式
等级4：表示不可用
等级5：表示带图形界面的多用户模式
等级6：表示重新启动

2.3. UpStart为了实现系统服务的即插即用，UpStart应运而生，在CentOS6系统中，SysVinit和UpStart是并存的，UpStart主要解决了服务的即插即用。对于服务顺序启动慢的问题，UpStart的解决办法是把相关的服务分组，组内的服务是顺序启动，组之间是并行启动。在CentOS6系统中，配置文件为/etc/inittab和/etc/init/\*.conf。
2.4. systemd随着移动互联网的到来，SysVinit服务启动慢的问题显得越来越突出，许多移动设备都是基于Linux内核，比如安卓。移动设备启动比较频繁，每次启动都要等待服务顺序启动，显然难以接受，systemd就是为了解决这个问题诞生的。在CentOS7系统中，配置文件为/usr/lib/systemd/system/ 和 /etc/systemd/system/ 中的文件。
systemd的设计思路是：尽可能的快速启动服务；尽可能的减少系统资源占用。
3. systemctl常用命令3.1. 启停服务以启动docker服务为例
systemctl start docker

以停止docker服务为例
systemctl stop docker

以重启docker服务为例
systemctl restart docker

3.2. kill服务以杀死docker服务为例
systemctl kill docker

3.3. 查看服务状态以查看docker服务状态为例
systemctl status docker

查看 Drop-In 字段，就能找到配置文件位置。从这个配置文件中，又可以查看到其他配置文件的位置。
查看docker服务状态，日志不截断
systemctl status docker -l

3.4. 开机自启动# 开机自启动systemctl enable docker# 关闭开机自启动systemctl disable docker

更多内容参考文档：《Linux服务开机自启动配置》
3.5. 重新加载单元配置systemctl daemon-reload
经常和启停服务配合使用。停止服务 -&gt; 修改单元配置 -&gt; 重新加载单元配置 -&gt; 启动服务修改单元配置 -&gt; 重新加载单元配置 -&gt; 重启服务
3.6. 重新加载某个单元systemctl reload docker.service

3.7. 查看服务参数配置以查看docker服务的MountFlags配置为例
sudo systemctl show --property=MountFlags docker.servicesudo systemctl show docker.service

3.8. 重新加载配置以重新加载sshd服务配置为例
systemctl reload sshd

3.9. 查看所有单元systemctl list-unit-files

3.10. 查看所有自启动的单元systemctl list-unit-files | grep enable

3.11. 查看某个单元是否开机自启动systemctl is-enabled docker.service

3.12. 查看所有活跃的单元systemctl list-units

3.13. 查看所有活跃的服务systemctl list-units -t service

3.14. 查看所有失败的单元systemctl --failed



]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>troubleshooting</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：top命令</title>
    <url>/dev-linux-top/</url>
    <content><![CDATA[1. top命令简介top命令是一种常用的 Linux 实时性能监控工具，可以显示系统资源使用情况和进程信息，可以帮助用户了解当前系统的运行状态。
参考文档：

《shell命令之系统管理》
Linux 使用 top 命令查看系统的运行情况



2. 常见用法
显示实时进程信息：在终端输入 top 命令即可，按下键盘上的 q 键退出。

按 CPU 使用率排序：按下 Shift + P 键，即可将进程按照 CPU 使用率的高低排序。

按内存使用率排序：按下 Shift + M 键，即可将进程按照内存使用率的高低排序。

以批处理方式运行：在终端输入 top -b 命令，可以将 top 命令的输出结果输出到文件中，以批处理的方式运行。

显示特定用户进程：在终端输入 top -u &lt;username&gt; 命令，可以只显示特定用户的进程。

显示特定进程信息：在终端输入 top -p &lt;pid&gt; 命令，可以只显示指定进程的信息。


3. top命令输出在不同的操作系统中，top 命令输出的行列可能略有不同，但大体上是类似的。
示例：
top - 15:15:44 up 13 days,  1:30,  1 user,  load average: 0.00, 0.01, 0.05Tasks: 140 total,   1 running, 139 sleeping,   0 stopped,   0 zombie%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni, 99.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 stKiB Mem :  8008124 total,  3366332 free,   355280 used,  4286512 buff/cacheKiB Swap:        0 total,        0 free,        0 used.  2325788 avail Mem  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND  615 root      20   0  477124   8900   6808 S   0.3  0.1  93:41.43 NetworkManager    1 root      20   0   43584   3804   2416 S   0.0  0.0   0:20.89 systemd    2 root      20   0       0      0      0 S   0.0  0.0   0:00.27 kthreadd    4 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/0:0H

3.1. 第一行
top：当前系统时间和运行时间。
up：系统运行的时间。
users：当前登录的用户数。
load average：系统的平均负载情况，分别表示过去 1 分钟、5 分钟和 15 分钟的平均值。

3.2. 第二行
Tasks：总的进程数。
running：正在运行的进程数。
sleeping：休眠状态的进程数。
stopped：已停止的进程数。
zombie：僵尸进程的数量。

3.3. 第三行
Cpu(s)：CPU 的使用情况。
us：用户空间程序使用 CPU 时间占比。
sy：内核空间程序使用 CPU 时间占比。
ni：进程优先级（nice）。
id：CPU 空闲时间占比。
wa：等待 I/O 的 CPU 时间占比。
hi：硬件中断占用 CPU 时间占比。
si：软件中断占用 CPU 时间占比。
st：被偷走的 CPU 时间占比。

3.4. 第四行
total：系统总内存。
free：空闲内存。
used：已使用内存。
buff/cache：用于缓存的内存，包括内核缓存和用户空间缓存。

3.5. 第五行
total：交换空间总大小。
free：空闲交换空间大小。
used：已使用的交换空间大小。
avail Mem：可以被应用程序使用的内存大小。

3.6. 第六行以后
PID：进程 ID。
USER：进程所属用户。
%CPU：进程使用 CPU 时间占比。
%MEM：进程使用内存占比。
VSZ：进程虚拟内存使用量。
RSS：进程物理内存使用量。
TTY：进程绑定的终端（如果有）。
STAT：进程状态，包括 S（睡眠状态）、R（正在运行）、D（不可中断的睡眠状态）、Z（僵尸状态）等。
START：进程启动时间。
TIME+：进程使用 CPU 时间总计。
COMMAND：进程所对应的命令。

]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux问题排查常用命令</title>
    <url>/dev-linux-troubleshooting/</url>
    <content><![CDATA[1. 前言本文记录linux中排查问题时常用的shell命令，备忘。相关文档：《shell命令之系统管理》


2. 查看资源使用情况2.1. 查看CPU负载toptop # 按1，查看每个逻辑CPU的使用情况uptimehtop

一个逻辑CPU可以处理一个线程，一般当 load average 超过逻辑CPU数量的70%，就可以认为 CPU 的负载较高。
2.2. 查看内存占用查看内容总的占用情况
free -h

查看哪个进程占用内存较多
top # 然后输入大写的Mps aux --sort=-%mem | head

2.3. 查看磁盘空间占用du -sh * # 不包含隐藏文件du -sh .[!.]* # 查看隐藏文件cd /du -sh * --exclude=proc --exclude=data* # 通过字符串匹配排除一些目录或文件，也会排除匹配到的子目录和子文件du -sh * --threshold=1G # 筛选大于1G的目录或文件

2.4. 查看磁盘空间占用（快速版）当文件比较多时，du执行速度太慢，这时可以使用它的升级版ncdu（NCurses Disk Usage）
1、安装ncdu
yum install -y ncdu

ncdu二进制安装方法
# wget https://dev.yorhel.nl/download/ncdu-2.2.1.tar.gzwget https://dev.yorhel.nl/download/ncdu-2.2.1-linux-x86_64.tar.gz --no-check-certificatetar -xzvf ncdu-2.2.1-linux-x86_64.tar.gzcp ncdu /usr/local/binln -s /usr/local/bin/ncdu /usr/bin/ncdu

2、扫描指定目录磁盘空间占用
ncdu /home/voidking

3、全局扫描磁盘空间占用
ncdu / --exclude=/data/mntnfs

等到扫描结束后，通过上下按键和Enter按键，可以查看每一个目录的磁盘空间占用情况。
4、先扫描保存结果，然后再查看结果（对于超大量文件，ncdu 作者建议的优化方式）
ncdu -o scan_result /datancdu -f scan_result

2.5. 查看磁盘IO负载iostat -x

在iostat命令的输出中，磁盘IO请求队列长度对应的是avgqu-sz字段，表示平均每秒的I/O请求队列长度。它可以用来判断系统的磁盘IO瓶颈情况，如果该值超过了磁盘的最大IO队列长度，就说明磁盘IO请求队列积压了，系统IO性能可能会受到影响。一般来说，当avgqu-sz值超过1时，就可能会有IO瓶颈的风险。
2.6. 查看进程的磁盘IOiotop
iotop工作方式非常类似于 top 命令，但它专门对 I/O 进行监控。iotop 可以为运行中的进程提供实时的磁盘I/O报告，并且通过报告每个进程/线程的磁盘I/O使用情况，帮助我们找出哪个进程/线程在导致磁盘I/O的相关问题。
2.7. 记录进程的磁盘IO使用 iotop 可以查看进程的磁盘IO，但是，这种查看是实时的，阅后即无。如果想要记录进程的磁盘IO，可以配置使用一些监控工具，不过会比较复杂。
最简单的记录方法，是将 iotop 的结果输出到控制台，然后重定向存储到文件。同时，因为 iotop 命令是一个持续命令，随意需要配合 timeout 来使用。例如，如果我们需要记录60s内的数据，那么命令为：
timeout 60 iotop -botqqq &gt; iotop_output.txt

如果需要长时间记录，可以使用定时任务：
* * * * * timeout 3 iotop -botqqq &gt; /data/iotop/iotop-`date +\%Y\%m\%d\%H\%M\%S`.log

3. 查看网络情况3.1. 查看网络负载iftop

详情参考《iftop命令》
3.2. 查看会话数ss

输出示例：
State      Recv-Q Send-Q      Local Address:Port       Peer Address:PortLISTEN     0      128                 *:22                  *:*LISTEN     0      100           127.0.0.1:25                  *:*


State: 显示套接字的状态，比如 LISTEN 表示正在监听连接，ESTAB 表示连接已经建立。
Recv-Q: 接收队列的大小。
Send-Q: 发送队列的大小。
Local Address:Port: 本地 IP 地址和端口号。
Peer Address:Port: 远程 IP 地址和端口号。

3.3. 查看本机IP地址1、查看本机当前namespace所有IP地址ifconfig 或者 ip address
2、获取某个网卡的IP地址ifconfig eth0 | awk &#39;NR==2&#123;print $2&#125;&#39;
3.4. 查看外网出口IPcurl myip.ipip.net
3.5. 查看局域网所有IP需求：查看当前局域网已经分配的IP，已知局域网为192.168.56.0/241、通过扫描更新arp缓存
# 执行快，但是某些情况下无法更新arp缓存nmap -sP 192.168.56.0/24# 执行慢，但是肯定可以更新arp缓存nmap -PR 192.168.56.0/24

2、查看本地arp缓存  
cat /proc/net/arp | awk &#x27;&#123;print $1&#125;&#x27; | grep &#x27;192.168&#x27; | sort  arp -a | awk &#x27;&#123;print $2&#125;&#x27; | grep &#x27;192.168&#x27; | sort

3.6. 根据IP排序需求：/proc/net/arp中的内容，根据IP进行排序
cat /proc/net/arp | grep 192.168.56 | sort -t&#x27;.&#x27; -k1n,1 -k2n,2 -k3n,3 -k4n,4

3.7. 测试mtu值MTU是Maximum Transmission Unit的缩写，表示最大传输单元，MTU的单位是字节。大部分网络设备的MTU都是1500。把本机的MTU设成比网关的MTU小或相同，就可以减少丢包。如果本机的MTU比网关的MTU大，大的数据包就会被拆开来传送，这样会产生很多数据包碎片，增加丢包率。把数据包长度加上数据包头28字节，就得到MTU的值。
ping -c 3 -s 1472 -M do www.baidu.comping -c 3 -s 1473 -M do www.baidu.com

3.8. 带宽测试1、安装测试工具iperf
yum install -y iperf

2、启动iperf服务端
iperf -s

3、使用iperf客户端测试网速
iperf -c 192.168.56.112 -i 3

4. 查看进程和端口4.1. 查看进程和线程# 查看所有进程ps -ef# orps aux# 查看所有进程，并且显示进程启动的全部命令ps auxww# 查看所有进程，并且显示进程的线程ps -efL# 查看某个进程的线程ps -T -p 12345

4.2. 根据端口找进程已知端口号（例如22），怎样查找对应的进程（PID）？怎样找到该进程启动命令？
方法一：使用lsof
lsof -i:22ps -ef | grep 12345

方法二：使用netstat
netstat -nlp | grep &quot;:22&quot;ps -ef | grep 12345

4.3. 查看端口是否可以正常连接nc -v 192.168.56.101 8080nc -zv -w 5 192.168.56.101 8080# 成功返回0，5s超时失败返回1telnet 192.168.56.101 8080echo &#x27;&#x27; | timeout --signal=9 5 telnet 192.168.56.101 8080echo &#x27;q&#x27; | timeout --signal=9 5 telnet -e &#x27;q&#x27; 192.168.56.101 8080# 成功返回1，5s超时失败返回137

4.4. 查看进程资源占用1、查看进程的实时资源占用
top -p 12234# orcat /proc/12345/status

2、查看进程从启动到此刻的资源占用平均值
ps aux | grep 12345

参考文档：

纠结ps和top的cpu占用率不一致问题
/proc/pid/status简要分析
Linux下进程信息/proc/pid/status的深入分析

4.5. 根据TCP连接查找进程redis主机（192.168.56.101），redis连接数被用完了。
127.0.0.1:6379&gt; info clientsERR max number of clients reached

查看哪些主机在连接redis主机：
netstat -ano | grep 6379

发现一台 192.168.56.102 有大量的连接。
登录 192.168.56.102 主机，执行：
lsof -i @192.168.56.101:6379

找到连接redis的具体的进程。
5. 查看日志5.1. 查看系统日志dmesg -Tdmesg -T | grep -i -e &quot;error&quot; -e &quot;fail&quot;journalctl -xb

dmesg用于显示内核环境下的系统日志，包括内核启动时的信息、硬件检测信息、内核模块加载信息、内核错误等。通常用于诊断系统启动和运行时出现的问题。dmesg命令显示的日志信息来自系统内核环缓冲区，因此是最新的系统日志。
journalctl -xb命令用于查看 systemd 日志，输出系统启动时的所有日志信息，包括内核消息、系统服务和应用程序的日志信息。其中，-x 参数显示完整的日志信息，-b 参数表示显示系统启动时的日志信息。
通过 journalctl -xb 命令，可以查看系统启动时的各个服务启动情况，应用程序启动情况，以及其他系统级别的消息。可以用来诊断系统启动过程中的错误和问题。
更多journalctl的用法，参考文档《systemctl命令》。
5.2. 查看重启前的系统日志/var/log目录下存在dmesg日志文件：
dmesgdmesg.0dmesg.1.gz...
dmesg 日志文件中保存的是本次重启后的日志，而 dmesg.0 是这次重启前的 dmesg 日志，dmesg.1.gz 是上次重启前的 dmesg 日志，依次类推。
journalctl -k -b -1journalctl -r -k -b -1

5.3. 查看OOM日志方法一：
dmesg -T

方法二：
grep &quot;Out of memory&quot; /var/log/messages

方法三：
egrep -i -r &#x27;killed process&#x27; /var/log

参考文档：Linux进程被杀掉（OOM killer），查看系统日志
6. 检查磁盘和文件系统6.1. 测试磁盘读写性能测试读取性能：
dd if=/path/to/testfile of=/dev/null bs=1M count=1000
从 /path/to/testfile 测试文件中读取数据并将其丢弃，将结果输出到/dev/null，以避免干扰测试。bs=1M指定每个数据块的大小为1MB，count=1000指定要读取的数据块数量。
测试写入性能：
dd if=/dev/zero of=/path/to/testfile bs=1M count=1000 conv=fdatasync
将/path/to/testfile替换为要进行测试的文件路径，bs=1M和count=1000指定了数据块的大小和数量，conv=fdatasync确保数据同步写入磁盘。
6.2. 修复文件系统fsck（file system check）是一个用于检查和修复文件系统中错误的命令。它可以检测和修复文件系统中的逻辑错误、链接错误、磁盘损坏、超级块错误等。fsck通过扫描文件系统和相关数据结构，以确定文件系统的状态，并尝试修复错误。它可以改善文件系统的健康状态和性能，并保护用户数据。
文件系统出错时的表现：

系统无法启动。
系统上的文件损坏（通常您可能会看到输入/输出错误）。
连接的驱动器（包括闪存驱动器/SD 卡）未按预期工作。

1、卸载挂载
umount /dev/sdb

2、检查并修复文件系统
fsck -y /dev/sdb

参考文档：如何使用fsck修复Linux文件系统错误
6.3. 检测磁盘坏扇区badblocks是一个用于检测坏磁盘扇区的命令。它通过扫描磁盘或磁盘分区，并标记坏扇区。坏扇区可能会导致数据丢失或读写错误，因此通过badblocks命令可以帮助识别和排除这些扇区。badblocks可以在文件系统创建之前或之后使用，以确保磁盘或分区没有坏扇区。
1、检测整个磁盘并输出到文件
badblocks -s -v -o /root/bb-sdb.log /dev/sdb
如果报错：badblocks: Value too large for defined data type invalid end block (15625879552): must be 32-bit value
那么需要修改命令为：
badblocks -b 5120 -s -v -o /root/bb-sdb.log /dev/sdb

-b 参数默认为512，意思为512字节一扇区，对于大硬盘需要把这个参数修改为5120，那么就能顺利执行了。最后得到的结果也要除以10，就是真正的坏道位置了。
2、暴力写测试整个磁盘
badblocks -s -w /dev/sdb 

参考文档：badblocks
7. 查看安全情况7.1. 查看异常进程tophtop

7.2. 查看登录记录查看登录记录：
lastlastlog

查看登录失败记录：
lastb

CentOS专用查看登录失败记录：
grep &quot;Failed password&quot; /var/log/secure

Ubuntu/Debian专用查看登录失败记录：
grep &quot;Failed password&quot; /var/log/auth.log




]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>troubleshooting</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
        <tag>iperf</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux后台运行脚本或程序</title>
    <url>/dev-nohup-and-screen/</url>
    <content><![CDATA[1. 前言经常遇到的一个需求，是需要Linux后台运行脚本或程序。对于这个需求，有两个最常用的工具：nohup和screen。

nohup - run a command immune to hangups, with output to a non-ttyscreen - screen manager with VT100/ANSI terminal emulation

nohup启动一个后台进程，进程的输出输出到非终端。screen可以在终端中创建和管理多个子终端窗口，通过在子终端窗口中运行程序，实现后台运行的效果。
这两个工具能够实现的功能基本相同。本文中，我们就来学习一下这两个工具的使用方法。
顺便说一下，TTY是电传打字机的缩写，后来发展为虚拟的电子TTY，也就是我们现在说的终端模拟器（terminal emulator）。
参考文档：

Linux 黑话解释：TTY 是什么？



2. nohup2.1. 后台执行脚本nohup /root/test.sh &amp;nohup /root/test.sh &gt; test.log 2&gt;&amp;1 &amp;

2.2. 查看后台脚本jobs

2.3. 调到前台fg %2

当执行 fg 将后台的命令调到前台来继续执行时，由于此时系统正处于执行该命令的前台运行状态，此时不能直接再调回到后台执行。需要先把前台运行的命令挂起，再用 bg 命令恢复执行，命令才会调至后台继续执行。
2.4. 放回后台并继续执行ctrl+Zjobsbg %2


2.5. 停止后台脚本方法一：
jobsfg %2ctrl+C

方法二：
ps -ef | grep test.shkill -9 $PID

2.6. 定时任务通过crontab设置定时任务时，nohup命令可以省略。例如：
#0 12 * * * nohup /home/voidking/restart.sh &gt; /home/voidking/log/daily-`date +\%Y\%m\%d\%H\%M\%S`.log 2&gt;&amp;1 &amp;0 12 * * * /home/voidking/restart.sh &gt; /home/voidking/log/daily-`date +\%Y\%m\%d\%H\%M\%S`.log 2&gt;&amp;1 &amp;

3. screen3.1. 使用新的screen运行脚本screen ./main.sh

3.2. 把当前screen放入后台ctrl+A+D
3.3. screen运行脚本并自动放入后台screen -dmS main ./main.sh

3.4. 查看screenscreen -ls

3.5. 返回screenscreen -r 23500.ttys005.B000000349215A

4. 后记除了使用nohup和screen之外，想要让程序后台常驻运行，还有两个办法：

一个是使用supervisor，详情参考《Linux安装配置Supervisor》
另一个是把服务配置为系统服务，详情参考《Linux服务开机自启动配置》

]]></content>
      <categories>
        <category>engineering</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
        <tag>nohup</tag>
        <tag>screen</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：Shell脚本编程入门篇（下）</title>
    <url>/dev-shell-script-program-1/</url>
    <content><![CDATA[1. 运算符1.1. 算术运算符#!/bin/sha=10b=20echo &quot;a=$a, b=$b&quot;val=`expr $a + $b`echo &quot;a + b : $val&quot;val=`expr $a - $b`echo &quot;a - b : $val&quot;val=`expr $a \* $b`echo &quot;a * b : $val&quot;val=`expr $b / $a`echo &quot;b / a : $val&quot;val=`expr $b % $a`echo &quot;b % a : $val&quot;if [ $a == $b ]then   echo &quot;a is equal to b&quot;fiif [ $a != $b ]then   echo &quot;a is not equal to b&quot;fi



1.2. 关系运算符#!/bin/sha=10b=20echo &quot;a=$a, b=$b&quot;if [ $a -eq $b ]then   echo &quot;$a -eq $b : a is equal to b&quot;else   echo &quot;$a -eq $b: a is not equal to b&quot;fiif [ $a -ne $b ]then   echo &quot;$a -ne $b: a is not equal to b&quot;else   echo &quot;$a -ne $b : a is equal to b&quot;fiif [ $a -gt $b ]then   echo &quot;$a -gt $b: a is greater than b&quot;else   echo &quot;$a -gt $b: a is not greater than b&quot;fiif [ $a -lt $b ]then   echo &quot;$a -lt $b: a is less than b&quot;else   echo &quot;$a -lt $b: a is not less than b&quot;fiif [ $a -ge $b ]then   echo &quot;$a -ge $b: a is greater or  equal to b&quot;else   echo &quot;$a -ge $b: a is not greater or equal to b&quot;fiif [ $a -le $b ]then   echo &quot;$a -le $b: a is less or  equal to b&quot;else   echo &quot;$a -le $b: a is not less or equal to b&quot;fi


1.3. 布尔运算符#!/bin/sha=10b=20echo &quot;a=$a, b=$b&quot;if [ $a != $b ]then   echo &quot;$a != $b : a is not equal to b&quot;else   echo &quot;$a != $b: a is equal to b&quot;fiif [ $a -lt 100 -a $b -gt 15 ]then   echo &quot;$a -lt 100 -a $b -gt 15 : returns true&quot;else   echo &quot;$a -lt 100 -a $b -gt 15 : returns false&quot;fiif [ $a -lt 100 -o $b -gt 100 ]then   echo &quot;$a -lt 100 -o $b -gt 100 : returns true&quot;else   echo &quot;$a -lt 100 -o $b -gt 100 : returns false&quot;fiif [ $a -lt 5 -o $b -gt 100 ]then   echo &quot;$a -lt 100 -o $b -gt 100 : returns true&quot;else   echo &quot;$a -lt 100 -o $b -gt 100 : returns false&quot;fi

1.4. 字符串运算符#!/bin/sha=&quot;abc&quot;b=&quot;efg&quot;echo &quot;a=$a, b=$b&quot;if [ $a = $b ]then   echo &quot;$a = $b : a is equal to b&quot;else   echo &quot;$a = $b: a is not equal to b&quot;fiif [ $a != $b ]then   echo &quot;$a != $b : a is not equal to b&quot;else   echo &quot;$a != $b: a is equal to b&quot;fiif [ -z $a ]then   echo &quot;-z $a : string length is zero&quot;else   echo &quot;-z $a : string length is not zero&quot;fiif [ -n $a ]then   echo &quot;-n $a : string length is not zero&quot;else   echo &quot;-n $a : string length is zero&quot;fiif [ $a ]then   echo &quot;$a : string is not empty&quot;else   echo &quot;$a : string is empty&quot;fi

1.5. 文件测试运算符#!/bin/shfile=&quot;../start/helloworld.sh&quot;if [ -r $file ]then   echo &quot;File has read access&quot;else   echo &quot;File does not have read access&quot;fiif [ -w $file ]then   echo &quot;File has write permission&quot;else   echo &quot;File does not have write permission&quot;fiif [ -x $file ]then   echo &quot;File has execute permission&quot;else   echo &quot;File does not have execute permission&quot;fiif [ -f $file ]then   echo &quot;File is an ordinary file&quot;else   echo &quot;This is sepcial file&quot;fiif [ -d $file ]then   echo &quot;File is a directory&quot;else   echo &quot;This is not a directory&quot;fiif [ -s $file ]then   echo &quot;File size is not zero&quot;else   echo &quot;File size is zero&quot;fiif [ -e $file ]then   echo &quot;File exists&quot;else   echo &quot;File does not exist&quot;fi


2. 条件2.1. if…fi#!/bin/sha=10b=20echo &quot;a=$a, b=$b&quot;if [ $a == $b ]then   echo &quot;a is equal to b&quot;fiif [ $a != $b ]then   echo &quot;a is not equal to b&quot;fi

2.2. if…else…fi#!/bin/sha=10b=20echo &quot;a=$a, b=$b&quot;if [ $a == $b ]then   echo &quot;a is equal to b&quot;else   echo &quot;a is not equal to b&quot;fi

2.3. if…elif…fi#!/bin/sha=10b=20echo &quot;a=$a, b=$b&quot;if [ $a == $b ]then   echo &quot;a is equal to b&quot;elif [ $a -gt $b ]then   echo &quot;a is greater than b&quot;elif [ $a -lt $b ]then   echo &quot;a is less than b&quot;else   echo &quot;None of the condition met&quot;fi

2.4. testtest 命令用于检查某个条件是否成立，与方括号[]类似。
#!/bin/shnum1=$[2*3]num2=$[1+5]if test $[num1] -eq $[num2]then    echo &#x27;The two numbers are equal!&#x27;else    echo &#x27;The two numbers are not equal!&#x27;fi


2.5. casecase … esac 与其他语言中的 switch … case 语句类似，是一种多分枝选择结构。
#!/bin/shecho -e &quot;Input a number between 1 to 4: \c&quot;read aNumcase $aNum in    1)  echo &#x27;You select 1&#x27;    ;;    2)  echo &#x27;You select 2&#x27;    ;;    3)  echo &#x27;You select 3&#x27;    ;;    4)  echo &#x27;You select 4&#x27;    ;;    *)  echo &#x27;You do not select a number between 1 to 4&#x27;    ;;esac

#!/bin/bashoption=&quot;$&#123;1&#125;&quot;case $&#123;option&#125; in    -f) FILE=&quot;$&#123;2&#125;&quot;        echo &quot;File name is $FILE&quot;    ;;    -d) DIR=&quot;$&#123;2&#125;&quot;        echo &quot;Dir name is $DIR&quot;    ;;    *)         echo &quot;`basename $&#123;0&#125;`:usage: [-f file] | [-d directory]&quot;        exit 1 # Command to come out of the program with status 1    ;;esac

3. 循环3.1. for#!/bin/bashfor loop in 1 2 3 4 5do    echo &quot;The value is: $loop&quot;donefor str in &#x27;This is a string&#x27;do    echo $strdonefor FILE in $HOME/.bash*do   echo $FILEdone

3.2. while#!/bin/bashCOUNTER=0while ( $COUNTER &lt;= 5 )do    COUNTER=`expr $COUNTER + 1`    echo $COUNTERdonewhile [ $COUNTER -lt 10 ]do    COUNTER=`expr $COUNTER + 1`    echo $COUNTERdoneecho &#x27;type &lt;CTRL-D&gt; to terminate&#x27;echo -n &#x27;enter your most liked film: &#x27;while read FILMdo    echo &quot;Yeah! great film the $FILM&quot;done

3.3. until#!/bin/basha=0until [ ! $a -lt 10 ]do    echo $a    a=`expr $a + 1`done

3.4. for_break#!/bin/bashfor var1 in 1 2 3do    for var2 in 0 5    do        if [ $var1 -eq 2 -a $var2 -eq 0 ]        then            break 2        else            echo &quot;$var1 $var2&quot;        fi    donedone

3.5. while_break#!/bin/bashwhile :do    echo -n &quot;Input a number between 1 to 5: &quot;    read aNum    case $aNum in        1|2|3|4|5) echo &quot;Your number is $aNum!&quot;        ;;        *) echo &quot;You do not select a number between 1 to 5, game is over!&quot;            break        ;;    esacdone

3.6. for_continue#!/bin/bashNUMS=&quot;1 2 3 4 5 6 7&quot;for NUM in $NUMSdo    Q=`expr $NUM % 2`    if [ $Q -eq 0 ]    then        echo &quot;Number is an even number!!&quot;        continue    fi    echo &quot;Found odd number&quot;done

3.7. while_continue#!/bin/bashwhile :do    echo -n &quot;Input a number between 1 to 5: &quot;    read aNum    case $aNum in        1|2|3|4|5) echo &quot;Your number is $aNum!&quot;        ;;        *) echo &quot;You do not select a number between 1 to 5!&quot;            continue            echo &quot;Game is over!&quot;        ;;    esacdone

4. 函数4.1. hello#!/bin/bash# Define your function hereHello () &#123;   echo &quot;http://www.voidking.com&quot;&#125;# Invoke your functionHello

4.2. return#!/bin/bashfunWithReturn()&#123;    echo &quot;The function is to get the sum of two numbers...&quot;    echo -n &quot;Input first number: &quot;    read aNum    echo -n &quot;Input another number: &quot;    read anotherNum    echo &quot;The two numbers are $aNum and $anotherNum !&quot;    return $(($aNum+$anotherNum))&#125;funWithReturn# Capture value returnd by last commandret=$?echo &quot;The sum of two numbers is $ret !&quot;

4.3. two_function#!/bin/bash# Calling one function from anothernumber_one () &#123;    echo &quot;Url_1 is http://www.voidking.com&quot;    number_two&#125;number_two () &#123;    echo &quot;Url_2 is http://www.baidu.com&quot;&#125;number_onenumber_three () &#123;    echo &quot;Url_3 is http://www.baidu.com&quot;&#125;unset -f number_three

4.4. 参数#!/bin/bashfunWithParam()&#123;    echo &quot;The value of the first parameter is $1 !&quot;    echo &quot;The value of the second parameter is $2 !&quot;    echo &quot;The value of the tenth parameter is $4 !&quot;    # 参数个数    echo &quot;The amount of the parameters is $# !&quot;    # 传递给函数的所有参数      echo &quot;The string of the parameters is $* !&quot;  &#125;funWithParam 1 2 3 4

5. 获取脚本绝对路径scriptpath=$(cd &quot;$(dirname &quot;$0&quot;)&quot;; pwd)

6. 非零状态立即停止set -e 设置脚本在执行过程中任何命令返回非零退出状态时立即停止。
#!/bin/bashset -eecho &quot;This is a successful command.&quot;ls non-existent-directory  # 这个命令会失败，导致脚本停止执行echo &quot;This command will not be executed.&quot;

7. 源码分享https://github.com/voidking/shell.git
]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>shell</tag>
        <tag>好好学Shell</tag>
        <tag>vim</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Shell：Shell脚本编程入门篇（上）</title>
    <url>/dev-shell-script-program-0/</url>
    <content><![CDATA[1. Shell简介Shell 是什么？Shell 这个单词的原意是“外壳”，跟 kernel（内核）相对应，比喻内核外面的一层，即用户跟内核交互的对话界面。
具体来说，Shell 这个词有多种含义。
首先，Shell 是一个程序，提供一个与用户对话的环境。这个环境只有一个命令提示符，让用户从键盘输入命令，所以又称为命令行环境（command line interface，简写为 CLI）。Shell 接收到用户输入的命令，将命令送入操作系统执行，并将结果返回给用户。  
其次，Shell 是一个命令解释器，解释用户输入的命令。它支持变量、条件判断、循环操作等语法，所以用户可以用 Shell 命令写出各种小程序，又称为脚本（script）。这些脚本都通过 Shell 的解释执行，而不通过编译。  
最后，Shell 是一个工具箱，提供了各种小工具，供用户方便地使用操作系统的功能。  
对于用户来说，Shell是最重要的实用程序，深入了解和熟练掌握Shell的特性极其使用方法，是用好Unix/Linux系统的关键。可以说，Shell使用的熟练程度反映了用户对Unix/Linux使用的熟练程度。
Shell 有两种执行命令的方式：交互式（Interactive）：解释执行用户的命令，用户输入一条命令，Shell就解释执行一条。批处理（Batch）：用户事先写一个Shell脚本，其中有很多条命令，让Shell一次把这些命令执行完。
Shell脚本是解释型语言，不需要编译。Shell程序从脚本中一行一行读取并执行这些命令，相当于一个用户把脚本中的命令一行一行敲到Shell提示符下执行。
Unix/Linux 上常见的 Shell 脚本解释器有 bash、sh、csh、ksh 等，习惯上把它们称作一种Shell。我们常说有多少种 Shell，其实说的是 Shell 脚本解释器。
Bash 是大多数 Linux 标准默认的 Shell，因此是我们学习的重点。
参考文档：

《Bash 脚本教程》
Linux Shell脚本教程：30分钟玩转Shell脚本编程



2. 体验Shell脚本2.1. helloworld1、新建helloworld.sh，内容为：
#!/bin/bashecho &quot;Hello World !&quot;

#! 是一个约定的标记，它告诉系统这个脚本需要什么解释器来执行，即使用哪一种Shell。echo命令用于向窗口输出文本。
2、添加执行权限chmod +x helloworld.sh
3、执行脚本./helloworld.sh
PS：输入gg跳到首行，.,$d，表示从当前行到末行全部删除掉。或者，1,$d，表示从首行到末行全部删除掉。
2.2. echoecho是Shell的一个内部指令，用于在屏幕上打印出指定的字符串。
#!/bin/bash# 显示转义字符echo &quot;\&quot;It is a test\&quot;&quot;# 显示变量name=&quot;VoidKing&quot;echo &quot;Welcome, $name&quot;# 变量与其它字符相连时需要使用大括号month=8echo &quot;$&#123;month&#125;-1-2009&quot;# 显示换行echo -e &quot;OK!\n&quot;echo &quot;It is a test&quot;# 显示不换行echo -e &quot;OK!\c&quot;echo &quot;It is a test&quot;# 显示结果重定向至文件echo &quot;It is a test&quot; &gt; myfile# 原样输出字符串echo &#x27;$name\&quot;&#x27;# 显示命令执行结果echo `date`

2.3. printfprintf 命令用于格式化输出， 是echo命令的增强版。它是C语言printf()库函数的一个有限的变形，并且在语法上有些不同。
注意：printf 由 POSIX 标准所定义，移植性要比 echo 好。
#!/bin/bash# format-string为双引号printf &quot;%d %s\n&quot; 1 &quot;abc&quot;# 单引号与双引号效果一样 printf &#x27;%d %s\n&#x27; 1 &quot;abc&quot; # 没有引号也可以输出printf %s abcdef# 一个格式多个参数printf %s abc defprintf &quot;%s\n&quot; abc defprintf &quot;%s %s %s\n&quot; a b c d e f g h i j# %s默认为空，%d默认为0printf &quot;%s and %d \n&quot; # 如果以%d的格式来显示字符串，那么会有警告，并且默认置为 0printf &quot;always prints %s,%d\n&quot; Hello Shell

2.4. 交互使用 read 命令从 stdin 获取输入并赋值给 PERSON 变量，最后在 stdout 上输出。
#!/bin/bashecho &quot;What is your name?&quot;read PERSONecho &quot;Hello, $PERSON&quot;

2.5. 重定向#!/bin/bash# stdout重定向到filels -l &gt; filels -l &gt;&gt; file# stdout和stderr合并后重定向到myfilels -l &gt; myfile 2&gt;&amp;1ls -l &gt;&gt; myfile 2&gt;&amp;1# 屏蔽 stdout 和 stderrls -l &gt; /dev/null 2&gt;&amp;1

#!/bin/bashcat &lt;&lt; EOFMy name is VoidKing.I&#x27;m good at programming.EOF

2.6. 包含脚本url.sh内容为
url=&quot;http://www.voidking.com&quot;

main.sh内容为
#!/bin/bash. ./helloworld.shsource ./helloworld.sh. ./url.shecho $url

3. 变量3.1. 基本变量#!/bin/bash# 定义变量your_name=&quot;voidking&quot;your_age=25# 显示变量echo -e &quot;your name is $&#123;your_name&#125; \n welcome to shell world&quot;echo &quot;your age is $&#123;your_age&#125;&quot;your_email=&quot;voidking@qq.com&quot;# 设置your_email为只读变量readonly your_emailecho &quot;your email is $your_email&quot;your_address=&quot;中国吉林长春&quot;# 删除变量unset your_address

注意，变量名和等号之间不能有空格，这可能和我们熟悉的所有编程语言都不一样。shell里没有多行注释，只能每一行加一个#号。
上面的脚本中，我们使用了一些中文。在CentOS7.2中，如果出现乱码，那么，我们需要添加CentOS7.2的中文支持。locale，显示LANG=C。
vim /etc/locale.conf，修改locale.conf如下：
LANG=&quot;zh_CN.UTF-8&quot;LANGUAGE=&quot;zh_CN.UTF-8:zh_CN.UTF-8:zh_CN&quot;SUPPORTED=&quot;zh_CN.UTF-8:zh_CN:zh:en_US.UTF-8:en_US:en&quot;SYSFONT=&quot;lat0-sun16&quot;

locale，显示：
LANG=zh_CN.UTF-8LC_CTYPE=&quot;zh_CN.UTF-8&quot;LC_NUMERIC=&quot;zh_CN.UTF-8&quot;LC_TIME=&quot;zh_CN.UTF-8&quot;LC_COLLATE=&quot;zh_CN.UTF-8&quot;LC_MONETARY=&quot;zh_CN.UTF-8&quot;LC_MESSAGES=&quot;zh_CN.UTF-8&quot;LC_PAPER=&quot;zh_CN.UTF-8&quot;LC_NAME=&quot;zh_CN.UTF-8&quot;LC_ADDRESS=&quot;zh_CN.UTF-8&quot;LC_TELEPHONE=&quot;zh_CN.UTF-8&quot;LC_MEASUREMENT=&quot;zh_CN.UTF-8&quot;LC_IDENTIFICATION=&quot;zh_CN.UTF-8&quot;LC_ALL=

3.2. 特殊变量#!/bin/bashecho &quot;Process ID: $$&quot;echo &quot;File Name: $0&quot;echo &quot;First Parameter: $1&quot;echo &quot;Second Parameter: $2&quot;echo &quot;Quoted Values: $@&quot;echo &quot;Quoted Values: $*&quot;echo &quot;Total Number of Parameters: $#&quot;


3.3. 变量替换#!/bin/bashecho $&#123;var:-&quot;Variable is not set&quot;&#125;echo &quot;1 - Value of var is $&#123;var&#125;&quot;echo $&#123;var:=&quot;Variable is not set&quot;&#125;echo &quot;2 - Value of var is $&#123;var&#125;&quot;unset varecho $&#123;var:+&quot;This is default value&quot;&#125;echo &quot;3 - Value of var is $var&quot;var=&quot;Prefix&quot;echo $&#123;var:+&quot;This is default value&quot;&#125;echo &quot;4 - Value of var is $var&quot;echo $&#123;var:?&quot;Print this message&quot;&#125;echo &quot;5 - Value of var is $&#123;var&#125;&quot;

3.4. 命令替换#!/bin/bashDATE=`date`echo &quot;Date is $DATE&quot;USERS=`who | awk &#x27;&#123;print $1&#125;&#x27;`echo &quot;Logged in user are $USERS&quot;UP=`date ; uptime`echo &quot;Uptime is $UP&quot;

3.5. 字符串#!/bin/bash# 单引号字符串中的变量无效，任何字符都会原样输出# 单引号字串中不能出现单引号（对单引号使用转义符后也不行）str=&#x27;this is a string&#x27;# 双引号里可以有变量，可以出现转义符str=&quot;Hello, I know your are \&quot;$your_name\&quot;! \n&quot;# 拼接字符串your_name=&quot;voidking&quot;greeting=&quot;hello, &quot;$your_name&quot; !&quot;greeting_1=&quot;hello, $&#123;your_name&#125; !&quot;echo $greeting $greeting_1# 获取字符串长度string=&quot;abcd&quot;echo $&#123;#string&#125;# 提取子字符串string=&quot;alibaba is a great company&quot;echo $&#123;string:1:4&#125; # 查找子字符串string=&quot;alibaba is a great company&quot;echo `expr index &quot;$string&quot; is`

3.6. 数组#!/bin/shname[0]=&quot;VoidKing0&quot;name[1]=&quot;VoidKing1&quot;name[2]=&quot;VoidKing2&quot;echo &quot;Index0: $&#123;name[0]&#125;&quot;echo &quot;Index1: $&#123;name[1]&#125;&quot;echo &quot;All names: $&#123;name[*]&#125;&quot;echo &quot;All names: $&#123;name[@]&#125;&quot;age=(24 25 26)age[2]=25# 取得数组元素的个数name_length=$&#123;#name[@]&#125;age_length=$&#123;#age[*]&#125;echo &quot;name_length: $name_length,age_length: $age_length&quot;# 取得数组单个元素的长度lengthn=$&#123;#name[0]&#125;echo &quot;Index0 length: $lengthn&quot;

4. 源码分享https://github.com/voidking/shell.git
]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>shell</tag>
        <tag>好好学Shell</tag>
        <tag>vim</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：bash_profile和bashrc的区别</title>
    <url>/dev-bashprofile-bashrc/</url>
    <content><![CDATA[1. bash_profile和bashrc是啥？bash_profile和bashrc都是linux里的初始化脚本，作用是对shell环境进行初始化。在安装和配置软件的时候，经常需要修改这两个配置文件，以便每次登录自动生效或者每次执行脚本都自动生效。
这两个文件的区别是什么？它们有什么联系？什么情况下需要修改bash_profile？什么情况下需要修改bashrc？本文就来详细了解一下，主要引用阮一峰大佬的Bash 启动环境。


2. Session用户每次使用 Shell，都会开启一个与 Shell 的 Session（对话）。
Session 有两种类型：登录 Session 和非登录 Session，也可以叫做 login shell 和 non-login shell。
2.1. 登录 Session登录 Session 是用户登录系统以后，系统为用户开启的原始 Session，通常需要用户输入用户名和密码进行登录。
登录 Session 一般进行整个系统环境的初始化，启动的初始化脚本依次如下。

/etc/profile：所有用户的全局配置脚本。
/etc/profile.d：目录里面所有.sh文件
~/.bash_profile：用户的个人配置脚本。如果该脚本存在，则执行完就不再往下执行。
~/.bash_login：如果~/.bash_profile没找到，则尝试执行这个脚本（C shell 的初始化脚本）。如果该脚本存在，则执行完就不再往下执行。
~/.profile：如果~/.bash_profile和~/.bash_login都没找到，则尝试读取这个脚本（Bourne shell 和 Korn shell 的初始化脚本）。

Linux 发行版更新的时候，会更新/etc里面的文件，比如/etc/profile，因此不要直接修改这个文件。如果想修改所有用户的登陆环境，就在/etc/profile.d目录里面新建.sh脚本。
如果想修改你个人的登录环境，一般是写在~/.bash_profile里面。下面是一个典型的.bash_profile文件。
# .bash_profilePATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/binPATH=$PATH:$HOME/binSHELL=/bin/bashMANPATH=/usr/man:/usr/X11/manEDITOR=/usr/bin/viPS1=&#x27;\h:\w\$ &#x27;PS2=&#x27;&gt; &#x27;if [ -f ~/.bashrc ]; then. ~/.bashrcfiexport PATHexport EDITOR
可以看到，这个脚本定义了一些最基本的环境变量，然后执行了~/.bashrc
bash命令的 –login 参数，会强制执行登录 Session 会执行的脚本。bash --loginbash命令的 –noprofile 参数，会跳过上面这些 Profile 脚本。bash --noprofile
2.2. 非登录 Session非登录 Session 是用户进入系统以后，手动新建的 Session，这时不会进行环境初始化。比如，在命令行执行bash命令，就会新建一个非登录 Session。
非登录 Session 的初始化脚本依次如下。

/etc/bash.bashrc：对全体用户有效。其中rc的含义是run commands。
~/.bashrc：仅对当前用户有效。

对用户来说，~/.bashrc通常是最重要的脚本。非登录 Session 默认会执行它，而登录 Session 一般也会通过调用执行它。每次新建一个 Bash 窗口，就相当于新建一个非登录 Session，所以~/.bashrc每次都会执行。
注意，执行脚本相当于新建一个非互动的 Bash 环境，但是这种情况不会调用~/.bashrc。
bash命令的 –norc 参数，可以禁止在非登录 Session 执行~/.bashrc脚本。bash --norcbash命令的 –rcfile 参数，指定另一个脚本代替.bashrc。bash --rcfile testrc
2.3. bash_logout~/.bash_logout脚本在每次退出 Session 时执行，通常用来做一些清理工作和记录工作，比如删除临时文件，记录用户在本次 Session 花费的时间。
如果没有退出时要执行的命令，这个文件也可以不存在。
3. 小结
全局配置修改 /etc/profile 、/etc/profile.d 和 /etc/bash.bashrc
个人配置修改 ~/.bash_profile 和 ~/.bashrc
~/.bash_profile登录session的初始化脚本，每次登录系统后都会执行它，它会调用bashrc。
~/.bashrc是非登录session的初始化脚本，执行 bash 命令后进入新的bash环境时会执行它。
macos上的初始化脚本是~/.bash_profile，没有~/.bashrc。
一般情况下，修改~/.bash_profile即可，~/.bashrc修改较少。

]]></content>
      <categories>
        <category>engineering</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Dockerfile使用Supervisor管理Tomcat</title>
    <url>/dev-dockerfile-supervisor/</url>
    <content><![CDATA[1. 问题描述容器中，使用supervisor管理tomcat，supervisor报错。这是因为，使用supervisor监控管理的进程必须以nodaemon启动，而tomcat的startup.sh脚本是daemon方式的。


2. 解决办法一1、修改startup.sh脚本在startup.sh的最后的
exec &quot;$PRGDIR&quot;/&quot;$EXECUTABLE&quot; start &quot;$@&quot; 

改为    
exec &quot;$PRGDIR&quot;/&quot;$EXECUTABLE&quot; run &quot;$@&quot;


2、修改supervisor_tomcat.conf然后supervisor_tomcat.conf内容为：
[program:tomcat]directory=/opt/apache-tomcat-8.0.44command=/opt/apache-tomcat-8.0.44/bin/startup.shenvironment=JAVA_HOME=&quot;/usr/lib/jvm/jdk1.8.0_131&quot;,JAVA_BIN=&quot;/usr/lib/jvm/jdk1.8.0_131/bin&quot;user=rootautostart=trueautorestart=truestdout_logfile=/var/log/supervisor/%(program_name)s.logstderr_logfile=/var/log/supervisor/%(program_name)s.log

3. 解决办法二不需要修改startup.sh，使用catalina.sh。然后supervisor_tomcat.conf内容为：
[program:tomcat]directory=/opt/apache-tomcat-8.0.44command=/opt/apache-tomcat-8.0.44/bin/catalina.sh runenvironment=JAVA_HOME=&quot;/usr/lib/jvm/jdk1.8.0_131&quot;,JAVA_BIN=&quot;/usr/lib/jvm/jdk1.8.0_131/bin&quot;user=rootautostart=trueautorestart=truestdout_logfile=/var/log/supervisor/%(program_name)s.logstderr_logfile=/var/log/supervisor/%(program_name)s.log

4. 构建supervisor管理tomcat的镜像1、准备Dockerfile、supervisord.conf和supervisor_tomcat.conf，内容可以参考hexo-storage/dockerfile-supervisor
2、下载 jdk-8u131-linux-x64.tar.gz 和 apache-tomcat-8.0.44.tar.gz 到download目录
3、构建一个名为centos/supervisor:v1.0的镜像
docker build -t centos/supervisor:v1.0 .

4、启动容器
docker run --name supervisor -d \-p 18080:8080 \centos/supervisor:v1.0docker logs supervisor

5、测试访问
curl localhost:18080

5. 后记本文中的这种管理方式，其实是违背容器使用的基本原则的。容器中，应该只跑一个程序，作为1号进程。年少无知，本文就不删了，留作纪念吧。
6. 书签
中国第一套Docker实战案例视频课程（入门到高级）
Docker实战案例源码
Docker实战案例文档
Linux后台进程管理利器：supervisor
tomcat使用supervisor管理

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>troubleshooting</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>问题排查</tag>
        <tag>java</tag>
        <tag>tomcat</tag>
        <tag>supervisor</tag>
        <tag>dockerfile</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：使用Dockerfile构建Tomcat镜像</title>
    <url>/dev-dockerfile-tomcat/</url>
    <content><![CDATA[1. 前言如果只是想要一个tomcat环境，很简单，直接拉取一个tomcat镜像就可以了。本文中，我们在centos7镜像的基础上，学习使用Dockerfile构建Tomcat镜像。同时会对比手工构建镜像和使用Dockerfile构建镜像的不同，以便更好地理解构建过程。


2. 手工构建镜像2.1. 启动容器假设我们的所有安装程序都放在了宿主机的/download目录下，现在需要将其挂载到容器的/mnt/software目录下。
service docker startdocker run -i -t -v /download/:/mnt/software/ centos:7 /bin/bash

2.2. 安装jdk1、进入容器环境。
2、可选操作，安装vim增强版。
yum install vim-enhanced

3、解压jdk到/usr/lib/jvm目录。
cd /mnt/softwaremkdir -p /usr/lib/jvmtar zxvf jdk-8u131-linux-x64.tar.gz -C /usr/lib/jvm/

4、配置JAVA_HOME和JRE_HOME，vi /etc/profile，在最后添加：
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_131export JRE_HOME=$JAVA_HOME/jreexport CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATHexport PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH

然后使配置文件生效
source /etc/profile

5、测试jdk是否配置成功
javajavac

2.3. 安装tomcat1、进入容器环境。
2、解压tomcat到/opt目录。
cd /mnt/softwaretar zxvf apache-tomcat-8.0.44.tar.gz -C /opt

3、启动tomcat。
cd /opt/apache-tomcat-8.0.44/bin/./startup.sh

4、测试访问。
yum install curlcurl localhost:8080

2.4. 编写启动脚本1、进入根目录，新建run.sh文件。
cd /vi run.sh

2、编辑run.sh内容为：
#!/bin/shexport JAVA_HOME=/usr/lib/jvm/jdk1.8.0_131export JRE_HOME=$JAVA_HOME/jreexport CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATHexport PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATHsh /opt/apache-tomcat-8.0.44/bin/catalina.sh run

3、更改run.sh权限
chmod 777 run.sh

2.5. 保存容器保存容器为centos/tomcat镜像
docker ps -ldocker commit 206f centos/tomcat

docker镜像的命名规则 registry_url/namespace/image_name:tag 默认tag是latest。
2.6. 使用镜像1、启动镜像
docker run -d -p 18080:8080 centos/tomcat /run.sh

2、测试访问
curl localhost:18080

3. 使用Dockerfile构建镜像3.1. 编写dockerfile1、准备Dockerfile和run.sh内容可以参考hexo-storage/dockerfile-tomcat
2、下载 jdk-8u131-linux-x64.tar.gz 和 apache-tomcat-8.0.44.tar.gz 到download目录
3、构建一个名为centos/tomcat8.0:v1.0的镜像
docker build -t centos/tomcat8.0:v1.0 .

3.2. 使用镜像1、启动镜像
docker run -d -p 28080:8080 centos/tomcat8.0:v1.0 /run.sh

2、测试访问
curl localhost:28080






]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>tomcat</tag>
        <tag>dockerfile</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Dockerfile入门篇</title>
    <url>/dev-dockerfile-start/</url>
    <content><![CDATA[1. 什么是Dockerfile？正如《Docker入门篇》一文中所说，容器技术包括容器镜像和容器运行时。其中容器镜像是一个软件集装箱，包含软件运行需要的所有文件。那么，怎样制作一个容器镜像呢？两个方法：第一个方法是修改容器运行时，然后提交修改变成新容器镜像。第二个方法就是使用Dockerfile。

Dockerfile是一个文本文档，其中包含一些指令（类似于shell命令）。Docker可以通过阅读Dockerfile中的指令，来自动构建镜像。

更多内容，参考Dockerfile reference 、菜鸟Docker Dockerfile 和 Dockerfile 使用介绍。
本文，我们就来学习一下Dockerfile的编写和使用。


2. helloworld1、创建一个Dockerfile文件，内容为
FROM busybox:1.25ENTRYPOINT [&quot;echo&quot;]CMD [&quot;helloworld&quot;]

2、打包新镜像docker build -t busybox-echo:1.25 .
3、运行新镜像docker run busybox-echo:1.25可以看到输出了helloworld，至此，入门仪式完成。
3. Dockerfile简析Dockerfile可以分为四部分：

基础镜像信息
维护者信息
镜像操作命令
容器启动命令

其中必要的是基础镜像信息，其他都可以不要。只不过这种情况下，构建时就相当于拷贝了一个镜像。
以一个完整的例子说明Dockerfile的四个部分：
# This dockerfile is a demo.# 基础镜像信息FROM ubuntu:16.04# 维护者信息MAINTAINER voidking voidking@qq.com# 镜像操作命令ENV TZ=&quot;Asia/Shanghai&quot;RUN apt update &amp;&amp; apt install -y tzdata jq# 容器启动命令CMD [&quot;/bin/bash&quot;,&quot;-c&quot;,&quot;echo $(date) &amp;&amp; echo &#x27;&#123;\&quot;name\&quot;:\&quot;voidking\&quot;&#125;&#x27; | jq .&quot;]
FROM：定制的镜像都是基于 FROM 的镜像，这里的 ubuntu:16.04 就是需要的基础镜像。MAINTAINER：维护者信息。ENV：设置环境变量，设置docker容器的时区为Shanghai。RUN：用于执行后面跟着的命令行命令。CMD：容器启动时默认执行的命令。
此外，常用的镜像命令还有COPY和ADD。COPY命令格式：COPY  源路径  目标路径源路径如果是文件，COPY命令拷贝文件到目标路径下。源路径如果是目录，COPY命令拷贝目录下所有文件到目标路径下，目标路径不存在则自动创建。
ADD指令是COPY命令的加强版（addition）：拷贝压缩文件会自动解压，支持从url中拷贝文件。
PS：构建并运行镜像
docker build -t ubuntu-jq:16.04 .docker run ubuntu-jq:16.04

4. 从镜像还原DockerfileDockerImage=&quot;busybox-echo:1.25&quot;docker history --format &#123;&#123;.CreatedBy&#125;&#125; --no-trunc=true $DockerImage |sed &quot;s/\/bin\/sh\ -c\ \#(nop)\ //g&quot;|sed &quot;s/\/bin\/sh\ -c/RUN/g&quot; | tac

5. 删除Dockerfile缓存docker build失败后，会产生缓存，可以通过下面的命令清除缓存
docker rm $(docker ps -qf status=exited)docker rmi $(docker images -q -f dangling=true)



]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>Yii框架实战</title>
    <url>/dev-yii-in-action/</url>
    <content><![CDATA[1. 前言新入手的项目需要使用Yii框架开发，那就仿照之前写的《ThinkPHP实战》，整理一下必须掌握的三个部分：路由控制、模板渲染、增删改查。


2. 环境准备参照《ThinkPHP开发环境搭建》。
3. 下载安装1、下载Yii2的基本应用程序模板。
2、解压出basic目录。
3、移动basic目录到wampserver的www目录下。
4、修改 basic/config/web.php 文件，给 cookieValidationKey 配置项 添加一个密钥：
&#x27;cookieValidationKey&#x27; =&gt; &#x27;voidking&#x27;,

5、启动wampserver，浏览器访问：http://localhost/basic/web/index.php 。如果安装成功，就会看到“Congratulations! You have successfully created your Yii-powered application.”。
4. 项目结构basic/                  应用根目录    composer.json       Composer 配置文件, 描述包信息    config/             包含应用配置及其它配置        console.php     控制台应用配置信息        web.php         Web 应用配置信息    commands/           包含控制台命令类    controllers/        包含控制器类    models/             包含模型类    runtime/            包含 Yii 在运行时生成的文件，例如日志和缓存文件    vendor/             包含已经安装的 Composer 包，包括 Yii 框架自身    views/              包含视图文件    web/                Web 应用根目录，包含 Web 入口文件        assets/         包含 Yii 发布的资源文件（javascript 和 css）        index.php       应用入口文件    yii                 Yii 控制台命令执行脚本

一般来说，应用中的文件可被分为两类：在 basic/web 下的和在其它目录下的。 前者可以直接通过 HTTP 访问，后者不能也不应该被直接访问。
5. helloworld5.1. 创建动作对于“Hello”任务，需要创建一个 say 动作， 从请求中接收 message 参数并显示给最终用户。如果请求没有提供 message 参数， 动作将显示默认参数 “Hello”。

 动作是最终用户可以直接访问并执行的对象。 动作被组织在控制器中。 一个动作的执行结果就是最终用户收到的响应内容。

动作必须声明在控制器中。为了简单起见， 你可以在现存的 SiteController 控制器里声明 say 动作。这个控制器定义在 controllers/SiteController.php 类文件中。 以下是一个动作的声明：
&lt;?phpnamespace app\controllers;use yii\web\Controller;class SiteController extends Controller&#123;    // ...现存的代码...    public function actionSay($message = &#x27;Hello&#x27;)    &#123;        return $this-&gt;render(&#x27;say&#x27;, [&#x27;message&#x27; =&gt; $message]);    &#125;&#125;

在上述 SiteController 代码中，say 动作被定义为 actionSay 方法。 Yii 使用 action 前缀区分普通方法和动作。 action 前缀后面的名称被映射为动作的 ID。
动作 ID 总是被以小写处理，如果一个操作 ID 由多个单词组成， 单词之间将由破折号连接（如 create-comment）。动作 ID 映射为方法名时移除了破折号， 将每个单词首字母大写，并加上 action 前缀。 比如：动作 ID create-comment 对应方法名 actionCreateComment。
上述代码中的动作方法接受一个参数 message，它的默认值是 “Hello”。 
在动作方法中，yii\web\Controller::render() 被用来渲染一个名为 say 的视图文件。 message 参数也被传入视图，这样就可以在里面使用。动作方法会返回渲染结果。 结果会被应用接收并显示给最终用户的浏览器（作为整页 HTML 的一部分）。
5.2. 创建视图视图是你用来生成响应内容的脚本。为了说 “Hello”， 你需要创建一个 say 视图，以便显示从动作方法中传来的 message 参数。
&lt;?phpuse yii\helpers\Html;?&gt;&lt;?= Html::encode($message) ?&gt;

say 视图应该存为 views/site/say.php 文件。当一个动作中调用了 yii\web\Controller::render() 方法时， 它将会寻找名为 views/控制器 ID/视图名.php 的PHP文件。。
当然了，你大概会在 say 视图里放入更多内容。内容可以由 HTML 标签，纯文本， 甚至 PHP 语句组成。实际上 say 视图就是一个由 yii\web\Controller::render() 执行的 PHP 脚本。 视图脚本输出的内容将会作为响应结果返回给应用。应用将依次输出结果给最终用户。
5.3. 试运行创建完动作和视图后，你就可以通过下面的 URL 访问新页面了：http://localhost/basic/web/index.php?r=site/say&amp;message=helloworld
6. 路由控制6.1. URL美化从上面的helloworld例子中，我们看出，访问的URL分成几个部分：

表示主机信息的 http://localhost/basic/web/
表示入口脚本的 index.php
表示路由的 r=site/say
表示普通参数的 message=helloworld

该URL，如果变形成 http://localhost/basic/web/index.php/site/say?message=helloworld ，是不是好看很多？
该URL，如果变形成 http://localhost/basic/web/site/say?message=helloworld ，是不是更好看？
该URL，如果变形成 http://localhost/basic/web/say?message=helloworld ，是不是更好看？
该URL，如果变形成 http://localhost/basic/web/say/helloworld ，是不是更好看？
Yii有专门的 yii\web\UrlManager 来进行处理，其中：

隐藏入口脚本可以通过 yii\web\UrlManager::showScriptName = false 来实现
路由的路径化可以通过 yii\web\UrlManager::enablePrettyUrl = true 来实现
参数的路径化可以通过路由规则来实现
加入假后缀(fake suffix) .html 可以通过 yii\web\UrlManager::suffix = &#39;.html&#39; 来实现

6.2. 路由规则路由规则是指 urlManager 用于解析请求或生成URL的规则。
1、打开basic/config/web.php，找到urlManager，取消注释。
&#x27;urlManager&#x27; =&gt; [    &#x27;enablePrettyUrl&#x27; =&gt; true,    &#x27;showScriptName&#x27; =&gt; false,    &#x27;rules&#x27; =&gt; [    ],],

这时，http://localhost/basic/web/index.php/site/say?message=helloworld 就可以使用了。
2、找到apache的配置文件httpd.conf，去掉去掉rewrite前的#。
LoadModule rewrite_module modules/mod_rewrite.so

3、在index.php所在目录，添加.htaccess文件（不添加该文件也可以）。
Options +FollowSymLinks  IndexIgnore */*  RewriteEngine on  # if a directory or a file exists, use it directly  RewriteCond %&#123;REQUEST_FILENAME&#125; !-f  RewriteCond %&#123;REQUEST_FILENAME&#125; !-d# otherwise forward it to index.php  RewriteRule . index.php

这时，http://localhost/basic/web/site/say?message=helloworld 就可以使用了。
4、打开basic/config/web.php，修改rules。
&#x27;urlManager&#x27; =&gt; [    &#x27;enablePrettyUrl&#x27; =&gt; true,    &#x27;showScriptName&#x27; =&gt; false,    &#x27;rules&#x27; =&gt; [        &#x27;say&#x27; =&gt; &#x27;site/say&#x27;    ],],

这时，http://localhost/basic/web/say?message=helloworld 就可以使用了。
5、继续修改rules。
&#x27;urlManager&#x27; =&gt; [    &#x27;enablePrettyUrl&#x27; =&gt; true,    &#x27;showScriptName&#x27; =&gt; false,    &#x27;rules&#x27; =&gt; [        &#x27;say&#x27; =&gt; &#x27;site/say&#x27;,        &#x27;say/&lt;message:\w+&gt;&#x27;=&gt;&#x27;site/say&#x27;    ],],

这时， http://localhost/basic/web/say/helloworld 就可以使用了。
7. 模板渲染7.1. 默认模板引擎1、在controllers/SiteController.php中添加函数：
public function actionTemplate()&#123;    $data = &#x27;测试数据&#x27;;    $dataArr = array(        array(&#x27;name&#x27;=&gt;&#x27;郝锦&#x27;,&#x27;age&#x27;=&gt;&#x27;24&#x27;),        array(&#x27;name&#x27;=&gt;&#x27;小帅&#x27;,&#x27;age&#x27;=&gt;&#x27;22&#x27;),        array(&#x27;name&#x27;=&gt;&#x27;小飞&#x27;,&#x27;age&#x27;=&gt;&#x27;22&#x27;)    );    $dataObj = new userInfo();    return $this-&gt;render(&#x27;template&#x27;,         [&#x27;data&#x27; =&gt; $data,        &#x27;dataArr&#x27; =&gt; $dataArr,        &#x27;dataObj&#x27; =&gt; $dataObj        ]);&#125;

2、同时，在文件最后添加一个userInfo类：
class userInfo&#123;    public $name = &#x27;郝锦&#x27;;    public $age = &#x27;24&#x27;;    function show()&#123;        echo &#x27;一个函数&#x27;;    &#125;&#125;

3、在views/site中添加template.php：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Template&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h1&gt;模板&lt;/h1&gt;    &lt;p&gt;&lt;?php echo $data; ?&gt;&lt;/p&gt;    &lt;p&gt;&lt;?php echo $dataObj-&gt;name; ?&gt;&lt;/p&gt;    &lt;p&gt;&lt;?php echo $dataObj-&gt;age; ?&gt;&lt;/p&gt;    &lt;p&gt;        &lt;?php             foreach ($dataArr as $value) &#123;                echo $value[&#x27;name&#x27;];                echo $value[&#x27;age&#x27;];            &#125;        ?&gt;    &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

4、测试访问url： http://localhost/basic/web/site/template
7.2. smarty上面的渲染中，我们发现，template.php中的写法不友好。因为默认情况下，Yii 使用 PHP 作为其默认的模板引擎语言。但是，我们可以配置 Yii 以扩展的方式支持其他的渲染引擎， 比如 Twig 或 Smarty等。下面我们把模板引擎换成smarty。
1、下载安装Composer。
2、在basic目录，执行命令composer require --prefer-dist yiisoft/yii2-smarty，安装smarty插件。
如果提示输入token，解决办法为：进入 https://github.com/settings/tokens 点击 「Generate new token」 新建一个 Token，选择默认新建就行，然后就会得到一个 Token，然后输入这个值就 OK 了。
3、打开basic/config/web.php，找到components，添加使用smarty：
&#x27;components&#x27; =&gt; [    // other settings    &#x27;view&#x27; =&gt; [        &#x27;renderers&#x27; =&gt; [            &#x27;tpl&#x27; =&gt; [                &#x27;class&#x27; =&gt; &#x27;yii\smarty\ViewRenderer&#x27;,                //&#x27;cachePath&#x27; =&gt; &#x27;@runtime/Smarty/cache&#x27;,            ],        ],    ],],

4、修改actionTemplate为：
public function actionTemplate()&#123;    $data = &#x27;测试数据&#x27;;    $dataArr = array(        array(&#x27;name&#x27;=&gt;&#x27;郝锦&#x27;,&#x27;age&#x27;=&gt;&#x27;24&#x27;),        array(&#x27;name&#x27;=&gt;&#x27;小帅&#x27;,&#x27;age&#x27;=&gt;&#x27;22&#x27;),        array(&#x27;name&#x27;=&gt;&#x27;小飞&#x27;,&#x27;age&#x27;=&gt;&#x27;22&#x27;)    );    $dataObj = new userInfo();    return $this-&gt;render(&#x27;template.tpl&#x27;,         [&#x27;data&#x27; =&gt; $data,        &#x27;dataArr&#x27; =&gt; $dataArr,        &#x27;dataObj&#x27; =&gt; $dataObj        ]);&#125;

5、在views/site中添加template.tpl：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Template&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h1&gt;模板&lt;/h1&gt;    &lt;p&gt;&#123;$data&#125;&lt;/p&gt;    &lt;p&gt;&#123;$dataObj-&gt;name&#125;&lt;/p&gt;    &lt;p&gt;&#123;$dataObj-&gt;age&#125;&lt;/p&gt;    &lt;p&gt;        &#123;foreach from=$dataArr key=mykey item=$value&#125;            &#123;$value.name&#125;&amp;nbsp;&#123;$value.age&#125;        &#123;/foreach&#125;    &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

6、测试访问url： http://localhost/basic/web/site/template
8. 增删改查8.1. 数据库配置利用navicat等工具连接到本地mysql数据库，创建数据库basic，在数据库中创建表bas_project(int id, varchar title, varchar content)。注意，编码格式选择utf8。
8.2. 连接配置打开basic/config/db.php，修改内容如下：
&lt;?phpreturn [    &#x27;class&#x27; =&gt; &#x27;yii\db\Connection&#x27;,    &#x27;dsn&#x27; =&gt; &#x27;mysql:host=localhost;dbname=basic&#x27;,    &#x27;username&#x27; =&gt; &#x27;root&#x27;,    &#x27;password&#x27; =&gt; &#x27;&#x27;,    &#x27;charset&#x27; =&gt; &#x27;utf8&#x27;,    &#x27;tablePrefix&#x27; =&gt; &#x27;bas_&#x27;,];

8.3. 新建model在basic/models下，新建Project.php，内容如下：
&lt;?phpnamespace app\models;use yii\db\ActiveRecord;/** * Project model */class Project extends ActiveRecord&#123;    public static function model($className=__CLASS__)    &#123;        return parent::model($className);    &#125;&#125;

8.4. 新建Controller在basic/controllers下，新建ProjectController.php，内容如下：
&lt;?phpnamespace app\controllers;use Yii;use yii\filters\AccessControl;use yii\web\Controller;use yii\filters\VerbFilter;use app\models\LoginForm;use app\models\ContactForm;use app\models\Project;class ProjectController extends Controller&#123;    public function actionAdd($title, $content)&#123;        $project = new Project();        $project-&gt;title = $title;        $project-&gt;content = $content;         $success = $project-&gt;save();        if($success)&#123;            $result = array(                &#x27;code&#x27; =&gt; &#x27;0&#x27;,                &#x27;ext&#x27; =&gt; &#x27;success&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;    &#125;    public function actionEdit($id, $title, $content)&#123;        $project = Project::find()-&gt;where([&#x27;id&#x27;=&gt;$id])-&gt;one();        $project-&gt;title = $title;        $project-&gt;content = $content;        $success = $project-&gt;save();        if($success)&#123;            $result = array(                &#x27;code&#x27;=&gt; &#x27;0&#x27;,                &#x27;ext&#x27;=&gt; &#x27;success&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;else &#123;            $result = array(                &#x27;code&#x27;=&gt; &#x27;1&#x27;,                &#x27;ext&#x27;=&gt; &#x27;fail&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;    &#125;    public function actionDelete($id)&#123;        $project = Project::find()-&gt;where([&#x27;id&#x27;=&gt;$id])-&gt;one();        $success = $project-&gt;delete();        if($success)&#123;            $result = array(                &#x27;code&#x27;=&gt; &#x27;0&#x27;,                &#x27;ext&#x27;=&gt; &#x27;success&#x27;            );            echo json_encode($result);        &#125;else &#123;            $result = array(                &#x27;code&#x27;=&gt; &#x27;1&#x27;,                &#x27;ext&#x27;=&gt; &#x27;fail&#x27;            );            echo json_encode($result);        &#125;    &#125;    public function actionList()&#123;        $projectList = Project::find()-&gt;asArray()-&gt;all();        $result = array(                &#x27;code&#x27;=&gt; &#x27;0&#x27;,                &#x27;ext&#x27;=&gt; &#x27;success&#x27;,                &#x27;projectList&#x27; =&gt; $projectList        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;    public function actionPage($pageSize,$pageNum)&#123;        $project = new Project();        $total = $project-&gt;find()-&gt;count();        $totalPage = $total%$pageSize ? (int)($total/$pageSize)+1 : (int)($total/$pageSize);        $projectList = $project-&gt;find()-&gt;offset(($pageNum-1)*$pageSize)-&gt;limit($pageSize)-&gt;asArray()-&gt;all();                if($projectList)&#123;            $resultArr = array(                &#x27;totalPage&#x27;=&gt; $totalPage,                &#x27;pageNum&#x27;=&gt; $pageNum,                &#x27;projectList&#x27;=&gt; $projectList            );            $result = array(                &#x27;code&#x27;=&gt; &#x27;0&#x27;,                &#x27;ext&#x27;=&gt; &#x27;success&#x27;,                &#x27;obj&#x27;=&gt; $resultArr            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;    &#125;    public function actionFind($id)&#123;        $project = new Project();        $item = $project-&gt;find()-&gt;where([&#x27;id&#x27;=&gt;$id])-&gt;asArray()-&gt;one();        $result = array(            &#x27;code&#x27;=&gt; &#x27;0&#x27;,            &#x27;ext&#x27;=&gt; &#x27;success&#x27;,            &#x27;obj&#x27;=&gt; $item        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;&#125;

8.5. 测试接口1、添加：http://localhost/basic/web/project/add?title=voidking-title&amp;content=voidking-content
2、修改：http://localhost/basic/web/project/edit?id=1&amp;title=voidking-title&amp;content=voidking-content
3、查找全部：http://localhost/basic/web/project/list
4、查找一页：http://localhost/basic/web/project/page?pageSize=10&amp;pageNum=1
5、查找一条：http://localhost/basic/web/project/find?id=1
6、删除：http://localhost/basic/web/project/delete?id=1
路由控制、模板渲染、增删改查，至此全部跑通，可以进行简单的开发了。至于yii框架提供的其他更加强大的功能，在需要时查查文档就好。
9. 源码分享1、下载安装：git clone https://github.com/voidking/yii-basic.git basic
2、利用navicat等工具连接到本地mysql数据库，创建数据库basic，在数据库中创建表bas_project(int id, varchar title, varchar content)。注意，编码格式选择utf8。
10. 书签Yii Framework中文社区
下载安装Yii
Yii 2.0 权威指南 
URL Management(网址管理)
Yii2.0数据库操作增删改查详解
ActiveRecord
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>yii</tag>
      </tags>
  </entry>
  <entry>
    <title>使用aria2离线下载</title>
    <url>/hobby-aria2-download/</url>
    <content><![CDATA[1. 前言迅雷、旋风、百度云盘等下载工具，非VIP有限速，VIP有资源下载限制。在线云播，广告繁多，缓存慢，如今更是被封了99%。
郝同学在知乎找到了一个很好的解决方案：aria2。aria2 + aria2webui完成下载，emby或plex完成下载后播放功能。
本文，就来研究一下aria2的安装部署。


2. aria2简介
aria2 is a lightweight multi-protocol &amp; multi-source command-line download utility. It supports HTTP/HTTPS, FTP, SFTP, BitTorrent and Metalink. aria2 can be manipulated via built-in JSON-RPC and XML-RPC interfaces.


aria2官网
aria2源码
aria2下载地址

3. webui简介aria2webui包括yaaw、webui-aria2、AriaNg等。
3.1. yaaw
YAAW: Yet Another Aria2 Web Frontend in pure HTML/CSS/Javascirpt.No HTTP server, backend or server-side program. All you need is just a browser.


yaaw源码地址
Aria2 &amp; YAAW 官方使用说明
yaaw管理页面
yaaw管理页面2
yaaw管理页面3

3.2. webui-aria2
The aim for this project is to create the worlds best and hottest interface to interact with aria2. aria2 is the worlds best file downloader, but sometimes the command line brings more power than necessary. The project was initially created as part of the GSOC scheme, however it has rapidly grown and changed with tremendous support and feedback from the aria2 community.


webui-aria2源码
webui-aria2管理页面
webui-aria2管理页面2

3.3. AriaNg
AriaNg is a web frontend making aria2 better. AriaNg is written in pure html &amp; javascript, thus it does not need any compilers or runtime environment. You can just put AriaNg in your web server and open it in your browser. AriaNg uses responsive layout, and supports any desktop or mobile devices.


AriaNg源码地址
AriaNg管理页面

4. aria2命令行aria2命令行在windows和linux中通用。
1、Download from WEBaria2c -c -s 5 http://example.org/mylinux.iso
其中-c代表断点续传，-s代表线程数。
2、Download from 2 sourcesaria2c http://a/f.iso   ftp://b/f.iso
3、Download using 2 connections per hostaria2c -x2 http://a/f.iso
4、BitTorrent Magnet URIaria2c &#39;magnet:?xt=urn:btih:248D0A1CD08284299DE78D5C1ED359BB46717D8C&#39;
5、Metalinkaria2c http://example.org/mylinux.metalink
6、Download URIs found in text filearia2c -i uris.txt
7、BTaria2c /tmp/CentOS-6.3-i386-bin-DVD1to2.torrent
aria2c http://mirrors.163.com/centos/6.6/isos/x86_64/CentOS-6.6-x86_64-minimal.torrent
注意：当源地址存在诸如 &amp;, * 等 shell 的特殊字符，请使用单引号或双引号把 URI 包含起来。
5. windows安装使用aria25.1. 安装aria21、访问aria2项目，下载最新的aria2，这里我们下载aria2-1.34.0-win-64bit-build1.zip。
2、解压aria2-1.34.0-win-64bit-build1.zip，并且重命名为aria2，最终路径为D:\develop\aria2。
5.2. 配置aria2参考aria2 Online Manual和Windows配置Aria2及Web管理面板教程，配置aria2。
1、下载配置文件git clone https://github.com/voidking/aria2-conf.git
项目包含两个目录，一个是windows，一个是centos。windows目录下包含如下文件：

aria2.conf，配置文件
aria2.log，日志文件
aria2.session，下载历史
Start.bat，启动aria2
Stop.bat，停止aria2
Restart.bat，重启aria2
Start.vbs，隐藏cmd窗口启动aria2
Status.bat，查看aria2状态
Boot.bat，开启或取消aria2开机启动

2、aria2-conf/windows中的全部文件复制到D:\develop\aria2中。
3、根据需要，修改aria2.conf文件。
4、双击Start.bat，启动aria2服务。
5.3. AriaNg安装配置1、访问ariang项目，下载最新版的ariang，这里我们下载aria-ng-0.4.0.zip。
2、解压aria-ng-0.4.0.zip，并且重命名为ariang，最终路径为D:\develop\ariang。
3、下载EasyWebSvr.exe，这里提供一个下载地址，密码为1ey1。
4、双击EasyWebSvr.exe，点击底部的锤子图标，选择设置，选择主目录为D:\develop\ariang目录，确定，点击底部的锤子图标，选择启动服务器。
如果不想搭建Web服务器的话，可以访问别人搭建的AriaNg管理页面。
PS：除了easywebsvr，还可以把airang扔到nginx、apache或tomcat等服务器的web目录下，效果是一样的。
5、访问 http://localhost ，进入Aria2 Web管理页面。
6、在Aria2 Web管理页面点击新建，可以添加HTTP、FTP、BT任务等，同时添加多个任务每行一个URL，添加镜像URL用空格分割，点击文件夹图标可以打开种子文件等。
文件默认下载到D:\develop\aria2\download文件夹，这是在aria2.conf中配置的。
注意：

在Web管理面板删除下载任务后，Aria2并不会删除下载文件或者缓存，需要自己去下载文件夹删除掉。
在同一个局域网内，其他设备输入当前设备IP地址，也可以访问Web管理界面，如：192.168.1.2 。
由于Web管理界面只是一个调用的作用，所以即使浏览器关闭也不影响Aria2进行下载。

6. centos安装配置aria26.1. yum安装aria21、手动安装EPEL源
wget http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpmrpm -ivh epel-release-latest-7.noarch.rpmyum repolist //查看是否成功安装epel

2、自动安装EPEL源
yum install epel-releaseyum repolist

3、安装aria2yum install aria2
4、查看安装情况aria2c -v
6.2. 编译安装aria21、访问aria2项目，下载最新的aria2，这里我们下载aria2-1.34.0.tar.gz。
wget --no-check-certificate --content-disposition https://github.com/aria2/aria2/releases/download/release-1.34.0/aria2-1.34.0.tar.gz
或者：curl -LJO https://github.com/aria2/aria2/releases/download/release-1.34.0/aria2-1.34.0.tar.gz
2、解压aria2-1.34.0.tar.gz并编译安装。
mv aria2-1.34.0.tar.gz /optcd /opt/tar -zxvf aria2-1.34.0.tar.gzcd aria2-1.34.0/./configure --enable-bittorrent --enable-metalink  makemake install
默认情况下，会在/usr/local/bin 目录创建 aria2c 可执行程序。
3、查看安装情况。aria2c -v
6.3. 配置aria21、下载配置文件git clone https://github.com/voidking/aria2-conf.git
项目包含两个目录，一个是windows，一个是centos。centos目录下包含如下文件：

aria2.conf，配置文件
aria2.log，日志文件
aria2.session，下载历史

2、移动文件mkdir -p /data/aria2
cd aria2-conf/centos &amp;&amp; mv ./* /data/aria2/
3、启动测试aria2c --conf-path=/data/aria2/aria2.conf
4、后台启动aria2c --conf-path=/data/aria2/aria2.conf -D
6.4. AriaNg安装使用1、访问ariang项目，下载最新版的ariang，这里我们下载aria-ng-0.4.0.zip。curl -LJO https://github.com/mayswind/AriaNg/releases/download/0.4.0/aria-ng-0.4.0.zip
2、参考Hexo加速访问，在/opt/www目录下创建aria2目录。
mkdir -p /opt/www/aria2
3、移动文件并解压mv aria-ng-0.4.0.zip /opt/www/aria2
cd /opt/www/aria2 &amp;&amp; unzip aria-ng-0.4.0.zip
rm aria-ng-0.4.0.zip
4、配置nginxcd /etc/nginx/conf.d/
vim www.voidking.com.conf，内容为：
server &#123;    listen 80;    server_name aria2.voidking.com;    location / &#123;        root /opt/www/aria2/;        index index.html;    &#125;&#125;

5、重启nginx/usr/sbin/nginx -s reload
6、在dns服务器上添加aria2，解析到centos服务器ip。
7、测试访问访问地址：http://aria2.voidking.com
6.5. 无法连接问题在ariang页面，如果发现无法连接到aria2，那么参照如下方法解决：
方法一：开放端口（推荐）1、查询6800端口是否开放firewall-cmd --query-port=6800/tcp
2、打开6800端口firewall-cmd --add-port=6800/tcp
方法二：关闭防火墙（不推荐）systemctl stop firewalld.service
7. bt下载问题有些bt文件，在aria2上进行下载，却没有速度。参考更新Tracker，解决Aria2 BT下载无速度和Aria2 bt 没速度? 试试自动更新BT Tracker服务器列表的方法，进行配置。
1、新建addtrackers.sh脚本，内容为
#!/bin/bashlist=`wget -qO- https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt|awk NF|sed &quot;:a;N;s/\n/,/g;ta&quot;`if [ -z &quot;`grep &quot;bt-tracker&quot; /data/aria2/aria2.conf`&quot; ]; then    sed -i &#x27;$a bt-tracker=&#x27;$&#123;list&#125; /data/aria2/aria2.conf    echo add......else    sed -i &quot;s@bt-tracker.*@bt-tracker=$list@g&quot; /data/aria2/aria2.conf    echo update......fi

2、执行脚本chmod +x addtrackers.sh
./addtrackers.sh
3、重启aria2进行测试aria2c --conf-path=/data/aria2/aria2.conf
PS：或者，直接访问trackerslist项目，复制list，然后添加到aria2.conf文件中。
8. 磁力链下载问题所有磁力链在aria2上进行下载，没有速度。
9. 后记关于emby和plex，暂时没有研究。
10. 书签CentOS 下搭建 aria2 远程下载环境
CentOS7安装Aria2
在线磁力播放引擎
DIY一套NAS+私有云盘+下载机
Ubuntu16 下载软件Aria2 全局配置方法(最全组合)
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>computer</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker部署JavaWeb项目</title>
    <url>/dev-docker-deploy-javaweb/</url>
    <content><![CDATA[1. 部署要求宿主机为CentOS7，上面安装了docker、nginx和mysql。
1、把已经开发好的JavaWeb项目（pandawork-start）部署到docker容器里。2、该JavaWeb项目使用宿主机的mysql。3、容器设置静态ip为192.168.34.10，80端口映射为宿主机28080端口。4、nginx配置可以通过网址pandawork.voidking.com访问该JavaWeb项目。


2. 软件准备1、《Docker安装tomcat服务》一文中配置好的ubuntu/tomcat镜像。
2、假设宿主机中的mysql数据库密码为voidking，那么需要修改pandawork-start项目中的数据库配置文件（mybatis-config.xml），主机修改为192.168.34.1，密码修改为voidking。
3、打包JavaWeb项目的war包一份。在pom.xml所在文件夹，执行mvn package -Dmaven.test.skip=true命令，在target目录下就会生成war包。（把该war包放到tomcat的webapps文件夹里，会自动解压。）
4、使用xftp，上传war包到CentOS7的/download目录下。
3. 数据库准备1、使用navicat连接到宿主机mysql。2、创建数据库pandawork，数据库名为pandawork，字符集选择utf8–UTF-8 Unicode，排序规则选择utf8_general_ci。3、导入pandawork-start项目中的pandawork.sql文件。
4. 部署项目4.1. 部署war包1、启动ubuntu/tomcat镜像。docker run -i -t -v /download/:/mnt/software/ ubuntu/tomcat /bin/bash
2、移动war包到webapps。cd /mnt/software
mv pandawork-start.war /opt/apache-tomcat-8.0.44/webapps/
3、启动tomcat，自动解压war包。source /etc/profile
cd /opt/apache-tomcat-8.0.44/bin/
./startup.sh
4、测试访问。curl localhost:8080/pandawork-start/hello
5、关闭tomcat。./shutdown.sh
4.2. tomcat配置1、配置tomcat端口为80。cd /opt/apache-tomcat-8.0.44/conf/
vi server.xml
找到：
&lt;Connector port=&quot;8080&quot; protocol=&quot;HTTP/1.1&quot;               connectionTimeout=&quot;20000&quot;               redirectPort=&quot;8443&quot; /&gt;
修改为：
&lt;Connector port=&quot;80&quot; protocol=&quot;HTTP/1.1&quot;               connectionTimeout=&quot;20000&quot;               redirectPort=&quot;8443&quot; /&gt;

2、配置域名访问。找到：
&lt;Engine name=&quot;Catalina&quot; defaultHost=&quot;localhost&quot;&gt;
修改为：
&lt;Engine name=&quot;Catalina&quot; defaultHost=&quot;pandawork.voidking.com&quot;&gt;

找到：
&lt;Host name=&quot;localhost&quot;  appBase=&quot;webapps&quot;      unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;&gt;  &lt;!-- SingleSignOn valve, share authentication between web applications       Documentation at: /docs/config/valve.html --&gt;  &lt;!--  &lt;Valve className=&quot;org.apache.catalina.authenticator.SingleSignOn&quot; /&gt;  --&gt;  &lt;!-- Access log processes all example.       Documentation at: /docs/config/valve.html       Note: The pattern used is equivalent to using pattern=&quot;common&quot; --&gt;  &lt;Valve className=&quot;org.apache.catalina.valves.AccessLogValve&quot; directory=&quot;logs&quot;         prefix=&quot;localhost_access_log&quot; suffix=&quot;.txt&quot;         pattern=&quot;%h %l %u %t &amp;quot;%r&amp;quot; %s %b&quot; /&gt;&lt;/Host&gt;
修改为：
&lt;Host name=&quot;pandawork.voidking.com&quot;  appBase=&quot;webapps&quot;      unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;&gt;  &lt;Context path=&quot;&quot; docBase=&quot;pandawork-start&quot; reloadable=&quot;true&quot; /&gt;  &lt;!-- SingleSignOn valve, share authentication between web applications       Documentation at: /docs/config/valve.html --&gt;  &lt;!--  &lt;Valve className=&quot;org.apache.catalina.authenticator.SingleSignOn&quot; /&gt;  --&gt;  &lt;!-- Access log processes all example.       Documentation at: /docs/config/valve.html       Note: The pattern used is equivalent to using pattern=&quot;common&quot; --&gt;  &lt;Valve className=&quot;org.apache.catalina.valves.AccessLogValve&quot; directory=&quot;logs&quot;         prefix=&quot;localhost_access_log&quot; suffix=&quot;.txt&quot;         pattern=&quot;%h %l %u %t &amp;quot;%r&amp;quot; %s %b&quot; /&gt;&lt;/Host&gt;


4.3. 测试启动1、启动tomcat。cd /opt/apache-tomcat-8.0.44/bin/
./startup.sh
2、在/etc/hosts文件中，添加
127.0.0.1       pandawork.voidking.com

3、测试访问。curl pandawork.voidking.com
4、关闭tomcat。./shutdown.sh
4.4. 保存容器1、退出容器exit
2、查看dockeriddocker ps -l
3、保存容器为ubuntu/pandawork镜像。docker commit 87be ubuntu/pandawork
5. 网络配置1、创建自定义网络，选取192.168.34.0网段。
docker network create --subnet=192.168.34.0/24 voidkingnet

2、选取192.168.34.10作为静态IP地址，启动ubuntu/pandawork镜像。
docker run -d -p 28080:80 --name pandawork1.0 --add-host pandawork.voidking.com:127.0.0.1 --net voidkingnet --ip 192.168.34.10 ubuntu/pandawork /run.sh

我们已经在容器中修改了/etc/hosts文件，为什么还需要–add-host参数？这是因为容器中的hosts文件，在重启后会恢复初始设置，改了也没用。
实际上，在容器中配置tomcat的时候，如果
&lt;Engine name=&quot;Catalina&quot; defaultHost=&quot;localhost&quot;&gt;

&lt;Host name=&quot;localhost&quot;  appBase=&quot;webapps&quot;      unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;&gt;  &lt;Context path=&quot;&quot; docBase=&quot;pandawork-start&quot; reloadable=&quot;true&quot; /&gt;  &lt;!-- SingleSignOn valve, share authentication between web applications       Documentation at: /docs/config/valve.html --&gt;  &lt;!--  &lt;Valve className=&quot;org.apache.catalina.authenticator.SingleSignOn&quot; /&gt;  --&gt;  &lt;!-- Access log processes all example.       Documentation at: /docs/config/valve.html       Note: The pattern used is equivalent to using pattern=&quot;common&quot; --&gt;  &lt;Valve className=&quot;org.apache.catalina.valves.AccessLogValve&quot; directory=&quot;logs&quot;         prefix=&quot;localhost_access_log&quot; suffix=&quot;.txt&quot;         pattern=&quot;%h %l %u %t &amp;quot;%r&amp;quot; %s %b&quot; /&gt;&lt;/Host&gt;

那么，–add-host参数不加也可以。貌似画蛇添足，实际上不然，有些项目可移植性比较差，必须配置本地hosts。
3、查看运行状态。docker ps
4、测试访问。curl 192.168.34.10或者curl localhost:28080
5、关闭容器docker kill pandawork1.0
6、再次启动容器docker start pandawork1.0
6. nginx配置1、在宿主机中，找到nginx安装位置ps aux | grep nginx
2、进入nginx配置目录在《在CentOS7上配置PHP运行环境》一文中，郝同学的nginx是通过EZHTTP安装的。
cd /usr/local/nginx/conf，进入nginx配置文件目录。cd vhost，具体的某个站点的目录，都在vhost目录下。
3、修改nginx配置文件参照项目：https://github.com/voidking/nginx-conf ，在vhost目录下，新建pandawork.voidking.com.conf文件，内容如下：
upstream pandawork&#123;    server 192.168.34.10;    #server 192.168.34.11;用来负载均衡&#125;server&#123;    listen 80;    server_name pandawork.voidking.com;    location / &#123;        proxy_pass http://pandawork;    &#125;&#125;

4、重新加载nginx配置文件cd /usr/local/nginx/sbin，./nginx -s reload
报错：
nginx: [error] open() &quot;/usr/local/nginx/logs/nginx.pid&quot; failed (2: No such file or directory)

解决办法：/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
然后重新执行./nginx -s reload。
7. hosts配置1、在宿主机中，修改/etc/hosts文件，添加：
127.0.0.1       pandawork.voidking.com

2、测试访问。在宿主机中，curl pandawork.voidking.com。
8. 最终测试1、在宿主机中关闭防火墙，systemctl stop firewalld.service。
2、假设宿主机IP为192.168.56.101，那么在宿主机同局域网PC中配置hosts：
192.168.56.101    pandawork.voidking.com

3、浏览器中输入http://pandawork.voidking.com，看到“Hello World!”。
4、验证注册登录等功能，全部正常。此次，大功告成。
9. 重启宿主机后报错重新启动宿主机后，启动docker，启动pandawork容器。执行docker start pandawork1.0时，报错如下：
Error response from daemon: driver failed programming external connectivity on endpoint pandawork1.0 (9f3cbc9ebc5030c0cbbbbbac81628ce48374fd392f6995f4d75ac20bce9d2a30):  (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 28080 -j DNAT --to-destination 192.168.34.10:80 ! -i br-024154a13bb1: iptables: No chain/target/match by that name. (exit status 1))

解决办法：pkill docker
iptables -t nat -F
ifconfig docker0 down
最后重启docker，docker restart docker。
10. 书签Docker部署JavaWeb项目实战
使用 Docker 搭建 Java Web 运行环境
解决docker容器中文乱码，修改docker容器编码格式
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>nginx</tag>
        <tag>ubuntu</tag>
        <tag>tomcat</tag>
        <tag>jdk</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker安装Tomcat服务</title>
    <url>/dev-docker-ubuntu-tomcat/</url>
    <content><![CDATA[1. 部署要求宿主机为CentOS7，上面安装了docker。
1、docker镜像选择ubuntu，在ubuntu中安装tomcat。2、容器8080端口映射为宿主机18080端口。


2. 软件准备1、ubuntu下可以使用的jdk一份，jdk-8u131-linux-x64.tar.gz。下载地址：http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
2、ubuntu下可以使用的tomcat一份，apache-tomcat-8.0.44.tar.gz。下载地址：http://tomcat.apache.org/download-80.cgi
3、使用xftp，上传jdk和tomcat到CentOS7的/download目录下。
3. docker准备3.1. 下载镜像docker search ubuntu
docker pull ubuntu
docker images
3.2. 启动镜像假设我们的所有安装程序都放在了宿主机的/download目录下，现在需要将其挂载到容器的/mnt/software目录下。
docker run -i -t -v /download/:/mnt/software/ ubuntu /bin/bash
注意，上面的ubuntu也可以换成ubuntu的镜像ID。

-i：表示以“交互模式”运行容器。
-t：表示容器启动后会进入其命令行。  
-v：表示需要将本地哪个目录挂载到容器中，格式：-v &lt;宿主机目录&gt;:&lt;容器目录&gt; 。

3.3. 启动容器退出容器后，可以再次启动容器（使用容器ID或容器NAMES）。docker ps -l
docker start 87be
注意，先运行docker run，然后被stop掉的容器才可以使用上面的命令。
上面的命令不能进入容器，要进入容器还需要使用：docker attach 87be
4. 安装jdk1、进入容器环境。
2、更新vim，否则上下左右和退格键无法使用。apt-get update
apt-get remove vim-common
apt-get install vim
3、进入jdk所在目录cd /mnt/software
然后参考《全平台安装JDK》，完成剩下的配置。
5. 安装tomcat1、进入容器环境。
2、解压tomcat到/opt目录。cd /mnt/software
tar zxvf apache-tomcat-8.0.44.tar.gz -C /opt
3、启动tomcat。cd /opt/apache-tomcat-8.0.44/bin/
./startup.sh
4、测试访问。apt-get install curl
curl localhost:8080
5、关闭tomcat。./shutdown.sh
6. 编写启动脚本1、进入根目录，新建run.sh文件。cd /
vim run.sh
2、编辑run.sh内容为：
#!/bin/shexport JAVA_HOME=/usr/lib/jvm/jdk1.8.0_131export JRE_HOME=$JAVA_HOME/jreexport CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATHexport PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATHsh /opt/apache-tomcat-8.0.44/bin/catalina.sh run

注意：run.sh中的启动脚本不可以写为：
sh /opt/apache-tomcat-8.0.44/bin/startup.sh run

如果这么写的话，在使用docker run的时候，会自动退出。
因为想要Docker容器后台运行，就必须有一个前台进程。容器运行的命令如果不是那些一直挂起的命令（比如运行top，tail），就是会自动退出的。
3、更改run.sh权限chmod 777 run.sh
4、测试访问。./run.sh
curl localhost:8080
7. 保存容器1、退出容器exit
2、查看dockeriddocker ps -l
3、保存容器为ubuntu/tomcat镜像。docker commit 87be ubuntu/tomcat
8. 启动镜像1、启动ubuntu/tomcat镜像docker run -d -p 18080:8080 --name tomcat8.0 ubuntu/tomcat /run.sh

-d：表示以守护模式执行/run.sh脚本，此时 Tomcat 控制台不会出现在输出终端上。  
-p：表示宿主机与容器的端口映射，此时将容器内部的 8080 端口映射为宿主机的 18080 端口。  
–name：表示容器名称，用一个有意义的名称命名即可。  

2、查看容器运行状态docker ps
3、关闭容器docker kill tomcat8.0 或者 docker stop tomcat8.0
4、再次启动容器docker start tomcat8.0
5、测试访问在宿主机中，执行命令 curl localhost:18080。
或者，查到容器IP后，执行命令 curl &lt;容器IP&gt;:8080。
或者，在宿主机中关闭防火墙，systemctl stop firewalld.service。然后，在宿主机同局域网PC浏览器中输入http://&lt;宿主机IP&gt;:18080。
9. 书签Docker部署JavaWeb项目实战
使用 Docker 搭建 Java Web 运行环境
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>docker</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>ubuntu</tag>
        <tag>tomcat</tag>
      </tags>
  </entry>
  <entry>
    <title>蚁群算法解决TSP问题</title>
    <url>/dev-matlab-aco-tsp/</url>
    <content><![CDATA[1. TSP问题旅行商问题，即TSP问题（Travelling Salesman Problem）又译为旅行推销员问题、货郎担问题，是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市，他必须选择所要走的路径，路径的限制是每个城市只能拜访一次，而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。


2. 设计思路1、随机生成n个位置（坐标），把坐标点绘制到页面上。
2、设置蚂蚁数量为m，把m只蚂蚁随机放在n个位置上。
3、设置每条路径的初始信息素为 $info_{ij}(0) = C$。
4、计算t时刻蚂蚁k（1到m）由位置i移动到位置j的概率 $probability_{ij}^k$。$$probability_{ij}^k =\begin{cases}\frac{info_{ij}^α(t) reciprocal_{ij}^β}{\sum info_{is}^α(t) reciprocal_{is}^β}, &amp; j \in allowed_k,s \in allowed_k \0, &amp; \text{other}  \\end{cases}$$
（1）$info_{ij}(t)$ 表示t时刻ij上的信息素。$$info_{ij}(t+1) = ρ \cdot info_{ij}(t) + Δinfo_{ij}$$
ρ表示信息素挥发因子，控制信息素保留多少。$Δinfo_{ij}$表示（本次移动）ij路径遗留的信息素。
$$Δinfo_{ij} = \sum_{k=1}^m Δinfo_{ij}^k$$
$$Δinfo_{ij}^k =\begin{cases}\frac{Q}{L_k}, &amp; \text{当第k只蚂蚁经过ij时} \0, &amp; \text{当不经过时}  \\end{cases}$$
$L_k$表示（本次移动）蚂蚁k经过的路径和。
（2）$reciprocal_{ij}$ 表示ij之间距离的倒数，较近的坐标有较大的可能被选中。$$distance_{ij} = \sqrt{(x_i-x_j)^2+(y_i-y_j)^2}$$$$reciprocal_{ij} = \frac{1}{distance_{ij}}$$
（3）α表示信息启发式因子，控制信息素对概率的影响力大小，进而控制蚂蚁选择坐标。
（4）β表示期望值启发式因子，控制距离对概率的影响力的大小，进而控制蚂蚁选择坐标。（5）$allowed_k$表示蚂蚁k下一步允许选择的坐标集合，$travelled_k$表示蚂蚁k已经走过的坐标集合。
5、比较选择每个坐标的概率，依次为每只蚂蚁选择下一个坐标。
6、重复4-5，直到蚂蚁走完所有坐标。
7、使用 $travelled_k$集合，分别计算m只蚂蚁走过的路径和，选择出最小的路径和，作为本次迭代的最优解。
8、把m只蚂蚁随机放在n个位置上。
9、重复4-8，直到达到指定的迭代次数，最后一次迭代的最优解，就是我们找到的最优解。
3. 源码分享https://github.com/voidking/TSP_MATLAB.git
4. 书签Mathjax与LaTex公式简介http://mlworks.cn/posts/introduction-to-mathjax-and-latex-expression/
]]></content>
      <categories>
        <category>engineering</category>
        <category>algorithm</category>
      </categories>
      <tags>
        <tag>知识工程</tag>
        <tag>mathjax</tag>
        <tag>matlab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker镜像迁移/分享</title>
    <url>/dev-docker-image-migrate/</url>
    <content><![CDATA[1. 问题描述在没有外网的情况下，怎样把 docker image 分享给局域网的其他用户？
解决办法有两个：

1）搭建本地镜像仓库
2）导出导入镜像



2. 搭建本地镜像仓库搭建本地镜像仓库，把镜像push到本地镜像仓库，其他机器pull即可。
搭建本地镜像仓库方法参考：《Harbor入门篇》 和 《Docker镜像站的配置和使用》
假设需求为本机有镜像 busybox:1.25，现在想要把这个镜像分享给其他机器。
1、有镜像的机器执行：
docker tag busybox:1.25 harbor.voidking.com/base/busybox:1.25docker login harbor.voidking.comdocker push harbor.voidking.com/base/busybox:1.25

2、其他机器执行：
docker pull harbor.voidking.com/base/busybox:1.25docker tag harbor.voidking.com/base/busybox:1.25 busybox:1.25

3. 导出导入镜像导出导入镜像，主要依赖docker save和docker load命令。
3.1. 单个镜像出导入假设需求为本机有镜像 busybox:1.25，现在想要把这个镜像分享给其他机器。
1、有镜像的机器导出镜像  
docker save -o busybox.tar busybox:1.25

2、拷贝压缩包到其他机器
3、其他机器导入镜像  
docker load -i busybox.tar

3.2. 全部镜像导出导入假设需求为本机有很多个镜像，现在想要把这些镜像全部导出到其他机器。
1、有镜像的机器导出所有镜像  
docker save $(docker images | grep -v REPOSITORY | awk &#x27;BEGIN&#123;OFS=&quot;:&quot;;ORS=&quot; &quot;&#125;&#123;print $1,$2&#125;&#x27;) -o all.tar

2、拷贝压缩包到其他机器
3、其他机器导入镜像  
docker load -i all.tar

3.3. docker save/load存在的问题这种导出导入存在一个问题：导入后的镜像，不会作为base layer。
已知：A主机上docker save了一个镜像X，拷贝到B主机后，B主机上docker load了镜像X。接着A主机上基于镜像X做了一个镜像Y，上传到harbor。问题：B主机上docker pull拉取harbor中的镜像Y，会全量拉取镜像吗？
理论上，不会全量拉取镜像，只会增量拉取。但是实测第一次拉取依然会全量拉取Y。神奇的是，如果执行过docker rmi Y，再次拉取Y确实会变成增量拉取。暂时不知道原因，待解。
参考文档：

How to speed up docker load then docker pull the incremental changes
Docker pull/push won’t recognize images from docker load.. sometimes?

]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>harbor</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker镜像压缩技术</title>
    <url>/dev-docker-image-compress/</url>
    <content><![CDATA[1. 前言Docker是一种开源的应用容器引擎，允许开发人员将应用程序及其依赖打包到一个可移动的镜像中，并在任何Linux设备运行。然而，随着应用程序和依赖项的增加，Docker镜像的大小可能会迅速增长，这可能会影响到应用的部署速度和运行效率。因此，了解如何压缩Docker镜像成为了一个重要的技术需求。本文将学习几种可行的Docker镜像压缩技术。
具体包括：

多阶段构建(Multi-stage builds)
优化Dockerfile
docker-slim
squash参数
docker export/import

参考文档：

如何优化 docker 镜像体积
Docker镜像瘦身



2. 镜像压缩技术概述2.1. 多阶段构建(Multi-stage builds)在Docker 17.05版本之后，引入了多阶段构建的功能。多阶段构建可以让你在一个Dockerfile中使用多个FROM语句。每个FROM语句可以看作是新的阶段的开始，另一个阶段可以基于前一个阶段，你只需要最终那个小型且包含所有产物的阶段。例如，在构建一个java应用时，我们可能需要maven来构建应用，但最终运行时，我们可能只需要一个JRE环境，这就是多阶段构建能够帮助你优化镜像大小的地方。
2.2. 优化Dockerfile优化Dockerfile是另一种有效的压缩Docker镜像的策略。像Alpine Linux这样的更小的基础镜像可以显著减小最终镜像的大小。另外，尽量在Dockerfile中合并RUN命令，因为每个RUN命令都会创建一个新的镜像层。在构建过程结束后清理不需要的缓存和文件也是一个好主意。
2.3. squash参数docker build命令的squash选项可以将所有的docker层合并为一个层，从而创建一个更小的镜像。需要注意的是，这个选项需要Docker daemon开启experimental实验特性才可使用。
2.4. docker-squash工具docker-squash，docker镜像压缩工具，可以压缩镜像的最后 n 层。
2.5. docker-slim工具docker-slim，可以帮助我们自动优化和减小Docker镜像。通过分析我们的Docker镜像，它能够自动压缩和优化它们，使它们更小和更安全。
2.6. docker export/importdocker export 和 docker import 是 Docker 提供的两个指令，它们主要被用来备份和恢复一个 Docker 容器。也可以用来进行镜像压缩，因为导出再导入后，相当于删除掉了镜像中的所有叠加层，只保留最后的效果。
参考文档：

docker 打包的问题请教一下

3. docker export/import实践3.1. Docker Exportdocker export 命令用来将一个正在运行的或者停止的容器导出到一个 tar 文件中。其命令语法如下：
docker export CONTAINER &gt; somefile.tar

这里 CONTAINER 是需要导出的容器的 ID。export 命令会将容器的文件系统导出成 tar 文件，但需要注意的是，导出操作不会包含容器所使用的历史命令和元数据。也就是说，导出后的容器并不能完全反映原来的容器状态。
3.2. Docker Importdocker import 命令用来从一个 tar 文件或者一个 URL 中创建一个新的文件系统镜像。其命令语法如下：
docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]

这里 file 或 URL 是需要导入的文件，REPOSITORY[:TAG] 是可选的镜像仓库和标签。比如，我们可以将上面导出的 tar 文件导入成一个新的 Docker 镜像：
docker import somefile.tar newimagename:tagname

这样我们就从 tar 文件中创建了一个新的 Docker 镜像。注意，docker import 操作是创建镜像，而不是创建容器。这个镜像同样也没有原来容器的运行历史和元数据信息。
3.3. 镜像压缩实践docker create --name test harbor.voidking.com/xxx/yyy:v0.1 tail -f /dev/nulldocker export test &gt; test.tardocker import test.tar harbor.voidking.com/xxx/yyy:v0.1.1

或者：
docker export $(docker create harbor.voidking.com/xxx/yyy:v0.1) | docker import - harbor.voidking.com/xxx/yyy:v0.1-squashed

具体能压缩多少，取决于镜像层中误增加和反复修改的内容量。
4. 后记Docker镜像压缩是一种重要的技术，它可以提高应用的部署速度和运行效率。然而，我们在压缩Docker镜像时，也需要考虑到镜像的可移植性和可维护性。过度的压缩可能会引入复杂性，影响镜像的可读性和可维护性。在实际应用中，我们需要根据具体的需求和条件，选择最合适的压缩策略。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker镜像站的配置和使用</title>
    <url>/dev-docker-registry-mirror/</url>
    <content><![CDATA[1. 前言使用 Docker 的时候，经常需要从官方获取镜像，但是有时候拉取镜像的非常慢，体验很差。
解决办法：使用 docker.io 的国内镜像站，加速docker镜像的下载。
本文中，我们就来学习配置使用docker镜像站。
参考文档：

镜像加速器
yeasy/docker_practice
镜像站可用性测试



2. 推荐镜像站
Docker中国区官方镜像：https://registry.docker-cn.com
网易：https://hub-mirror.c.163.com
中国科技大学：https://docker.mirrors.ustc.edu.cn
阿里云：https://cr.console.aliyun.com
腾讯云（已废弃）：https://mirror.ccs.tencentyun.com

个人专属加速地址：

阿里云：访问阿里云镜像加速器，得到一个专属的加速地址。
DaoCloud：访问配置 Docker 加速器，得到一个专属加速地址。

3. 配置使用镜像站1、修改docker配置文件创建或修改 /etc/docker/daemon.json ，添加 registry-mirrors 配置
&#123;    &quot;registry-mirrors&quot; : [        &quot;https://registry.docker-cn.com&quot;,        &quot;https://docker.mirrors.ustc.edu.cn&quot;,        &quot;https://hub-mirror.c.163.com&quot;,        &quot;https://cr.console.aliyun.com&quot;    ]&#125;

2、重启docker
systemctl daemon-reloadsystemctl restart docker

3、验证配置  
docker info

看到 Registry Mirrors 部分有我们配置的镜像站，就表明配置成功。
4. 自建镜像站自建镜像站的优点，是可以统一配置代理，下载需要科学上网才能下载的镜像。
参考文档：Docker Registry
4.1. 启动镜像站1、下载registry镜像
docker pull registry:2.7.1

2、启动本地镜像站启动docker.io镜像站：
docker run --name docker_io_mirror -d registry:2.7.1 \-p 5000:5000 \-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \-v /data/docker_io_registry:/var/lib/registry \--restart always 

启动gcr.io镜像站：
docker run --name gcr_io_mirror -d registry:2.7.1 \-p 5001:5000 \-e REGISTRY_PROXY_REMOTEURL=https://gcr.io \-e HTTPS_PROXY=socks5://192.168.56.1:7891 \-v /data/gcr_io_registry:/var/lib/registry \--restart always

4.2. 使用本地镜像站假设镜像站宿主机IP地址为：192.168.56.101那么想要使用镜像站的机器上， /etc/docker/daemon.json 修改为：
&#123;    &quot;registry-mirrors&quot;: [      &quot;http://192.168.56.101:5000&quot;,      &quot;http://192.168.56.101:5001&quot;    ],    &quot;insecure-registries&quot;: [      &quot;http://192.168.56.101:5000&quot;,      &quot;http://192.168.56.101:5001&quot;    ]&#125;

4.3. K8S中搭建镜像站K8S中搭建镜像站，可以参考docker-registry-mirror/manifests
4.4. Harbor作为镜像站除了使用docker registry作为镜像站，harbor也可以作为镜像站，具体配置方法参考文档使用 Harbor 搭建 Mirror Registry。
不过不建议这么使用，因为harbor作为mirror站后，只能作为mirror站，大材小用，原本的很多功能就不能使用了。
5. Docker Proxy
Docker Proxy提供多平台容器镜像代理服务,支持 Docker Hub, GitHub, Google, k8s, Quay等镜像仓库。

5.1. Docker Hub 官方镜像代理5.1.1. 常规镜像代理官方命令：docker pull stilleshan/frpc:latest代理命令：docker pull dockerproxy.com/stilleshan/frpc:latest
5.1.2. 根镜像代理官方命令：docker pull nginx:latest代理命令：docker pull dockerproxy.com/library/nginx:latest
5.2. GitHub Container Registry5.2.1. 常规镜像代理官方命令：docker pull ghcr.io/username/image:tag代理命令：docker pull ghcr.dockerproxy.com/username/image:tag
5.3. Google Container Registry5.3.1. 常规镜像代理官方命令：docker pull gcr.io/username/image:tag代理命令：docker pull gcr.dockerproxy.com/username/image:tag
5.4. Google Kubernetes5.4.1. 常规镜像代理官方命令：docker pull k8s.gcr.io/username/image:tag官方命令：docker pull registry.k8s.io/username/image:tag代理命令：docker pull k8s.dockerproxy.com/username/image:tag
5.4.2. 根镜像代理官方命令：docker pull k8s.gcr.io/coredns:1.6.5官方命令：docker pull registry.k8s.io/coredns:1.6.5代理命令：docker pull k8s.dockerproxy.com/coredns:1.6.5
5.5. Quay.io5.5.1. 常规镜像代理官方命令：docker pull quay.io/username/image:tag代理命令：docker pull quay.dockerproxy.com/username/image:tag
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker容器配置静态IP</title>
    <url>/dev-docker-static-ip/</url>
    <content><![CDATA[1. 前言Docker容器运行的时候默认会自动分配一个默认网桥所在网段的IP地址。但很多时候我们可能需要让容器运行在预先指定的静态IP地址上，因为早期的版本不支持静态IP，因此网上大部分方法都是借助pipework等去实现，然而在最新的版本中，Docker已经内嵌支持在启动时指定静态IP了。
Docker守护进程启动以后会创建默认网桥docker0，其IP网段通常为172.17.0.1。在启动Container的时候，Docker将从这个网段自动分配一个IP地址作为容器的IP地址。最新版(1.10.3)的Docker内嵌支持在启动容器的时候为其指定静态的IP地址。


2. 创建自定义网络选取了192.168.34.0网段，也可以指定其他任意空闲的网段。
docker network create --subnet=192.168.34.0/24 voidkingnet

注：voidkingnet为自定义网桥的名字，可自己任意取名。
查看所有docker网段：
docker network ls


3. 设置静态IP在创建的网段中选取了192.168.34.10作为静态IP地址。这里以启动learn/ping为例。
docker run -d -p 2001:2001 --net voidkingnet --ip 192.168.34.10 learn/ping ping www.baidu.com

4. 查看容器ip方法一：
docker inspect $(docker ps -q) | grep IPAddress

方法二：
docker inspect --format=&#x27;&#123;&#123;.NetworkSettings.IPAddress&#125;&#125;&#x27; $(docker ps -q)


5. 关闭容器方法一：
docker stop $(docker ps -q)
关闭所有容器。
方法二：
docker psdocker stop 3e2efd019b9a
查询容器id，关闭指定id的容器。
6. 书签为Docker容器指定自定义网段的固定IP/静态IP地址http://blog.csdn.net/gobitan/article/details/51104362
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker常用命令</title>
    <url>/dev-docker-command/</url>
    <content><![CDATA[1. 前言本文记录Docker常用命令，备忘。
参考文档：

什么是Docker?
docker入门教程
Docker入门实战
中国第一套Docker实战案例视频课程（入门到高级）
Docker实战案例源码
Docker实战案例文档



2. 查看docker版本和帮助# 查看版本docker version# 查看帮助dockerdocker -h# 查看docker安装配置信息docker info# 查看docker start帮助docker start --helpdocker help start

3. 操作镜像3.1. docker hub说明github是存储代码的仓库，而docker hub是存储docker镜像的仓库。
docker hub上面的镜像命名方式为：&lt;username&gt;/&lt;imagename&gt;:&lt;tag&gt;，比如voidking/nginx:v1.0其中 tag 可以省略，省略则默认使用 latest 。
有一类比较特殊的镜像，经过官方的验证值得信任，命名方式为：&lt;imagename&gt;:&lt;tag&gt;，比如 busybox:1.25
3.2. 镜像命名规范docker registry镜像名称格式为：DOCKER_REGISTRY/repo/name:tag，各个字段具体含义如下：

DOCKER_REGISTRY：企业统一的Docker Registry地址；
repo：镜像仓库，用来管理某一类镜像；
name：某个镜像的具体名称，一般的命名规则为：系统名称+系统版本+服务名+服务版本（如果公司约定了主要使用的系统名称和版本，则可以省略系统名称+系统版本部分，直接使用服务名作为镜像的名称）。例如：centos7.6-nginx-1.47。

镜像的名称可以包含的字符限制为[a-z0-9]和[-._]，不能出现中文以及中文符号。
参考文档容器学习：容器镜像命名规范及版本管理规范
docker默认的镜像仓库为 docker.io，web ui为 hub.docker.com
harbor镜像名称格式为：HARBOR_REGISTRY/repo/path/name:tag和docker registry镜像名称相比，主要是支持了多级路径。
3.3. 查找镜像方法一：在docker hub上查找镜像，直接搜索即可方法二：使用命令行的工具搜索，例如搜索 busybox 镜像  
docker search busyboxcurl -s &quot;https://registry.hub.docker.com/v1/repositories/busybox/tags&quot; | jq -r .[].name


3.4. 指定TAG下载镜像1、指定TAG下载镜像以下载 busybox:1.25 镜像为例  
docker pull busybox:1.25


2、查看本地镜像  
docker images

3.5. 指定DIGEST下载镜像参考文档docker pull，我们了解到，除了指定TAG下载镜像之外，还可以指定DIGEST下载镜像。之所以需要DIGEST，是因为同一个镜像TAG，我们可能使用多次，实际上就是不同的版本了。用户指定TAG拉取镜像时，默认会拉取最新的那个版本，那有的用户就想使用以前的版本，这时就可以使用DIGEST。
DIGEST可以在dockerhub的镜像页面找到，以busybox为例，对于同一个TAG多版本的镜像，我们可以指定一个DIGEST来下载。
docker pull busybox@sha256:e02e811dd08fd49e7f6032625495118e63f597eb150403d02e3238af1df240ba

3.6. 删除镜像# 删除单个镜像 docker rmi &lt;image_name&gt;docker rmi &lt;image_id&gt;docker rmi &lt;image_name&gt; -fdocker rmi &lt;image_id&gt; -f# 删除所有镜像docker rmi $(docker images -q)

3.7. 选择镜像如果一个软件有很多版本的镜像，该怎么选择？1、如果对软件版本有要求，那么按照版本要求下载。2、如果对软件版本没有要求，那么选择下载3到6个月前的版本，最好是大版本的最后一个小版本。因为如果版本太新，就容易当小白鼠，可能有很多未知的坑。而老一点的版本，遇到问题一般也能搜索到解决办法。
3.8. 修改容器并保存成镜像进入容器后，我们对容器进行了修改，比如创建了一个文件 /tmp/test.txt 。现在想要把这个容器保存成镜像，方便以后复用，该怎么操作？答：使用docker commit命令，类似于git commit。  
docker commit vk-busybox voidking/vk-busybox:v1.0
以上命令，把修改后的 vk-busybox 容器保存成了镜像 voidking/vk-busybox:v1.0 。docker commit时，除了使用容器名称，也可以使用容器id。
3.9. 分享镜像我们在本地保存了 voidking/vk-busybox:v1.0 镜像，怎样把它分享出去？答：使用docker push命令，类似于git push。  
docker push voidking/vk-busybox:v1.0
该命令把 voidking/vk-busybox:v1.0 镜像推送到 docker hub，所属账户为 voidking 。  
记得push前，需要docker login，进行登录鉴权。
3.10. 修改镜像名修改镜像名，上传到自己的镜像仓库
docker tag voidking/vk-busybox:v1.0 harbor.voidking.com/voidking/vk-busybox:v1.0docker login harbor.voidking.comdocekr push harbor.voidking.com/voidking/vk-busybox:v1.0

如果docker login或者docker push报错，可以参考《Harbor入门篇》进行解决
3.11. 下载国外镜像常用的国外镜像仓库包括：

gcr.io
ghcr.io
quay.io

对于这些镜像仓库，国内一般不能下载。如果确实需要下载，建议通过科学上网下载到本地，然后上传到hub.docker.com或者本地镜像仓库。
例如下载mysql热备份软件xtrabackup的镜像，备份到自己的镜像仓库：
docker pull gcr.io/google-samples/xtrabackup:1.0# 备份到 docker hubdocker tag gcr.io/google-samples/xtrabackup:1.0 voidking/gcr.io:google-samples.xtrabackup.1.0docker logindocker push voidking/gcr.io:google-samples.xtrabackup.1.0# 备份到 harbordocker tag gcr.io/google-samples/xtrabackup:1.0 harbor.voidking.com/gcr.io/google-samples/xtrabackup:1.0docker login harbor.voidking.comdocker push harbor.voidking.com/gcr.io/google-samples/xtrabackup:1.0

如果想要备份docker hub中的镜像到自己的镜像仓库，方法也是一样的：
docker pull busybox:1.25docker tag voidking/docker.io:busybox.1.25docker push voidking/docker.io:busybox.1.25

4. 操作容器4.1. 创建&amp;启动容器怎样创建&amp;启动一个容器，并让它保持运行呢？答：在启动容器时，指定一个长期运行的前台进程作为1号进程。    
docker run --name vk-busybox -d \busybox:1.25 sleep 3600

命令说明： 

docker run 表示启动一个容器
–name 指定容器名为 vk-busybox
-d 表示后台运行容器，并返回容器id
busybox:1.25 是镜像名
sleep 是1号进程，3600是sleep的参数

4.2. 映射端口和挂载目录docker run --name vk-nginx -d --privileged=true \-p 80:80 \-v /etc/localtime:/etc/localtime \nginx:1.17.7

参数说明：

-p 表示宿主机端口（冒号前的端口）映射给容器端口（冒号后的端口）
-v 表示宿主机目录（冒号前目录）挂载给容器目录（容器目录）

详情参考《使用Docker安装配置Nginx》
这里有一个比较有意思的问题：如果指定端口81:81，通过nc探测宿主机81端口是否是通的？答：也是通的。这个应该是docker的机制，docker占用了宿主机的81端口，因此nc是通的。但是通过veth pair把流量转发给容器后，因为容器内的81端口并不提供服务，所以通过curl访问宿主机81端口，最终并不能获得服务。
4.3. 启停容器# 停止一个正在运行的容器docker stop vk-busybox# 启动一个已经停止的容器docker start vk-busybox

4.4. 开机启动容器# 开机启动dockersystemctl enable docker# 开机启动容器docker run --restart=always vk-busyboxdocker update --restart=always vk-busybox

4.5. 查看容器# 查看运行中的容器 docker ps# 查看历史容器docker ps -l# 查看所有容器  docker ps -a# 查看容器启动命令  docker ps --no-trunc# 查看容器详细信息  docker inspect &lt;container_name&gt;docker inspect &lt;container_id&gt;

4.6. 查看容器日志查看容器日志
docker logs &lt;container_name&gt;docker logs --tail=100 &lt;container_name&gt;

查找容器日志
docker logs --tail=100 vk-busybox | grep xxxdocker logs --tail=100 vk-busybox 2&gt;&amp;1 | grep xxx

管道符只对stdout有效，如果容器日志输出到了stderr，就会发现grep无效，这时就需要重定向。
4.7. 拷贝文件在宿主机和容器之间拷贝单个文件
docker cp /tmp/test.txt vk-busybox:/tmpdocker cp vk-busybox:/tmp/test.txt /tmp/test.txt

在宿主机和容器之间拷贝整个目录
docker cp /tmp/testdir vk-busybox:/tmpdocker cp vk-busybox:/tmp/testdir /tmp/ 
表示目录的最后一个斜杠/，加和不加效果相同。
在宿主机和容器之间拷贝目录中的所有文件
docker cp /tmp/. vk-busybox:/tmpdocker cp vk-busybox:/tmp/. /tmp/ 

4.8. 启动容器的同时拷贝文件使用cat代替cp：
docker run --rm \    --entrypoint cat \    prom/prometheus:v2.43.1 \    /etc/prometheus/prometheus.yml &gt; prometheus.yml

挂载目录拷贝：
docker run --rm \    -u root \    --entrypoint cp \    -v /opt:/opt \    prom/prometheus:v2.43.1 \    -rf /etc/prometheus /opt/

4.9. 进入容器docker exec -it vk-busybox /bin/sh


4.10. 查看容器id和容器进程查看容器进程ID  
docker psdocker top vk-busyboxdocker inspect vk-busybox | grep pid -i

查看容器进程的namespace  
ll /proc/$$/nsll /proc/&lt;pid&gt;/ns

在容器net namespace执行命令（需要宿主机上有route命令）  
nsenter -t &lt;pid&gt; -n ip addnsenter -t &lt;pid&gt; -n route -nnsenter -t &lt;pid&gt; -n iptables -A OUTPUT -p tcp --dport 7535 -j DROP

4.11. 查找占用某个端口的容器以查找占用80端口的容器为例。
1、查找进程ID
netstat -nlp | grep 80

看到进程为 10538/docker-proxy
2、容器IP
ps -Af | grep 10538
看到容器启动信息为 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip  -container-port 8000
3、找到容器
docker inspect &lt;container_id&gt; | grep IPAddressdocker inspect &lt;container_id&gt; | grep 172.23.0.4

4.12. 删除容器# 删除单个容器docker rm &lt;container_name&gt;docker rm &lt;container_id&gt;# 删除所有容器  docker rm $(docker ps -a -q)

5. 操作volume查看volume信息
docker volume lsdocker volume inspect xxx

6. 清理无用资源6.1. 清理所有资源删除停止的容器、删除没有被使用的网络、删除没有被使用的镜像、删除构建产生的缓存
docker system prunedocker system prune -adocker system prune -f

删除以上内容，同时删除没有被使用的volumes  
docker system prune -a --volumes

6.2. 清理指定资源# 删除没有被使用的volumesdocker volume prunedocker volume rm $(docker volume ls -qf dangling=true)# 删除停止的容器 docker container prune# 删除没有被使用的镜像 docker image prune# 删除docker build缓存docker builder prune# 删除没有被使用的网络  docker network prune# 删除所有悬空镜像（没有名称的镜像），不删除未使用镜像docker rmi $(docker images -f &quot;dangling=true&quot; -q)# 删除未使用镜像和悬空镜像 docker rmi $(docker images -q)# 删除所有已退出的容器 docker rm -v $(docker ps -aq -f status=exited)# 删除所有状态为dead的容器docker rm -v $(docker ps -aq -f status=dead)





]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker入门篇</title>
    <url>/dev-docker-start/</url>
    <content><![CDATA[1. 容器简介奔驰车生产好之后，经过集装箱封装，从纽约运送到了上海，开箱后奔驰车可以跑在上海的大街上。软件在环境A开发好之后，经过集装箱封装，从环境A运送到了环境B，开箱后软件可以跑在环境B。这种软件集装箱技术，就是容器技术，英文叫做 Linux Container。
容器技术的主要优点是轻量虚拟化（良好的隔离性、高资源利用率）、快捷和可移植性。
起初，容器技术做的最好的就是Docker，Docker也几乎就是容器的代名词，后来CoreOS、Mesos、Containerd也都相继发展壮大。为了统一容器标准，OCI（Open Container Initiative）组织成立了，并于2016年4月推出了第一个开放容器标准，标准主要包括image镜像标准和runtime运行时标准。


2. Docker简介
Docker是一个开源的容器引擎（管理容器的工具），可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署，包括VMs（虚拟机）、bare metal、OpenStack 集群和其他的基础应用平台。 

应用场景：开发环境搭建、自动化构建、自动化测试、自动化部署。
Docker系统有两个程序：docker服务端和docker客户端。其中docker服务端是一个服务进程，管理着所有的容器。docker客户端则扮演着docker服务端的远程控制器，可以用来控制docker的服务端进程。大部分情况下，docker服务端和客户端运行在一台机器上。
Docker优点：Build, Ship and Run.Build once, run anywhere.
参考文档：

Docker官方文档
什么是Docker?
Docker入门实战
中国第一套Docker实战案例视频课程（入门到高级）
Docker实战案例源码
Docker实战案例文档

3. Docker安装3.1. centos7安装docker参考文档Install Docker Engine on CentOS
1、安装yum-utils  
yum install yum-utils

2、添加docker-ce源
# 官方源yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo# 阿里云源，国内更快yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

3、更新yum包  
yum makecache fast

4、查看docker版本 
yum list | grep dockeryum list docker-ce --showduplicates | sort -r

看到如下内容：
docker-ce.x86_64            3:23.0.1-1.el7                     docker-ce-stabledocker-ce.x86_64            3:23.0.1-1.el7                     @docker-ce-stabledocker-ce.x86_64            3:23.0.0-1.el7                     docker-ce-stabledocker-ce.x86_64            3:20.10.9-3.el7                    docker-ce-stabledocker-ce.x86_64            3:20.10.8-3.el7                    docker-ce-stabledocker-ce.x86_64            3:20.10.7-3.el7                    docker-ce-stable...docker-ce.x86_64            17.03.1.ce-1.el7.centos            docker-ce-stabledocker-ce.x86_64            17.03.0.ce-1.el7.centos            docker-ce-stable

5、安装最新版本docker
yum install docker-ce

PS：安装指定版本docker
yum install docker-ce-20.10.9 # 安装指定版本（不推荐，可能架构不匹配）# yum install &lt;softname&gt;-&lt;version&gt;.&lt;arch&gt;yum install docker-ce-3:20.10.9-3.el7.x86_64 # 安装指定版本（推荐）

6、启动docker-ce并设置开机启动
systemctl start dockersystemctl enable docker

3.2. ubuntu18安装docker参考文档Install Docker Engine on Ubuntu
注意：ubuntu不要使用snap安装docker，否则配置和其他系统不一致很麻烦。
1、删除自带的老版本
apt-get remove docker docker-engine docker.io containerd runc

2、安装依赖
apt-get updateapt-get install \    ca-certificates \    curl \    gnupg \    lsb-release

3、添加docker官方GPG key
mkdir -p /etc/apt/keyringscurl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg

4、建立仓库
mkdir -p /etc/apt/sources.list.d/echo \  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \  $(lsb_release -cs) stable&quot; | tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

5、安装最新版本docker
apt-get updateapt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

PS：安装指定版本docker
apt-cache madison docker-ce | awk &#x27;&#123; print $3 &#125;&#x27;# apt list -a docker-ceVERSION_STRING=5:20.10.13~3-0~ubuntu-bionicapt-get install docker-ce=$VERSION_STRING docker-ce-cli=$VERSION_STRING containerd.io docker-compose-plugin

3.3. win10安装docker参考文档：《Win10安装配置Docker》
3.4. 升级和降级docker参考文档：《好好学Docker：Docker升级和降级》
4. helloworld4.1. 运行容器国际惯例，先跑一个helloworld。
docker pull busybox:1.25docker run busybox:1.25 echo &quot;helloworld&quot;

4.2. docker pull说明docker pull busybox:1.25 从镜像仓库下载一个名称为 busybox:1.25 的镜像。等价于 docker pull docker.io/busybox:1.25 ，这是因为docker默认的镜像仓库就是 docker.io
4.3. docker run说明docker run busybox:1.25 echo &quot;helloworld&quot;busybox:1.25 镜像作为模板启动一个容器，容器中运行echo程序，参数为”helloworld”。容器启动后，输出了 helloworld ，然后就停止了。这是因为，1号进程（echo进程）停止了，容器也随之停止。
docker镜像运行起来，便会生成一个docker容器。docker容器可以理解为在沙盒中运行的进程，这个沙盒包含了该进程运行所必须的资源，包括文件系统、系统类库、shell环境等等。这个沙盒默认不会运行任何程序，我们需要指定一个程序在启动容器时运行。这个程序对应的进程是该容器的1号进程，当该进程结束的时候，容器也会完全停止。
5. Docker常用命令参考《Docker常用命令》
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>harbor</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker升级和降级</title>
    <url>/dev-docker-upgrade-and-downgrade/</url>
    <content><![CDATA[1. 前言为了解决稳定性和兼容性等方面的问题，我们可能需要升级或降级Docker。升级和降级Docker有两个方案：利用包管理工具和利用二进制包。本文中，我们会学习这两种方案。


2. 包管理工具升降级Docker2.1. ubuntu18中升降级docker1、停止Docker
docker versionsystemctl stop docker

2、查询Docker可用版本
apt updateapt-cache madison docker-ceapt-cache madison docker-ce-cli

3、升级/降级Docker（假设目标版本为24.0.2）
apt install docker-ce=5:24.0.2-1~ubuntu.18.04~bionicapt install docker-ce-cli=5:24.0.2-1~ubuntu.18.04~bionic

4、启动Docker
systemctl start dockerdocker version

2.2. centos7中升降级docker1、停止Docker
docker versionsystemctl stop dockersystemctl stop docker.socketsystemctl stop containerd

2、查询Docker可用版本
yum list docker-ce --showduplicates | sort -r

3、升级/降级Docker（假设目标版本为19.03.15）
version=19.03.15# 降级yum downgrade --setopt=obsoletes=0 -y docker-ce-$&#123;version&#125; docker-ce-cli-$&#123;version&#125; docker-ce-selinux-$&#123;version&#125; containerd.io# 升级yum upgrade --setopt=obsoletes=0 -y docker-ce-$&#123;version&#125; docker-ce-cli-$&#123;version&#125; docker-ce-selinux-$&#123;version&#125; containerd.io

4、启动Docker
systemctl start containerdsystemctl start dockerdocker version

3. 二进制包升降级Docker二进制包替换不是官方推荐的更新方法，因为它需要手动干预更多的步骤，但是在一些定制化环境或缺少包管理工具的系统上可能是必须的。
在 Linux 系统中，通过二进制包替换方式升级 Docker 主要涉及到以下步骤。
1、下载指定版本的 Docker 二进制文件从 Docker Download 官方网站 下载对应的二进制包，例如我们需要 x86_64 系统架构的23.0.6版本。
wget https://download.docker.com/linux/static/stable/x86_64/docker-23.0.6.tgztar -xzvf docker-23.0.6.tgzls -l docker

其中包含多个二进制文件：

containerd  
containerd-shim-runc-v2  
ctr  
docker  
dockerd  
docker-init  
docker-proxy  
runc

2、确认二进制文件原本的路径
which containerd  which containerd-shim-runc-v2  which ctr  which docker  which dockerd  which docker-init  which docker-proxy  which runc

3、停止运行的Docker服务
systemctl stop docker.socketsystemctl stop dockersystemctl stop containerd

4、备份当前的 Docker 程序 (可选)
cp /usr/bin/containerd&#123;,.bak&#125;cp /usr/bin/containerd-shim-runc-v2&#123;,.bak&#125;  cp /usr/bin/ctr&#123;,.bak&#125;  cp /usr/bin/docker&#123;,.bak&#125;  cp /usr/bin/dockerd&#123;,.bak&#125;  cp /usr/bin/docker-init&#123;,.bak&#125;  cp /usr/bin/docker-proxy&#123;,.bak&#125;cp /usr/sbin/runc&#123;,.bak&#125;

5、将二进制文件移动到 /usr/bin 目录下
cp docker/* /usr/bin/mv /usr/bin/runc /usr/sbin/runc

拷贝过程中，如果出现报错：cp: 无法创建普通文件’/usr/bin/containerd-shim-runc-v2’: 文本文件忙解决办法为：
lsof /usr/bin/containerd-shim-runc-v2# 找到进程后kill -9 xxx

6、重新启动 Docker 服务
systemctl start containerdsystemctl start docker

7、检查 Docker 是否已经成功升级
docker versiondocker info












]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Win10安装配置Docker Desktop</title>
    <url>/dev-win10-docker-desktop/</url>
    <content><![CDATA[1. Docker Desktop简介
Docker Desktop is a one-click-install application for your Mac, Linux, or Windows environment that enables you to build and share containerized applications and microservices.


It provides a straightforward GUI (Graphical User Interface) that lets you manage your containers, applications, and images directly from your machine. Docker Desktop can be used either on it’s own or as a complementary tool to the CLI.


Docker Desktop reduces the time spent on complex setups so you can focus on writing code. It takes care of port mappings, file system concerns, and other default settings, and is regularly updated with bug fixes and security updates.

参考文档：Docker Desktop
本文中，我们学习在Win10中安装配置Docker Desktop的方法。


2. 安装WSL2参考文档：《Win10安装配置Linux和Docker》
3. 安装配置Docker Desktop3.1. 安装Docker下载Docker Desktop for Windows，双击安装。安装时勾选使用WSL2 backend，在Windows上运行一个真正的Linux环境，但是无需传统虚拟机或双系统的额外负担，性能更好。
3.2. 进入根路径的方法方法一：打开文件资源管理器，点击左侧Linux
方法二：Windows资源管理器路径中输入
\\wsl.localhost\docker-desktop-data

3.3. 使用示例以安装MySQL为例：
docker pull mysql:8.0.28docker run --name mysql -d -p 3306:3306 -v D:\DockerData\mysql\mysql-files:/var/lib/mysql-files -v D:\DockerData\mysql\conf.d:/etc/mysql/conf.d -v D:\DockerData\mysql\mysql:/var/lib/mysql -v D:\DockerData\mysql\log:/var/log/mysql -e MYSQL_ROOT_PASSWORD=voidking mysql:8.0.28

注意：这里挂载宿主机目录使用的是Win10中的目录D:\DockerData\mysql，方便查找文件。如果依然使用/opt/mysql，那么文件很难找到（我没找到，捂脸）。
更多内容参考文档：《使用Docker安装配置MySQL》
4. 更改Docker数据盘到D盘参考文档：WSL2下修改Docker Desktop镜像存放路径
WSL发行版默认安装在C盘，在%LOCALAPPDATA%/Docker/wsl目录。Docker的运行数据、镜像文件都存在%LOCALAPPDATA%/Docker/wsl/data/ext4.vhdx中。
更改Docker数据盘方法：
1、关闭docker desktop
2、关闭所有linux发行版
wsl -l --allwsl -l -vwsl --shutdown

3、导出docker-desktop-data到D盘（原有的docker images不会一起导出）只需要迁移docker-desktop-data一个发行版就行，docker-desktop不用管，它占用空间很小。手动创建目录D:\DockerData\docker-desktop-data\，然后执行导出命令
wsl --export docker-desktop-data D:\DockerData\docker-desktop-data\docker-desktop-data.tar

4、注销 docker-desktop-data
wsl --unregister docker-desktop-data

5、导入 docker-desktop-data
wsl --import docker-desktop-data D:\DockerData\docker-desktop-data\ D:\DockerData\docker-desktop-data\docker-desktop-data.tar --version 2

6、启动docker desktop
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>windows</tag>
        <tag>wsl2</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Win10中的Docker使用USB设备</title>
    <url>/dev-win10-docker-usb/</url>
    <content><![CDATA[1. 前言《Win10安装配置Docker》一文中，其中方法（4）是在Win10中安装WSL2 Linux系统，然后在Linux系统中安装Docker。
如果想要在Docker中使用Win10宿主机上的USB设备，该怎么做？本文中我们就来学习一下。
参考文档：

连接 USB 设备



2. 准备条件
运行 Windows 11 (内部版本 22000 或更高版本) 。 （可提供 Windows 10 支持，请参见下面的注释）
需要具有 x64/x86 处理器的计算机。 (arm64 目前不支持 usbipd-win) 。
Linux 发行版已安装并设置为 WSL 2。
运行 Linux 内核 5.10.60.1 或更高版本。

升级Linux内核方法：1、升级内核到最新版本wsl --update2、重启电脑
3. 安装 USBIPD-WIN 项目WSL 本身并不支持连接 USB 设备，因此你需要安装开源 usbipd-win 项目。
1、转到 usbipd-win 项目的最新发布页。2、选择 .msi 文件，该文件将下载安装程序。3、运行下载的 usbipd-win_x.msi 安装程序文件。
4. 安装 USBIP 工具和硬件数据库USB/IP 项目完成安装后，需要安装用户空间工具和 USB 硬件标识符数据库。 这些说明适用于 Ubuntu — 其他分发版可能需要不同的 usbip 客户端包。
在 Ubuntu 上，运行以下命令：
sudo apt install linux-tools-5.4.0-77-generic hwdatasudo update-alternatives --install /usr/local/bin/usbip usbip /usr/lib/linux-tools/5.4.0-77-generic/usbip 20
此时，一个服务正在 Windows 上运行以共享 USB 设备，并且在 WSL 中安装了必要的工具来连接到共享设备。
5. 附加 USB 设备在附加 USB 设备之前，请确保 WSL 命令行已打开。 这将使 WSL 2 轻型 VM 保持活动状态。
1、通过以管理员模式打开 PowerShell 并输入以下命令，列出所有连接到 Windows 的 USB 设备：
usbipd wsl list
可以看到BUSID、VID:PID、DEVICE、STATE，其中STATE都是Not attached。
注意：设备连接不同的USB端口，BUSID也是不同的。
2、选择要附加到 WSL 的设备总线 ID，然后运行下面的命令。 
wsl -l -vwsl --set-default Ubuntu-18.04usbipd wsl attach --busid &lt;busid&gt;usbipd wsl attach -d Ubuntu-18.04 --busid &lt;busid&gt;

此时可能报错：usbip: error: Attach Request for 1-24 failed - Device in error stateusbipd: error: Failed to attach device with busid ‘1-24’.
请检查确认想要挂载的设备busid是否正确，如果是正确的设备busid，一般可以正常挂载。
3、打开 Ubuntu（或首选的 WSL 命令行），使用以下命令列出附加的 USB 设备：
lsusb
你应会看到刚刚附加的设备，并且能够使用常规 Linux 工具与之交互。其中设备的 ID 对应Win10中看到的 VID:PID 。根据你的应用程序，你可能需要配置 udev 规则以允许非根用户访问设备。
4、在 WSL 中完成设备使用后，可物理断开 USB 设备，或者在管理员模式下从 PowerShell 运行此命令：
usbipd wsl detach --busid &lt;busid&gt;

若要详细了解此操作的工作原理，请参阅 Windows 命令行博客和 GitHub 上的 usbipd-win 存储库。
有关视频演示，请参阅 WSL 2：连接 USB 设备（制表符与空格显示）。
6. 使用 USB 设备1、Ubuntu18中查看USB设备
lsusb

假设看到的结果为：
Bus 002 Device 002: ID 2ba2:4d55Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hubBus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

那么设备 2ba2:4d55 对应的路径为：
/dev/bus/usb/002/002

2、挂载设备到容器中
docker run --name test -d \-v /dev/bus/usb:/dev/bus/usb \busybox sleep 7200

3、容器中查看USB设备
lsusb

至此，容器中的程序就可以正常访问USB设备了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>windows</tag>
        <tag>wsl2</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Win10安装配置Linux和Docker</title>
    <url>/dev-win10-linux-docker/</url>
    <content><![CDATA[1. Win10安装配置Docker的方法在Win10中安装配置Docker，有四种方法：（1）在Win10上安装Docker Desktop。Docker Desktop 利用了 Windows 10 的 Hyper-V 虚拟化技术，来创建一个轻量级的 Linux 虚拟机（VM），并在其中运行 Docker 引擎和容器。这样，用户就可以在 Windows 上享受到 Linux 上的 Docker 功能和性能。（2）在Win10上安装Docker Toolbox。Docker Toolbox 是一种适用于不支持 Hyper-V 的旧版 Windows 系统的替代方案，它使用了 Oracle VirtualBox 来创建一个 Linux VM，并在其中运行 Docker 引擎和容器。Docker Toolbox 需要用户手动配置一些网络和存储设置，以便让 Windows 和 VM 之间进行通信和数据交换。（3）使用VirtualBox等虚拟化工具启动一个Linux虚拟机（与方法（2）类似），然后在虚拟机中安装使用Docker。（4）在Win10上安装一个WSL2 Linux系统，然后在Linux系统中安装Docker。
无论哪种方法，想要在 Windows 上运行 Docker，都需要一个 Linux VM 来托管 Docker 引擎和容器，因为 Docker 是基于 Linux 内核的技术，而不能直接在 Windows 内核上运行。但是，用户可以通过命令行或图形界面来操作和管理 Docker 容器，就像它们是本地应用程序一样。
方法（1），Docker Desktop的安装配置方法，参考文档《Win10安装配置Docker Desktop》方法（2），Docker Toolbox的安装配置方法，参考文档《K8S入门篇》。方法（3），虚拟机中安装使用Docker的方法，和Linux中安装使用Docker的方法没有区别，参考文档《Docker入门篇》。
本文中，我们学习方法（4），在Win10中安装Linux，并在Linux中安装Docker。


2. WSL2简介WSL全拼是Windows Subsystem for Linux，WSL2是适用于Linux的Windows子系统体系结构的一个新版本。WSL2是基于Hyper-V虚拟化技术的，它在Windows上启动一个真正的Linux系统，使用一个定制的Linux内核。这个内核支持完整的Linux系统调用，所以可以在WSL2中运行任何Linux应用程序。
WSL2和Windows之间相当于两台处于一个网络中但分别独立的主机，我们称这个网络为WSL网络。我们可以在Windows主机上使用ipconfig命令来查看Windows主机在WSL网络中的地址，相应地，在WSL2中你可以使用ifconfig命令来查看WSL2在WSL网络中的地址2。这样我们就可以在两个系统之间进行网络通信，比如访问Web服务或数据库等。
WSL2还保留了WSL1的无缝集成特性，比如我们可以从Windows资源管理器中访问Linux文件系统，或者从Linux终端中运行Windows应用程序等。我们还可以通过wsl命令来管理和配置WSL2。
参考文档：

适用于 Linux 的 Windows 子系统文档
使用 WSL 在 Windows 上安装 Linux
旧版 WSL 的手动安装步骤
WSL 的基本命令

3. 安装WSL21、打开PowerShell搜索PowerShell，以管理员身份运行
2、启用适用于 Linux 的 Windows 子系统
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

3、启用虚拟机功能
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

4、下载 Linux 内核更新包下载Linux 内核更新包，双击安装。
5、将WSL2设置为默认版本
wsl --set-default-version 2

4. Win10中安装Ubuntu18参考文档：

旧版 WSL 的手动安装步骤
设置 WSL 开发环境

4.1. 安装Ubuntu18打开Microsoft Store，搜索Ubuntu，选择Ubuntu18版本，获取或者直接打开Ubuntu 18.04 LTS，获取
首次启动新安装的 Linux 发行版时，将打开一个控制台窗口，系统会要求你等待一分钟或两分钟，以便文件解压缩并存储到电脑上。 未来的所有启动时间应不到一秒。
然后，需要为新的 Linux 发行版创建用户帐户和密码。
4.2. 设置WSL2默认发行版wsl -l -vwsl --set-default Ubuntu-18.04
设置默认WSL2默认发行版之后，就不用使用指定-d指定发行版了。
详情参阅WSL 文档的基本 comands
4.3. 登录Ubuntu18方法一：搜索Ubuntu，打开Ubuntu 18.04.5 on Windows，会直接登录到发行版内部
方法二：PowerShell中执行wsl命令登录进入发行版内部
wsl -l -vwsl -d Ubuntu-18.04wsl -d Ubuntu-18.04 -u root

PS：docker-desktop-data 发行版是不支持 wsl -d 登录的。
4.4. 磁盘路径说明C盘对应Ubuntu18路径/mnt/c，D盘对应Ubuntu18路径/mnt/d
4.5. 配置SSH登录参考文档：Win10安装Ubuntu
4.6. 配置远程登录Ubuntu18支持SSH登录了，但是只能从Win10宿主机上登录，相同局域网的其他主机还不能访问到Ubuntu18。本节配置使局域网其他主机也可以访问到Ubuntu18，操作步骤如下：1、固定Ubuntu18的IP地址2、配置端口转发，发往Win10特定端口的请求转发给Ubuntu183、关闭Win10防火墙
参考文档：

如何在局域网的其他主机上中访问本机的WSL2
wsl转wsl2与局域网内网访问填坑
如何用笔记本ssh连接局域网内其他电脑上的wsl2 ubuntu

4.6.1. 固定IP地址每次重启WSL2 Ubuntu18重启后，IP地址都会发生变化，无法固定路由规则，因此需要配置固定IP。
参考文档：

Open WSL2 Set static ip?
将bat文件设置为开机启动

1、编写 setip.bat 脚本
@echo offsetlocal enabledelayedexpansion:: set windows ipipconfig | findstr &quot;172.29.128.1&quot; &gt; nulif !errorlevel! equ 0 (    echo windows ip has set) else (    netsh interface ip add address &quot;vEthernet (WSL)&quot; 172.29.128.1 255.255.240.0    echo set windows ip success: 172.29.128.1):: set wsl2 ipwsl -d Ubuntu-18.04 -u root ip addr | findstr &quot;172.29.132.34&quot; &gt; nulif !errorlevel! equ 0 (    echo wsl2 ip has set) else (    wsl -d Ubuntu-18.04 -u root ip addr add 172.29.132.34/20 broadcast 172.29.143.255 dev eth0 label eth0:1    echo set wsl2 ip success: 172.29.132.34)pause

2、以管理员身份运行脚本
3、登录到Ubuntu18，查看IP和网关
ifconfigroute -n
正常的话，可以查看到IP地址为172.29.132.34，网关为172.29.128.1（Win10虚拟网卡的IP）。
注意：这里配置固定IP不能使用常规方法（《Ubuntu18配置静态IP》），因为常规方法会报错：System has not been booted with systemd as init system (PID 1). Can’t operate.
4.6.2. 配置端口转发Win10中以管理员身份打开PowerShell，执行
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=22 connectaddress=172.29.132.34 connectport=22netsh interface portproxy show v4tov4#netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=22

参考文档：Netsh interface portproxy 命令
配置路由规则之后，本地访问本机IP:22端口，会自动转发到172.29.132.34:22。
如果局域网中其他主机也想要访问到172.29.132.34:22，那么本机还需要关闭局域网防火墙（专有网络防火墙）。
4.6.3. 关闭防火墙搜索“防火墙”，选择Windows Defender 防火墙，启用或关闭Windows Defender防火墙，关闭防火墙。
或者操作开放端口，参考文档：WIN10电脑怎么开放可访问端口
关闭防火墙之后，使用局域网内的其他主机尝试远程登录Ubuntu18进行测试。
5. 配置开机自启动5.1. 配置开机自启动概述配置开机自启动的常用方法包括：

Startup目录中放入快捷方式，快捷方式可能链接到bat脚本或者exe程序
Startup目录中放入vbs脚步
任务计划程序中添加要执行的程序，配置触发器
使用第三方配置开机自启动的程序，例如NSSM和FireDaemon

5.2. 启动ssh参考文档：

WSL 服务自动启动的正确方法
让 wsl 中的服务随 Win10 开机自启动
在 Windows 10 中添加在启动时自动运行的应用

方法一：使用 vbs 脚本
1、创建 vbs 脚本在 Windows 中，开始-运行，输入shell:startup，进入Startup目录创建 ubuntu18.vbs 脚本，内容为：
Set ws = CreateObject(&quot;Wscript.Shell&quot;)ws.run &quot;wsl -d Ubuntu-18.04 -u root /etc/init.d/ssh start&quot;, vbhide

2、重启机器测试重启机器后，打开任务管理器，详细信息，启动，可以看到 ubuntu18.vbs 被调用了。
方法二：使用快捷方式
1、创建快捷方式在 Windows 中，开始-运行，输入shell:startup，进入Startup目录新建快捷方式，起始位置为D:\Windows\System32，目标为wsl.exe -d Ubuntu-18.04 -u root /etc/init.d/ssh start
2、重启机器测试重启机器后，打开任务管理器，详细信息，启动，可以看到 wsl.exe 被调用了。
5.3. 配置固定IP地址方法一：使用启动目录
1、编写Win10开机自启动脚本 ubuntu18-init.bat功能包括：自动设置固定IP地址、配置路由规则
@echo offsetlocal enabledelayedexpansion:: set windows ipipconfig | findstr &quot;172.29.128.1&quot; &gt; nulif !errorlevel! equ 0 (    echo windows ip has set) else (    netsh interface ip add address &quot;vEthernet (WSL)&quot; 172.29.128.1 255.255.240.0    echo set windows ip success: 172.29.128.1):: set wsl2 ipwsl -d Ubuntu-18.04 -u root ip addr | findstr &quot;172.29.132.34&quot; &gt; nulif !errorlevel! equ 0 (    echo wsl2 ip has set) else (    wsl -d Ubuntu-18.04 -u root ip addr add 172.29.132.34/20 broadcast 172.29.143.255 dev eth0 label eth0:1    echo set wsl2 ip success: 172.29.132.34):: set portproxynetsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=22 connectaddress=172.29.132.34 connectport=22echo set set protproxy success: 22 -&gt; 172.29.132.34:22:: start sshdwsl -d Ubuntu-18.04 -u root /usr/sbin/service ssh startecho start sshd success: port 172.29.132.34:22pause

2、创建快捷方式右键 ubuntu18-init.bat，创建快捷方式。右键快捷方式，属性，高级，勾选用管理员身份运行。
3、设置开机自启动ubuntu18-init.bat 的快捷方式，放入到 Startup 目录中，开机后会自动执行它。
win+R打开运行，输入shell:startup，进入Startup目录。Startup 目录全路径为 C:\Users\&lt;username&gt;\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
4、重启机器测试重启机器后，打开任务管理器，详细信息，启动，可以看到 ubuntu18-init.bat 被调用了。
PS：这个设置开机自启动的方法，不止适用于 bat 脚本，对于常见的应用程序也是可行的，比如QQ、微信等。
方法二：使用计划任务
参考文档：Windows开机启动bat文件
1、打开任务计划程序按键盘上的Windows + R键以打开运行，然后键入taskschd.msc，回车。
2、创建任务点击创建任务，进行任务配置。
常规参数：

名称：ubuntu18-init
勾选不管用户是否登录都要运行
勾选不存储密码。该任务将只有访问本地计算机资源的权限。
勾选使用最高权限运行。

触发器，新建，开始任务选择启动时，延迟任务时间选择1分钟，勾选已启用，确定。操作，新建，操作选择启动程序，程序或脚本通过浏览选中ubuntu18-init.bat，添加参数空白，起始于空白（在哪个路径中执行），确定。
3、查看任务右键任务计划程序库，刷新，即可看到新增的任务。
4、重启机器测试
手动测试计划任务：1、在powershell或者cmd中执行计划任务中定义的命令和参数2、单击计划任务，点击右侧开始
6. Ubuntu18中安装Docker安装方法参考文档：《Docker入门篇》
6.1. 启动Docker使用service命令启动Docker
service docker start

6.2. 使用示例以安装MySQL为例：
docker pull mysql:8.0.28docker run --name mysql -d \-p 3306:3306 \-v /opt/mysql/mysql-files:/var/lib/mysql-files \-v /opt/mysql/conf.d:/etc/mysql/conf.d \-v /opt/mysql/data:/var/lib/mysql \-v /opt/mysql/log:/var/log/mysql \-e MYSQL_ROOT_PASSWORD=voidking \mysql:8.0.28

更多内容参考文档：《使用Docker安装配置MySQL》
7. 更改Ubuntu18数据盘到D盘参考文档：WSL2下修改Docker Desktop镜像存放路径
1、关闭Ubuntu18
wsl -l --allwsl -l -vwsl -t Ubuntu-18.04

2、导出Ubuntu18备份文件到D:\Ubuntu18（需要提前创建目录）
wsl --export Ubuntu-18.04 D:\Ubuntu18\Ubuntu-18.04.tar

3、注销Ubuntu18
wsl --unregister Ubuntu-18.04

5、导入Ubuntu18
wsl --import Ubuntu-18.04 D:\Ubuntu18\ D:\Ubuntu18\Ubuntu-18.04.tar --version 2

6、启动Ubuntu18
wsl -d Ubuntu-18.04

8. 服务探活8.1. 交互式探活@echo offset /p host=&quot;Enter IP: &quot;set /p port=&quot;Enter PORT: &quot;powershell -Command &quot;if((Test-NetConnection -ComputerName %host% -Port %port%).TcpTestSucceeded)&#123;write-host &#x27;connect to %host%:%port% success&#x27;&#125;else&#123;write-host &#x27;connect to %host%:%port% failed&#x27;&#125;&quot;pause

8.2. 一键探活假设 Ubuntu-18.04 中安装了MySQL和Redis，想要在Win10中一键探活，那么可以使用下面的脚本。
@echo offREM 探测MySQL状态set mysql_host=172.29.132.34set mysql_port=3306powershell -Command &quot;if((Test-NetConnection -ComputerName %mysql_host% -Port %mysql_port%).TcpTestSucceeded)&#123;write-host &#x27;connect to mysql %mysql_host%:%mysql_port% success&#x27;&#125;else&#123;write-host &#x27;connect to mysql %mysql_host%:%mysql_port% failed&#x27;&#125;&quot;REM 探测Redis状态set redis_host=172.29.132.34set redis_port=6379powershell -Command &quot;if((Test-NetConnection -ComputerName %redis_host% -Port %redis_port%).TcpTestSucceeded)&#123;write-host &#x27;connect to redis %redis_host%:%redis_port% success&#x27;&#125;else&#123;write-host &#x27;connect to redis %redis_host%:%redis_port% failed&#x27;&#125;&quot;pause




]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>好好学Docker</tag>
        <tag>windows</tag>
        <tag>wsl2</tag>
      </tags>
  </entry>
  <entry>
    <title>使用IDEA开发Maven JavaWeb迷你项目</title>
    <url>/dev-idea-maven-javaweb-miniproject/</url>
    <content><![CDATA[1. 项目简介本文中，我们使用Mybatis+Spring+SpringMVC来完成一个迷你JavaWeb项目：用户管理系统。功能：普通用户可以注册登录，管理员可以管理普通用户。
2. 环境准备2.1. 安装jdk参考《全平台安装JDK》。
2.2. 安装maven下载地址： http://maven.apache.org/download.cgi
1、解压到自己喜欢的目录（这里郝同学放到D:\Server路径下）。2、添加环境变量M2_HOME，值为D:\Server\apache-maven-3.3.93、在Path中添加;%M2_HOME%\bin;。
打开命令提示符，输入mvn -v，如果能够看到maven版本号，说明安装成功。
2.3. 安装tomcat1、tomcat下载地址：http://tomcat.apache.org/download-80.cgi
2、假设解压目录为D:\Server\apache-tomcat-8.5.9。
3、进入目录D:\Server\apache-tomcat-8.5.9\bin，双击startup.bat。
4、浏览器访问http://localhost:8080，启动成功则显示tomcat管理页面。
2.4. idea配置idea配置参考《IDEA快捷键和配置》。
3. 新建Web项目1、打开idea，File，New，Project，右边导航栏选择Maven，勾选Create from archetype，选择maven-archetype-webapp。
2、Next，输入GroupId为“com.voidking.pandawork”，输入ArtifactId为“pandawork-start”。
3、Next，选择自己安装的Maven中的User settings file，选择settings.xml中配置的Local repository。
4、Next，输入Project name，选择Project location，Finish。
5、稍等几十秒，Web项目便可以创建成功。
4. 初始化项目结构1、展开pandawork-start项目，展开src文件夹。
2、右键main文件夹，New，Directory，输入directory name为“java”。
3、右键main/java文件夹，New，Package，输入package name为“com.voidking.pandawork”。右键“com.voidking.pandawork”，新建文件README.md。
3、右键main/resources文件夹，New，Directory，输入directory name为“com.voidking.pandawork”。
3、至此，初始化项目结构完成。


5. 持久层5.1. 新建数据库1、使用navicat新建mysql数据库，数据库名为pandawork，字符集选择utf8–UTF-8 Unicode，排序规则选择utf8_general_ci。
2、新建表t_user，包括id、username、password三个字段。
DROP TABLE IF EXISTS `t_user`;CREATE TABLE `t_user` (  `id` int(8) NOT NULL AUTO_INCREMENT,  `username` varchar(16) NOT NULL,  `password` varchar(16) NOT NULL,  PRIMARY KEY (`id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8;

5.2. mybatis的maven配置在pom.xml中，添加：
&lt;dependency&gt;  &lt;groupId&gt;org.mybatis&lt;/groupId&gt;  &lt;artifactId&gt;mybatis&lt;/artifactId&gt;  &lt;version&gt;3.4.4&lt;/version&gt;&lt;/dependency&gt;

5.3. 连接mysql1、在pom.xml中，添加：
&lt;!-- mysql驱动 --&gt;&lt;dependency&gt;  &lt;groupId&gt;mysql&lt;/groupId&gt;  &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;  &lt;version&gt;5.1.30&lt;/version&gt;&lt;/dependency&gt;


2、右键resources文件夹，新建文件mybatis-config.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE configuration        PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;        &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&gt;&lt;configuration&gt;    &lt;environments default=&quot;development&quot;&gt;        &lt;environment id=&quot;development&quot;&gt;            &lt;transactionManager type=&quot;JDBC&quot;/&gt;            &lt;dataSource type=&quot;POOLED&quot;&gt;                &lt;property name=&quot;driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;                &lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/pandawork&quot;/&gt;                &lt;property name=&quot;username&quot; value=&quot;root&quot;/&gt;                &lt;property name=&quot;password&quot; value=&quot;mysql&quot;/&gt;            &lt;/dataSource&gt;        &lt;/environment&gt;    &lt;/environments&gt;    &lt;mappers&gt;        &lt;mapper resource=&quot;com/voidking/pandawork/mapper/user.mapper.xml&quot;/&gt;    &lt;/mappers&gt;&lt;/configuration&gt;

3、main/java文件夹下，右键com.voidking.pandawork，新建包util。
4、右键包uitl，新建连接数据库的类ConnetDB.java，内容如下：
package com.voidking.pandawork.util;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;import java.io.InputStream;public class ConnectDB &#123;    private static volatile ConnectDB instance=null;    private SqlSessionFactory sqlSessionFactory=null;    private ConnectDB()&#123;        try &#123;            String resource = &quot;mybatis-config.xml&quot;;            InputStream inputStream = Resources.getResourceAsStream(resource);            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;    public static ConnectDB getInstance()&#123;        if(instance==null)&#123;            synchronized(ConnectDB.class)&#123;                if(instance==null)&#123;                    instance=new ConnectDB();                &#125;            &#125;        &#125;        return instance;    &#125;    public SqlSessionFactory getSqlSessionFactory()&#123;        return sqlSessionFactory;    &#125;&#125;

5.4. entity1、main/java文件夹下，右键com.voidking.pandawork，新建包entity。2、右键包entity，新建类User.java。
5.5. mapper1、main/java文件夹下，右键com.voidking.pandawork，新建包mapper。右键包mapper，新建接口UserMapper.java。2、main/resources文件夹下，右键com.voidking.pandawork，新建包mapper。右键包mapper，新建文件user.mapper.xml。
5.6. service1、main/java文件夹下，右键com.voidking.pandawork，新建包service。2、右键包service，新建接口UserService.java。3、右键包service，新建包impl。4、打开UserService.java，鼠标光标聚焦到“public interface UserService”一行，按下alt+enter（或者选择code，generate），选择com.voidking.pandawork.service.impl包，新建接口实现文件UserServiceImpl.java。
5.7. test打开UserServiceImpl.java，鼠标光标聚焦到“public class UserServiceImpl implements UserService”一行，按下alt+enter，选择com.voidking.pandawork包，新建测试文件UserServiceTest.java。这样，就在test/java/com.voidking.pandawork包中生成了测试文件。
6. 业务逻辑层6.1. spring包在pom.xml中，添加springmvc最小化依赖：
&lt;dependency&gt;  &lt;groupId&gt;org.springframework&lt;/groupId&gt;  &lt;artifactId&gt;spring-context&lt;/artifactId&gt;  &lt;version&gt;4.3.8.RELEASE&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;  &lt;groupId&gt;org.springframework&lt;/groupId&gt;  &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;  &lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;&lt;/dependency&gt;

6.2. applicationContext.xml右键main/resources文件夹，新建applicationContext.xml，不配置任何bean，内容如下：
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;       xmlns:context=&quot;http://www.springframework.org/schema/context&quot; xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans                           http://www.springframework.org/schema/beans/spring-beans-4.0.xsd                           http://www.springframework.org/schema/tx                           http://www.springframework.org/schema/tx/spring-tx-4.0.xsd                           http://www.springframework.org/schema/aop                           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd                           http://www.springframework.org/schema/context                           http://www.springframework.org/schema/context/spring-context-4.0.xsd&quot;&gt;&lt;/beans&gt;

6.3. springmvc-servlet.xml右键main/resources文件夹，新建springmvc-servlet.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xmlns:mvc=&quot;http://www.springframework.org/schema/mvc&quot;       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;       xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans                           http://www.springframework.org/schema/beans/spring-beans-4.0.xsd                           http://www.springframework.org/schema/mvc                           http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd                           http://www.springframework.org/schema/context                           http://www.springframework.org/schema/context/spring-context-4.0.xsd                           http://www.springframework.org/schema/aop                           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd                           http://www.springframework.org/schema/tx                           http://www.springframework.org/schema/tx/spring-tx-4.0.xsd&quot;&gt;    &lt;!-- 设置使用注解的类所在的jar包 --&gt;    &lt;context:component-scan base-package=&quot;com.voidking.pandawork.controller&quot; /&gt;&lt;/beans&gt;

6.4. web.xml打开main/webapp/WEB-INFO/web.xml，修改内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app  version=&quot;3.1&quot; xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;          xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;          xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee                              http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd&quot;&gt;  &lt;context-param&gt;    &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;    &lt;param-value&gt;classpath:applicationContext.xml&lt;/param-value&gt;    &lt;!-- 默认是/WEB-INF/applicationContext.xml --&gt;  &lt;/context-param&gt;  &lt;listener&gt;    &lt;listener-class&gt;      org.springframework.web.context.ContextLoaderListener    &lt;/listener-class&gt;  &lt;/listener&gt;  &lt;servlet&gt;    &lt;servlet-name&gt;SpringMVC&lt;/servlet-name&gt;    &lt;servlet-class&gt;org.springframework.web.servlet.DispatcherServlet&lt;/servlet-class&gt;    &lt;init-param&gt;      &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;      &lt;param-value&gt;classpath:springmvc-servlet.xml&lt;/param-value&gt;      &lt;!-- 默认是/WEB-INF/[servlet名字]-servlet.xml --&gt;    &lt;/init-param&gt;    &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;  &lt;/servlet&gt;  &lt;servlet-mapping&gt;    &lt;servlet-name&gt;SpringMVC&lt;/servlet-name&gt;    &lt;url-pattern&gt;/&lt;/url-pattern&gt;  &lt;/servlet-mapping&gt;&lt;/web-app&gt;

6.5. helloworld1、main/java文件夹下，右键com.voidking.pandawork，新建包controller。右键包controller，新建类HelloWorld.java，内容如下：
package com.voidking.pandawork.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controllerpublic class HelloWorld &#123;    @RequestMapping(&quot;/hello&quot;)    public @ResponseBody String test() &#123;        return &quot;hello, world! This com from spring!&quot;;    &#125;&#125;

2、部署项目到tomcat的8080端口，访问http://localhost:8080/hello，即可看到“hello, world! This com from spring!”。
7. 迷你JavaWeb项目实现具体配置和代码实现请自行阅读代码：pandawork-start
8. 书签
mybatis文档
SpringMVC+Spring4+Mybatis3集成，开发简单Web项目+源码下载
手把手教你搭建SpringMVC——最小化配置

]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>编辑器</tag>
        <tag>java</tag>
        <tag>maven</tag>
        <tag>idea</tag>
        <tag>spring</tag>
        <tag>springmvc</tag>
        <tag>mybatis</tag>
      </tags>
  </entry>
  <entry>
    <title>IDEA常用快捷键和配置</title>
    <url>/dev-idea-shortcut-and-config/</url>
    <content><![CDATA[1. 前言工欲善其事，必先利其器。IDEA作为当今最流行的IDE之一，有很多快捷键和配置可以帮助我们更好地开发。本文记录一下自己常用的一些快捷键和配置，备忘。
2. 快捷键2.1. 查看快捷键1、菜单栏，IntelliJ IDEA，Preferences…。2、Keymap，Main menu，Refactor，Extract，Introduce Variable…。
2.2. 自动补全函数返回值windows: ctrl+alt+V，回车macos: option+command+V，回车
2.3. 函数跳转command+单击，command+]，command+[
2.4. 代码格式化macos: option+command+L


3. 配置3.1. idea配置maven1、打开idea，File，Settings，搜索“maven”。2、Maven home directory选择D:\Server\apache-maven-3.3.9。3、User settings file选择D:\Server\apache-maven-3.3.9\conf\settings.xml。
3.2. idea配置tomcat1、打开idea，View，勾选Toolbar，显示工具栏。
2、单击工具栏上的下三角（Select Run/Debug Configuration），Edit Configurations。
3、单击打开窗口的“+”号，Add New Configuration，Tomcat Server，Local。
4、Name修改为Tomcat8.5，JRE选择C:\Program Files\Java\jdk1.8.0_111\jre。
5、勾选Show this page，单击Fix，选择project_name:war exploded，OK。
6、单击工具栏的绿色三角，启动tomcat。
3.3. 自动导包打开idea，File，Settings，Editor，General，Auto Import，界面上能勾选的全部勾选。
3.4. 自动换行1、编码时自动换行Preferences &gt; Editor &gt; Code Style，勾选wrap on typing在编码时，如果超出最大行宽，则自动换行。
2、代码格式化时自动换行Preferences &gt; Editor &gt; Code Style &gt; Python，勾选 Ensure right margin is not exceeded在使用快捷键手动格式化代码时，自动换行。
3.5. 批量替换换行符参考Configuring Line Separators。
为新文件配置换行符：

Press ⌘, to open IDE settings and select Editor | Code Style.
To configure line separators for new projects, go to File | New Projects Setup | Settings/Preferences for New Projects | Editor | Code Style.
The line separator style applied to the current file is indicated in the status bar.

存量文件替换换行符：

Select a file or directory in the Project tool window ⌘1.Note that if a directory is selected, the line ending style applies to all nested files recursively.

From the main menu, choose File | File Properties | Line Separators, and then select a line ending style from the list.


3.6. python代码检查pylint 是一个能够检查Python编码质量、编码规范的工具。它分析 Python 代码中的错误，查找不符合代码风格标准（Pylint 默认使用的代码风格是 PEP 8）和有潜在问题的代码。
参考文档： 

Google Python Style Guide
编码风格与自动检查
Python 编码风格与规范

pylint安装配置方法如下：
1、安装pylintpip install pylint
2、常用命令
pylint --versionpylint --help# 查看pylint安装路径which pylint# 生成配置文件pylint --persistent=n --generate-rcfile &gt; .pylintrc# 默认使用当前目录下的.pylintrc配置文件pylint test.py# 指定配置文件pylint --rcfile=.pylintrc test.py

C(convention)：规范，违反了编码风格标准W(warning)：警告，某些python特定问题E(error)：错误，可能是代码中的错误R(refactor)：重构，代码比较糟糕F(fatal error)：致命错误
3、idea配置pylintPreferences… &gt; Tools &gt; External Tools，点击 + 号添加

Name：pylint
Program：/Users/haojin01/Library/Python/2.7/bin/pylint 
Arguments：--rcfile=$ProjectFileDir$/.pylintrc $FilePath$
Working directory：$FileDir$

4、使用Tools &gt; External Tools &gt; pylint
3.7. python代码自动规范化black 是一个官方的 Python 代码格式化工具，git地址 psf/black。
black安装配置方法如下：
1、安装blackpip3 install black
2、常用命令
black --helpblack --diff test.pyblack test.pyblack --line-length 120 test.pyblack --config pyproject.toml test.py

3、官方 pyproject.toml 示例
# Example configuration for Black.# NOTE: you have to use single-quoted strings in TOML for regular expressions.# It&#x27;s the equivalent of r-strings in Python.  Multiline strings are treated as# verbose regular expressions by Black.  Use [ ] to denote a significant space# character.[tool.black]line-length = 88target-version = [&#x27;py36&#x27;, &#x27;py37&#x27;, &#x27;py38&#x27;]include = &#x27;\.pyi?$&#x27;extend-exclude = &#x27;&#x27;&#x27;/(  # The following are specific to Black, you probably don&#x27;t want those.  | blib2to3  | tests/data  | profiling)/&#x27;&#x27;&#x27;# Build system information below.# NOTE: You don&#x27;t need this in your own Black configuration.[build-system]requires = [&quot;setuptools&gt;=41.0&quot;, &quot;setuptools-scm&quot;, &quot;wheel&quot;]build-backend = &quot;setuptools.build_meta&quot;[tool.pytest.ini_options]# Option below requires `tests/optional.py`optional-tests = [  &quot;no_python2: run when `python2` extra NOT installed&quot;,  &quot;no_blackd: run when `d` extra NOT installed&quot;,  &quot;no_jupyter: run when `jupyter` extra NOT installed&quot;,]

4、自动换行无效问题
black --line-length 120 test.pyblack --config pyproject.toml test.py
以上两个命令，都无效，不能实现自动换行，很奇怪，应该是个bug，先不管它。
5、idea配置blackPreferences… &gt; Tools &gt; External Tools，点击 + 号添加

Name：black
Program：/usr/local/bin/black 
Arguments：--config $ProjectFileDir$/pyproject.toml $FilePath$
Working directory：$FileDir$

6、使用Tools &gt; External Tools &gt; black
3.8. 设置Python注释模板1、打开注释模板配置Preferences &gt; Editor &gt; Code Style &gt; File and Code Templates &gt;  Python Script
2、填入注释内容
#!/usr/bin/env python3# -*- coding:utf-8 -*-################################################################################## Copyright (c) 2021 Baidu.com, Inc. All Rights Reserved#################################################################################&quot;&quot;&quot;Authors: voidkingDate:    $&#123;DATE&#125;&quot;&quot;&quot;

3、新建Python文件，文件头部就会自动出现注释了。
3.9. 新建文件自动git add1、打开注释模板配置Preferences &gt; Version Control &gt; Confirmation ，选择Add silently。
2、新建文件，文件会被自动git add跟踪。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>编辑器</tag>
        <tag>java</tag>
        <tag>tomcat</tag>
        <tag>maven</tag>
        <tag>idea</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux中安装配置Supervisor</title>
    <url>/dev-linux-supervisor/</url>
    <content><![CDATA[1. Supervisor简介
Supervisor is a client/server system that allows its users to monitor and control a number of processes on UNIX-like operating systems.


It shares some of the same goals of programs like launchd, daemontools, and runit. Unlike some of these programs, it is not meant to be run as a substitute for init as “process id 1”. Instead it is meant to be used to control processes related to a project or a customer, and is meant to start like any other program at boot time.

Supervisor是用Python开发的一套通用的进程管理程序，能将一个普通的命令行进程变为后台daemon，并监控进程状态，异常退出时能自动重启。它是通过fork/exec的方式把这些被管理的进程当作supervisor的子进程来启动，这样只要在supervisor的配置文件中，把要管理的进程的可执行文件的路径写进去即可。也实现当子进程挂掉的时候，父进程可以准确获取子进程挂掉的信息，可以选择是否自己启动和报警。supervisor还提供了一个功能，可以为supervisord或者每个子进程，设置一个非root的user，这个user就可以管理它对应的进程。
相关文档：

Supervisor官网
Supervisor使用详解



2. 安装Supervisor2.1. 基于CentOS71、安装supervisor
# 方法一yum install epel-releaseyum install supervisor# 方法二yum install python-pippip install supervisor

2、准备配置文件
mkdir -p /etc/supervisor/echo_supervisord_conf &gt; /etc/supervisord.confvim /etc/supervisord.conf

修改配置文件，添加：
[include]files = /etc/supervisor/*.ini;files = /etc/supervisor/*.conf

3、启动服务&amp;设置开机启动
systemctl start supervisordsystemctl enable supervisord

2.2. 基于pyenv如果系统的Python版本不适合Supservisor运行，那么可以使用pyenv切换Python环境安装Supervisor。参考《Python版本管理器pyenv》，安装好python2.7.13。
1、新建supervisor环境
pyenv virtualenv 2.7.13 supervisor

2、激活supervisor环境
source /root/.pyenv/versions/2.7.13/envs/supervisor/bin/activate supervisor# orsource activate supervisor

3、安装supervisor
pip install supervisor

4、准备配置文件
mkdir -p /etc/supervisor/echo_supervisord_conf &gt; /etc/supervisord.confvim /etc/supervisord.conf

修改配置文件，添加：
[include]files = /etc/supervisor/*.ini;files = /etc/supervisor/*.conf

5、测试运行
/root/.pyenv/versions/2.7.13/envs/supervisor/bin/supervisord -c /etc/supervisord.conf

6、配置supervisord.service
vi /usr/lib/systemd/system/supervisord.service

修改为：
[Unit]Description=Process Monitoring and Control DaemonAfter=rc-local.service nss-user-lookup.target[Service]Type=forkingExecStart=/root/.pyenv/versions/2.7.13/envs/supervisor/bin/supervisord -c /etc/supervisord.conf  ExecReload=/root/.pyenv/versions/2.7.13/envs/supervisor/bin/supervisorctl reload       ExecStop=/root/.pyenv/versions/2.7.13/envs/supervisor/bin/supervisorctl shutdown      [Install]WantedBy=multi-user.target

7、重新启动服务&amp;设置开机启动
ps aux | grep supervisordsystemctl stop supervisordsystemctl start supervisordsystemctl enable supervisord

3. 常用命令重启supervisor
systemctl retart supervisord

查看被管理的服务状态
supervisorctl status

读取有更新或增加的配置文件，但并不加载。
supervisorctl reread

重新加载配置，并根据新的配置重新启动、停止进程
supervisorctl reload

停止一个服务
supervisorctl stop service_name

启动一个服务
supervisorctl start service_name

重启一个服务
supervisorctl restart service_name

4. Supervisor实战4.1. 管理tomcat参考《CentOS7设置tomcat开机自启动》。
4.2. 管理Jupyter参考《CentOS安装配置Jupyter》。
4.3. 管理Django参考Django部署到线上（修改版）。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>python</tag>
        <tag>supervisor</tag>
        <tag>pyenv</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python版本管理器pyenv</title>
    <url>/dev-python-pyenv/</url>
    <content><![CDATA[1. pyenv简介pyenv使我们可以轻松地在多个版本的Python之间切换。它用法简单，遵循UNIX的一站式工具传统，可以很好地完成不同Python版本之间的切换。与之类似的，还有conda。
参考文档：

github - pyenv
Command Reference
《Python包管理工具Conda》
《Python包管理工具pip》



2. 安装pyenv2.1. linuxcurl https://pyenv.run | bash

2.2. macosbrew updatebrew install pyenv

PS：卸载pyenv
rm -rf $(pyenv root)brew uninstall pyenv# 注释或删除 .bash_profile 中关于pyenv的配置

3. 配置pyenv1、在 .bash_profile 中添加
export PYENV_ROOT=&quot;$HOME/.pyenv&quot;command -v pyenv &gt;/dev/null || export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;eval &quot;$(pyenv init -)&quot;

2、使配置生效：
source ~/.bash_profile

3、查看安装情况
pyenv versions

4. pyenv常用命令查看可安装的python版本列表
pyenv install -l

安装指定版本的python
pyenv install 3.6.10

查看已安装的python
pyenv versions

查看当前设为默认的python版本
pyenv version

5. 安装python以安装python3.6.10版本为例
pyenv install -lpyenv install -v 3.6.10pyenv rehash

如果安装很慢，这里提供一个小技巧：提前下载安装包。
mkdir ~/.pyenv/cachecd ~/.pyenv/cachewget https://www.python.org/ftp/python/3.6.10/Python-3.6.10.tar.xz

6. 切换python版本1、切换为3.6.10版本
pyenv versionspyenv global 3.6.10pyenv rehashpython -V

2、切换为原版本
pyenv global systempyenv rehashpython -V




]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>python</tag>
        <tag>好好学Python</tag>
        <tag>pyenv</tag>
      </tags>
  </entry>
  <entry>
    <title>全平台安装JDK（Java开发环境）</title>
    <url>/dev-install-jdk-on-all-platforms/</url>
    <content><![CDATA[1. 前言经常需要安装JDK（Java Development Kit，Java开发环境），本文就对常用系统的JDK安装方法做一个汇总，具体包括Windows、MacOS、Ubuntu和CentOS。


2. JDK下载地址
官方最新版下载地址：Java SE Downloads
官方历史版下载地址：Previous Java releases
编程宝库java jdk镜像
华为java-jdk镜像

3. Windows1、下载exe文件本节选择下载jdk-8u111-windows-x64.exe
2、双击安装jdk，按照提示一步一步安装即可假设安装目录为C:\Program Files\Java\jdk1.8.0_111和C:\Program Files\Java\jre1.8.0_111
3、添加环境变量JAVA_HOME为C:\Program Files\Java\jdk1.8.0_111
4、在环境变量Path中添加;%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;
5、添加环境变量CLASSPATH为.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
6、在DOS命令行窗口输入javac，输出帮助信息即为配置正确
4. MacOS4.1. 安装JDK14.01、下载dmg文件并安装本节选择下载jdk-14.0.1_osx-x64_bin.dmg
2、配置环境变量编辑 ~/.bash_profile 文件，添加jdk相关配置
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-14.0.1.jdk/Contents/HomeCLASSPATH=$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:.PATH=$JAVA_HOME/bin:$PATH:.export JAVA_HOMEexport CLASSPATHexport PATH

3、测试安装
source ~/.bash_profilejava -version

4.2. 卸载JDK14.01、查看jdk安装路径
echo $JAVA_HOMEls /Library/Java/JavaVirtualMachines/

2、删除jdk
cd /Library/Java/JavaVirtualMachines/sudo rm -rf jdk-14.0.2.jdk

注意，这里不能给jdk-14.0.2.jdk重命名而不删除，因为/usr/bin/java会自动发现最新版的jdk并关联。
3、删除环境变量编辑 ~/.bash_profile 文件，删除jdk相关配置。
4、测试卸载
java -version

参考文档：

【教程】macOS完全卸载Java开发环境
Installation of the JDK on macOS

4.3. 安装JDK1.81、下载dmg文件并安装本节选择下载jdk-8u161-macosx-x64.dmg
2、配置环境变量编辑 ~/.bash_profile 文件，添加jdk相关配置
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_161.jdk/Contents/HomeCLASSPATH=$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:.PATH=$JAVA_HOME/bin:$PATH:.export JAVA_HOMEexport CLASSPATHexport PATH

3、测试安装
source ~/.bash_profilejava -version

4.4. 卸载JDK1.81、查看jdk安装路径
echo $JAVA_HOMEls /Library/Java/JavaVirtualMachines/

2、删除jdk
cd /Library/Java/JavaVirtualMachines/sudo rm -rf jdk1.8.0_161.jdk

3、删除jdk相关文件
sudo rm -rf &quot;/Library/Internet Plug-Ins/JavaAppletPlugin.plugin&quot;sudo rm -rf &quot;/Library/PreferencePanes/JavaControlPanel.prefpane&quot;

4、删除环境变量编辑 ~/.bash_profile 文件，删除jdk相关配置。
5、测试卸载
java -version

参考文档：

【教程】macOS完全卸载Java开发环境
JDK 8 Installation for OS X

5. Ubuntu/CentOS1、下载jdk8u161
wget --no-check-certificate --no-cookies --header &quot;Cookie: oraclelicense=accept-securebackup-cookie&quot; http://download.oracle.com/otn-pub/java/jdk/8u161-b12/2f38c3b165be4555a1fa6e98c45e0808/jdk-8u161-linux-x64.tar.gz

从oracle官网下载jdk比较麻烦，可以选择从镜像站下载。
2、解压到jvm目录
mkdir -p /usr/lib/jvmtar -xzvf jdk-8u161-linux-x64.tar.gz -C /usr/lib/jvm/

3、配置JAVA_HOME和JRE_HOME，vi /etc/profile，在最后添加：
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161export JRE_HOME=$JAVA_HOME/jreexport CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATHexport PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
然后使配置文件生效，source /etc/profile。
4、测试jdk是否配置成功。
java -versionjavajavac

6. CentOS独有方法（不推荐）CentOS中安装JDK，除了可以自己下载安装包进行安装之外，还可以使用yum命令，下面是安装配置流程。
1、删除自带jdk
rpm -e --nodeps `rpm -qa | grep java`

2、查看yum库中有哪些jdk版本。
yum search java | grep jdk

3、选择java-1.8.0-openjdk-devel.x86_64 : OpenJDK Development Environment版本进行安装。
yum install java-1.8.0-openjdk-devel.x86_64

默认安装目录为/usr/lib/jvm/，其中的文件包括：
java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64java -&gt; /etc/alternatives/java_sdkjava-1.8.0 -&gt; /etc/alternatives/java_sdk_1.8.0java-1.8.0-openjdk -&gt; /etc/alternatives/java_sdk_1.8.0_openjdkjava-openjdk -&gt; /etc/alternatives/java_sdk_openjdkjre -&gt; /etc/alternatives/jrejre-1.8.0 -&gt; /etc/alternatives/jre_1.8.0jre-1.8.0-openjdk -&gt; /etc/alternatives/jre_1.8.0_openjdkjre-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64 -&gt; java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/jrejre-openjdk -&gt; /etc/alternatives/jre_openjdk

第一个是真实目录，其他的全部都是软链接，指向/etc/alternatives/目录下的文件。有意思的是，查看/etc/alternatives/目录，里面居然也是软链接，而且指回了/usr/lib/jvm/目录！
/etc/alternatives/java_sdk -&gt; /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/etc/alternatives/java_sdk_1.8.0 -&gt; /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/etc/alternatives/java_sdk_1.8.0_openjdk -&gt; /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/etc/alternatives/java_sdk_openjdk -&gt; /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/etc/alternatives/jre -&gt; /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/jre/etc/alternatives/jre_1.8.0 -&gt; /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/jre/etc/alternatives/jre_1.8.0_openjdk -&gt; /usr/lib/jvm/jre-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/etc/alternatives/jre_openjdk -&gt; /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/jre......

而且，所有的java_sdk，都指向是/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64；所有的jre，都指向/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/jre。jre_1.8.0_openjdk是个例外，也许是软件的错误。
综上，/usr/lib/jvm/目录中的java、java-1.8.0、java-1.8.0-openjdk、java-openjdk、java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64是等价的，在配置JAVA_HOME的时候可以任选其一。
4、配置全局变量
vim /etc/profile

在最后添加：
#set java environmentJAVA_HOME=/usr/lib/jvm/java-1.8.0JRE_HOME=$JAVA_HOME/jreCLASS_PATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/libPATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/binexport JAVA_HOME JRE_HOME CLASS_PATH PATH

5、让修改立即生效
source /etc/profile

6、查看安装结果
java -versionjavajavac

7. Dockerfile1、下载jdk-8u161-linux-x64.tar.gz到当前目录
2、编写Dockerfile
FROM python:3.7.10-slim-busterADD jdk-8u161-linux-x64.tar.gz /usr/lib/jvm/ENV JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161ENV JRE_HOME=$JAVA_HOME/jreENV CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATHENV PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH

3、生成包含java环境的镜像
docker built -t python:3.7.10-slim-buster-java1.8 .



]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>java</tag>
        <tag>dockerfile</tag>
      </tags>
  </entry>
  <entry>
    <title>蚁群算法</title>
    <url>/dev-aco/</url>
    <content><![CDATA[1. 来龙去脉知识工程这门课，主要讲三类算法：演化计算、群智能计算和神经计算。
演化计算，包括遗传算法、遗传规划、进化策略和进化规划。群智能计算，包括蚁群算法和粒子群算法。神经计算，包括人工神经网络和深度学习神经网络。
本文，主要介绍一下群智能计算中的蚁群算法。


什么是蚁群算法？蚁群算法（ant colony optimization， ACO），又称蚂蚁算法，是一种用来在图中寻找优化路径的机率型算法。它由Marco Dorigo于1992年在他的博士论文中提出，其灵感来源于蚂蚁在寻找食物过程中发现路径的行为。蚁群算法是一种模拟进化算法，初步的研究表明该算法具有许多优良的性质，并且现在已用于我们生活的方方面面。
2. 主要内容基本概念（最好举例说明）、蚁群算法描述、蚁群算法实例、蚁群算法特点等。（细节介绍，通常20分钟左右）
3. 应用领域利用文献法来总结蚁群算法各种领域的应用现状，把你阅读的文献（至少3篇）提前3天发到云盘中. （重点内容介绍，通常讲解20分钟左右），比如，a)蚁群算法具体如何实现的？b)性能如何？存在怎样的问题？改进方向如何？c)在哪些领域的哪些方面应用了蚁群算法？
3.1. 蚁群算法实现蚂蚁在运动过程中，会留下一种称为信息素的东西，并且会随着移动的距离，播散的信息素越来越少，所以往往在家或者食物的周围，信息素的浓度是最强的，而蚂蚁自身会根据信息素去选择方向，当然信息素越浓，被选择的概率也就越大，并且信息素本身具有一定的挥发作用。蚂蚁的运动过程可以简单归纳如下：
1、当周围没有信息素指引时，蚂蚁的运动具有一定的惯性，并有一定的概率选择其他方向。2、当周围有信息素的指引时，按照信息素的浓度强度概率性的选择运动方向。3、找食物时，蚂蚁留下家相关的A信息素，找家时，蚂蚁留下食物相关的B信息素，并随着移动距离的增加，洒播的信息素越来越少。4、随着时间推移，信息素会自行挥发。
一个简单的例子，如果现在有两条通往食物的路径，一条较长路径A，一条较短路径B，虽然刚开始A，B路径上都有蚂蚁，又因为B比A短，蚂蚁通过B花费的时间较短，随着时间的推移和信息素的挥发，逐渐的B上的信息素浓度会强于A，这时候因为B的浓度比A强，越来越多的蚂蚁会选择B，而这时候B上的浓度只会越来越强。如果蚂蚁一开始只在A上呢，注意蚂蚁的移动具有一定小概率的随机性，所以当一部分蚂蚁找到B时，随着时间的推移，蚂蚁会收敛到B上，从而可以跳出局部最优。
3.2. 性能、问题和改进问题当蚂蚁在一条路径上觅食很久时，再放置一个近的食物基本没有效果。可以理解为当一只蚂蚁找到一条路径时，过了很久的时间，大多数蚂蚁都选择了这条路径，就在这时候，突然有一只蚂蚁找到了较近的食物，但因为时间过得太久，两条路径上浓度相差太大（浓度越大，被选择的概率就越大），整个系统基本已经停滞了，陷入了局部最优。所以简单的蚂蚁系统是存在一些问题的，如：
1、搜索到一定程度，会出现停滞状态，陷入局部最优的情况。2、盲目的随机搜索，搜索时间较长。
改进解决以上问题的办法：
1、增加蚂蚁的数量。2、减小残留的信息素（所有蚂蚁都减小，部分蚂蚁减小）。3、减小蚂蚁选择信息素强的路径的概率。4、给蚂蚁和环境一定的记忆能力，减少搜索空间，从而减少搜索时间。
改进小结
1、蚂蚁数量的选择。蚂蚁数量 m 是蚁群算法的重要参数之一。蚂蚁数量多，可以提高蚁群算法的全局搜索能力以及算法的稳定性，但大量被搜索过的路径信息素变得平均，会减弱信息正反馈的作用，使搜索的随机性增强，从而降低收敛速度；反之，蚂蚁数量少，特别是当要处理的问题规模比较大时，而从未搜索过的路径信息素接近0，蚁群会倾向选择信息量大的路径，会使搜索的随机性减弱，虽然收敛速度加快，但会使算法的全局搜索能力降低，稳定性差，容易出现停滞现象（局部最优）。
2、信息启发式因子的选择。信息启发式因子α的大小反映了信息素因素作用的强度。其值越大，蚂蚁选择以前走过路径的可能性越大，搜索的随机性减弱。当α值过大时会使蚁群的搜索过早陷于局部最优；当α值较小时，搜索的随机性增强，算法收敛速度减慢。
3、期望值启发式因子的选择。期望值启发式因子β的大小反映了先验性、确定性因素作用的强度。其值越大，蚂蚁在某个局部点上选择局部最短路径的可能性越大，算法的随机性减弱，易于陷入局部最优；而β过小，将导致蚂蚁群体陷入纯粹的随机搜索，很难找到最优解。
4、信息素挥发因子的选择。信息素挥发因子 ρ 的大小直接关系到蚁群算法的全局搜索能力及其收敛速度。 ρ 就是信息素残留系数，反映了信息消逝程度。较大时，由于信息正反馈的作用占主导地位，以前搜索过的路径被再次选择的可能性过大，搜索的随机性减弱；反之，当 ρ 很小时，信息正反馈的作用相对较弱，搜索的随机性增强，因此蚁群算法收敛速度很慢。
改进方法：最大最小蚁群算法、最优最差蚁群算法、分段变异蚁群算法、排序蚁群算法、基于遗传算法的蚁群算法等。
3.3. 蚁群算法应用组合优化问题旅行商问题、0-1背包问题、加工调度问题、装箱问题、图着色问题、聚类问题、最大团问题、最大割问题。理论上每个组合优化问题都可以通过枚举的方法得到最优解，但枚举是以时间为代价的。有的枚举时间还能接受，有的则不可能接受，即所谓的“组合爆炸”。对于NP完全的组合优化问题，至今尚无很好的解析算法，一般采用启发式算法来解决。
4. 参考文档自话蚁群算法
自话粒子群算法
自话遗传算法
NP完全问题
]]></content>
      <categories>
        <category>engineering</category>
        <category>algorithm</category>
      </categories>
      <tags>
        <tag>知识工程</tag>
      </tags>
  </entry>
  <entry>
    <title>计算机网络摘要（杨鲲教授）</title>
    <url>/dev-computer-network/</url>
    <content><![CDATA[1. 前言英国essex大学杨鲲教授，给我们上了一周的计算机网络课程，收获了很多“common sense”。以下简单记录一下，备忘。


2. 三个重要概念2.1. 协议协议即约定，对等层之间的约定。
2.2. 服务相邻两层之间，下层为上层提供服务。
2.3. 地址网卡地址、IP地址、网址等等。
3. 网络分层分几层不重要，为什么分层？
拿到协议、设备、地址，先看在哪一层。
4. TCP和UDPTCP可靠，有连接。
UDP实时，没必要重传，无连接。
5. socketsocket，套接字，48位。IP（32位）+ 端口号（16位） = 48位。
6. 其他多用图片，解释原理。
大学里，最重要的是收获“common sense”。
优等生和差生，最大的区别，在于思考问题的方式。优等生不是为了研究而研究，而是为了解决问题而研究。而差生，在论文中用了很多方法，但是连问题是什么都没搞清楚。很多时候，提出问题比解决问题更重要。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
      </tags>
  </entry>
  <entry>
    <title>函数式编程之Scheme入门</title>
    <url>/dev-scheme-start/</url>
    <content><![CDATA[1. 前言Lisp是Fortran语言之后第二古老的高级编程语言，自成立之初已发生了很大变化。如今，最广为人知的通用的是Lisp方言：Common Lisp和Scheme。
Common Lisp和Scheme有什么不同呢？个人认为，Common Lisp和Scheme，就像狮子和家犬。狮子更加强大，难以驯服；家犬更加小巧，容易驯服。而选择哪一种，取决于实际需要。鉴于郝同学只是需要学习一下函数式编程的思想，所以Scheme足够了。
PS：MIT 的两本著名教材 SICP（Structure and Interpretation of Computer Programs）和  HTDP（How to Design Programs）都是以Scheme为基础的。


2. 安装mit-scheme1、进入mit-scheme官网，下载对应平台的mit-scheme（下文以windows为例）。
2、安装下载的mit-scheme（下文简称scheme）。
3、双击scheme快捷方式，如果出现如下界面，表明安装成功。
PS：如果打开scheme的时候报错“Requested allocation is too large. Try with smaller argument to –heap”。那么，在程序的快捷方式上右键，属性，在“目标”里加上--heap 512。 
4、把安装路径加入环境变量。例如，我的scheme安装路径为C:\Program Files (x86)\MIT-GNU Scheme，那么，在path中加入C:\Program Files (x86)\MIT-GNU Scheme\bin。同时，新建系统变量MITSCHEME_LIBRARY_PATH，值为C:\Program Files (x86)\MIT-GNU Scheme\lib。
5、测试scheme。打开命令行，输入mit-scheme，弹出如下界面，表明安装配置成功。
6、退出scheme。在scheme命令窗口输入(exit)，选择y，退出scheme命令窗口。
3. hello world在scheme命令窗口中输入代码非常麻烦，光标不能回退和上下移动，所以比较简单的方法就是运行已经写完的文件。
3.1. 方法一1、新建hello.scm文件，内容如下：
;The first program(begin    (display &quot;Hello, World!&quot;)    (newline))

2、运行hello.scm。mit-scheme -load hello.scm，效果如下：
PS：如果此时报错“Requested allocation is too large. Try with smaller argument to –heap”，那么，修改命令为mit-scheme --heap 128 -load hello.scm。
3.2. 方法二打开mit-scheme窗口后，(cd &quot;e:\\temp&quot;)，(load &quot;hello.scm&quot;)。
4. 基本语法4.1. 数据类型Scheme中的简单数据类型包含 boolean（布尔类型），number（数字类型），character（字符类型） 和 symbol（标识符类型）。
复合数据类型是以组合的方式通过组合其它数据类型数据来获得。包括string、vector、dotted pair、list等。
数据类型详解：http://www.kancloud.cn/wizardforcel/teach-yourself-scheme/147165
4.2. 代码结构函数式编程只用“表达式”，不用“语句”。
“表达式”（expression）是一个单纯的运算过程，总是有返回值；“语句”（statement）是执行某种操作，没有返回值。函数式编程要求，只使用表达式，不使用语句。也就是说，每一步都是单纯的运算，而且都有返回值。
原因是函数式编程的开发动机，一开始就是为了处理运算（computation），不考虑系统的读写（I/O）。”语句”属于对系统的读写操作，所以就被排斥在外。当然，实际应用中，不做I/O是不可能的。因此，编程过程中，函数式编程只要求把I/O限制到最小，不要有不必要的读写行为，保持计算过程的单纯性。
基本格式：(function_name arg0,arg1,...)
示例：
(display &quot;Hello, World!&quot;)(newline)(exit)(number? 42)(eqv? 42 42)(&gt;= 4.5 3)

迄今为止我们提供的Scheme示例程序都是s-表达式。这对所有的Scheme程序来说都适用：程序是数据。
Scheme运行一个列表形式的代码结构时，首先要检测列表第一个元素，或列表头。如果这个列表头是一个过程，则代码结构的其余部分则被当成将传递给这个过程的参数集，而这个过程将接收这些参数并运算。
如果这个代码结构的列表头是一个特殊的代码结构，则将会采用一种特殊的方式来运行。常见的特殊的代码结构有begin， define和 set!。
begin可以让它的子结构可以有序的运算，而最后一个子结构的结果将成为整个代码结构的运行结果。define会声明并会初始化一个变量。set! 可以给已经存在的变量重新赋值。
4.3. 过程（函数）定义4.3.1. 匿名过程我们已经见过了许多系统过程，比如，cons， string-&gt;list等。用户可以使用代码结构lambda来创建自定义的过程。例如，下面定义了一个过程可以在它的参数上加上2：
(lambda (x) (+ x 2))

第一个子结构，(x)，是参数列表。其余的子结构则构成了这个过程执行体。这个过程可以像系统过程一样，通过传递一个参数完成调用：
((lambda (x) (+ x 2)) 5)


4.3.2. 一般定义如果我们希望能够多次调用这个相同的过程，我们可以每次使用lambda重新创建一个复制品，但我们有更好的方式。我们可以使用一个变量来承载这个过程：
(define add2  (lambda (x) (+ x 2)))

只要需要，我们就可以反复使用add2为参数加上2：
(add2 4) =&gt;  6(add2 9) =&gt;  11
定义过程还可以有另一种简单的方式，直接用define而不使用lambda来创建：
(define (add2 x)       (+ x 2))

4.3.3. 过程的参数lambda 过程的参数由它的第一个子结构（紧跟着lambda标记的那个结构）来定义。add2是一个单参数或一元过程，所以它的参数列表是只有一个元素的列表(x)。标记x作为一个承载过程参数的变量而存在。在过程体中出现的所有x都是指代这个过程的参数。对这个过程体来说x是一个局部变量。
我们可以为两个参数的过程提供两个元素的列表做参数，通常都是为n个参数的过程提供n个元素的列表。下面是一个可以计算矩形面积的双参数过程。它的两个参数分别是矩形的长和宽。
(define area  (lambda (length breadth)    (* length breadth)))
我们看到area将它的参数进行相乘，系统过程*也可以实现相乘。我们可以简单的这样做：
(define area *)

4.4. apply过程apply过程允许我们直接传递一个装有参数的list 给一个过程来完成对这个过程的批量操作。
(define x &#x27;(1 2 3))(apply + x)

通常，apply需要传递一个过程给它，后面紧接着是不定长参数，但最后一个参数值一定要是list。它会根据最后一个参数和中间其它的参数来构建参数列表。然后返回根据这个参数列表来调用过程得到的结果。例如:
(apply + 1 2 3 x)

4.5. 顺序执行我们使用begin这个特殊的结构来对一组需要有序执行的子结构来进行打包。许多Scheme的代码结构都隐含了begin。例如，我们定义一个三个参数的过程来输出它们，并用空格间格。一种正确的定义是：
(define display3  (lambda (arg1 arg2 arg3)    (begin      (display arg1)      (display &quot; &quot;)      (display arg2)      (display &quot; &quot;)      (display arg3)      (newline))))
在Scheme中，lambda的语句体都是隐式的begin代码结构。因此，display3语句体中的begin不是必须的，不写时也不会有什么影响。
display3更简化的写法是：
(define display3  (lambda (arg1 arg2 arg3)    (display arg1)    (display &quot; &quot;)    (display arg2)    (display &quot; &quot;)    (display arg3)    (newline)))

4.6. 条件语句4.6.1. if最基本的结构就是if：
(if 测试条件    then-分支    else-分支)
如果测试条件运算的结果是真(非#f的任何其它值)，then分支将会被运行(即满足条件时的运行分支)。否则，else分支会被运行。else分支是可选的。
(define p 80)(if (&gt; p 70)     &#x27;safe    &#x27;unsafe)(if (&lt; p 90)    &#x27;low-pressure) ;no ``else&#x27;&#x27; branch

4.6.2. when 和 unless当我们只需要一个基本条件语句分支时（”then”分支或”else”分支），使用when 和 unless会更方便。
(define a 10)(define b 20)(when (&lt; a b)    (display &quot;a是&quot;)    (display a)    (display &quot;b是&quot;)    (display b)    (display &quot;a小于b&quot; ) )
先判断a是否小于b，这个条件成立时会输出倒序的5条信息。
同样的功能还可以像下面这样用unless来写(unless和when的意思正好相反)：
(define a 10)(define b 20)(unless (&gt;= a b)   (display &quot;a是&quot;)   (display a)   (display &quot;b是&quot;)   (display b)   (display &quot;a小于b&quot; ) )
并不是所有的Scheme环境都提供when和unless。如果你的Scheme中没有，你可以用宏来自定义出when和unless。
使用if实现相同的程序会是这样：
(define a 10)(define b 20)(if (&lt; a b)    (begin        (display &quot;a是&quot;)        (display a)        (display &quot;b是&quot;)        (display b)        (display &quot;a小于b&quot; ) ))
先判断a是否小于b，这个条件成立时会输出正序的5条信息。
在mit-scheme下，中文会显示乱码，为方便查看，建议修改成英文。
4.6.3. cond多重if结构为：
(define c #\c)(if (char&lt;? c #\c) -1    (if (char=? c #\c) 0        1))
这样的结构都可以使用cond来这样写：
(cond ((char&lt;? c #\c) -1)    ((char=? c #\c) 0)    (else 1))
cond就是这样的一种多分支条件结构。每个从句都包含一个判断条件和一个相关的操作。第一个判断成立的从句将会引发它相关的操作执行。如果任何一个分支的条件判断都不成立则最后一个else分支将会执行(else分支语句是可选的)。
cond的分支操作都是begin结构。
4.6.4. case当cond结构的每个测试条件是一个测试条件的分支条件时，可以缩减为一个case表达式。
(define c #\c)(case c    ((#\a) 1)    ((#\b) 2)    ((#\c) 3)    (else 4))
分支头值是#\c 的分支将被执行。
4.6.5. and 和 orScheme提供了对boolean值进行逻辑与and和逻辑或or运算的结构。(我们已经见过了布尔类型的求反运算not过程。)
当所有子结构的值都是真时，and的返回值是真，实际上，and的运行结果是最后一个子结构的值。如果任何一个子结构的值都是假，则返回#f。
(and 1 2)  =&gt;  2(and #f 1) =&gt;  #f
而or会返回它第一个为值为真的子结构的结果。如果所有的子结构的值都为假，or则返回#f。
(or 1 2)  =&gt;  1(or #f 1) =&gt;  1
and和or都是从左向右运算。当某个子结构可以决定最终结果时，and和or会忽略剩余的子结构，即它们是“短路”的。
(and 1 #f expression-guaranteed-to-cause-error)(or 1 #f expression-guaranteed-to-cause-error)

4.7. 词法变量Scheme的变量有一定的词法作用域，即它们在程序代码中只对特定范围的代码结构可见。
(define x 9)(define add2 (lambda (x) (+ x 2)))(add2 3)(add2 x) 
这里有一个全局变量x，值一直为9；还有一个局部变量x，就是在过程add2中那个字母x。
而set!代码结构可修改变量的赋值。
(set! x 20)
上面代码将全局变量x的值9修改为20，因为对于set!全局变量是可见的。如果set!是在add2过程体内被调用，那修改的就是局部变量x：
(define add2  (lambda (x)    (set! x (+ x 2))    x))
这里set!在局部变量x上加上2，并且会返回局部变量x的新值。(从结果来看，我们无法区分这个过程和先前的add2过程)。
(define counter 0)(define bump-counter  (lambda ()    (set! counter (+ counter 1))    counter))
bump-counter是一个没有参数的过程(没有参数的过程也称作thunk)。 它没有引入局部变量和参数，这样就不会隐藏任何值。在每次调用时，它会修改全局变量counter的值，让它增加1，然后返回它当前的值。下面是一些bump-counter的成功调用示例:
(bump-counter) =&gt;  1(bump-counter) =&gt;  2(bump-counter) =&gt;  3

4.7.1. let 和 let*并不是一定要显式的创建过程才可以创建局部变量。有个特殊的代码结构let可以创建一列局部变量以便在其结构体中使用:
(let ((x 1)      (y 2)      (z 3))    (list x y z))=&gt;  (1 2 3)
和lambda一样，在let结构体中，局部变量x（赋值为1）会暂时隐藏全局变量x（赋值为20）。
局部变量x、y、z分别被赋值为1、2、3，这个初始化的过程并不作为let过程结构体的一部分。因此，在初始化时对x的引用都指向了全局变量x，而不是局部变量x。
(let ((x 1)      (y x))  (+ x y))=&gt;  21

在初始化区域中，可以用先创建的变量来为后创建的变量赋值，let*结构就可以这样做：
(let* ((x 1)       (y x))  (+ x y))=&gt;  2

这个例子完全等价于下面这个let嵌套的程序，更深了说，实际上就是let嵌套的缩写。
(let ((x 1))  (let ((y x))    (+ x y)))=&gt;  2

我们也可以把一个过程做为值赋给变量：
(let ((cons (lambda (x y) (+ x y))))  (cons 1 2))=&gt;  3
在这个let构结体中，变量cons将它的参数进行相加。而在let结构的外面，cons还是用来创建点对。
4.7.2. fluid-let一个词法变量如果没有被隐藏，在它的作用域内一直都为可见状态。有时候，我们有必要将一个词法变量临时的设置为一个固定的值，为此我们可使用fluid-let结构。
(fluid-let ((counter 99))  (display (bump-counter)) (newline)  (display (bump-counter)) (newline)  (display (bump-counter)) (newline))

这和let看起来非常相像，但并不是暂时的隐藏了全局变量counter的值，而是在fluid-let执行体中临时的将全局变量counter的值设置为了99直到执行体结束。因此执行体中的三句display产生了结果
100 101 102 

当fluid-let表达式计算结束后，全局变量counter会恢复成之前的的值。
注意fluid-let和let的效果完全不同。fluid-let不会和let一样产生一个新的变量。它会修改已经存的变量的值绑定，当fluid-let结束时这个修改也会结束。
4.8. 递归一个过程体中可以包含对其它过程的调用，特别的是也可以调用自己。
(define factorial (lambda (n)    (if (= n 0) 1        (* n (factorial (- n 1))))))
这个递归过程用来计算一个数的阶乘。如果这个数是0，则结果为1。对于任何其它的值n，这个过程会调用其自身来完成n-1阶乘的计算，然后将这个子结果乘上n并返回最终产生的结果。
互递归过程也是可以的。下面判断奇偶数的过程相互进行了调用。
(define is-even? (lambda (n)    (if (= n 0) #t        (is-odd? (- n 1)))))(define is-odd? (lambda (n)    (if (= n 0) #f        (is-even? (- n 1)))))
这里提供的两个过程的定义仅作为简单的互递归示例。Scheme已经提供了简单的判断过程even?和odd?。
4.8.1. letrec如果希望将上面的过程定义为局部的，我们需要使用letrec结构。
(letrec ((local-even? (lambda (n)                        (if (= n 0) #t                            (local-odd? (- n 1)))))         (local-odd? (lambda (n)                       (if (= n 0) #f                           (local-even? (- n 1)))))) (list (local-even? 23) (local-odd? 23)))
用letrec创建的词法变量不仅可以在letrec执行体中可见而且在初始化中也可见。letrec是专门为局部的递归和互递归过程而设置的。(这里也可以使用define来创建两个子结构的方式来实现局部递归)
4.8.2. 命名let使用letrec定义递归过程可以实现循环。如果我们想显示10到1的降数列，可以这样写：
(letrec ((countdown (lambda (i)                      (if (= i 0) &#x27;liftoff                          (begin                            (display i)                            (newline)                            (countdown (- i 1))))))) (countdown 10))
这会在控制台上输出10到1，并会返回结果liftoff。
Scheme允许使用一种叫“命名let”的let变体来更简洁的写出这样的循环:
(let countdown ((i 10)) (if (= i 0) &#x27;liftoff      (begin        (display i)        (newline)        (countdown (- i 1)))))
注意在let的后面立即声明了一个变量用来表示这个循环。这个程序和先前用letrec写的程序是等价的，可以将“命名let”看成一个对letrec结构进行扩展的宏。
4.8.3. 迭代上面定义的countdown函数事实上是一个递归的过程。Scheme只有通过递归才能定义循环，不存在特殊的循环或迭代结构。
尽管如此，上述定义的循环是一个“真”循环，与其他语言实现它们的循环的方法完全相同。也就是说，Scheme十分注意确保上面使用过的递归类型不会产生过程调用/返回开销。
Scheme通过一种消除尾部调用（tail-call elimination）的过程完成这个功能。如果你注意观察countdown的步骤，你会注意到当递归调用出现在countdown主体内时，就变成了“尾部调用”，或者说是最后完成的事情——countdown的每次调用要么不调用它自身，要么当它调用自身时把这个动作留在最后。对于一个Scheme语言的实现来说（解释器），这会使递归不同于迭代。因此，尽管用递归去写循环吧，这是安全的。
这是又一个有用的尾递归程序的例子：
(define list-position  (lambda (o l)    (let loop ((i 0) (l l))      (if (null? l) #f          (if (eqv? (car l) o) i              (loop (+ i 1) (cdr l)))))))

list-position发现了o对象在列表l中第一次出现的索引。如果在列表中没有发现对象，过程将会返回#f。
这又是一个尾部递归过程，它将自身的参数列表就地反转，也就是使现有的列表内容产生变异，而没有分配一个新的列表：
(define reverse!  (lambda (s)    (let loop ((s s) (r &#x27;()))      (if (null? s) r      (let ((d (cdr s)))            (set-cdr! s r)        (loop d s))))))
reverse!是一个十分有用的过程，它在很多Scheme方言中都能使用。
4.8.4. 用自定义过程映射整个列表有一种特殊类型的迭代，对列表中每个元素，它都会重复相同的动作。Scheme为这种情况提供了两种程序：map和for-each。
map程序为给定列表中的每个元素提供了一种既定程序，并返回一个结果的列表。例如：
(map add2 &#x27;(1 2 3))=&gt;  (3 4 5)

for-each程序也为列表中的每个元素提供了一个程序，但返回值为空。
(for-each display  (list &quot;one &quot; &quot;two &quot; &quot;buckle my shoe&quot;))

这个由map和for-each用在列表上的程序并不一定是单参数程序。举例来说，假设一个n参数的程序，map会接受n个列表，每个列表都是由一个参数所组成的集合，而map会从每个列表中取相应元素提供给程序。例如：
(map cons &#x27;(1 2 3) &#x27;(10 20 30))=&gt;  ((1 . 10) (2 . 20) (3 . 30))(map + &#x27;(1 2 3) &#x27;(10 20 30))=&gt;  (11 22 33)

4.9. 输入输出Scheme的输入/输出程序，可以从输入端口读取或者写入到输出端口。端口可以关联到控制台，文件和字符串。
4.9.1. 读取Scheme的读取程序带有一个可选的输入端口参数。如果端口没有特别指定，则默认为当前端口（一般是控制台）。
读取的内容可以是一个字符，一行数据或是S表达式。当每次执行读取时，端口的状态就会改变，因此下一次就会读取当前已读取内容后面的内容。如果没有更多的内容可读，读取程序将返回一个特殊的数据（文件结束符或EOF对象），这个对象只能用eof-object?函数来判断。
read-char程序会从端口读取下一个字符。read-line程序会读取下一行数据，并返回一个字符串（不包括最后的换行符），read程序则会读取下一个S表达式。
4.9.2. 写入Scheme的写入程序接受一个要被写入的对象和一个可选的输出端口参数。如果未指定端口，则默认为当前端口（一般为控制台）。
写入的对象可以是字符或是S表达式。
write-char程序可以向输出端口写入一个给定的字符（不包括#\）。write和display程序都可以向端口写入一个给定的S表达式，唯一的区别是：write程序会使用机器可读型的格式而display程序却不用。例如，write用双引号表示字符串，用#\句法表示字符，但display却不这么做。
newline程序会在输出端口输出一个换行符。
4.9.3. 文件端口如果端口是标准的输入和输出端口，Scheme的I/O程序就不需要端口参数。但是，如果你明确需要这些端口，则current-input-port和current-output-port这些零参数程序会提供这个功能，例如：
(display 9)(display 9 (current-output-port))
拥有相同的效果。
一个端口通过打开文件和这个文件关联在一起。open-input-file程序会接受一个文件名作为参数，并返回一个和这个文件关联的新的输入端口。open-output-file程序会接受一个文件名作为参数，并返回一个和这个文件关联的新的输出端口。如果打开一个不存在的输入文件，或者打开一个已经存在的输出文件，程序都会出错。
已经在一个端口执行完输入或输出后，需要使用close-input-port或close-output-port程序将它关闭。
在下述例子中，假如文件io.txt文件只包含一个单词hello。
(define i (open-input-file &quot;io.txt&quot;))(read-char i)(define j (read i))j

假如文件greeting.txt在下述程序运行前不存在：
(define o (open-output-file &quot;greeting.txt&quot;))(display &quot;hello&quot; o)(write-char #\space o)(display &#x27;world o)(newline o)(close-output-port o)
现在greeting.txt文件将会包含“hello world”。
文件端口的自动打开和关闭
Scheme提供了call-with-input-file和call-with-output-file过程，这些过程会照顾好打开的端口并在你使用完后将端口关闭。
call-with-input-file程序接受一个文件名参数和一个过程。这个过程被应用在一个已打开的文件输入端口。当程序结束时，它的结果会在保证端口关闭后返回。
(call-with-input-file &quot;io.txt&quot;  (lambda (i)    (let* ((a (read-char i))           (b (read-char i))           (c (read-char i)))      (list a b c))))=&gt;  (#\h #\e #\l)
call-with-output-file程序会对输出文件提供类似的服务。
4.9.4. 字符串端口一般来说将字符串与端口相关联是很方便的。因此，open-input-string程序将一个给定的字符串和一个端口关联起来。读取这个端口的程序将读出下述字符串：
(define i (open-input-string &quot;hello world&quot;))(read-char i)=&gt;  #\h(read i)=&gt;  ello(read i)=&gt;  world
open-output-string创建了一个输出端口，最终可以用于创建一个字符串：
(define o (open-output-string))(write &#x27;hello o)(write-char #\, o)(display &quot; &quot; o)(display &quot;world&quot; o)
现在你可以使用get-output-string程序得到保留在字符串端口o中的字符串：
(get-output-string o)=&gt;  &quot;hello, world&quot;
字符串端口不需要显式地去关闭。
4.9.5. 加载文件load程序可以加载包含Scheme代码的文件，load一个文件意味着按顺序求值文件中每一个Scheme表达式。load中的路径参数是相对当前Scheme工作目录计算的，该工作目录一般是调用Scheme可执行文件时的目录。
一个文件可以加载其他的文件，这在包含许多文件的大项目中十分有用。但是，除非使用绝对路径，否则load参数中的文件位置将依赖于执行Scheme的当前目录。而提供绝对路径名并不是很方便，因为我们更愿意把项目文件作为一个单元（保留它们的相对路径名）在很多不同机器中运行。
4.10. 宏在Scheme中，对宏的处理与C语言类似，也分为两步：第一步是宏展开，第二步则是编译展开之后的代码。这样，通过宏和基本的语言构造，可以对Scheme语言进行扩展——C语言的宏则不具备扩展语言的能力。
宏通过define-macro来定义。例如，如果你的Scheme缺少条件表达式when，你就可以以下述宏定义when：
(define-macro when  (lambda (test . branch)    (list &#x27;if test      (cons &#x27;begin branch))))
这样定义的when转换器能够把一个when表达式转换为等价的if表达式。
(when (&lt; (pressure tube) 60)   (open-valve tube)   (attach floor-pump tube)   (depress floor-pump 5)   (detach floor-pump tube)   (close-valve tube))
上面的when表达式，在宏展开后为：
(apply  (lambda (test . branch)    (list &#x27;if test      (cons &#x27;begin branch)))  &#x27;((&lt; (pressure tube) 60)      (open-valve tube)      (attach floor-pump tube)      (depress floor-pump 5)      (detach floor-pump tube)      (close-valve tube)))

这个转换产生了一个列表：
(if (&lt; (pressure tube) 60)    (begin      (open-valve tube)      (attach floor-pump tube)      (depress floor-pump 5)      (detach floor-pump tube)      (close-valve tube)))

Scheme将会对这个表达式进行求值，就像它对其他表达式所做的一样。
再来看unless的宏定义：
(define-macro unless  (lambda (test . branch)    (list &#x27;if          (list &#x27;not test)          (cons &#x27;begin branch)))) 

另外，我们可以调用when放进unless定义中：
(define-macro unless  (lambda (test . branch)    (cons &#x27;when          (cons (list &#x27;not test) branch))))
宏表达式可以引用其他的宏。
4.11. 结构我们可以使用Scheme提供的复合数据结构（如向量和列表），来表示一种“结构”。例如：我们正在处理与树木相关的一组数据。数据中的元素包括：高度，周长，年龄，树叶形状和树叶颜色共5个字段。这样的数据可以表示为5元向量，这些字段可以利用vector-ref访问，或使用vector-set!修改。
我们使用Scheme的宏defstruct去定义一个结构，基本上你可以把它当作一种向量，不过它提供了很多方法诸如创建结构实例、访问或修改它的字段等等。因此，我们的树结构应这样定义：
(defstruct tree height girth age leaf-shape leaf-color)
这样它自动生成了一个名为make-tree的构造过程，以及每个字段的访问方法，命名为tree.height，tree.girth等等。构造方法的使用方法如下：
(define coconut   (make-tree &#x27;height 30             &#x27;leaf-shape &#x27;frond             &#x27;age 5))
这个构造函数的参数以成对的形式出现，字段名后面紧跟着其初始值。这些字段能以任意顺序出现，或者不出现——如果字段的值没有定义的话。
访问过程的调用如下所示：
(tree.height coconut) =&gt;  30(tree.leaf-shape coconut) =&gt;  frond(tree.girth coconut) =&gt;  &lt;undefined&gt;
tree.girth存取程序返回一个未定义的值，因为我们没有为coconut这个tree结构指定girth的值。
修改过程的调用如下所示：
(set!tree.height coconut 40)(set!tree.girth coconut 10)
如果我们现在重新调用访问过程去访问这些字段，我们会得到新的值：
(tree.height coconut) =&gt;  40(tree.girth coconut) =&gt;  10

4.11.1. 默认初始化我们可以在定义结构时进行一些初始化的设置，而不是在每个实例中都进行初始化。因此，我们假定leaf-shape和leaf-color在默认情况下分别为frond和green。我们可以在调用make-tree时通过显式的初始化来覆盖掉这些默认值，或者在创建一个结构实例后使用上面提到的字段修改过程：
(defstruct tree height girth age                (leaf-shape &#x27;frond)                (leaf-color &#x27;green))(define palm (make-tree &#x27;height 60))(tree.height palm) =&gt;  60(tree.leaf-shape palm) =&gt;  frond(define plantain   (make-tree &#x27;height 7             &#x27;leaf-shape &#x27;sheet))(tree.height plantain) =&gt;  7(tree.leaf-shape plantain) =&gt;  sheet(tree.leaf-color plantain) =&gt;  green

4.12. 关联表关联表是Scheme一种特殊形式的列表。列表的每一个元素都是一个点对，其中的car（左边的元素）被称为键，cdr（右边的元素）被称为和该键关联的值。例如：
((a . 1) (b . 2) (c . 3))

4.13. 系统接口Scheme程序经常需要与底层操作系统进行交互。
4.13.1. 检查和删除文件file-exists?会检查它的参数字符串是否是一个文件。delete-file接受一个文件名字符串作为参数并删除相应的文件。这些程序并不是Scheme标准的一部分，但是在大多数Scheme实现中都能找到它们。用这些过程操作目录（而不是文件）并不是很可靠，操作结果与具体的Scheme实现有关。
file-or-directory-modify-seconds过程接受一个文件名或目录名为参数，并返回这个目录或文件的最后修改时间。时间是从格林威治标准时间1970年1月1日0点开始记时的。例如：
(file-or-directory-modify-seconds &quot;hello.scm&quot;)=&gt;  893189629
假定hello.scm文件最后一次修改的时间是1998年4月21日的某个时间。
4.13.2. 调用操作系统命令system函数把它的参数字符串当作操作系统命令来执行。如果命令成功执行并返回0，则它会返回真，如果命令执行失败并返回某非0值，则它会返回假。命令产生的任何输出都会进入标准的输出。
(system &quot;ls&quot;);lists current directory(define fname &quot;spot&quot;)(system (string-append &quot;test -f &quot; fname)) ;tests if file `spot&#x27; exists(system (string-append &quot;rm -f &quot; fname)) ;removes `spot&#x27;
最后两个命令等价于：
(file-exists? fname)(delete-file fname)

4.13.3. 环境变量过程getenv返回操作系统环境变量的设定值，如：
(getenv &quot;HOME&quot;)=&gt;  &quot;/home/dorai&quot;(getenv &quot;SHELL&quot;)=&gt;  &quot;/bin/bash&quot;


4.14. 对象和类http://www.kancloud.cn/wizardforcel/teach-yourself-scheme/147175
5. 源码分享https://github.com/voidking/scheme-start.git
6. 书签函数式编程初探
函数式编程入门教程
Lisp教程
Scheme语言简明教程
MIT/GNU Scheme Documentation
Racket官网
符号: 抽象、语义
编程语言的基石——Lambda calculus
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>函数式编程</tag>
        <tag>形式语义学</tag>
        <tag>lisp</tag>
        <tag>scheme</tag>
      </tags>
  </entry>
  <entry>
    <title>遗传算法解决TSP问题</title>
    <url>/dev-matlab-ga-tsp/</url>
    <content><![CDATA[1. TSP问题旅行商问题，即TSP问题（Travelling Salesman Problem）又译为旅行推销员问题、货郎担问题，是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市，他必须选择所要走的路径，路径的限制是每个城市只能拜访一次，而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。


2. 设计思路1、随机生成N的位置（坐标），把坐标点绘制到页面上。2、设置种群规模为M，每个个体生成1到N的随机序列。3、计算每个个体的适应度（路径总长度）。4、寻找出当前迭代的最优解，把最优解路线绘制到页面上。5、交叉，变异，生成新的一代种群。6、重复3-5，直到迭代次数到达指定次数。
3. 对比实验TSP问题的解法，还有回溯法、分支限界法、近似算法、动态规划法、蚁群算法等。对比遗传算法和其他算法求解TSP问题的数据量、数据分布以及实验结果（求解时间等），找出遗传算法的优缺点，适用于数据量多少，数据分布是什么情况。
然而，工作量很大，暂时搁置，完成后也能写篇不错的论文了吧。
4. 源码分享https://github.com/voidking/TSP_MATLAB.githttps://github.com/voidking/GA4TSPProblem.git
]]></content>
      <categories>
        <category>engineering</category>
        <category>algorithm</category>
      </categories>
      <tags>
        <tag>知识工程</tag>
        <tag>matlab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：CentOS安装多版本MySQL</title>
    <url>/dev-centos-multiple-mysql/</url>
    <content><![CDATA[1. 前言卓音工作室的服务器中，已经安装了MySQL5.7。但是，confluence和jira不支持MySQL5.7，为了安装这两款软件，必须安装MySQL5.6或更低的版本。
本文中，我们会在已经具备MySQL5.7的机器上，编译安装MySQL5.6，让这两个版本的MySQL同时存在。
参考文档：

MySQL之——Centos中安装多个mysql数据库的配置实例
centos6.5 MySQL多实例安装与配置
centos编译安装mysql 5.6及安装多个mysql实例 
centos7安装运行多个mysql实例笔记



2. 编译安装MySQL2.1. 安装编译工具yum -y install wget gcc-c++ gcc* \  perl ncurses ncurses-devel \  bison cmake make \  openssl openssl-devel curl-devel \  libxml2 libxml2-devel \  libjpeg* libpng* freetype* \  --setopt=protected_multilib=false

2.2. 下载MySQL源码搜狐镜像：http://mirrors.sohu.com/mysql/MySQL-5.6/选择下载：mysql-5.6.34.tar.gz
2.3. 准备用户和目录1、创建一个Mysql用户
useradd mysql

2、新建data和log子目录
mkdir -p /usr/local/mysql&#123;3306,3307&#125;/data            mkdir -p /usr/local/mysql&#123;3306,3307&#125;/log  

3、修改目录的所属者以及所属组权限 
chown -R mysql:mysql /usr/local/mysql&#123;3306,3307&#125;/data/  chown -R mysql:mysql /usr/local/mysql&#123;3306,3307&#125;/log/  chmod 750 /usr/local/mysql&#123;3306,3307&#125;/data        chmod 750 /usr/local/mysql&#123;3306,3307&#125;/log  

4、创建mysql相关目录并配置权限
mkdir -p /usr/local/mysql&#123;3306,3307&#125;/etc  chown -R mysql.mysql /usr/local/mysql&#123;3306,3307&#125;/etc  mkdir -p /var/run/mysqld&#123;3306,3307&#125;  chown -R mysql.mysql /var/run/mysqld&#123;3306,3307&#125;  mkdir -p /var/lib/mysqld&#123;3306,3307&#125;  chown -R mysql.mysql /var/lib/mysqld&#123;3306,3307&#125;

5、创建mysql.sock文件
touch /tmp/mysql&#123;3306,3307&#125;.sock

2.4. 编译安装MySQL1、解压MySQL源码
tar -zxvf mysql-5.6.34.tar.gzcd mysql-5.6.34

2、配置编译
cmake \  -DCMAKE_INSTALL_PREFIX=/usr/local/mysql3307 \  -DMYSQL_DATADIR=/usr/local/mysql3307/data \  -DSYSCONFDIR=/usr/local/mysql3307/etc \  -DWITH_MYISAM_STORAGE_ENGINE=1 \  -DWITH_INNOBASE_STORAGE_ENGINE=1 \  -DWITH_MEMORY_STORAGE_ENGINE=1 \  -DWITH_READLINE=1 \  -DMYSQL_TCP_PORT=3307 \  -DENABLED_LOCAL_INFILE=1 \  -DWITH_PARTITION_STORAGE_ENGINE=1 \  -DEXTRA_CHARSETS=all \  -DDEFAULT_CHARSET=utf8 \  -DDEFAULT_COLLATION=utf8_general_ci \  -DMYSQL_USER=mysql \  -DWITH_DEBUG=0 \  -DWITH_SSL=system

3、编译安装
makemake install
这一步时间较长，请耐心等待。
3. 配置MySQL1、拷贝模板，作为MySQL服务的配置文件
cp /usr/local/mysql3307/support-files/my-default.cnf /usr/local/mysql3307/etc/my.cnf

2、修改my.cnf
# For advice on how to change settings please see# http://dev.mysql.com/doc/refman/5.6/en/server-configuration-defaults.html# *** DO NOT EDIT THIS FILE. It&#x27;s a template which will be copied to the# *** default location during install, and will be replaced if you# *** upgrade to a newer version of MySQL.[mysqld]# Remove leading # and set to the amount of RAM for the most important data# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.# innodb_buffer_pool_size = 128M# Remove leading # to turn on a very important data integrity option: logging# changes to the binary log between backups.# log_bin# These are commonly set, remove the # and set as required.basedir = /usr/local/mysql3307datadir = /usr/local/mysql3307/dataport = 3307# server_id = .....socket = /tmp/mysql3307.sock# Remove leading # to set options mainly useful for reporting servers.

3、初始化mysql数据库。
/usr/local/mysql3307/scripts/mysql_install_db \  --basedir=/usr/local/mysql3307 \  --datadir=/usr/local/mysql3307/data \  --user=mysql

4. 配置MySQL开机自启动参考文档《CentOS7中安装配置Nginx》
5. 启动MySQL1、添加mysql被service命令管理
cp /usr/local/mysql3307/support-files/mysql.server /etc/init.d/mysqld3307

2、启动mysql
service mysqld3307 start

3、设置超级管理员密码
/usr/local/mysql3307/bin/mysqladmin -u root password voidking -S /tmp/mysql3307.sock

4、登录mysql
mysql -u root -p -S /tmp/mysql3307.sock

输入密码voidking，成功登录mysql5.6。
至此，多版本mysql安装配置成功！
6. 远程连接1、关闭防火墙
systemctl stop firewalld.service

2、授权远程访问
mysql&gt; grant all privileges on *.* to &#x27;root&#x27;@&#x27;%&#x27; identified by &#x27;voidking&#x27; with grant option;

3、远程连接测试方法一：使用navicat等图形化工具。方法二：使用mysql命令，例如 mysql -h 192.168.56.101 -P 3307 -u root -pvoidking
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>mysql</tag>
        <tag>好好学MySQL</tag>
      </tags>
  </entry>
  <entry>
    <title>KISSY入门篇</title>
    <url>/dev-kissy-start/</url>
    <content><![CDATA[1. WHAT IS KISSY ?KISSY 是一款跨终端、模块化、高性能、使用简单的 JavaScript 框架。除了完备的工具集合如 DOM、Event、Ajax、Anim 等，它还提供了经典的面向对象、动态加载、性能优化解决方案。作为一款全终端支持的 JavaScript 框架，KISSY 为移动终端做了大量适配和优化，让你的程序在全终端均能流畅运行。

2. 引入KISSY1、引入线上kissy。
&lt;script src=&quot;http://g.alicdn.com/kissy/k/1.4.7/seed.js&quot;&gt;&lt;/script&gt;

2、引入本地kissy。
&lt;script src=&quot;./lib/kissy-1.3.2/build/seed.js&quot;&gt;&lt;/script&gt;

3. hello world1、启动：Hello World!
KISSY.ready(function(S)&#123;    alert(&#x27;Hello World!&#x27;);&#125;);

2、DOM操作：获取一个className叫continue的button，并将它的内容改为”Hello Kissy”。
KISSY.use(&#x27;node&#x27;,function(S,Node)&#123;    Node.one(&#x27;button.continue&#x27;).html(&#x27;Hello Kissy!&#x27;);&#125;);

3、事件处理：点击一个id为click-me的button，显示#banner-msg的内容。
KISSY.use(&#x27;node&#x27;,function(S,Node)&#123;    Node.one(&#x27;#click-me&#x27;).on(&#x27;click&#x27;,function(e)&#123;        Node.one(&#x27;#banner-msg&#x27;).show();    &#125;);&#125;);

4、Ajax：请求本地数据/data/ajax-data.json，带入参数zipcode，将结果显示在#weather-con中。
KISSY.use(&#x27;io,node&#x27;,function(S,io,Node)&#123;    io(&#123;        url:&#x27;./data/ajax-data.json&#x27;,        data:&#123;            zipcode:10010        &#125;,        success:function(data)&#123;            Node.one(&#x27;#weather-con&#x27;).html(&#x27;&lt;em&gt;&#x27; + data.weather + &#x27;&lt;/em&gt; 摄氏度&#x27;);        &#125;    &#125;);&#125;);


4. 源码分享https://github.com/voidking/kissy-start.git
5. 书签KISSY - A Powerful JavaScript Frameworkhttp://docs.kissyui.com/
KISSY项目地址https://github.com/kissyteam/kissy
KISSY 指导手册http://docs.kissyui.com/1.4/docs/html/guideline/get-started.html
jQuery - KISSY Rosetta Stonehttp://cyj.me/jquery-kissy-rosetta/
KISSY 模块定义规范（KMD）http://docs.kissyui.com/1.4/docs/html/guideline/kmd.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>jira简单使用</title>
    <url>/dev-jira-start/</url>
    <content><![CDATA[1. 前言本文记录jira的一些常用操作，备忘。
参考文档：JIRA入门教程


2. jira重要子目录参考文档：Jira中重要的目录和文件
data：该目录包含 JIRA 实例的应用数据，包括附件（存储在 JIRA 中的每个附件的每个版本）。
export：JIRA 将其自动备份档案放置在此目录中。
log：JIRA 将其日志放置在此目录中。（注意：如果未配置 JIRA 主目录，则日志会置于当前正在使用的目录中）。
plugins：该目录是存储 Atlassian Plugin Framework 2 上构建的插件（即“Plugins 2”插件）的位置。如果您要安装新的“Plugins 2”插件，则需要将此插件部署到此目录的 installed-plugins 子目录中。
caches：该目录可供 JIRA 存储缓存：Lucene 索引 和 OSGi 框架缓存，这些文件对 JIRA 性能至关重要，一定不能在 JIRA 运行时从外部修改或移除这些文件。
tmp：针对各种运行时功能（如导出、导入、文件上传和索引）创建的临时内容均存储在此目录中。您可以在 JIRA 运行过程中删除此目录的文件，但我们建议在更改此目录的内容之前先关闭 JIRA。
3. 新建项目1、项目，创建项目，Scrum开发方法，下一步，选择。2、输入名称和关键字，其中关键字会成为项目中每个问题的前缀，提交。3、项目设置，问题类型，操作，编辑问题类型，移除或添加问题类型。4、项目设置，问题类型，界面，移除或添加字段，其中模块字段必须保留。5、模块，输入模块名称（Default）、说明、选择默认经办人，添加。6、新建，创建问题，测试创建问题。
4. 新建sprintsprint，全速短跑。新建sprint，也就是新建一轮迭代。1、在顶部导航栏选择项目，打开项目。2、单击左侧导航栏Backlog（积压未办之事，存货），打开Backlog页面。3、单击“create sprint”按钮，创建sprint。4、单击新建的sprint名称，重命名该sprint。
5. 添加issuesissues包括Task、Story、Bug、Epic，常用Task和Bug。
1、在backlog页面，把issues拉入新建的sprint。2、单击任意issue，右侧显示issue详情（此处可以选择把该issue分配给自己）。
6. 更新issue1、单击左侧导航栏Active sprints，打开active sprints页面。2、在active sprints页面，可以看到issues进度，并且可以改变issue进度。
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>jira</tag>
      </tags>
  </entry>
  <entry>
    <title>wiki简单使用</title>
    <url>/dev-wiki-start/</url>
    <content><![CDATA[1. 创建空间1、导航栏点击空间，创建空间，选择团队空间。2、填入空间信息，创建。


2. 新建页面1、进入空间后，导航栏点击创建，然后新建一个页面。2、进入一个页面，导航栏点击创建，然后新建一个页面。
3. 切换主题1、导航栏选择空间，空间目录。2、点击空间名称后面的小圆圈（Space Details），进入空间管理页面。或者在进入空间后点击右上角工具，以层级方式查看。3、点击外观，主题，然后选择Documentation Theme，确认。
4. 页面树1、进入空间后，点击右上角编辑。2、删除不需要的模块，保留需要的模块。3、插入，其他宏，导航，页面树。4、配置页面树，插入。
5. 重建索引有时候，进入空间目录，却看不到我们新建的空间，这是因为索引没有更新。1、导航栏单击管理（右上角齿轮），一般配置。2、下拉在左边导航栏找到管理，内容索引。3、单击重新创建，等待片刻即可。
6. wiki转邮件有时候，编辑好了wiki页面，然后想作为邮件发送。但是，如果直接复制粘贴到outlook，会发现格式全部都乱了。比较好的做法是导出为word，打开word，复制粘贴到outlook。
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>wiki</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：MySQL问题记录</title>
    <url>/dev-mysql-problem/</url>
    <content><![CDATA[1. 前言本文记录使用MySQL过程中遇到的一些问题，备忘。


2. ERROR 2002 (HY000)2.1. 问题描述mysql -u root -p，然后输入密码，登录mysql，报错如下：
ERROR 2002 (HY000): Can&#x27;t connect to local MySQL server through socket &#x27;/tmp/mysql.sock&#x27; (2)

2.2. 排查解决参考文档：MySQL错误ERROR 2002 (HY000)
1、查看mysql.sock文件位置find / -name mysql.sock，结果为：/data/db/3306/mysql.sock
和报错中的/tmp/mysql.sock不一致。
在/tmp下没有mysql.sock文件，依次执行touch mysql.sock，chmod 666 mysql.sock。
重启mysql，service mysqld restart，再次登录mysql，报错如下：
ERROR 2002 (HY000): Can&#x27;t connect to local MySQL server through socket &#x27;/tmp/mysql.sock&#x27; (111)

2、找到mysql配置文件ps aux | grep mysql，在结果中找到：
--defaults-file=/opt/mysql/my.cnf

3、查看mysql.sock配置more /opt/mysql/my.cnf | grep sock，结果如下：
socket=/data/db/3306/mysql.socksocket = /data/db/3306/mysql.sock

4、修改my.cnfvim /opt/mysql/my.cnf，修改两个socket如下：
socket = /tmp/mysql.socksocket = /tmp/mysql.sock

重启mysql，再次登录，成功！
3. Lost connection to MySQL server during query3.1. 问题描述Python使用tortoise-orm库连接MySQL，偶尔报错：
2013, &#x27;Lost connection to MySQL server during query ([Errno 104] Connection reset by peer)&#x27;

3.2. 排查解决这是报错，是因为mysql自动断开了长时间没有任何动作的连接。
解决办法：使用pool_recycle参数，并且小于wait_timeout和interactive_timeout
原理：mysql根据wait_timeout和interactive_timeout的配置，自动断开长时间没有任何动作的连接。
tortoise-orm的pool_recycle参数可以避免MySQL自动断开长时间没有任何动作的连接，是因为它会定期检查连接的有效性，并在超过指定时间后关闭并重新打开连接。这样，就可以保证连接池中的连接都是新鲜的，不会因为MySQL服务器的超时设置而被断开。
pool_recycle应该小于wait_timeout和interactive_timeout。
MySQL配置timeout的方法：1、查看wait_timeout和interactive_timeout
show global variables like &#x27;%timeout%&#x27;;show variables like &#x27;%timeout%&#x27;;

2、编辑/etc/my.cnf，如下修改
wait_timeout=28800interactive_timeout=28800

3、重启MySQL
参考文档：How to Fix MySQL Error 2013: Lost Connection to MySQL Server
4. caching_sha2_password cannot be loaded4.1. 问题描述mysql命令行连接mysql8.0，报错：
ERROR 2059 (HY000): Authentication plugin &#x27;caching_sha2_password&#x27; cannot be loaded: /usr/lib64/mysql/plugin/caching_sha2_password.so: cannot open shared object file: No such file or directory

4.2. 排查解决原因：该错误通常是由于 MySQL 8 使用的默认身份验证插件已更改而引起的。MySQL 8 默认使用 caching_sha2_password 身份验证插件，而之前版本默认使用 mysql_native_password。
解决方法一：服务端改用 mysql_native_password 插件
[mysqld]default-authentication-plugin=mysql_native_password

解决方法二：升级客户端，安装支持mysql8.0的client，参考文档《使用Docker安装配置MySQL》
5. root无权grant问题5.1. 问题描述root用户登录mysql后，执行命令 
grant all privileges on *.* to &#x27;root&#x27;@&#x27;%&#x27; identified by &#x27;vkpassword2&#x27;;
报错：Error 1045 (28000): Access denied for user ‘root‘@’localhost’ (Using password: YES)
5.2. 问题排查原因：root@localhost 不具备grant权限。
show grants for root@&#x27;localhost&#x27;;

如果具备grant权限，那么会看到下面的输出：
+---------------------------------------------------------------------+| Grants for root@localhost                                           |+---------------------------------------------------------------------+| GRANT ALL PRIVILEGES ON *.* TO &#x27;root&#x27;@&#x27;localhost&#x27; WITH GRANT OPTION || GRANT PROXY ON &#x27;&#x27;@&#x27;&#x27; TO &#x27;root&#x27;@&#x27;localhost&#x27; WITH GRANT OPTION        |+---------------------------------------------------------------------+

如果没有grant权限，那么第一行不会出现 WITH GRANT OPTION。
解决办法：
update mysql.user set Grant_priv=&quot;Y&quot; where user=&quot;root&quot; and host=&quot;localhost&quot;;flush privileges;exit;

重新登录后，就可以正常执行授权命令了。
6. mysql身份认证插件问题6.1. 问题描述python连接mysql8.0，正常。后来切换了一个新的mysql8.0数据库，连接报错：
RuntimeError: &#x27;cryptography&#x27; package is required for sha256_password or caching_sha2_password auth methods

6.2. 解决办法参考文档：浅谈 MySQL 新的身份验证插件 caching_sha2_password
问题原因：mysql8.0之后，默认身份认证插件从 mysql_native_password 换成了 sha256_password，使用老的数据库没有问题，说明老的数据库使用的是 mysql_native_password，因此新的数据库也要换成 mysql_native_password。
具体操作：1、修改mysql配置
vim /etc/mysql/conf.d/my.cnf

添加配置：
default_authentication_plugin = mysql_native_password

2、重启mysql
systemctl restart mysqld

3、验证修改是否成功
SHOW VARIABLES LIKE &#x27;default_authentication_plugin&#x27;;

4、配置用户使用默认认证插件添加新用户：
CREATE USER &#x27;vkuser&#x27;@&#x27;%&#x27; IDENTIFIED WITH mysql_native_password BY &#x27;vk123456&#x27;;GRANT all privileges on vkdb.* TO &#x27;vkuser&#x27;@&#x27;%&#x27; WITH GRANT OPTION;FLUSH PRIVILEGES;

或者修改已有用户的认证插件：
ALTER USER &#x27;vkuser&#x27;@&#x27;%&#x27; IDENTIFIED WITH mysql_native_password BY &#x27;vk123456&#x27;;FLUSH PRIVILEGES;

7. 无法通过主机IP连接MySQL问题7.1. 问题描述参考《好好学MySQL：使用Docker安装配置MySQL》安装mysql8.0.28。mysql容器运行后，mysql容器内测试登录，发现登录时host指定宿主机eth0或者docker0 IP时会报错。具体表现如下：
mysql -uroot -p# 可以正常登录mysql -h127.0.0.1 -uroot -p# 可以正常登录mysql -h192.168.56.101 -uroot -p# host指定宿主机eth0 IP，报错：ERROR 1045 (28000): Access denied for user &#x27;root&#x27;@&#x27;192.168.56.101&#x27; (using password: YES)mysql -h172.17.0.1 -uroot -p# host指定宿主机docker0 IP，报错：ERROR 1045 (28000): Access denied for user &#x27;root&#x27;@&#x27;192.168.56.101&#x27; (using password: YES)

7.2. 问题排查7.2.1. 教程问题？怀疑是《好好学MySQL：使用Docker安装配置MySQL》文档有误。
1、是否是mysql8.0.28镜像近期有了更新，原本的安装方法不适用了？检查docker hub上的mysql8.0.28镜像更新时间，两年前更新，不是镜像问题。
2、是否是启动时需要加上-e MYSQL_ROOT_HOST=%参数？加上该参数后，问题依旧。
3、是否是时区问题？启动命令加上 -v /etc/localtime:/etc/localtime:ro ，问题依旧。
7.2.2. 远程登录配置问题？根据以前的测试，默认是允许远程登录的才对。假设默认没有允许远程登录，那么手动配置允许远程登录。
mysql -uroot -p&gt; CREATE USER &#x27;root&#x27;@&#x27;172.20.0.145&#x27; IDENTIFIED BY &#x27;mysql123&#x27;;&gt; GRANT ALL PRIVILEGES ON *.* TO &#x27;root&#x27;@&#x27;192.168.56.101&#x27;;&gt; FLUSH PRIVILEGES;

配置后，问题依旧。
7.2.3. 端口占用问题？其他程序占用了3306端口？
netstat -nlp | grep 3306
确实是mysql服务占用了3306端口，没有问题。
7.2.4. 主机问题？换一台主机，同样的方法安装mysql8.0.28，host指定宿主机eth0或者docker0 IP时，可以正常登录。证明了是主机本身存在什么问题。
两台主机，docker版本相同（19.03.15），/etc/docker/daemon.json 配置相同，docker network ls网络配置相同。
最终有两个怀疑，但是没有验证，可供参考：1、网络问题。有问题的主机，同时也是k8s master节点，也许网络受到ipvs的影响。2、内核问题。有问题的主机内核版本为 4.19.188-10.el7.ucloud.x86_64 ，正常主机的内核版本为 3.10.0-1160.36.2.el7.x86_64。
临时解决办法：启动mysql8.0.28容器时，使用 --network=host 。
7.2.5. 端口转发问题继续排查发现，主机上之前配置过一个端口映射，3306端口转发到了另一个主机的mysql服务。具体配置方法可以参考《iptables实现端口映射》。
至此，问题原因就清楚了：主机上配置了3306端口的转发，登录时host指定宿主机eth0或者docker0 IP时，实际上尝试登录的，是另外一个mysql。
解决办法：启动mysql8.0.28容器时，把3306换成另外一个端口。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>database</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>mysql</tag>
        <tag>好好学MySQL</tag>
        <tag>问题排查</tag>
      </tags>
  </entry>
  <entry>
    <title>遗传规划</title>
    <url>/dev-genetic-programming/</url>
    <content><![CDATA[1. 来龙去脉在什么情况下，存在什么问题，遗传规划是从什么样的视角来解决问题的？（整体介绍，通常10分钟左右）


1.1. 什么是进化算法进化算法，又称演化算法 ，是一个算法集合，包括遗传算法、遗传规划、进化策略和进化规划4种典型方法。进化算法借鉴了进化生物学的遗传、突变、自然选择以及杂交等现象，利用“优胜劣汰”，来处理最优化问题。
由于遗传算法、进化规划和进化策略是不同领域的研究人员分别独立提出的，在相当长的时期里相互之间没有正式沟通。直到90年代，才有所交流。
他们发现彼此的基本思想具有惊人的相似之处，于是提出将这类方法统称为“进化计算” ( Evolutionary Computation ) 。
1.2. 什么是遗传规划遗传规划，又称遗传编程，是进化算法的一个分支，与遗传算法中每个个体是一段染色体编码不同，它的个体是一个计算机程序。
首先利用计算机随机生成的千百万个程序，然后根据一个程序完成给定的任务的能力来确定某个程序的适合度，应用达尔文的自然选择（适者生存）确定胜出的程序，计算机程序间也模拟两性组合，变异，基因复制，基因删除等代代进化，直到达到预先确定的某个中止条件为止。
1.3. 遗传规划与遗传算法的关系遗传规划是遗传算法的改进算法。
遗传算法使用字符串作为染色体去表达所研究的问题，而且字符串的长度常常是固定的。然而，现实中的问题往往很复杂，有时不能用简单的字符串表达问题的所有性质，于是就产生了遗传规划。遗传规划用广义的计算机形式表达问题，它的结构和大小都是可以变化的，从而可以更灵活地表达复杂的事物性质，更有利于算法收敛到全局最优解，同时也弥补了遗传算法在某些领域得不到有效应用的不足。
2. 主要内容基本概念（最好举例说明）、遗传规划描述、遗传规划实例、遗传规划特点等。（细节介绍，通常20分钟左右）
3. 主要研究问题利用文献法来总结各种问题的研究现状，把你阅读的文献（至少3篇）提前3天发到云盘中。（重点内容介绍，通常讲解20分钟左右）a)编码问题：比如，在哪年由谁提出了怎样的编码方案？性能如何？主要特点是什么？主要用来解决哪些类问题等。b)参数设置问题：比如，各种操作的参数选择方法等。c)各种启发式实现方法：比如，有记忆的遗传规划等。d)… …
4. 应用领域利用文献法来总结遗传规划各种领域的应用现状，把你阅读的文献（至少3篇）提前3天发到云盘中. （重点内容介绍，通常讲解20分钟左右），比如，a)在哪些领域的哪些方面应用了遗传规划？b)为什么选用这个算法？具体如何实现的？c)性能如何？存在怎样的问题？改进方向如何？
4.1. 进化算法应用1、结构性优化。通常，工程技术的优化包括结构优化和参数优化。对于后者，人们已经成功地使用了许多方法，如运筹学、数理统计、有限元等数值计算。然而对于结构优化，还缺乏成熟、有效的方法。近年来，人们运用进化算法，成功解决了建筑框架结构、飞机结构设计、电网及管网等网络结构等结构型问题，充分显示进化算法在这一领域的广阔引用前景。
2、人工智能。进化算法继模糊数学、专家系统、人工神经网络之后，成为处理人工智能的又一个有力工具。许多研究工作者利用这种新技术，从事机器学习、自动程序设计、聚类分析、博弈对策等工作。在知识工程方面，进化算法发挥越来越重要的作用。
3、复杂问题的优化。当所要解决的问题具有非线性、多峰值、不确定性时，使用传统的优化方法常常不能奏效。进化算法由于是一种黑箱式的框架型技术，不要求有明确的因果关系数学表达式，因此它是解决这类问题的有力工具，可以解决诸如液体流动、气候变化以及军事战略等非线性动态系统问题。
4、复杂系统分析。多年来，人们应用进化算法从事聚类分析、模式识别、图像处理、调度组织等工作，将表面上杂乱无章的复杂事物条理化。对于这类复杂系统的分析和归纳，进化算法具有很大的引用价值。
5、综合应用。随着科学技术的发展，各种学科不断交叉渗透，相互促进。同样，进化算法也要和其他技术手段相结合，各自发挥特长，综合解决问题。例如，人们已将遗传算法和人工神经网络相结合，成功地解决了机器学习等问题。
由于进化算法具有广阔的应用范围，我们无法一一列举具体的应用领域，只能概括地指明应用方向。随着时间的推移，它的应用范围还会不断扩大。
4.2. 遗传算法应用1、函数优化是遗传算法的经典应用领域;
2、组合优化实践证明，遗传算法对于组合优化中的NP完全问题非常有效;
3、自动控制如基于遗传算法的模糊控制器优化设计、基于遗传算法的参数辨识、利用遗传算法进行人工神经网络的结构优化设计和权值学习等;
4、机器人智能控制遗传算法已经在移动机器人路径规划、关节机器人运动轨迹规划、机器人逆运动学求解、细胞机器人的结构优化和行动协调等;
5、组合图像处理和模式识别目前已在图像恢复、图像边缘持征提取、几何形状识别等方面得到了应用;
6、人工生命基于遗传算法的进化模型是研究人工生命现象的重要理论基础，遗传算法已在其进化模型、学习模型、行为模型等方面显示了初步的应用能力；
7、遗传程序设计Koza发展了遗传程序设计的慨念，他使用了以LISP语言所表示的编码方法，基于对一种树型结构所进行的遗传操作自动生成计算机程序;
4.3. 选用和实现1、为什么选用遗传规划？为了解决结构性优化问题等传统方法解决不了的问题。
2、实现的例子以曲线拟合为例，说明遗传规划的基本原理。图1-5的曲线表示实验的结果，现在要确定实验结果的的函数关系y=f(x)。
遗传规划的工作原理大致如下：（1）选择初始结构。采用随机产生的方法，假设y=f(x)的表达式有下述四种：$$1) y = A + Bx$$$$2) y = A + Bx + Cx^2$$$$3) y = xsinx$$$$4) y = Dxsinx$$
（2）计算适应度。将不同的$x_i$带入四种初始表达式中，从而得出一组不同$y_j$，将计算所得的$y_j$与实验数据$y_i$相比较，可以衡量初始表达式的优劣。假设第3种表达式最佳，第1种表达式最差。
（3）复制。根据优胜劣汰的原则，复制效果最佳的第3种表达式，淘汰效果最差的第1种表达式，于是，新一代的表达式由下述方程组成：$$1) y = xsinx$$$$2) y = A + Bx + Cx^2$$$$3) y = xsinx$$$$4) y = Dxsinx$$
（4）交换。为了产生新的表达式，需要使用交换。采用随机选择的方法，假设第2、3种表达式进行交换，交换位置在第一项，则新的表达式为：$$1) y = xsinx$$$$2) y = x + Bx + Cx^2$$$$3) y = Asinx$$$$4) y = Dxsinx$$
（5）突变。在遗传规划中也可以采用突变产生新个体。例如，将表达式中的$sinx$变为$cosx$，不过，遗传规划中的突变远不及在遗传算法中那样重要。
上述（2）~（5）反复执行，使函数表达式y=f(x)不断变化，逐步得到所要求的表达式。
4.4. 性能、问题和改进1、性能没有找到具体性能评估数据，猜测是用时间和适应度两方面进行衡量。
2、问题（1）求解的是一个描述问题的程序（或者说是一个算法）。（2）通常用树型结构来表示，描述相对复杂。（3）每一代的个体的长度（深度）一般是不同的，即使在同一代中的个体之间的长度（深度）也是不同的。（4）所消耗的资源是不可控的（这里所指的不可控是指不能精确的描述），需要消耗大量的内存空间，因而每一代的进化都比较慢。
3、改进（1）确定初始群体规模的方法的改进。（2）编码技术和程序表达技术的改进。（3）复制、交换、突变等遗传操作的改进。（4）适应度的表达和计算的改进。（5）寻求其他有效的遗传算子，防止近缘杂交、过早收敛等弊病。
5. 参考文档进化算法遗传算法遗传编程
]]></content>
      <categories>
        <category>engineering</category>
        <category>algorithm</category>
      </categories>
      <tags>
        <tag>知识工程</tag>
        <tag>mathjax</tag>
      </tags>
  </entry>
  <entry>
    <title>《知识工程》教学大纲</title>
    <url>/dev-knowledge-engineering/</url>
    <content><![CDATA[1. 课程目标人工智能为知识工程的发展提供了理论基础，知识工程的发展进一步将人工智能推向了更广泛的应用领域。具体目标包括：（1）丰富和完善对人工智能领域的理论、方法的学习和研究。（2）学习和研究计算智能的启发式算法。通过自主/合作学习与研究的方法，训练和提升学生分析问题、发现问题和解决实际问题的能力。（3）提高学生获取文献的能力。同时，通过书写文章来提高学生归纳、总结、抽象、提炼研究成果的学术研究素养，为开题报告和毕业论文的完成和将来从事科学研究工作奠定坚实的基础。


2. 课程内容《知识工程》这门课是研究生第一学期《人工智能》专业基础课的后续课程。由于《人工智能》课中对基本的知识表示、知识推理、知识获取、专家系统等方面的基本理论和方法进行了详细的讲授和讨论，所以在《知识工程》这门课中将主要从计算智能的角度来开展教学和研究。本课程共包括4个模块，含7个专题。模块一为人工智能与知识工程概述（专题1）。模块二为传统人工智能基本理论和方法的梳理（专题2-3）。模块三为计算智能研究（专题4-6）。模块四为科技论文书写探讨（专题7）。其中的重难点部分是第三模块。
2.1. 专题一：人工智能与知识工程概述（2学时 2017-3-1）
1、核心内容人工智能的研究途径；人工智能的应用领域；在大数据环境下人工智能的研究方法和发展趋势；知识工程与知识管理等。
2、思考与讨论问题大数据和人工智能的关系。从IBM的Watson机器人到谷歌DeepMind实验室研发的人工智能围棋程序AlphaGo，给你怎样的启示。什么是计算智能？计算智能和传统人工智能是什么关系？什么是启发式算法？比较著名的启发式算法有哪些？讨论本课程的教学目标、教学内容、教学方式、考核方式等的设置是否合理？
3、阅读文献参考文献1中的第1、7、8章参考文献2中的第1章参考文献3中的第1章参考文献5
2.2. 专题二：知识表示、知识推理和知识获取的方法（2学时 2017-3-8）
1、核心内容知识表示方法及特点的分析和总结；基本推理方法和实用推理技术的梳理；知识获取的基本过程、手段和工具的分析和总结；各类机器学习方法的回顾与比较等。
2、思考与讨论问题如何理解和应用启发式搜索策略？知识获取的途径有哪些？机器学习、知识发现和数据挖掘是什么关系？
3、阅读文献参考文献1中的第5章参考文献2中的第2、3、4、8章参考文献5
2.3. 专题三：专家系统（2学时 2017-3-15）
1、核心内容专家系统的结构、原理、建造及评价的分析和总结。探讨大数据环境下的专家系统的设计及实现的关键问题。
2、思考与讨论问题基于知识的系统（专家系统）的研究瓶颈问题有哪些？破解思路是什么？深层知识专家系统、模糊专家系统和神经网络专家系统的深入研究。大型协同分布式专家系统的设计与实现的关键问题分析。
3、阅读文献参考文献1中的第2章参考文献2中的第7章参考文献5
2.4. 专题四：演化计算（6 学时 2017-3-22 ~2017-4-5）
1、核心内容演化计算概述；遗传算法（比如，模式、编码、适应度等方面的内容）及应用案例的学习与探讨；遗传规划及应用案例的学习与探讨；应用遗传算法来解决TSP问题的关键问题研究。
2、思考与讨论问题编码问题：在哪年由谁提出了怎样的编码方案？性能如何？主要特点是什么？主要用来解决哪些类问题等。对有记忆的遗传算法的深入研究。参数设置问题：各种操作的参数选择方法等。
3、作业 :作业1：利用遗传算法来实现求解TSP问题（或类似问题）的软件系统。具体要求见附录V.
4、阅读文献参考文献1中的第4章参考文献2中的第6章参考文献3中的第4章参考文献4、5
2.5. 专题五：群智能计算（10学时 2017-4-12 ~2017-5-10）1、核心内容群智能计算概述；蚁群算法及应用实例的学习与探讨；粒子群算法及应用实例的学习与探讨；应用蚁群算法、粒子群算法来解决TSP问题的关键问题研究；遗传算法、蚁群算法、粒群算法在解决优化问题方面的比较研究。
2、思考与讨论问题蚁群算法的改进方法比较与发展趋势分析。蚁群算法、粒子群算法的实现技巧梳理遗传算法、蚁群算法和粒子群算法的综合应用
3、作业作业2：利用蚁群算法来实现求解TSP问题（或类似问题）的软件系统。具体要求见附录V.作业3：利用粒子群算法来实现求解TSP问题（或类似问题）的软件系统。具体要求见附录V.
4、阅读文献参考文献1中的第4章参考文献2中的第9章参考文献3中的第5、6章参考文献4、5
2.6. 专题六：神经计算（ 10学时 2017-5-17 ~2017-6-14）1、核心内容人工神经网络概述；典型BP前向网络及应用实例的学习与探讨；典型Hopfield反馈网络及应用实例的学习与探讨；应用人工神经网络技术来解决TSP问题的关键问题研究；模糊神经网络的学习与探讨；在解决组合优化问题方面本课中涉及到的各类优化算法的比较研究。
2、思考与讨论问题人工神经网络中的学习方法的比较与选择。如何理解和应用模糊逻辑推理技术？各种优化算法的综合应用。什么是深度学习？它对我们的生活有什么影响？你是如何看待人工智能发展的未来的？
3、作业作业4：利用人工神经网络技术（比如，Hopfield）来实现求解TSP问题（或类似问题）的软件系统。具体要求见附录V.
4、阅读文献参考文献2中的第4、5章参考文献3中的第2、3章参考文献4、5
2.7. 专题七：科技论文书写（ 4学时 2017-6-21 ~2017-6-28）
1、核心内容科技论文书写的一般格式；科技论文写作的方法；科技论文写作应注意的问题；论文实例的阅读、分析与探讨；自己动手书写论文，对论文质量进行评价，选择合适的期刊杂志和会议等出版单位进行投稿。
2、思考与讨论问题结合这门课的具体内容，来确定论文的选题、框架、突出的主题、针对主题的讨论、结论等。展示自己的成果，和老师、同学一起讨论修改方案，直至形成论文终稿。 
3、阅读文献参考文献5
3. 教学方式本课程主要采用讲授法、研讨法、任务法、文献法等教学方式。基本理论、方法和算法以及具体任务的布置等内容采用讲授法。学生在自主学习/合作学习（组建小组、小组分工、协作讨论、确定方案、成果呈现）过程中经历了搜集资料、制定和实现方案、展示成果等活动，主要应用了文献法、任务法和研讨法等。
4. 学习资源1、知识工程与知识管理，陈文伟，陈晟，清华大学出版社,2010。2、人工智能技术， 曹承志，清华大学出版社，2010。3、计算智能，张军，詹志辉，清华大学出版社，2009.4、自选Matlab 教材.5、高质量的学位论文及核心期刊的相关文章。文献检索例子见附录I。
5. 学习建议1、为了深入地理解和应用各类启发式优化算法，需要学生具备一定的数学基础和会应用Matlab编程环境。2、本课程是实践性很强的课程，学生一定要动手开发具体的应用系统，这就要求同学们要具备相关的系统开发基础。3、要跟上老师的教学进度，及时阅读老师提供的参考资料，作业按时完成，否者会影响后续课程的学习。 
6. 分组注: 每组第一人为组长。第一组3月22日讲授遗传算法及应用案例介绍；第二组3月29日开始讲授遗传规划及应用案例介绍；第三组4月19日开始讲授蚁群算法及应用实例介绍。
7. 考核方式1、总分100分。2、每五个人一组，完成两个作业（具体见分组表），每个作业占总成绩的30分。本部分成绩由老师（80%）和学生（20%）的打分综合形成。要求和评价标准见附录III 和附录IV。 (小组成员成绩有区别)3、根据选择的作业内容，每人写一篇3000字左右的小论文，占总成绩的20分。要求至少15篇高质量的参考文献。4、课堂表现（出勤、演讲、参与度等）占总成绩的20分。本部分成绩由老师（80%）和学生（20%）的打分综合形成。要求和评价标准见附录III 和附录IV。5、作业必须按时完成，超期限者不能得分。
8. 附录8.1. 附录I：文献检索例子表一 相关文献的检索情况         
搜集到的论文篇数如表一所示。去除相关度太低和年限太久的文献，对剩余的文献进行分析。相关文献主要涵盖以下几方面：1、基本理论研究：涵盖概念、定义、特点、分类、理论方法、研究现状、研究趋势、应用价值等。2、相关技术研究：包括X技术，X X技术，… …3、技术应用研究：应用模式，应用领域，… …4、相关标准研究：A标准， B标准，… …5、… …本文对与本研究相关的XX方面的文献进行了详细阅读和分析整理。具体成果如下。读书报告… …
8.2. 附录II：项目报告格式及要求一、题目二、报告人三、项目意义、要求及条件：为什么要做这个项目，有哪些具体要求，提供了哪些条件去完成这个项目。四、具体内容1、系统简介：包括采用了哪些理论方法与算法，实现了哪些功能，解决了哪些实际问题。2、系统设计：给出框图，并对模块及模块之间的关系给出解释。3、系统开发环境：硬环境及软环境。4、系统使用说明。五、系统性能分析对好的方面，从理论方法上进行归纳总结。对存在问题的方面，找出原因，并给出改进方法及建议。六、心得体会: 开发中所遇到的困难及解决方法, 从中得到的经验和教训，对以后学习、科研等方面所带来的启示。对这门课的意见及建议。
8.3. 附录III：课堂讲解要求（以遗传算法为例）
1、来龙去脉：在什么情况下，存在什么问题，遗传算法是从什么样的视角来解决问题的？（整体介绍，通常10分钟左右）2、主要内容：基本概念（最好举例说明）、遗传算法描述、遗传算法实例、遗传算法特点等。（细节介绍，通常20分钟左右）3、主要研究问题：利用文献法来总结各种问题的研究现状，把你阅读的文献（至少3篇）提前3天发到云盘中。（重点内容介绍，通常讲解20分钟左右）a)编码问题：比如，在哪年由谁提出了怎样的编码方案？性能如何？主要特点是什么？主要用来解决哪些类问题等。b)参数设置问题：比如，各种操作的参数选择方法等。c)各种启发式实现方法：比如，有记忆的遗传算法等。d)… …4、应用领域：利用文献法来总结遗传算法各种领域的应用现状，把你阅读的文献（至少3篇）提前3天发到云盘中. （重点内容介绍，通常讲解20分钟左右），比如，a)在哪些领域的哪些方面应用了遗传算法？b)为什么选用这个算法？具体如何实现的？c)性能如何？存在怎样的问题？改进方向如何？
8.4. 附录IV：课堂讲解评价1、内容方面 （60分）a)准确b)全面2、PPT制作（20分）a)必须自己制作PPT讲稿，可以应用已有模板；b)内容衔接得当，清晰、有逻辑性；c)图片/图表清楚，显示性强，引人入胜，当然也不要过于花哨，喧宾夺主；d)文字要突出重点，不要整页的文字。3、讲解水平（20分）a)声音洪亮、吐字清晰；b)选用合适的串接词，讲出逻辑性，不要照着文字念；c)注意时间分配，突出重点；d)给出一些启发式的话题，适当地互动，集中大家的注意力。
8.5. 附录V：作业利用启发式算法（遗传算法、蚁群算法、粒群算法、人工神经网络等）来实现求解TSP问题（或类似问题）的软件系统。要求：1、实现一个应用系统，要求有GUI界面，可以对一些关键因子进行调整。同时，系统不仅显示最终结果，还要记录和显示中间结果，给出获得此结果的推理过程。2、在课堂上由一名小组成员演示并讲解所实现的系统。由全体成员来回答其他同学和老师的提问。然后由老师和同学为小组中的成员打分，讲解要求和评分标准见附录III和附录IV。3、提交项目报告（见附录II），源程序。
]]></content>
      <categories>
        <category>engineering</category>
        <category>algorithm</category>
      </categories>
      <tags>
        <tag>知识工程</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS7搭建Atlassian Jira</title>
    <url>/dev-centos-atlassian-jira/</url>
    <content><![CDATA[1. 前言Jira是Atlassian公司出品的项目与事务跟踪工具，被广泛应用于缺陷跟踪（bug管理）、客户服务、需求收集、流程审批、任务跟踪、项目跟踪和敏捷管理等工作领域。
写完了《自动部署工具Jenkins》和《CentOS7搭建Confluence Wiki》，感觉有些缺憾，决定把Jira的搭建方法也记录一下。


2. 准备2.1. 下载软件包开始搭建Jira前，需要下载一些软件包。

atlassian-jira-software-7.2.2-x64
JIRA Core-7.2.1-language-pack-zh_CN
mysql-connector-java-5.1.39-bin
atlassian-extras-3.1.2

2.2. 安装配置javayum install javajava -version

2.3. 安装配置mysql1、安装mysql后，登录mysql控制台，执行如下命令：
create database jira default character set utf8;grant all on jira.* to &#x27;jirauser&#x27;@&#x27;%&#x27; identified by &#x27;jirapasswd&#x27; with grant option;grant all on jira.* to &#x27;jirauser&#x27;@localhost identified by &#x27;jirapasswd&#x27; with grant option;flush privileges;

2、进入/usr/local/mysql文件夹，在my.cnf中添加：
binlog_format=mixed

3、重启mysql
service mysqld stopservice mysqld start

2.4. 关闭防火墙systemctl stop firewalld.service


3. 详细步骤3.1. 安装jira1、使用xftp，上传atlassian-jira-software-7.2.2-x64.bin到/root文件夹。
2、上传完成后，执行命令：
chmod 755 atlassian-jira-software-7.2.2-x64.bin./atlassian-jira-software-7.2.2-x64.bin
jira默认安装到/opt/atlassian/jira和/var/atlassian/application-data/jira目录下，并且jira监听的端口是8080。
3、jira的主要配置文件，是/opt/atlassian/jira/conf/server.xml。
4、此时不要测试访问，切记。
3.2. 破解jira1、关闭jira
/etc/init.d/jira stop

2、把atlassian-extras-3.1.2.jar和mysql-connector-java-5.1.39-bin.jar两个文件上传到/opt/atlassian/jira/atlassian-jira/WEB-INF/lib/里。其中atlassian-extras-3.1.2.jar是用来替换原来的atlassian-extras-3.1.2.jar文件，用作破解jira系统的；mysql-connector-java-5.1.39-bin.jar是用来连接mysql数据库的驱动软件包。
3、启动jira
/etc/init.d/jira start

4、测试访问，假设CentOS7的ip地址为192.168.56.101，那么在浏览器输入http://192.168.56.101:8080，即可看到jira的安装页面。
5、选择I’ll set it up myself，然后“Next”，进入数据库设置页面。
6、选择MySQL数据库，输入安装配置mysql中设置的账号和密码。点击“Test Connection”，确认数据库连接是否成功。 
7、点击“Next”，向数据库写入数据，这一步花费时间较长，请耐心等待。数据库的配置文件，是/var/atlassian/application-data/jira/dbconfig.xml
8、报错。
9、忽略以上错误，重启jira服务。
/etc/init.d/jira start/etc/init.d/jira stop

10、再次访问http://192.168.56.101:8080，进入jira配置页面。
3.3. 试用jira1、选择Private模式，在这个模式下，用户需要由管理员创建。而在Public模式下，用户可以自己进行注册。
2、点击generate a JIRA trial license，登录atlassian，获取试用license。
3、获取license后在这个页面查看。
4、拷贝license，粘贴到jira配置页面，“Next”。
5、再次报错，不过不要放弃。
6、重启，再次拷贝license，粘贴到jira配置页面，“Next”。
3.4. 配置管理员1、上一步后，成功进入管理员配置页。
2、配置管理员后，下一步进入邮件设置页面。
3、点击“Finish”，进入欢迎页面。
4、创建新项目，选择Scrum software development，“Next”。
5、稍等片刻，便会跳转到管理页面。
3.5. 查看破解点击右上角齿轮形状的管理图标，选择“Applications”，查看破解信息。可以看到，到期日期是2033年2月8日，破解成功。
3.6. 汉化1、点击右上角齿轮形状的管理图标，选择“Add-ons”，再选择“Manage add-ons”。
2、点击“Upload add-on”，选择上传JIRA Core-7.2.1-language-pack-zh_CN.jar。
3、依次点击“System”，“Edit Settings”。
4、找到Internationalization，修改Indexing language和Default language为中文，修改Default user time zone为亚洲上海。然后点击页面底部的“Update”按钮。
4. 后记至此，jira破解和汉化完成。虽然中途有些波折，但总归是安装成功了，nice！
5. 书签
jira7.2安装、中文及破解 
jira下载页
jira语言包下载页
JIRA数据库切换（HSQL Database到MySQL）

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>jira</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS7搭建Confluence Wiki</title>
    <url>/dev-centos-confluence-wiki/</url>
    <content><![CDATA[1. 前言在艾佳生活实习时，有三款团队协作系统特别喜欢：Wiki、Jira和Jenkins。对于Jenkins的搭建，之前《自动部署工具Jenkins》有过记录。这次，搭建一个Wiki，作为知识管理的工具，实现团队成员之间的协作和知识共享。


2. 准备2.1. 下载软件包开始搭建Wiki前，需要下载一些软件包。

atlassian-confluence-5.6.6-x64
Confluence-5.6.6-language-pack-zh_CN
mysql-connector
confluence_keygen

2.2. 安装配置javayum install javajava -version

2.3. 安装配置mysql1、安装mysql后，登录mysql控制台，执行如下命令：
create database confluence default character set utf8;grant all on confluence.* to &#x27;confluenceuser&#x27;@&#x27;%&#x27; identified by &#x27;confluencepasswd&#x27; with grant option;grant all on confluence.* to &#x27;confluenceuser&#x27;@localhost identified by &#x27;confluencepasswd&#x27; with grant option;flush privileges;

2、进入/usr/local/mysql文件夹，在my.cnf中添加：
binlog_format=mixed

3、重启mysql
service mysqld stopservice mysqld start

2.4. 关闭防火墙systemctl stop firewalld.service


3. 详细步骤3.1. 安装confluence1、使用xftp，上传atlassian-confluence-5.6.6-x64.bin到/root文件夹。
2、上传完成后，执行命令：
chmod 755 atlassian-confluence-5.6.6-x64.bin./atlassian-confluence-5.6.6-x64.bin
confluence默认安装到/opt/atlassian/confluence和/var/atlassian/application-data/confluence目录下，并且confluence监听的端口是8090。
3、confluence的主要配置文件，存放在/opt/atlassian/confluence/conf/server.xml文件中。
4、测试访问，假设CentOS7的ip地址为192.168.56.101，那么在浏览器输入http://192.168.56.101:8090，即可看到Confluence的欢迎界面。
3.2. 破解confluence1、点击“Start setup”，看到如下界面。
2、复制Server ID并保存，然后关闭confluence。
/etc/init.d/confluence stop

3、从/opt/atlassian/confluence/confluence/WEB-INF/lib中，拷贝atlassian-extras-decoder-v2-3.2.jar到windows，并重命名为atlassian-extras-2.4.jar。
4、在windows下，生成License Key。
java -jar confluence_keygen.jar
把第二步中复制的Server ID粘贴进去，然后点击“.gen!”，保存生成的key。
5、打补丁。点击“.patch!”，选择第3步中重命名的atlassian-extras-2.4.jar，会生成新的atlassian-extras-2.4.jar。
6、上传新的atlassian-extras-2.4.jar、Confluence-5.6.6-language-pack-zh_CN.jar、mysql-connector-java-5.1.39-bin.jar到/opt/atlassian/confluence/confluence/WEB-INF/lib，并且删除atlassian-extras-decoder-v2-3.2.jar。
5、启动confluence
/etc/init.d/confluence start

7、把生成的key复制粘贴到License Key框中，点击“Next”，如果顺利进入选择数据库页面，说明破解成功。
3.3. 配置数据库1、数据库选择MySQL，然后点击“External Database”，进入数据库配置页面。
2、点击“Direct JDBC”，User Name和Password填写安装配置mysql中设置的用户名和密码。
3、点击“Next”，这一步花费时间较长，请耐心等待。数据写入成功，进入如下页面。
4、第3步如果报错，请检查mysql数据库配置，然后卸载后重新安装，卸载命令如下。
/etc/init.d/confluence stopcd /opt/atlassian/confluence/./uninstall
或者：
/etc/init.d/confluence stoprm -rf /opt/atlassian/rm -rf /var/atlassian/

3.4. 配置管理员初始化一个样例站点，根据提示进行配置。
4. 书签
wiki系统confluence5.6.6安装、中文、破解及迁移
confluence wiki搭建使用
confluence下载页
confluence语言包下载页

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>wiki</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo中使用Mathjax的冲突问题</title>
    <url>/dev-hexo-mathjax/</url>
    <content><![CDATA[1. 问题描述写《信息量&amp;信息熵&amp;信息增益》时，用到了一些数学公式。但是，在使用hexo生成页面后，这些数学公式并没有被正确显示。查找资料发现markdwon本身的特殊符号与latex中的符号会出现冲突:
1、在markdown中，_是斜体，但是在latex中，却有下标的意思。2、在markdown中，\\会被转义为\,这样也会影响影响mathjax对公式中的\\进行渲染。
解决办法有三种，手动添加转义、更换hexo的markdown渲染引擎和修改hexo的渲染代码。郝同学更喜欢第三种，经过尝试，完美解决问题，本文记录下解决问题的过程。


2. 解决问题2.1. 确认开启mathjax以郝同学使用的yilia主题为例，主题本身已经集成了mathjax，其他主题可以参考添加。
在yilia\layout\_partial\after_footer.ejs 里有如下代码：
&lt;% if (theme.mathjax)&#123; %&gt;&lt;%- partial(&#x27;mathjax&#x27;) %&gt;&lt;% &#125; %&gt;
当_config.yml文件中的mathjax参数为true时，页面中便会引入mathjax.ejs。
在yilia\layout\_partial\mathjax.ejs里有如下代码：
&lt;! -- mathjax config similar to math.stackexchange --&gt;&lt;script type=&quot;text/x-mathjax-config&quot;&gt;MathJax.Hub.Config(&#123;    tex2jax: &#123;        inlineMath: [ [&#x27;$&#x27;,&#x27;$&#x27;], [&quot;\\(&quot;,&quot;\\)&quot;]  ],        processEscapes: true,        skipTags: [&#x27;script&#x27;, &#x27;noscript&#x27;, &#x27;style&#x27;, &#x27;textarea&#x27;, &#x27;pre&#x27;, &#x27;code&#x27;]    &#125;&#125;);MathJax.Hub.Queue(function() &#123;    var all = MathJax.Hub.getAllJax(), i;    for(i=0; i &lt; all.length; i += 1) &#123;        all[i].SourceElement().parentNode.className += &#x27; has-jax&#x27;;                     &#125;       &#125;);&lt;/script&gt;&lt;script type=&quot;text/javascript&quot; src=&quot;http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML&quot;&gt;&lt;/script&gt;
引入了mathjax，以及配置mathjax。
其实更好的做法，是把after_footer.ejs中的代码修改为：
&lt;% if (page.mathjax)&#123; %&gt;&lt;%- partial(&#x27;mathjax&#x27;) %&gt;&lt;% &#125; %&gt;
这样，是否在页面中引入mathjax，就交由了页面控制，更加灵活。写文档时，在头部加入mathjax: true，或者mathjax: false。
---title: Hexe中使用Mathjax的冲突问题toc: truemathjax: falsedate: 2017-03-14 16:00:00updated: 2017-03-14 16:00:00tags:- hexo- mathjaxcategories: 点滴发现---


2.2. 修改渲染代码找到hexo\node_modules\hexo-renderer-marked\node_modules\marked\lib\markd.js，备份后进行修改：
1、去掉\的转义
escape: /^\\([\\`*&#123;&#125;\[\]()# +\-.!_&gt;])/,
修改为
escape: /^\\([`*&#123;&#125;\[\]()# +\-.!_&gt;])/,

2、去掉_的斜体含义
em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
修改为
em:/^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,


3. 书签MathJaxhttps://www.mathjax.org/
期刊文章 - TeXIDE, 在线 LaTeX 编辑器https://www.texide.com/templates/journals/?
JaxEdit Websitehttp://jaxedit.com/
Cmd Markdown 编辑阅读器 - 作业部落出品https://www.zybuluo.com/mdeditor
在 Hexo 中完美使用 Mathjax 输出数学公式http://lukang.me/2014/mathjax-for-hexo.html
Hexo下mathjax的转义问题https://segmentfault.com/a/1190000007261752
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>mathjax</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>信息量&amp;信息熵&amp;信息增益</title>
    <url>/dev-entropy/</url>
    <content><![CDATA[1. 信息量确定性是指事先可以准确知道某个事件或某种决策的结果。或者说，如果事件或决策的可能结果只有一种，就会产生确定性。
信息论创始人香农对信息的定义：信息是确定性的增加。

信息量是信息多少的度量，也就是确定性增加的多少的度量，单位是比特。

如何计算信息量的多少？在日常生活中，极少发生的事件一旦发生是容易引起人们关注的，而司空见惯的事不会引起注意，也就是说，极少见的事件所带来的信息量多。如果用统计学的术语来描述，就是出现概率小的事件信息量多。因此，事件出现得概率越小，信息量愈大。即信息量的多少是与事件发生频繁（即概率大小）成反比。


如已知事件$x_i$已发生，则表示$x_i$所含有或所提供的信息量$$I(x_i) = -\log 2^{p(x_i)}$$
例题：若估计在一次国际象棋比赛中谢军获得冠军的可能性为0.1（记为事件A），而在另一次国际象棋比赛中她得到冠军的可能性为0.9（记为事件B）。试分别计算当你得知她获得冠军时，从这两个事件中获得的信息量各为多少？$$I(A) = -\log 2^{p(0.1)} ≈ 3.32（比特）$$
$$I(B) = -\log 2^{p(0.9)} ≈ 0.152（比特）$$
2. 信息熵不确定性是指事先不能准确知道某个事件或某种决策的结果。或者说，只要事件或决策的可能结果不止一种，就会产生不确定性。

信息熵就是用以消除事件的不确定性所需要的信息量，单位是比特。

根据郝同学的理解，给出两个对比描述：1、这个杯子的容量是2L，容量是指物体或者空间所能够容纳的单位物体的数量。2、这个事件的信息熵是3bit，信息熵是指消除事件的不确定性所需要的单位信息的数量。也就是说，就像容量是用来描述空间大小的度量一样，信息熵就是用来描述不确定性大小的度量。
通常我们使用H(X)表示随机变量（事件）的熵：$$H(X) = E(I(X))$$其中的 E 代表期望值函數， I(X) 代表信息量所形成的随机变量（变量X的函数结果表示I(X)也是变量）。
$$H(X) = E(I(X)) = \sum_{i=1}^n {p(x_i),I(x_i)} = -\sum_{i=1}^n {p(x_i) \log 2^{p(x_i)}}$$
其中$x_i$表示第i个状态（总共有n种状态），$p(x_i)$表示第i个状态出现的概率。

条件熵就是在事件X确定的条件下，用以消除事件Y的不确定性所需要的信息量。

以下推导，log函数默认以2为底数。
\begin{eqnarray}H(Y|X) &amp;=&amp; \sum_{x\in\mathcal X},p(x),H(Y|X=x)\&amp;=&amp; -\sum_{x\in\mathcal X}p(x)\sum_{y\in\mathcal Y},p(y|x),\log,p(y|x)\&amp;=&amp; -\sum_{x\in\mathcal X}\sum_{y\in\mathcal Y},p(y,x),\log,p(y|x)\&amp;=&amp; -\sum_{x\in\mathcal X, y\in\mathcal Y}p(x,y)\log,p(y|x).\end{eqnarray}
条件熵 H(Y|X) 相当于联合熵 H(Y,X) 减去单独的熵 H(X)，其数学式如下。
\begin{align}H(Y|X) = H(Y,X)-H(X)\end{align}

联合熵就是用以消除多个同时发生的事件的不确定性所需要的信息量。

以下证明联合熵和条件熵的关系。\begin{eqnarray}H(X,Y)&amp;=&amp; -\sum_{x\in\mathcal X, y\in\mathcal Y}p(x,y)log,p(x,y)\&amp;=&amp; -\sum_{x\in\mathcal X, y\in\mathcal Y}p(x,y)log\left(p(y|x)p(x)\right)\&amp;=&amp; -\sum_{x\in\mathcal X, y\in\mathcal Y}p(x,y)log,p(y|x) - \sum_{x\in\mathcal X, y\in\mathcal Y} p(x,y) log,p(x)\&amp;=&amp; H(Y|X)-\sum_{x\in\mathcal X, y\in\mathcal Y}p(x,y)log,p(x)\&amp;=&amp; H(Y|X)-\sum_{x\in\mathcal X}\sum_{y\in\mathcal Y}p(x,y)log,p(x)\ &amp;=&amp; H(Y|X)-\sum_{x\in\mathcal X}log,p(x)\sum_{y\in\mathcal Y}p(x,y)\ &amp;=&amp; H(Y|X)-\sum_{x\in\mathcal X}(log,p(x))p(x)\&amp;=&amp; H(Y|X)-\sum_{x\in\mathcal X}p(x)log,p(x)\&amp;=&amp; H(Y|X)+H(X)\ &amp;=&amp; H(X)+H(Y|X)\\end{eqnarray}
3. 信息增益特征选择是指从全部特征中选择一个特征子集，使构造出来的模型更好。
在机器学习的实际应用中，特征数量往往较多，其中可能存在不相关的特征，特征之间也可能存在相互依赖，容易导致训练时间过长、维度灾难等。特征选择能剔除不相关或冗余的特征，从而达到减少特征个数，提高模型精确度，减少运行时间的目的。另一方面，选取出真正相关的特征，简化了模型。
在训练样本集上，运行C4.5或其他决策树生成算法，待决策树充分生长后，再在树上运行剪枝算法，则最终决策树各分支处的特征就是选出来的特征子集。决策树方法一般使用信息增益作为评价函数。
在信息增益中，衡量标准是看特征能够为分类系统带来多少信息，带来的信息越多，该特征就越重要。对一个特征而言，系统有它和没它时信息量将发生变化，而前后信息量的差值就是这个特征给系统带来的信息量。

信息增益定义为父项熵减去分割父项后生成的子项的熵的加权平均。

特征T给聚类C或分类C带来的信息增益为：$$IG(T) = H(C) - H(C|T)$$
4. 书签信息量http://baike.baidu.com/item/%E4%BF%A1%E6%81%AF%E9%87%8F
信息熵http://baike.baidu.com/item/%E4%BF%A1%E6%81%AF%E7%86%B5
信息增益http://baike.baidu.com/item/%E4%BF%A1%E6%81%AF%E5%A2%9E%E7%9B%8A
互資訊與條件熵http://ccckmit.wikidot.com/st:mutualinformation
c4.5为什么使用信息增益比来选择特征？https://www.zhihu.com/question/22928442/answer/117189907
信息增益到底怎么理解呢？https://www.zhihu.com/question/22104055/answer/67014456
优达学城-机器学习入门-熵和信息增益
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：PHP接收ajax的json数组</title>
    <url>/dev-php-ajax-json-array/</url>
    <content><![CDATA[1. 前言前端和PHP端（服务器）通过ajax进行数据交互时，数据格式一般为字符串、数组、json、json数组。下面，我们针对这四种数据格式，进行前端和PHP端模拟交互，寻找一些规律和结论。


2. 前端var data = &#123;    product_name: &#x27;毛巾&#x27;,    product_arr: [&#x27;毛巾&#x27;,&#x27;肥皂&#x27;],    product_json: &#123;        product_name: &#x27;毛巾&#x27;,        product_num: &#x27;5&#x27;,        product_unit: &#x27;条&#x27;    &#125;,    product_json_arr: [&#123;        product_name: &#x27;毛巾&#x27;,        product_num: &#x27;5&#x27;,        product_unit: &#x27;条&#x27;    &#125;,&#123;        product_name: &#x27;肥皂&#x27;,        product_num: &#x27;10&#x27;,        product_unit: &#x27;块&#x27;    &#125;]&#125;console.log(data);$.ajax(&#123;    url: &#x27;/&#x27;,    type: &#x27;POST&#x27;,    dataType: &#x27;json&#x27;,    data: data,    success: function(data)&#123;        console.log(data);    &#125;,    error: function(xhr)&#123;        console.log(xhr);    &#125;&#125;);

3. PHP端3.1. 接收字符串$product_name = $_POST[&#x27;product_name&#x27;];echo json_encode($product_name,JSON_UNESCAPED_UNICODE);
由结果看出，PHP端获取到了product_name。
3.2. 接收数组$product_arr = $_POST[&#x27;product_arr&#x27;];echo json_encode($product_arr,JSON_UNESCAPED_UNICODE);
由结果看出，PHP端获取到了product_arr。
$product_arr = $_POST[&#x27;product_arr&#x27;];echo json_encode($product_arr[0],JSON_UNESCAPED_UNICODE);
由结果看出，PHP端拿到的确实是数组。
3.3. 接收json$product_json = $_POST[&#x27;product_json&#x27;];echo json_encode($product_json,JSON_UNESCAPED_UNICODE);
由结果看出，PHP获取到了product_json。
$product_json = $_POST[&#x27;product_json&#x27;];echo json_encode($product_json-&gt;product_name,JSON_UNESCAPED_UNICODE);
根据返回结果，猜测PHP端拿到的json数据是字符串。
$product_json = json_decode($_POST[&#x27;product_json&#x27;]);echo json_encode($product_json-&gt;product_name,JSON_UNESCAPED_UNICODE);
根据返回结果，证明PHP端拿到的json数据不是字符串。因为json字符串通过json_decode函数是可以转换成json格式数据的，而此处的json数据转换失败。
后端已经没有办法再变化，我们调整前端数据格式，把json数据先转化成字符串再传输。
var data = &#123;    product_json: JSON.stringify(&#123;        product_name: &#x27;毛巾&#x27;,        product_num: &#x27;5&#x27;,        product_unit: &#x27;条&#x27;    &#125;)&#125;
根据返回结果，证明前端json数据必须先转换成字符串，然后PHP端可以获取json字符串，接着正常处理即可。
3.4. json数组$product_json_arr = $_POST[&#x27;product_json_arr&#x27;];  echo json_encode($product_json_arr,JSON_UNESCAPED_UNICODE);
由结果看出，PHP端拿到了product_json_arr。
$product_json_arr = $_POST[&#x27;product_json_arr&#x27;];  echo json_encode($product_json_arr[0],JSON_UNESCAPED_UNICODE);
由结果看出，PHP端拿到的确实是数组。
$product_json_arr = $_POST[&#x27;product_json_arr&#x27;]; echo json_encode($product_json_arr[0]-&gt;product_name,JSON_UNESCAPED_UNICODE);
由结果看出，PHP端拿到的是数组，但是数组里的并不是json数据。
$product_json_arr = json_decode($_POST[&#x27;product_json_arr&#x27;],true);echo json_encode($product_json_arr[0]-&gt;product_name,JSON_UNESCAPED_UNICODE);
由结果看出，数组里的json数据不是字符串，猜测这里也需要前端先转json字符串。
调整前端数据格式，把json数组数据先转化成字符串再传输。
var data = &#123;    product_json_arr: JSON.stringify([&#123;        product_name: &#x27;毛巾&#x27;,        product_num: &#x27;5&#x27;,        product_unit: &#x27;条&#x27;    &#125;,&#123;        product_name: &#x27;肥皂&#x27;,        product_num: &#x27;10&#x27;,        product_unit: &#x27;块&#x27;    &#125;])&#125;

由结果看出，依然不行。
再次修改后端代码：
$product_json_arr = json_decode($_POST[&#x27;product_json_arr&#x27;]);echo json_encode($product_json_arr[0]-&gt;product_name,JSON_UNESCAPED_UNICODE);
输出成功，也就是说，问题出在json_decode上面。json_decode($str, true) 可以得到数组，第二参数不加默认为false，得到对象。而json数组出现时，要选择生成对象。
4. 小结通过上面的实验，我们可以得出结论：涉及到json格式的数据，需要在前端先使用JSON.stringify()函数转换为字符串；然后，在PHP端通过json_decode()函数转换为对象。
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>ajax</tag>
      </tags>
  </entry>
  <entry>
    <title>最终项目</title>
    <url>/dev-ml-final-project/</url>
    <content><![CDATA[1. 项目简介在此项目中，你将扮演侦探，运用你的机器学习技能构建一个算法，通过公开的安然财务和邮件数据集，找出有欺诈嫌疑的安然雇员。


2. 为何要进行此项目？此项目将通过机器学习的视角教授你数据调查的端到端流程。
它将教授你如何提取并识别最能代表你的数据的有用特征，当今最常用的机器学习算法，以及如何评估机器学习算法的性能。
3. 我将学到什么？项目结束时，你将能：

处理现实当中不完美的数据集
使用测试数据验证机器学习的结果
使用定量指标评估机器学习的结果
创建、选择和转换特征
比较机器学习算法的性能
为获得最大性能调整机器学习算法
清楚表述你的机器学习算法

4. 为什么这对我的职业发展很重要？机器学习是如今数据分析行业内的金牌敲门砖，能使你获得最令人兴奋的职业机会。
随着配套计算能力的不断发展，数据源数量与日俱增，借助数据快速进行深入探索并做出预测是最直接的方式之一。
机器学习将计算机科学及统计学结合在一起，从而获得强大的预测能力。
5. 我要如何完成此项目？在开始之前，你应该注意，此迷你项目需要大量数据点才能给出直观的结果，并且良好地运行起来。 此项目更为棘手的原因在于，我们使用了真实的数据，这些数据可以是杂乱无章的，而且在进行机器学习时不具有我们所希望的大量数据点。 不要失去信心——作为数据分析师，你只需要习惯不完美的数据！如果你遇到之前没有见过的事物，请退后一步想想聪明的解决之道。要相信自己！
6. 项目概述安然曾是 2000 年美国最大的公司之一。2002 年，由于其存在大量的企业欺诈行为，这个昔日的大集团土崩瓦解。 在随后联邦进行的调查过程中，大量有代表性的保密信息进入了公众的视线，包括成千上万涉及高管的邮件和详细的财务数据。 你将在此项目中扮演侦探，运用你的新技能，根据安然丑闻中公开的财务和邮件数据来构建相关人士识别符。 为了协助你进行侦查工作，我们已将数据与手动整理出来的欺诈案涉案人员列表进行了合并， 这意味着被起诉的人员要么达成和解，要么向政府签署认罪协议，再或者出庭作证以获得免受起诉的豁免权。
7. 需要的资源你的计算机上应有 python 和 sklearn，以及你随“机器学习入门”课程的首个迷你项目一并下载的初始代码（python 脚本和安然数据集）。 你可以从 git 上获取初始代码：
git clone https://github.com/udacity/ud120-projects.git
你可以在下载下来用于迷你项目的代码库 final_project 目录中找到初始代码。相关文件如下所示：
poi_id.py：用于 POI 识别符的初始代码，你将在此处撰写你的分析报告。你也将提交此文件的副本，用于评估人员检验你的算法和结果。
final_project_dataset.pkl：项目数据集，详情如下。
tester.py：在你提交供优达学城评估的分析报告时，你将随附算法、数据集和你使用的特征列表（这些是在 poi_id.py 中自动创建的）。 评估人员将在此后使用这一代码来测试你的结果，以确保性能与你在报告中所述类似。你无需处理这一代码，我们只是将它呈现出来供你参考。
emails_by_address：该目录包含许多文本文件，每个文件又包含特定邮箱的往来邮件。 你可以进行参考，并且可以根据邮件数据集的详细信息创建更多的高级特征。你无需处理电子邮件语料库来完成项目。
8. 迈向成功我们将给予你可读入数据的初始代码，将你选择的特征放入 numpy 数组中，该数组是大多数 sklearn 函数假定的输入表单。 你要做的就是设计特征，选择并调整算法，用以测试和评估识别符。 我们在设计数个迷你项目之初就想到了这个最终的项目，因此请记得借助你已完成的工作成果。
在预处理此项目时，我们已将安然邮件和财务数据与字典结合在一起，字典中的每对键值对应一个人。 字典键是人名，值是另一个字典（包含此人的所有特征名和对应的值）。 数据中的特征分为三大类，即财务特征、邮件特征和 POI 标签。
财务特征: [‘salary’, ‘deferral_payments’, ‘total_payments’, ‘loan_advances’, ‘bonus’, ‘restricted_stock_deferred’, ‘deferred_income’, ‘total_stock_value’, ‘expenses’, ‘exercised_stock_options’, ‘other’, ‘long_term_incentive’, ‘restricted_stock’, ‘director_fees’] (单位均是美元）
邮件特征: [‘to_messages’, ‘email_address’, ‘from_poi_to_this_person’, ‘from_messages’, ‘from_this_person_to_poi’, ‘shared_receipt_with_poi’] (单位通常是电子邮件的数量，明显的例外是 ‘email_address’，这是一个字符串）
POI 标签: [‘poi’] (boolean，整数)
我们鼓励你在启动器功能中制作，转换或重新调整新功能。如果这样做，你应该把新功能存储到my_dataset，如果你想在最终算法中使用新功能，你还应该将功能名称添加到 my_feature_list，以便于你的评估者可以在测试期间访问它。关于如何在数据集中添加具体的新要素的例子，可以参考“特征选择”这一课。
此外，我们还建议你可以在完成项目过程中做一些记号。你可以写出系列问题的答案（在下一页），将这个作为提交的项目的一部分，以便于评估者了解到你对于不同方面分析的方法。你的思维过程在很大程度上比你的最终项目更重要，我们将通过你在这些问题的解答中了解你的思维过程。
9. Project Evaluation9.1. Final Project Evaulation InstructionsWhen you’re finished, your project will have 2 parts: the code/classifier you create and some written documentation of your work. Share your project with others and self-evaluate your project according to the rubric here.
Before you start working on the project: Review the final project rubric carefully. Think about the following questions - How will you incorporate each of the rubric criterion into your project? Why are these aspects important? What is your strategy to ensure that your project “meets specifications” in the given criteria? Once you are convinced that you understand each part of the rubric, please start working on your project. Remember to refer to the rubric often to ensure that you are on the right track.
Items to include when sharing your work with others for feedback:
9.2. Code/ClassifierWhen making your classifier, you will create three pickle files (my_dataset.pkl, my_classifier.pkl, my_feature_list.pkl). The project evaluator will test these using the tester.py script. You are encouraged to use this script before checking to gauge if your performance is good enough. You should also include your modified poi_id.py file in case of any issues with running your code or to verify what is reported in your question responses (see next paragraph).
9.3. Documentation of Your WorkDocument the work you’ve done by answering (in about a paragraph each) the questions found here. You can write your answers in a PDF, Word document, text file, or similar format.
10. 源码分享https://github.com/voidking/ud120-projects
11. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>评估迷你项目</title>
    <url>/dev-ml-evaluation/</url>
    <content><![CDATA[1. 项目简介返回至你上节课的代码，你在其中使用决策树和一个特征构建了 POI 识别符的首个简单迭代。 将你构建的 POI 识别符复制到 evaluation/evaluate_poi_identifier.py 内的骨架代码中。 回忆一下在该项目结尾，你（测试集上的）识别符有 0.724 的准确率。不是很差，对吧？让我们更为仔细地探究你的预测值。


2. 测试集对于 POI 识别符的测试集，有多少 POI 被预测到了？答：4(注意我们说的是测试集！我们期望看到的不是整个数据集中 POI 的数量。)
你的测试集中的总人数是多少？答：29
如果测试集中所有人的识别符都被预测为 0（非 POI），其准确率会是多少？0.8620689655172413
3. True Positive 数量把你的过拟合模型的预测与真实测试标签比较，你得到 true positive 了吗？（在此情况下，我们定义的 true positive 中实际标签和预测标签均为 1）答：Nope
4. 精确率和召回率如你现在可能看到的，类似于安然数据集（非 POI 多于 POI）中的那些不平衡类会带来一些特殊的挑战，即你可以为每个点推测出更为常见的类标签，虽然这不是一个非常有水准的策略，但却仍然能够获得相当不错的准确率！
精确率和召回率可以更好地说明性能。使用 sklearn.metrics 中可用的 precision_score 和 recall_score 来计算这些数量。
精确率是多少？答：0
召回率是多少？答：0
（注意：你可能看到过类似于“用户警告：一些标签的精确率和召回率等于零”的消息。 就像其中所显示的，当精确率和/或召回率为零时，计算其他指标（比如 F1 分数）可能会出现问题，而且在问题发生时，警告消息会显示出来。） 
显然，这并不是一个优化得非常好的机器学习策略（我们没有尝试过决策树以外的任何算法，或调整过任何参数，也没有进行过任何特征选择）， 现在看来，精确率和召回率要比准确率更直观。
5. 混淆矩阵在最终项目中，你将使用在本课程中学习到的众多工具优化 POI 识别符。希望结果是你的精确率和/或召回率会提高，但是你必须能够解读它们。
此处为一些编造的预测值和假设的测试集的真标签；在以下方框中填空，练习识别 true positive、false positive、true negative 和 false negative。 让我们按照惯例，使用“1”表示正结果，“0”表示负结果。
预测值 = [0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1]
真实标签 = [0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0]
有多少 true positive？答：6
此示例中有多少 true negative？答：9
此示例中有多少 false positives？答：3
此示例中有多少 false negatives？答：2
这个分类器的精确率是多少？答：0.666666666667
这个分类器的召回率是多少？答：0.75
6. 理解指标1、填空：
“我的 true positive 率很高，这意味着当测试数据中出现（POI）时，我擅长于标记他或她。”
2、填空：
“我的识别符没有很好的（precision），但是有不错的（recall）。 这意味着，无论我测试集中的 POI 何时被标记，我都可以明确地知道那很有可能是真实的 POI 而非虚警。 另一方面，我为此付出的代价是我有时候会错过真实的 POI，因为我实际上不太情愿触及边界情形。 ”
3、填空：
“我的识别符没有很好的（recall），但是有不错的（precision）。 这意味着，无论我测试集中的 POI 何时被标记，我都可以明确地知道那很有可能是真实的 POI 而非虚警。 另一方面，我为此付出的代价是我有时候会错过真实的 POI，因为我实际上不太情愿触及边界情形”
4、填空：
“我的识别符有非常好的（F1 score）。这是两个世界中最好的识别符，我的 false positive 和 false negative 率均为（low），这意味着我可以可靠、准确地识别 POI。 如果我的识别符发现了 POI，那么此人几乎可以肯定是 POI，而且如果识别符没有标记某人，那么几乎可以肯定他们不是 POI。”
7. POI 识别符指标我们通常需要在精确率和召回率之间进行取舍——你认为在 POI 识别符中哪个更重要？ 答案没有对错，因为无论怎样都会得到很好的论证，但是你应能解读这两个指标，并且清楚表述你发现哪个最重要以及为什么。
8. 源码分享https://github.com/voidking/ud120-projects
9. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>验证迷你项目</title>
    <url>/dev-ml-validation/</url>
    <content><![CDATA[1. 项目简介在此迷你项目中，你将从分割训练数据和测试数据开始。这是你通往“构建 POI 识别符”这个最终项目的第一步。


2. 第一个（过拟合）POI 识别符你将先开始构建想象得到的最简单（未经过验证的）POI 识别符。 本节课的初始代码 (validation/validate_poi.py) 相当直白——它的作用就是读入数据，并将数据格式化为标签和特征的列表。 创建决策树分类器（仅使用默认参数），在所有数据（你将在下一部分中修复这个问题！）上训练它，并打印出准确率。 这是一颗过拟合树，不要相信这个数字！尽管如此，准确率是多少？答：
从 Python 3.3 开始，字典键被处理的顺序发生了变化，顺序在每次代码运行时都会得到随机化处理。 这会造成与评分工具和项目代码（均在 Python 2.7 下运行）的一些兼容性问题。 要更正这个问题，向 validate_poi.py 第 25 行调用的 featureFormat 添加以下参数：
sort_keys = &#x27;../tools/python2_lesson13_keys.pkl&#x27;
这将以 Python 2 的键顺序打开 tools 文件夹中的文件。
注意：如果你没有获得评分工具期望的结果，你可能会想查看 tools/feature_format.py 文件。 由于最终项目发生的变化，一些文件更改影响了此处所写任务的数量输出。 检查你是否从资源库获得了最新版本的文件，以便 featureFormat 具有 sort_keys = False 的默认参数，并且 keys = dictionary.keys() 能够产生结果。
3. 部署训练/测试机制现在，你将添加训练和测试，以便获得一个可靠的准确率数字。 使用 sklearn.cross_validation 中的 train_test_split 验证； 将 30% 的数据用于测试，并设置 random_state 参数为 42（random_state 控制哪些点进入训练集，哪些点用于测试；将其设置为 42 意味着我们确切地知道哪些事件在哪个集中； 并且可以检查你得到的结果）。更新后的准确率是多少？答：0.724137931034
4. 源码分享https://github.com/voidking/ud120-projects
5. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>主成分分析迷你项目</title>
    <url>/dev-ml-pca/</url>
    <content><![CDATA[1. 项目简介我们在讨论 PCA 时花费了大量时间来探讨理论问题，因此，在此迷你项目中，我们将要求你写一些 sklearn 代码。特征脸方法代码很有趣，而且内容丰富，足以胜任这一整个迷你项目的试验平台。
可在 pca/eigenfaces.py 中找到初始代码。此代码主要取自此处 sklearn 文档中的示例。
请注意，在运行代码时，对于在 pca/eigenfaces.py 的第 94 行调用的 SVC 函数，有一个参数有改变。对于“class_weight”参数，参数字符串“auto”对于 sklearn 版本 0.16 和更早版本是有效值，但将被 0.19 舍弃。如果运行 sklearn 版本 0.17 或更高版本，预期的参数字符串应为“balanced”。如果在运行 pca/eigenfaces.py 时收到错误或警告，请确保第 98 行包含与你安装的 sklearn 版本匹配的正确参数。
如果直接运行下载的代码，会先下载233MB的数据文件。你可以点击这里先下载数据集，再根据指示运行代码。


2. 主成分的可释方差我们提到 PCA 会对主成分进行排序，第一个主成分具有最大方差，第二个主成分 具有第二大方差，依此类推。第一个主成分可以解释多少方差？第二个呢？
我们发现，有时 Pillow 模块（本例中使用的）可能会造成麻烦。如果你收到与 fetch_lfw_people() 命令相关的错误，请尝试以下命令：pip install --upgrade PILLOW
如果运行时遇到错误，请注意对于在“pca/eigenfaces.py ”的第 94 行调用的“SVC”函数，有一个参数有改变。对于“class_weight”参数，参数字符串“auto”对于 sklearn 版本 0.16 和更早版本是有效值，但将被 0.19 版本舍弃。如果运行 sklearn 版本 0.17 或更高版本，预期的参数字符串应为“balanced”。如果在运行“pca/eigenfaces.py”时收到错误或警告，请确保第 98 行包含与你安装的 sklearn 版本匹配的正确参数。
3. 要使用多少个主成分？现在你将尝试保留不同数量的主成分。在类似这样的多类分类问题中（要应用两个以上标签），准确性这个指标不像在两个类的情形中那么直观。相反，更常用的指标是 F1 分数。
我们将在评估指标课程中学习 F1 分数，但你自己要弄清楚好的分类器的特点是具有高 F1 分数还是低 F1 分数。你将通过改变主成分数量并观察 F1 分数如何相应地变化来确定。
随着你添加越来越多的主成分作为训练分类器的特征，你认为它的性能会更好还是更差？答：更好
4. F1 分数与使用的主成分数将 n_components 更改为以下值：[10, 15, 25, 50, 100, 250]。对于每个主成分，请注意 Ariel Sharon 的 F1 分数。（对于 10 个主成分，代码中的绘制功能将会失效，但你应该能够看到 F1 分数。）
如果看到较高的 F1 分数，这意味着分类器的表现是更好还是更差？答：更好
5. 维度降低与过拟合在使用大量主成分时，是否看到过拟合的证据？答：是的，当使用大量主成分时，性能开始下降。
6. 源码分享https://github.com/voidking/ud120-projects
7. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>特征选择迷你项目</title>
    <url>/dev-ml-feature-selection/</url>
    <content><![CDATA[1. 项目简介Katie 在视频中解释了她在为“作者识别”项目准备 Chris 和 Sara 的邮件时遇到的一个问题， 即一个特征过于强大（就像签名一样，可以说给了算法一个有失公平的优势）。你将在此亲自探究这一发现。


2. 过拟合决策树此漏洞是在 Katie 试图为决策树迷你项目创建过拟合决策树的示例时发现的。 决策树作为传统算法非常容易过拟合，获得过拟合决策树最简单的一种方式就是使用小型训练集和大量特征。
如果决策树被过拟合，你期望测试集的准确率是非常高还是相当低？答：低
如果决策树被过拟合，你期望训练集的准确率是高还是低？答：高
3. 特征数量和过拟合过拟合算法的一种传统方式是使用大量特征和少量训练数据。你可以在 feature_selection/find_signature.py 中找到初始代码。 准备好决策树，开始在训练数据上进行训练，打印出准确率。
根据初始代码，有多少训练点？答：18849
特别说明：根据你何时下载 find_signature.py 代码，你可能需要将第 9 至 10 行的代码更改为
words_file = &quot;../text_learning/your_word_data.pkl&quot; authors_file = &quot;../text_learning/your_email_authors.pkl&quot;
这样一来，通过运行 vectorize_text.py 创建而来的文件就可以得到适当的体现了。
另外，如果你由于内存问题而无法运行代码，而且如果你的 scikit-learn 版本是 0.16.x， 你可以从 features_train 被创建出来的行中删除 .toarray() 函数， 以节省内存——该版本中的决策树分类器可以将稀疏数组而非仅仅是密集数组作为输入。
你刚才创建的决策树的准确率是多少？答：（记住，我们设置决策树用于过拟合——理想情况下，我们希望看到的是相对较低的测试准确率。）
4. 识别最强的特征选择（过拟合）决策树并使用 featureimportances 属性来获得一个列表， 其中列出了所有用到的特征的相对重要性（由于是文本数据，因此列表会很长）。 我们建议迭代此列表并且仅在超过阈值（比如 0.2——记住，所有单词都同等重要，每个单词的重要性都低于 0.01）的情况下将特征重要性打印出来。
最重要特征的重要性是什么？该特征的数字是多少？答：
5. 使用 TfIdf 获得最重要的单词为了确定是什么单词导致了问题的发生，你需要返回至 TfIdf，使用你从迷你项目的上一部分中获得的特征数量来获取关联词。 你可以在 TfIdf 中调用 get_feature_names() 来返回包含所有单词的列表；抽出造成大多数决策树歧视的单词。
这个单词是什么？类似于签名这种与 Chris Germany 或 Sara Shackleton 唯一关联的单词是否讲得通？答：
6. 删除重复从某种意义上说，这一单词看起来像是一个异常值，所以让我们在删除它之后重新拟合。 返回至 text_learning/vectorize_text.py，使用我们删除“sara”、“chris”等的方法，从邮件中删除此单词。 重新运行 vectorize_text.py，完成以后立即重新运行 find_signature.py。
有跳出其他任何的异常值吗？是什么单词？像是一个签名类型的单词？（跟之前一样，将异常值定义为重要性大于 0.2 的特征）。答： 
7. 再次检查重要特征再次更新 vectorize_test.py 后重新运行。然后，再次运行 find_signature.py。
是否出现其他任何的重要特征（重要性大于 0.2）？有多少？它们看起来像“签名文字”，还是更像来自邮件正文的“邮件内容文字”？答：
8. 过拟合树的准确率现在决策树的准确率是多少？答：0.816268486917
我们已经移除了两个“签名词语”，所以要让我们的算法拟合训练集，同时不出现过拟合更为困难。记住，我们这里是想要知道我们是否会让算法过拟合，准确率如何并不是关键！
9. 源码分享https://github.com/voidking/ud120-projects
10. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>文本学习迷你项目</title>
    <url>/dev-ml-text-learning/</url>
    <content><![CDATA[1. 项目简介本节课开始，你使用大量监督式分类算法，根据作者来识别邮件。 在这些项目中，我们为你做了预处理，将输入邮件转换到 TfIdf 中，这样你就能向算法提供这些邮件了。 现在，你将自行完成预处理工作，以便你能从原始数据直接得到经过处理的特征。
你将得到两个文本文件：一个包含来自 Sara 的所有邮件，一个包含 Chris 的邮件。 你还将访问 parseOutText() 函数，该函数接受作为参数的已读邮件，并且返回包含邮件中所有（被词干化的）单词的字符串。


2. 项目准备# 下载语料库import nltknltk.download()# 停止词from nltk.corpus import stopwordssw = stopwords.words(&#x27;english&#x27;)print len(sw)# 词干化from nltk.stem.snowball import SnowballStemmerstemmer = SnowballStemmer(&#x27;english&#x27;)stemmer.stem(&#x27;responsiveness&#x27;)stemmer.stem(&#x27;responsivity&#x27;)stemmer.stem(&#x27;unresponsive&#x27;)

3. parseOutText()你将从热身习题开始了解 parseOutText()。 前往工具目录并运行 parse_out_email_text.py，该程序包含 parseOutText() 和一封测试邮件，用以运行此函数。
parseOutText() 获得被打开的邮件，然后仅返回文字部分，除去了可能出现在邮件开头的元数据，接下来就是邮件正文了。 我们现在已经设置好了这一脚本，这样它就能将邮件正文打印到屏幕上，你运行 parseOutText() 时会得到怎样的正文？答：Hi Everyone  If you can read this message youre properly using parseOutText  Please proceed to the next part of the project
在 parseOutText() 中，添加以下注释： 
# words = text_string 

增强 parseOutText() ，这样返回的字符串就有了所有因使用 SnowballStemmer（使用 nltk 包，可在 http://www.nltk.org/howto/stem.html 找到我发现有用的一些示例）而获得的词干化单词。
重新运行 parse_out_email_text.py，该程序将使用你更新的 parseOutText() 函数。你现在的输出是什么？答：hi everyon if you can read this messag your proper use parseouttext pleas proceed to the next part of the project
提示：你需要将字符串分解成单个单词，词干化每个单词，然后再将所有单词重新组合成一个字符串。
4. 清除“签名文字”在 text_learning/vectorize_text.py 中，你将迭代所有来自 Chris 和 Sara 的邮件。 将每封已读邮件提供给 parseOutText() 并返回词干化的文本字符串。然后做以下两件事：

删除签名文字（“sara”、“shackleton”、“chris”、“germani”——如果你知道为什么是“germani”而不是“germany”，你将获得加分）
向 word_data 添加更新的文本字符串——如果邮件来自 Sara，向 from_data 添加 0（零），如果是 Chris 写的邮件，则添加 1。

完成此步骤后，你应该有两个列表：一个包含了每封邮件被词干化的正文，第二个应该包含用来编码（通过 0 或 1）谁是邮件作者的标签。
对所有邮件运行程序需要花一些时间（5 分钟或更长时间），所以我们添加了一个 temp_counter，将第 200 封之后的邮件切割掉。 当然，一切就绪后，你会希望对整个数据集运行程序。
在以下方框中，放入你得到的 word_data[152] 字符串。答：tjonesnsf stephani and sam need nymex calendar
5. 进行 TfIdf使用 sklearn TfIdf 转换将 word_data 转换为 tf-idf 矩阵。删除英文停止词。
你可以使用 get_feature_names() 访问单词和特征数字之间的映射，该函数返回一个包含词汇表所有单词的列表。有多少不同的单词？答：
你 TfId 中的单词编号 34597 是什么？答：stephen
需要说明的是，如果问题是“单词编号 100 是什么”，我们肯定会查找对应 vocab_list[100] 的单词。有时候，零索引数组谈论起来非常不好理解。
6. 源码分享https://github.com/voidking/ud120-projects
7. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>特征缩放迷你项目</title>
    <url>/dev-ml-feature-scaling/</url>
    <content><![CDATA[1. 项目简介在上一个项目中，你将安然人物的财务数据作为输入，对这些人物执行了 k-均值聚类。我们将更新那部分工作，以包含被缩放的特征，看看会有怎样的变化。


2. 缩放类型回顾 K-均值聚类迷你项目最后一部分。我们当时没有详细探讨缩放算法而部署了缩放，但是你现在更加了解具体的缩放算法了，并且可以分析出我们使用的是哪类缩放。
哪类缩放被部署了？
3. 计算重缩放特征对你在上一节课中的 k 均值聚类代码的“salary”和“exercised_stock_options”特征（仅这两项特征）运用特征缩放。 原始值为 20 万美元的“salary”特征和原始值为 1 百万美元的“exercised_stock_options”特征的重缩放值会是多少？ （确保呈现浮点型而非整数型数字！）答：0.17962407，0.02902059
4. 何时部署特征缩放有人可能会质疑是否必须重缩放财务数据，也许我们希望 10 万美元的工资和 4 千万美元的股票期权之间存在巨大差异。如果我们想基于“from_messages”（从一个特定的邮箱帐号发出的电子邮件数）和“salary”来进行集群化会怎样？ 在这种情形下，特征缩放是不必要的，还是重要的？答：重要的。
5. 源码分享https://github.com/voidking/ud120-projects
6. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>K-均值聚类迷你项目</title>
    <url>/dev-ml-k-means/</url>
    <content><![CDATA[1. 项目简介在此项目中，我们会将 k-均值聚类应用于安然财务数据。当然，我们最终的目标是识别相关人员；既然我们有了已标记数据，调用 k-均值聚类这种非监督式方法就不成问题。
尽管如此，在此项目中，你仍然会获得 k-均值的一些实际操作经验并尝试特征缩放，这会让你预先了解下一课的材料。可在此处找到安然数据集。


2. 聚类特征可以在 k_means/k_means_cluster.py 中找到初始代码，该代码会读入电子邮件 + 财务 (E+F) 数据集，让我们为聚类做好准备。首先你将基于两个财务特征开始执行 K-means，请查看代码并确定代码使用哪些特征进行聚类。
运行代码，这将创建数据的散点图。仔细思考如果创建两个聚类，你预期会产生哪两个聚类。答：salary，exercised_stock_options
3. 部署聚类在 financial_features 数据上部署 k-均值聚类，并将 2 个聚类指定为参数。将聚类预测存储到名为 pred 的列表，以便脚本底部的 Draw() 命令正常工作。在弹出的散点图中，聚类是否是你预期的？
4. 使用 3 个特征聚类向特征列表（features_list）中添加第三个特征：“total_payments”。现在使用 3 个，而不是 2 个输入特征重新运行聚类（很明显，我们仍然可以只显示原来的 2 个维度）。将聚类绘图与使用 2 个输入特征获取的绘图进行比较。是否有任何点切换群集？多少个点？这种使用 3 个牲的新聚类无法通过肉眼加以猜测——必须通过 k-均值算法才能识别它。
（你需要更改创建散点图的代码，以便容纳 3 个特征而不是 2 个，有关操作说明，请参阅初始代码中的注释。）
5. 股票期权范围在下一课中，我们将讨论特征缩放。它是一种特征预处理，应在执行某些分类和回归任务之前执行。这里只是快速预览，概述特征缩放的功能。
本例中使用的“exercised_stock_options”特征取的最大值和最小值是什么？
（注意：如果查看 finance_features，会发现有些“NaN”值已被清理并被零值取代——因此尽管那些值可能看起来像是最小值，但却具有欺骗性，因此它们更像是你不具有其相关信息而必须填入一个数字的点。对于此问题，请返回 data_dict 并查找显示的最大值和最小值，忽略所有“NaN”条目。）
6. 薪酬范围“salary”取的最大值和最小值是什么？
（注意：与上一个测试题中的注意事项相同。如果查看 finance_features，会发现有些“NaN”值已被清理并被零值取代——因此尽管那些值可能看起来像是最小值，但却具有欺骗性，因此它们更像是你不具有其相关信息而必须填入一个数字的点。对于此问题，请返回 data_dict 并查找显示的最大值和最小值，忽略所有“NaN”条目。）
7. 聚类更改下一张幻灯片上的绘图会显示你刚刚编写的聚类代码，但在本例中，我们在执行聚类之前应用了特征缩放。
我们希望你将（下一张幻灯片上）使用缩放的聚类与在聚类算法中使用*两个特征时生成的第一个聚类可视化效果进行比较。
请注意，特征范围现在已更改为 [0.0, 1.0]。这是我们所做的唯一更改。
在下一课中，你将详细了解特征缩放的含义，但现在，只需查看对聚类产生的影响–哪个/哪些点会切换它们关联的聚类？ 
8. 源码分享https://github.com/voidking/ud120-projects
9. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>异常值迷你项目</title>
    <url>/dev-ml-outliers/</url>
    <content><![CDATA[1. 项目简介此项目有两部分。在第一部分中将运行回归，然后识别并删除具有最大残差的 10% 的点。然后，根据 Sebastian 在课程视频中所建议的，从数据集中删除那些异常值并重新拟合回归。
在第二部分中，你将熟悉安然财务数据中的一些异常值，并且了解是否/如何删除它们。


2. 带有异常值Sebastian 向我们描述了改善回归的一个算法，你将在此项目中实现该算法。你将在接下来的几个测试题中运用这一算法。总的来说，你将在所有训练点上拟合回归。舍弃在实际 y 值和回归预测 y 值之间有最大误差的 10% 的点。
先开始运行初始代码 (outliers/outlier_removal_regression.py) 和可视化点。一些异常值应该会跳出来。部署一个线性回归，其中的净值是目标，而用来进行预测的特征是人的年龄（记得在训练数据上进行训练！）。
数据点主体的正确斜率是 6.25（我们之所以知道，是因为我们使用该值来生成数据）；你的回归的斜率是多少？答：5.07793064
当使用回归在测试数据上进行预测时，你获得的分数是多少？答：0.878262470366
3. 清理异常值后你将在 outliers/outlier_cleaner.py 中找到 outlierCleaner() 函数的骨架并向其填充清理算法。用到的三个参数是：predictions 是一个列表，包含回归的预测目标；ages 也是一个列表，包含训练集内的年龄；net_worths 是训练集内净值的实际值。每个列表中应有 90 个元素（因为训练集内有 90 个点）。你的工作是返回一个名叫cleaned_data 的列表，该列表中只有 81 个元素，也即预测值和实际值 (net_worths) 具有最小误差的 81 个训练点 (90 * 0.9 = 81)。cleaned_data 的格式应为一个元组列表，其中每个元组的形式均为 (age, net_worth, error)。
一旦此清理函数运行起来，你应该能看到回归结果发生了变化。新斜率是多少？是否更为接近 6.25 这个“正确”结果？答：6.36859481
当使用回归在测试集上进行预测时，新的分数是多少？答：0.983189455396
4. 安然异常值在本节回归课程的迷你项目中，你使用回归来预测安然雇员的奖金。如你所见，单一的异常值都可以对回归结果造成很大的差异。但是，我们之前没有跟你说过的是，你在项目中使用的数据集已经被清理过明显的异常值了。第一次看到数据集时，识别并清除异常值是你一直应该思考的问题，而你现在已经通过安然数据有了一定的实践经验。
你可以在 outliers/enron_outliers.py 中找到初始代码，该代码读入数据（以字典形式）并将之转换为适合 sklearn 的 numpy 数组。由于从字典中提取出了两个特征（“工资”和“奖金”），得出的 numpy 数组维度将是 N x 2，其中 N 是数据点数，2是特征数。对散点图而言，这是非常完美的输入；我们将使用 matplotlib.pyplot 模块来绘制图形。（在本课程中，我们对所有可视化均使用 pyplot。）将这些行添加至脚本底部，用以绘制散点图：
for point in data:    salary = point[0]    bonus = point[1]    matplotlib.pyplot.scatter( salary, bonus )matplotlib.pyplot.xlabel(&quot;salary&quot;)matplotlib.pyplot.ylabel(&quot;bonus&quot;)matplotlib.pyplot.show()

如你所见，可视化是查找异常值最强大的工具之一！
5. 识别最大的安然异常值有一个异常值应该会立即跳出来。现在的问题是识别来源。我们发现原始数据源对于识别工作非常有帮助；你可以在 final_project/enron61702insiderpay.pdf 中找到该 PDF。
该数据点的字典键名称是什么？（例如：如果是 Ken Lay，那么答案就是“LAY KENNETH L”）。最大 Enron 异常值的字典 key 值是什么？
6. 移除安然异常值在此数据集上运行机器学习时，该异常值是否像我们应该包含的数据点？是否应该删除它？答：应该移除
从字典中快速删除键值对的一种方法如以下行所示：
dictionary.pop( key, 0 )

写下这样的一行代码（你必须修改字典和键名）并在调用 featureFormat() 之前删除异常值。然后重新运行代码，你的散点图就不会再有这个异常值了。
所有异常值都没了吗？
我们认为还有 4 个异常值需要调查；让我们举例来看。两人获得了至少 5 百万美元的奖金，以及超过 1 百万美元的工资；换句话说，他们就像是强盗。
和这些点相关的名字是什么？
你是否会猜到这些就是我们应该删除的错误或者奇怪的电子表格行，你是否知道这些点之所以不同的重要原因？（换句话说，在我们试图构建 POI 识别符之前，是否应该删除它们？）答：否
7. 源码分享https://github.com/voidking/ud120-projects
8. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>回归迷你项目</title>
    <url>/dev-ml-regression/</url>
    <content><![CDATA[1. 项目简介在此项目中，你将使用回归来预测安然雇员和合伙人的财务数据。一旦你知道某位雇员的财务数据，比如工资，你是否会预测他们奖金的数额？


2. 目标和特征运行在 regression/finance_regression.py 中找到的初始代码。这将绘制出一个散点图，其中有所有的数据点。你尝试预测什么目标？用来预测目标的输入特征是什么？
在脑海中描绘出你大致预测的回归线（如果打印散点图并用纸笔来描绘，效果会更好）。
3. 可视化回归数据就像在分类中一样，你需要在回归中训练和测试数据。这在初始代码中已被设定。将 test_color 的值从“b”改为“r”（针对“red”），然后重新运行。
注意：对于将 Python 2 代码转换至 Python 3 的学员，请参见以下关于兼容性的重要备注。
你将仅使用蓝色（训练）点来拟合回归。（你可能已经注意到，我们放入测试集的是 50% 的数据而非标准的 10%—因为在第 5 部分中，我们将改变训练和测试数据集，并且平均分割数据使这种做法更加简单。）从 Python 3.3 版本开始，字典的键值顺序有所改变，在每次代码运行时，字典的键值皆为随机排序。这会让我们在 Python 2.7 环境下工作的评分者遭遇一些兼容性的问题。为了避免这个问题，请在 finance_regression.py 文件的第26行 featureFormat 调用时添加一个参数
sort_keys = ‘../tools/python2_lesson06_keys.pkl’
它会打开 tools 文件夹中带有 Python 2 键值顺序的数据文件。
4. 提取斜率和截距从 sklearn 导入 LinearRegression 并创建/拟合回归。将其命名为 reg，这样绘图代码就能将回归覆盖在散点图上呈现出来。回归是否大致落在了你期望的地方？
提取斜率（存储在 reg.coef_ 属性中）和截距。
斜率和截距是多少？答：斜率为5.44814029，截距为-102360.543294
5. 训练数据假设你是一名悟性不太高的机器学习者，你没有在测试集上进行测试，而是在你用来训练的相同数据上进行了测试，并且用到的方法是将回归预测值与训练数据中的目标值（比如：奖金）做对比。
你找到的分数是多少？你可能对“良好”分数还没有概念；此分数不是非常好（但却非常糟糕）。答：0.0455091926995
6. 测试数据现在，在测试数据上计算回归的分数。
测试数据的分数是多少？如果只是错误地在训练数据上进行评估，你是否会高估或低估回归的性能？答：-1.48499241737
7. 根据 LTI 回归奖金我们有许多可用的财务特征，就预测个人奖金而言，其中一些特征可能比余下的特征更为强大。例如，假设你对数据做出了思考，并且推测出“long_term_incentive”特征（为公司长期的健康发展做出贡献的雇员应该得到这份奖励）可能与奖金而非工资的关系更密切。
证明你的假设是正确的一种方式是根据长期激励回归奖金，然后看看回归是否显著高于根据工资回归奖金。根据长期奖励回归奖金—测试数据的分数是多少？答：-0.59271289995
8. 工资与 LTI如果你必须预测某人的奖金并且你只有一小段相关信息，你想要知道他们的工资还是长期奖励？答：长期奖励
9. 异常值破坏回归这是下节课的内容简介，关于异常值的识别和删除。返回至之前的一个设置，你在其中使用工资预测奖金，并且重新运行代码来回顾数据。你可能注意到，少量数据点落在了主趋势之外，即某人拿到高工资（超过 1 百万美元！）却拿到相对较少的奖金。此为异常值的一个示例，我们将在下节课中重点讲述它们。
类似的这种点可以对回归造成很大的影响：如果它落在训练集内，它可能显著影响斜率/截距。如果它落在测试集内，它可能比落在测试集外要使分数低得多。就目前情况来看，此点落在测试集内（而且最终很可能降低分数）。让我们做一些处理，看看它落在训练集内会发生什么。在 finance_regression.py 底部附近并且在 plt.xlabel(features_list[1]) 之前添加这两行代码：
reg.fit(feature_test, target_test)plt.plot(feature_train, reg.predict(feature_train), color=”b”)
现在，我们将绘制两条回归线，一条在测试数据上拟合（有异常值），一条在训练数据上拟合（无异常值）。来看看现在的图形，有很大差别，对吧？单一的异常值会引起很大的差异。
新的回归线斜率是多少？答：2.27410114
（你会发现差异很大，多数情况下由异常值引起。下一节课将详细介绍异常值，这样你就有工具来检测和处理它们了。）
10. 源码分享https://github.com/voidking/ud120-projects
11. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>安然迷你项目</title>
    <url>/dev-ml-enron/</url>
    <content><![CDATA[1. 项目简介安然欺诈案是一个混乱而又引人入胜的大事件，从中可以发现几乎所有想像得到的企业违法行为。安然的电子邮件和财务数据集还是巨大、混乱的信息宝藏，而且，在你稍微熟悉这些宝藏后，它们会变得更加有用。我们已将这些电子邮件和财务数据合并为一个数据集，而你将在此迷你项目中研究它。
开始：克隆这个 GitHub 库：https://github.com/udacity/ud120-projects运行开始代码：datasets_questions/explore_enron_data.py


2. 数据集大小聚合的安然电子邮件和财务数据集被存储到字典中，字典中的每个键是一个人名，而且值是包含此人所有特征的一个字典。
电子邮件和财务 (E+F) 数据字典被存储在 pickle 文件中，该文件可直接存储和加载 python 对象，非常方便。 使用 datasets_questions/explore_enron_data.py 加载数据集。
数据集中有多少数据点（人）？答：146
3. 数据集特征对于每个人，有多少个特征可用？答：21
4. 查找POI根据我们的定义，“POI”（Person of interest，嫌疑人）特征记录着一个人是否为相关人士的信息。
数据集中有多少 POI？也就是说，计算 data[person_name][“poi”]==1 时，字典中条目的数量。答：18
更换数据源后存在多少 POI？我们编辑了一个包含所有 POI 姓名的列表（在 ../final_project/poi_names.txt 中）并附上了相应的邮箱地址（在 ../final_project/poi_email_addresses.py 中）。
总共有多少 POI？(使用姓名列表，不要用邮箱地址，因为许多雇员不止一个邮箱，而且其中少数人员不是安然的雇员，我们没有他们的邮箱地址。)答：35
不完整数据你可以看到，我们在数据集中有许多POI，但不是所有。这为何会是一个潜在的问题？
我们将在稍后解释POI为何有可能不在安然数据集中，这样你就可以在往下继续之前完全理解这个问题。
5. 查询数据集和任何字典一样，个人特征可以这样被访问：
enron_data[“LASTNAME FIRSTNAME”][“feature_name”]或者enron_data[“LASTNAME FIRSTNAME MIDDLEINITIAL”][“feature_name”]
1、James Prentice 名下的股票总值是多少？答：1095040
2、有多少来自 Wesley Colwell 的发给嫌疑人的电子邮件？答：11
3、Jeffrey K Skilling 行使的股票期权价值是多少？答：19250000
6. 研究安然欺诈案在下面的课程中，我们将介绍为什么大部分情况下，最好的特征都来自我们的直觉。这意味着我们将对安然欺诈案稍作了解。
如果你能腾出一个半小时的时间，可以观看《安然：房间中最聪明的伙计（Enron: The Smartest Guys in the Room）》这部纪录片，它精彩概述了整个事件。另外，还有许多报纸和故事记录着安然的兴衰成败。
下面这些阴谋中，哪一个没有安然的参与？

每月底向空壳公司出售资产，并在次月初购回，以隐瞒会计损失
造成加州电网断电
非法获取政府报告，垄断浓缩橙汁行业
图谋为一名沙特王子加急办理美国公民身份
计划与卖座电影合作在网上传输电影

1、欺诈案发生的多数时间内，安然的 CEO 是谁？答：Jeffrey Skilling
2、安然的董事会主席是谁？答：Kenneth Lay
3、欺诈案发生的多数时间内，安然的 CFO（首席财务官）是谁？答：Andrew Fastow
4、这三个人（Lay、Skilling 和 Fastow）当中，谁拿回家的钱最多（“total_payments”特征的最大值）？答：LAY KENNETH L 103559793
5、对于数据集中的所有人，不是每一个特征都有值。当特征没有明确的值时，我们使用什么来表示它？答：NaN
6、此数据集中有多少雇员有量化的工资？已知的邮箱地址是否可用？答：95，111
7. 字典到数组转换不能将 python 字典直接读入到 sklearn 分类或回归算法中；它其实需要一个 numpy 数组，或者一个由列表组成的列表（此列表本身是一个列表，它的每个元素都是数据点，而较小列表的元素是该点的特征）。
我们编写了一些辅助函数（tools/feature_format.py 中的 featureFormat() 和 targetFeatureSplit()），它们可以获取特征名的列表和数据字典，然后返回 numpy 数组。
如果特征没有某个特定人员的值，此函数还会用 0（零）替换特征值。
8. 缺少POI如你刚才所见，不是每个 POI 在数据集中都有一个条目（比如：Michael Krautz）。那是因为数据集是通过你在 final_project/enron61702insiderpay.pdf 中找到的财务数据所创建的，这些数据中缺少了一些 POI（这些缺失的 POI 被传送至最终的数据集）。另一方面，对于这些“缺少的”POI，我们确实有他们的邮件。
尽管向数据集中添加这些 POI 和他们的信息，并且为财务信息设置“NaN”非常简单，但这会带来一个微妙的问题。你将在此处了解到这一问题。
1、（当前的）数据集中有多少人的薪酬总额被设置了“NaN”？数据集中这些人的比例占多少？答：21，14.3835616438%
2、数据集中有多少 POI 的薪酬总额被设置了“NaN”？这些 POI 占多少比例？答：0，0.0%
3、如果机器学习算法将 total_payments 用作特征，你希望它将“NaN”值关联到 POI 还是非 POI？答：非POI
4、如果你再次添加了全是 POI 的 10 个数据点，并且对这些雇员的薪酬总额设置了“NaN”，你刚才计算的数字会发生变化。数据集中这些人的数量变成了多少？薪酬总额被设置了“NaN”的雇员数变成了多少？答：156，31
5、数据集中的 POI 数量变成了多少？薪酬总额被设置了“NaN”的 POI 数量变成了多少？答：28，10
6、在添加了新的数据点后，你是否认为，监督式分类算法可将 total_payments 为“NaN”理解为某人是 POI 的线索？答：是
9. 混合数据源此例中加入了新的 POI，而我们没有任何人的财务信息，这就带来了一个微妙的问题，即算法可能会注意到我们缺少他们的财务信息，并将这一点作为他们是POI的线索。换个角度来看，为我们的两个类生成数据的方式现在有所不同 - 非POI的人全都来自财务电子表格，之后手动加入了许多POI。这种不同可能会诱使我们以为我们的表现优于实际状况 - 假设你使用 POI 检测器来确定某个未见过的新人是否是 POI，而且该人不在电子表格上。然后，他们的所有财务数据都将包含“NaN”，但该人极有可能不是 POI（世界上非 POI 的人比 POI 多得多，即使在安然也是如此）- 然而你可能会无意中将他们标识为 POI！
这就是说，在生成或增大数据集时，如果数据来自不同类的不同来源，你应格外小心。它很容易会造成我们在此展示的偏差或错误类型。可通过多种方法处理此问题。举例而言，如果仅使用了电子邮件数据，则你无需担心此问题（在这种情况下，财务数据中的差异并不重要，因为并未使用财务特征）。还可以通过更复杂的方法来估计这些偏差可能会对你的最终答案造成多大影响，不过此话题超出了本课程的范围。
目前的结论就是，要非常小心地对待引入来自不同来源（具体取决于类）的特征这个问题！引入此类特征常常会意外地带来偏差和错误。
10. 源码分享https://github.com/voidking/ud120-projects
11. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>选择自己的算法</title>
    <url>/dev-ml-new-algorithm/</url>
    <content><![CDATA[1. 为何要学习新算法？任何数据分析师具备的一项关键技能就是不断从机器学习中得到新的认识，这也是本节课的学习目标。这节课的内容是一个迷你项目。目标是用你选择的算法来做地形分类，并由你自己进行研究和部署。


可选的算法如下：
1、k nearest neighbors（k 最近邻 或 KNN）2、random forest（随机森林）3、adaboost（有时也叫“被提升的决策树”）
我们无法检查你的结果，因为你有太多的算法和参数组合可以尝试了，但是你看到过我们上一个算法（朴素贝叶斯、SVM、决策树）所得出的准确率，因此你可以自行评估新的算法是否更好。
你将在 choose_your_own/your_algorithm.py 文件中寻找初始代码来准备你的数据。以下视频还给出了更多你应该遵循的算法和过程的细节，不过你需要自行去发现。祝你好运！
2. 学习算法的步骤1、搜索相关资料，大致了解这个算法，能够向朋友解释这个算法。2、查找sklearn中关于这个算法的文档，运行文档给的demo。3、使用算法对实际问题进行预测。4、对算法进行评估，准确率和速度。
3. 挑战我们使用任一这些算法（朴素贝叶斯、SVM、决策树、AdaBoost、随机森林、KNN）能够获得的最高准确率是 93.6%。这是一个有趣的挑战：你可以打败我们吗？如果可以，请在方框中写下你的方法（算法和参数）。
答：knn算法，准确率94%。
4. 源码分享https://github.com/voidking/ud120-projects
5. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>决策树迷你项目</title>
    <url>/dev-ml-dt/</url>
    <content><![CDATA[1. 项目背景在本项目中，我们将再次尝试确认邮件作者，但这次使用的是决策树。初始代码可以在 decision_tree/dt_author_id.py 中找到。
你仍需要在你计算机上完成迷你项目，在浏览器中输入答案。你可以在这里找到决策树迷你项目的说明。


2. 运行起来使用 decision_tree/dt_author_id.py 中的初始代码，准备好决策树并将它作为分类器运行起来，设置 min_samples_split=40。可能需要等一段时间才能开始训练。
准确率是多少？
training time: 136.862 s0.978384527873

3. 加速你从 SVM 迷你项目中了解到，参数调整可以显著加快机器学习算法的训练时间。一般情况下，参数可以调整算法的复杂度，越复杂的算法通常运行起来越慢。
控制算法复杂度的另一种方法是通过你在训练/测试时用到的特征数量。算法可用的特征数越多，越有可能发生复杂拟合。我们将在“特征选择”这节课中详细探讨，但你现在可以提前有所了解。
1、从你的数据中找出特征的数量，数据是以 numpy 数组的形式排列的，其中数组的行数代表数据点的数量，列数代表特征的数量；为了提取这个数值，可以写一行这样的代码len(features_train[0])
3785

2、进入 tools/email_preprocess.py，会看到这样的代码：selector = SelectPercentile(f_classif, percentile=10) ，将 percentile 从 10 改为 1。现在的特征数量是多少呢？
379


3、你认为 SelectPercentile 起到什么作用？其他所有的都不变的情况下，赋予percentile的值较大是否得到一棵更加复杂的或者简化的决策树？答：赋予percentile的值较大，得到更复杂的决策树。
4、注意训练时间的不同取决于特征的数量。
5、当 percentile 等于 1 时，准确度是多少？
training time: 9.185 s0.966439135381


4. 源码分享https://github.com/voidking/ud120-projects
5. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>SVM迷你项目</title>
    <url>/dev-ml-svm/</url>
    <content><![CDATA[1. 项目背景在此迷你项目中，我们将解决与朴素贝叶斯迷你项目相同的电子邮件作者 ID 问题，不同的是我们将运用 SVM。我们的研究结果将阐明两种算法之间的一些实际差异。此项目还向我们提供比朴素贝叶斯更多的机会来使用参数，因此我们也将这样做。


2. 运行代码转到 svm 目录，查找初始代码 (svm/svm_author_id.py)。
使用 sklearn SVC 分类器进行导入、创建、训练和预测。在创建分类器时使用线性内核（如果你忘记此步骤，你会发现分类器要花很长的时间来训练）。
分类器的准确率是多少？
3. 时间跟你在朴素贝叶斯迷你项目中所做的一样，将计时代码用于拟合和预测函数。与朴素贝叶斯相比，训练和预测如何计时？
朴素贝叶斯结果：
training time: 1.517 s0.973265073948

SVM结果：
training time: 207.594 s0.984072810011

4. 减小训练集加快算法速度的一种方式是在一个较小的训练数据集上训练它。这样做换来的是准确率几乎肯定会下降。让我们更具体地探讨这个问题：在训练分类器之前，立即加入以下两行。 
features_train = features_train[:len(features_train)/100] labels_train = labels_train[:len(labels_train)/100] 

这两行有效地将训练数据集切割至原始大小的 1%，丢弃掉 99% 的训练数据。你可以使其他所有代码保持不变。
现在的准确率是多少？修改后SVM结果：
training time: 0.124 s0.884527872582

5. 速度与准确率如果速度是一个主要考虑因素（对于许多实时机器学习应用而言确实如此），并且如果牺牲一些准确率可加快你的训练/预测速度，则你可能会想这样做。
在以下哪些应用中，你可以想象非常快速地运行的算法尤其重要？
1、预测电子邮件作者2、标记信用卡欺诈，在欺诈发生之前阻止交易3、Siri 之类的语音识别
答案是2和3。
6. 部署RBF内核保留上一个测试题中的训练集代码段，以便仍在 1% 的完整训练集上进行训练。将 SVM 的内核更改为“rbf”。
这个更复杂的内核给出的准确率是多少？
training time: 0.139 s0.616040955631

7. 优化C参数保持训练集大小不变，并且保留上一个测试题中的 rbf 内核，但是尝试多个 C 值（比如：10.0、100.、1000. 和 10000.）。
哪个给出的准确率最高？结果依次为：
training time: 0.142 s0.616040955631training time: 0.139 s0.616040955631training time: 0.132 s0.821387940842training time: 0.127 s0.892491467577

优化C后的准确率在你为 RBF 内核优化了 C 值后，你会获得怎样的准确率？该 C 值是否对应更简单或者更复杂的决策边界？
（如果你不确定复杂度，请回顾本节课有关“SVM C 参数”的视频。你在该处发现的结果同样适用于此处，不过，在简单的散点图中画出决策边界现在变得更加困难甚至不可能。）
8. 优化C后使用完整训练集你已经为 RBF 内核优化了 C，现在恢复为使用完整的训练集。较大的训练集往往能提高算法的性能，所以（通过在大数据集上调整 C 和进行训练）我们应得到相当优化的结果。
经过优化的 SVM 的准确率是多少？
training time: 142.459 s0.990898748578

9. 从SVM提取预测你的 SVM（0 或 1，分别对应 Sara 和 Chris）将测试集中的元素10、元素26和元素50分别预测为哪一类？
（使用 RBF 内核、C=10000 和 1% 的训练集。通常，使用完整的训练集能获得最好的结果，但是我们发现使用 1% 的完整训练集不仅大幅加快计算过程，而且不会改变我们的结果，因此你在这里可以随意使用该快捷算法。）
而且需要说明的是，我们这里给出的数据点数字 (10, 26, 50) 假设使用的是零索引列表。因此，使用类似于 answer=predictions[100] 的表达式可找到元素 # 100 的正确答案。
10. 预测Chris的邮件数量There are over 1700 test events–how many are predicted to be in the “Chris” (1) class? (Use the RBF kernel, C=10000., and the full training set.) 
测试事件的数量超过 1700——其中多少预测在“Chris” (1) 类中？（使用 RBF 内核、C=10000. 以及完整的训练集。）
training time: 137.599 s877

11. 部署SVM最后提醒希望 Sebastian 在说朴素贝叶斯非常适合文本时，更清楚地表达了他的意思。对于这一具体问题，朴素贝叶斯不仅更快，而且通常比 SVM 更出色。当然，SVM 更适合许多其他问题。你在第一次求解问题时就知道该尝试哪个算法，这是机器学习艺术和科学性的一个体现。除了选择算法外，视你尝试的算法而定，你还需要考虑相应的参数调整以及过拟合的可能性（特别是在你没有大量训练数据的情况下）。
我们通常建议你尝试一些不同的算法来求解每个问题。调整参数的工作量很大，但你现在只需要听完这堂课，我们将向你介绍 GridCV，一种几乎能自动查找最优参数调整的优秀 sklearn 工具。
12. 源码分享https://github.com/voidking/ud120-projects
13. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>朴素贝叶斯迷你项目</title>
    <url>/dev-ml-naive-bayes/</url>
    <content><![CDATA[1. 项目背景几年前，J.K. 罗琳（凭借《哈利波特》出名）试着做了件有趣的事。她以 Robert Galbraith 的化名写了本名叫《The Cuckoo’s Calling》的书。尽管该书得到一些不错的评论，但是大家都不太重视它，直到 Twitter 上一个匿名的知情人士说那是 J.K. Rowling 写的。《伦敦周日泰晤士报》找来两名专家对《杜鹃在呼唤》和 Rowling 的《偶发空缺》以及其他几名作者的书进行了比较。分析结果强有力地指出罗琳就是作者，《泰晤士报》直接询问出版商情况是否属实，而出版商也证实了这一说法，该书在此后一夜成名。


我们也将在此项目中做类似的事。我们有一组邮件，分别由同一家公司的两个人撰写其中半数的邮件。我们的目标是仅根据邮件正文区分每个人写的邮件。在这个迷你项目一开始，我们将使用朴素贝叶斯，并在之后的项目中扩展至其他算法。
我们会先给你一个字符串列表。每个字符串代表一封经过预处理的邮件的正文；然后，我们会提供代码，用来将数据集分解为训练集和测试集（在下节课中，你将学习如何进行预处理和分解，但是现在请使用我们提供的代码）。
朴素贝叶斯特殊的一点在于，这种算法非常适合文本分类。在处理文本时，常见的做法是将每个单词看作一个特征，这样就会有大量的特征。此算法的相对简单性和朴素贝叶斯独立特征的这一假设，使其能够出色完成文本的分类。在这个迷你项目中，你将在计算机中下载并安装 sklearn，然后使用朴素贝叶斯根据作者对邮件进行分类。
2. 环境准备1、检查你是否装有可用的 python，版本最好是 2.6 或 2.7（这是我们使用的版本 - 其他版本应该也可以，但我们不敢保证）。2、我们会使用 pip 来安装一些包。首先，从此处获取并安装 pip。3、使用 pip 安装一系列 Python 包：

转到终端行界面（请勿打开 Python，只打开命令提示符）
安装 sklearn: pip install scikit-learn
此处包含 sklearn 安装说明，可供参考

4、安装自然语言工具包：pip install nltk5、获取机器学习简介源代码。你将需要 git 来复制资源库：git clone https://github.com/udacity/ud120-projects.git
你只需操作一次，基础代码包含所有迷你项目的初始代码。进入 tools/ 目录，运行 startup.py。该程序首先检查 python 模块，然后下载并解压缩我们在后期将大量使用的大型数据集。下载和解压缩需要一些时间，但是你无需等到全部完成再开始第一部分。
3. 运行代码在 naive_bayes/nb_author_id.py 中创建和训练朴素贝叶斯分类器，用其为测试集进行预测。准确率是多少？
training time: 1.517 s0.973265073948

在训练期间，你可能会看到以下错误：“用户警告：分数重复。结果可能取决于特征排序，或者你对回归任务使用了分类分数。” 警告（“分数重复。结果可能取决于特征排序。”）
邮件中两个以上的单词恰巧具有相同的使用模式时，会出现这一警告—对算法而言，这表示两个特征是相同的。当重复特征出现时，一些算法实际上会中断（数学上无法运行），或给出多个不同的答案（取决于特征排序），然后 sklearn 发出警告。这种信息能起到帮助作用，所以我们无需担心。
练习: 作者身份准确率在此问题中，一些学员在执行代码时会遇到内存问题。为了降低运行代码时看到内存错误的提示，我们建议你使用 RAM 至少为 2GB 的计算机。如果你发现代码造成内存错误，你也可以尝试在 email_preprocess.py 文件中设置 test_size = 0.5。
4. 对分类器计时我们之前未明确提及的一个重要主题是何时训练和测试算法。在你分类器所在行的上方和下方插入两行代码，就像这样：
t0 = time()&lt; 你的 clf.fit() 代码行 &gt;print &#x27;training time:&#x27;, round(time()-t0, 3), &#x27;s&#x27;

在你的 clf.predict() 代码行前后也加上这段代码，这样你可以比较训练分类器的所需时间，并作出预测。训练和预测哪一个更快？
我们会把朴素贝叶斯的用时与其他几种算法比较，所以请记下你得到的时间和准确率，在下一个迷你项目中，我们还会用到。
5. 源码分享https://github.com/voidking/ud120-projects
6. 书签机器学习入门https://cn.udacity.com/course/intro-to-machine-learning--ud120
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>滴滴算法大赛</title>
    <url>/dev-didi-competition/</url>
    <content><![CDATA[1. 前言本文转载自优达学城《机器学习工程师》
全球领先的出行方案提供商滴滴出行在2016年5月到7月成功举办了第一届算法大赛并取得了圆满的成功。优达学城作为协办方，也深刻地感受到了学生对解决基于企业真实数据题目的热情。为了让更多的学生能够有机会得到这样的锻炼，我们特地与滴滴出行协作，把算法比赛的题目作为纳米学位中的一个可选项目。
项目在设计时综合考量了纳米学位的内容与滴滴出行的招聘要求。让你能够把学到的内容应用在真实企业数据中。完成项目的优秀者可以得到滴滴出行的证书。为你的简历添砖加瓦。


项目制定原则：

滴滴出行按照自身招聘的需求设计项目。
优达学城结合机器学习纳米学位的课程内容进行微调。
留下足够多的自定空间给学生自由发挥，也可依此更好得区分学生水平。

2. 项目介绍在出行问题上，中国市场人数多、人口密度大，总体的出行频率远高于其他国家，这种情况在大城市尤为明显。然而，截至目前中国拥有汽车的人口只有不到10%，这意味着在中国人们的出行更加依赖于出租车、公共交通等市场提供的服务。另一方面，滴滴出行占领了国内绝大部分的网络呼叫出行市场，面对着巨大的数据量以及与日俱增的数据处理需求。截至目前，滴滴出行平台日均需处理1100万订单，需要分析的数据量达到50TB，路径规划服务请求超过90亿。面对如此庞杂的数据，我们需要通过不断升级、完善与创新背后的云计算与大数据技术，从而保证数据分析及相关应用的稳定，实现高频出行下的运力均衡。供需预测就是其中的一个关键问题。
供需预测的目标是准确预测出给定地理区域在未来某个时间段的出行需求量及需求满足量。调研发现，同一地区不同时间段的订单密度是不一样的，例如大型居住区在早高峰时段的出行需求比较旺盛，而商务区则在晚高峰时段的出行需求比较旺盛。如果能预测到在未来的一段时间内某些地区的出行需求量比较大，就可以提前对营运车辆提供一些引导，指向性地提高部分地区的运力，从而提升乘客的整体出行体验。

问题定义、数据描述及常见问题
数据集下载

3. 项目提交3.1. 提交和评估你的项目将由优达学城项目导师根据此要求进行评估。 提交前请务必仔细查看此要求。所有标准必须“符合规格”才能通过。

需提交一份PDF格式的项目报告，要求见这里。
解题代码需使用 python 2.7 的 ipynb 或 py 为后缀的文件。（后续会开放更多编程语言）
代码运行说明文件。纯文本或markdown格式，写明运行你的代码所需要的库，他们的安装方法和运行方法
预测结果: 纯文本或CSV文件。

对项目要求有疑问请致信 &#x73;&#x75;&#112;&#112;&#111;&#x72;&#x74;&#64;&#121;&#111;&#x75;&#x64;&#97;&#x78;&#117;&#x65;&#46;&#99;&#x6f;&#x6d;
准备好提交项目后，请点击下一项，在“项目提交”小节中提交。
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>机器学习工程师求职面试</title>
    <url>/dev-ml-interview/</url>
    <content><![CDATA[1. 前言本文转载自优达学城《机器学习工程师》
2. 简历项目在工作申请和应聘中，简历是必备品。一份好的简历，必须契合岗位需求。在这个项目中，你需要找到一条招聘信息，然后根据岗位的职责要求，更改你的简历。一旦你完成此项目，你就成功学会了定向寻找的方法，你就可以随时去找工作了！


2.1. 项目资源1、项目评审标准。优达学城的项目导师将根据此评审标准检查你的项目。2、项目检查表。提交项目前，你可以对照这个检查表检查你的项目。3、职业资源中心。在这里找到更多关于如何改进你的简历的贴士。
2.2. 简历模板从零开始编写你自己的简历，这样你的简历一定是独一无二的

Resume Genius: Resume Templates
Resume Builder

2.3. 技巧在介绍你参与过的项目和工作经验时，尽量使用点列表（bullet points）：

用动词开头
包含数字
强调结果
伯克利大学动词表（适用于简历和求职信）

3. 简历项目提交说明：此项目目前仅提供英文审阅服务，你需要用英文完成项目。
项目导师会根据该评估准则（与你制作简历时遵循的清单相符）对你的简历项目进行审核。
3.1. 提交说明1、在优达学城职业资源中心找到一个招聘岗位，要么是你现在就可以申请的，要么是你在纳米学位毕业后会申请的。判断这个岗位是否适合你。（注：如果你初步判断有75%的可能可以达到该工作要求，那么你就可能就是一个很好的候选人，你就该去试一下！）2、根据这个岗位的具体要求，修改完善你的简历。3、在提交时，在“给审核人留言”一项中复制并粘贴岗位内容，或是直接放置链接。4、参考：在简历中删除敏感信息，例如你的电话号码。5、提交 pdf 版本的简历。
3.2. 分享简历优达学城与雇主合作，雇主可以通过你的专业资料与优达学城学员和毕业生联系。 一旦你完成了简历审核，请务必将你已审核的简历更新到你的个人资料中！
4. 求职信项目在这个项目中，你需要撰写一封求职信来描述你所具备的各项技能。更重要的是在信中陈述你对这份工作的热情。我们建议你针对一个你在网上找到工作描述来撰写这封求职信。我们希望所有的学生能够通过练习撰写求职信来学习如何把你的技能与工作职位的要求联系起来。
说明：此项目目前仅提供英文审阅服务，你需要用英文完成项目。
项目导师会根据该评估准则（与你制作简历时遵循的清单相符）对你的简历项目进行审核。
4.1. 提交准备好提交求职信后，你需要：
1、删除/匿名化你的求职信上的所有个人联系人信息，以防泄漏隐私。2、创建 PDF 格式的简历，这样你的简历在不同操作系统/软件/屏幕中有一致的版面样式。3、点击下方的“提交项目”按钮，并根据说明操作。4、在“告知审核员”中，请包括任何有关你的职业目标（如申请的工作或领域），或任何有意偏离评估准则情况的详细信息。
注意：我们最多需要一周的时间来对项目进行评分，但多数情况下无需一周。项目评审完成后，你会收到电子邮件。如果你在提交项目时遇到问题，或者想查看提交状态，请发送电子邮件至 &#x73;&#117;&#112;&#112;&#111;&#x72;&#x74;&#x40;&#121;&#111;&#117;&#x64;&#x61;&#120;&#117;&#101;&#46;&#99;&#111;&#109;。
5. GitHub个人资料项目你的GitHub个人资料展示你创建的项目，并让他人了解你的工作风格和提交频率。通过展示你对 GitHub 的熟悉程度，其他开发人员和招聘经理能够知道你拥有他们需要能力，你就是他们正在寻找的人。你的代码库应表明你认真对待项目，并且能够编写干净、格式正确的代码。请查看 GitHub 上的这篇出色的博客文章，了解如何用 GitHub 帮助你持续学习，发展职业生涯。
为何要完成此 GitHub 个人资料项目？GitHub 越来越多地被用作招聘平台以及潜在雇主查看你的作品的方式。它还是与其他开发人员建立联系和展示你的技能的好地方。
我应该做什么？你应该以招聘人员或用人部门经理的眼光查看你的 GitHub 个人资料，重点关注你的个人资料、项目和代码如何将你描绘成公司的潜在候选人或项目的协作者。
为此，你需要更新你的 GitHub 个人资料，让其显得专业并适合用于求职。你还要完善项目，让它们方便招聘人员和对你的工作感兴趣的任何其他人浏览。因此，你需要添加自述文件和格式正确的提交说明。
如何完成此 GitHub 个人资料项目？在纳米学位课程的此阶段，我们假设你掌握了 Git 和 GitHub 的基础知识：如何克隆代码库，如何初始化代码库，如何提交并提供说明，以及如何向 GitHub 上传代码库。如果不了解这些基础知识，请查看我们的如何使用 Git 和 GitHub 课程。
首先通读 Udacity GitHub 个人资料检查表大致了解 GitHub 个人资料的目标和任务。
查看整个检查表；勾选已完成的每一项。对于未完成的项，修复并更新 GitHub 个人资料。建议你阅读此博客文章，了解有关 GitHub 这个在线平台的更多详细信息。还建议你查看职业资源中心，获取有关更新 GitHub 个人资料的更多指导。
如何在 GitHub 上创建 readme 文档？如果对如何在 GitHub 上创建 readme 文档不熟悉，可按照下面的说明开始创建：1、单击某个项目并滚动到下面的文件列表。如果尚未创建自述文件，将会看到类似这样的图像：2、单击添加自述文件按钮。3、在自述文件中，解释项目的目标，有关如何使用或查看项目的说明，以及寻求何种类型的协作。自述文件是使用 Markdown 编写的；请查看 GitHub 的 Markdown 指南了解详细信息。
完善个人资料的途径？GitHub 能够快速、直观地显示你的代码库活动。因此，强烈建议开发人员显示活动。你在此项目中完成的任务只是一个开端。GitHub会突出显示你在上一年的活动，因此我们有两条长期建议：1、每天提交细微的增量更改。像下图这样的绿色提交图表表明你正积极地向项目中添加内容。2、向开源代码库供稿。对于招聘人员，这是一个重要的积极信号。它表明你能够很好地与他人合作，并且更重要的是，它表明你真的对编码充满热情，愿意在业余时间不计报酬地做这件事。如果想知道如何开始，请查看 GitHub 指南。
6. GitHub项目提交你的 GitHub 主页是呈现给潜在雇主的“关键证据”，证明你会是一个很好的职位候选人，因为他们可以从中看到你过往工作的细节。 许多招聘人员使用 GitHub 搜寻候选者，许多纳米学位毕业生都因为他们在 GitHub 上的活跃记录获得了工作机会。 此外，通常，你与其他程序员使用 GitHub 合作完成项目的——这将显示你能够在工作中与工程团队的其他成员合作。
在此项目中，我们将指导你如何从招聘经理或人事部门经理的角度审视你的的 GitHub 主页，重点关注你的个人资料、项目和代码，以及它们如何能证明你是公司的潜在候选人或项目需要的工程师。虽然这个项目和其他职业发展项目一样是可选的，但我们建议所有学生都能完成此项目。
说明：此项目目前仅提供英文审阅服务，你需要用英文完成项目作业。
6.1. 提交说明1、在你已经连续编程至少两周后再提交此项目，这样审阅者才能向你提供有效的点评。2、在项目提交页面中输入你的 GitHub 个人主页（https://github.com/username）。
6.2. 项目资源1、项目评审标准。优达学城项目导师会依据此评审标准审阅你的项目。2、项目检查表。对照此检查表，检查你是否已经满足项目要求。3、职业资源中心。你可以找到关于如何改善你的个人主页的进一步的信息。
7. 领英个人资料项目你已经创建（或更新）了领英个人资料，它简明、清晰，精彩地讲述了你最好的一面，接下来你就可以提交它以供评审了！
这是一个可选项目。仅当你希望获得有关领英个人资料的反馈时，才应提交。这最适合想要找工作或想改进其领英个人资料的学生。
为何要完成此领英个人资料项目？领英是世界上最大的在线职业社交网络，在全球拥有 3 亿用户。最重要的是，它不仅仅是一份传统简历！领英还让你能够在线推销自己的成就，同时增强自己的职业社交网络。
95% 以上的招聘人员使用领英作为招聘工具。你的简历只能概述你的职业生涯，而 领英却可以生动地描绘你的情况，无论你有何背景。由于许多行业领导者都是领英社区的积极投稿人，因此 领英还让你有机会创建个人品牌，让潜在雇主与你联系并获得有关行业趋势的见解。
我应该做什么？你的领英个人资料应该帮助你在使用正确关键字的搜索结果中突显出来，并指示你不仅有相应的资质，而且积极上进。为了实现这些目标，请记住 3-30-180 规则。它是指招聘人员只在你的领英个人资料上花很短的时间。招聘人员花 3 秒钟来决定是否继续查看你的个人资料，花30 秒钟阅读第一遍，然后花 180 秒钟决定是否与你联系。有关 3-30-180 规则的更多信息，请参阅优达学城的这篇博客文章。
如何完成此领英个人资料项目？首先通读 优达学城领英个人资料检查表，大致了解领英个人资料的目标和任务。还应检查同行的个人资料，了解如何推介你自己和你的技能。
如果是从头开始创建领英个人资料，请转到下一部分。如果是更新领英个人资料，可以跳到拥有领英个人资料后部分。
如果是从头开始创建领英个人资料创建一个帐户，并登录领英。
拥有领英个人资料后查看整个优达学城领英个人资料检查表；勾选已完成的每一项。对于未完成的项，修复并更新你的 领英个人资料。勾选所有项后，移到下一部分。有关更多指导，请查看职业资源中心获取更多建议和资源。
8. 领英项目提交在这个项目中，你将站在招聘经理或者人力部门的角度来审视自己的领英个人资料，将着重点放在自己的工作经验，教育背景以及兴趣爱好上，向他人展示你是一个有潜力，有合作力的应聘者。虽然这个项目和其他职业发展项目一样是可选的，但我们推荐所有学员更新自己的领英个人资料，展示你新掌握的技能。
说明：此项目目前仅提供英文审阅服务，你需要用英文完成项目作业。
8.1. 反馈优达学城项目导师会根据该标准（与你制作领英简介时遵循的清单相符）对你的领英简介项目进行审核。
8.2. 提交准备好提交领英个人资料后：
1、若要提交，请点击下方的“提交项目”按钮，并根据说明操作。2、在“告知评审员”中，请包括任何有关你的职业目标（如预期工作领域）的详细信息。
注意：我们最多需要一周的时间来对项目进行评分，但多数情况下无需一周。项目评审完成后，你会收到电子邮件。如果你在提交项目时遇到问题，或者想查看提交状态，请发送电子邮件至 &#x73;&#x75;&#x70;&#112;&#x6f;&#x72;&#116;&#x40;&#121;&#111;&#117;&#100;&#97;&#x78;&#117;&#x65;&#x2e;&#x63;&#x6f;&#109;。
9. 技术面试项目对于此实战项目，你需要回答五个技术面试问题，这些问题涉及的是技术面试课程中讨论的各种主题。你应该用 Python 写出清晰高效的答案，并用文字解释代码的效率和设计理由。我们将安排合格的审核者检查你的答案，并针对任何精彩之处或欠缺的方面做出反馈：你的解决方案是最高效的吗？你很好地解释了你的想法吗？你的代码简洁易懂吗？
请回答以下问题：
问题 1: 假设有两个字符串 s 和 t，请判断 t 的变形词是否是 s 的子字符串。例如：如果 s = “udacity”，t = “ad”，则函数返回 True。你的函数定义应该为：“question1(s, t)”，并返回布尔值 True 或 False。 
问题 2: 设有字符串 a，请找到 a 中存在的最长回文子字符串。你的函数定义应该类似于“question2(a)”，并返回字符串。 
问题 3: 假设有个无向图 G，请找出 G 中的最小生成树。最小生成树是指连通图表中的所有顶点且边的总权值最小的子图。你的函数应该以邻接表为输入，并返回一个邻接表，表的格式如下：{‘A’:[(‘B’,2)],’B’:[(‘A’,2),(‘C’,5)],’C’:[(‘B’,5)]}。顶点表示为唯一字符串。函数定义应为“question3(G)”。 
问题 4: 请在二分查找树中找到两个节点之间的最近共同祖先。最近共同祖先是指同时为两个节点的祖先且离根最远的节点。例如，根节点是树上所有节点的共同祖先，但是如果两个节点都是根节点左侧子节点的子孙，则该左侧子节点可能是最低共同祖先。你可以假设两个节点都在树中，该树本身遵守的是 BST 规则。函数定义应该为“question4(T, r, n1, n2)”，其中 T 是用矩阵表示的树，列表的索引等于该节点中存储的整数，1 表示子节点，r 是表示根节点的非负整数，n1 和 n2 是表示这两个节点的非负整数，二者没有特定的顺序。例如，一个测试条件可以为 question4([[0,1,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[1,0,0,0,1],[0,0,0,0,0]],3,1,4)，答案应为 3。 
问题 5: 从单链表中找到倒数第 m 个元素。例如，如果单链表中有 5 个元素，则倒数第 3 个元素也是正数地 3 个元素。该函数的定义应为“question5(ll, m)”，其中 ll 是链表中的第一个节点，m 是“倒数第 m 个元素”。你应该复制粘贴下面的 Node 类，并用它来表示链表中的节点。请返回该位置的节点对应的值。
class Node(object):  def __init__(self, data):    self.data = data    self.next = None

10. 技术面试项目提交对于此项目，请回答五个技术面试问题，内容涵盖了技术面试课程中讨论的各种话题。请用 Python 写出简洁高效的答案，并用文字解释下代码的效率和设计理由。我们会安排一位合格的审阅人员检查你的答案，并对其中的亮点或不足之处做出反馈——你的解决方案是效率最高的吗？清晰地解释了自己的思路了吗？你的代码简洁、易于读懂吗？
10.1. 提交说明1、对于每个问题，请用 Python（版本 2）写出解决方案。所有解决方案的函数名称都应为“question1”、“question1”等等。可以根据需要创建其他辅助函数或类。代码解决方案必须放在称为“solutions.py”的文件内。
2、在同一 .py 文件中，请为每个解决方案提供至少 3 个测试案例。对于每个测试案例，请写出函数调用，输入值为你要测试的内容，并输出到控制台上，例如“print question1()”。在下一行，注释掉你预期会从该函数调用中获得的输出。至少有两个测试案例必须是极端情况，例如输入为 null 值、空输入值、非常大的值等等。
3、在单独的文本文件（称为“explanations.txt”）里写上每个问题的说明。不用详细解释所提供的代码，但是请说明代码中各个决策背后的原因。例如，为何使用该数据结构？同时还需要解释下解决方案的效率（时间效率和空间效率）。
4、将 Python 文件和文本文件压缩为 .zip 文件，然后提交了。
请参阅此处的评估标准！
11. 机器学习模拟面试具体内容看视频。
12. 坚持练习！你在该课程中完成的面试准备练习只是牛刀小试，我们会鼓励你进行更多练习。我们还准备了以下资源列表，可供你进行其他练习。而且如果你还知道其他资源，请不吝告知。并非所有资源都适合你所申请的工作，因此请务必在开始练习前进行调查！
12.1. 书籍
《程序员面试金典》
《Elements of Programming Interviews》

12.2. Web 资源
Project Euler
Matasano 公司的加密挑战赛
编程面试问题列表

12.3. 在线实践平台
LeetCode
CodeKata
Coderust
Interview Cake
Codewars
HackerRank
职业资源中心

不断练习的过程中，你要尽量全面地模仿真正的面试。例如你应该尝试与朋友一起练习行为问题，而不是一个人闷头练习。还要尝试使用计时方法，以确保给出的答案条理清晰，简明扼要。对于技术问题，你要尝试使用白板或在线文本编辑器，而非 IDE 和编译器进行编码。
现在我们开始面试排练！
13. 面试项目提交雇主通过面试来判断你是否适合这份工作，以及是否已经做好准备。面试不是一场考试，而是你和雇主之间的对话。建立你自己的策略，为面试到来的那一天做好准备吧！
说明：此项目目前仅提供英文审阅服务，你需要用英文完成项目作业。
13.1. 提交说明1、在下面的招聘网站中，选择一个你想要在毕业后去应聘的职位。

The Udacity Career Network on AfterCollege
FirstJob
R-users
kaggle
Jr.DevJobs
LaunchCode
Whitetruffle
AngelList
HIRED
Dice

2、假装你在接受一场真正的面试，回答下方的面试问题。3、将你的回答保存为 .pdf 或 .txt 格式提交。4、提交时，将你选择的招聘启事的的链接复制粘贴到给审阅者的补充说明（Notes to reviewer）中。
13.2. 面试问题（共 7 个）想象你在面试中回答以下问题，用你认为适当的两三段话解释和说明你的答案。对于编码答案，请解释你在编写代码时所作的相关选择。
问题 1 – 我们采用 A/B 测试方法测试了我们公司产品页面上两种不同风格的注册按钮。100 名访客访问了 A 页面，其中有 20 名点击了按钮；然而，70 名访客访问了 B 页面，且仅有 15 名点击了按钮。那么你能肯定地说 A 或 B 页面是更好的选择吗？为什么？
问题 2 - 你能否设计一个计划，仅靠观察推文来对 Twitter 用户进行分组？你没有任何可用的人口、地理或其他识别信息，只能利用他们发布的纯文本消息及每条消息的时间戳。
在 JSON 格式中，它们是这样的：
&#123;    &quot;user_id&quot;: 3,    &quot;timestamp&quot;: &quot;2016-03-22_11-31-20&quot;,    &quot;tweet&quot;: &quot;It&#x27;s #dinner-time!&quot;&#125;

假设你收到一些这样的推文流，请描述一下收集和分析它们的过程，你会采用什么转换/算法，你如何训练和测试你的模型及展示结果。
问题 3 - 在分类设置中，给定一个加标签示例数据集及你尝试拟合的机器学习模型，说明检测和防止过拟合的策略。
问题 4 - 你的团队正在为你们的旗舰 3D 建模工具设计下一代用户体验。特别是，你的任务是实现一个智能上下文菜单，它能够学习建模者的菜单选项使用，然后显示最有益的选项。例如，我经常使用“编辑”&gt;“表面”&gt;“光滑表面”（Edit &gt; Surface &gt; Smooth Surface），而我希望只要右击就会出现一个“光滑表面”选项，就像“剪切”、“复制”和“粘贴”一样。注意并不是所有命令在所有上下文下均有意义，例如我需要选择一个表面才对它进行光滑处理。那么，你如何设计一个能够执行这种行为的学习系统/代理。
问题 5 - 通过一个情景举例说明正则化对学习好的模型的必要性，以及提供一个正则化没有意义的情景。
问题 6 -你附近的杂货店想向顾客有针对性地发放对他们来说有用的优惠券。假设你可以访问每位顾客的购买历史和店铺商品目录，你将设计怎样一个系统，来决定发放哪些优惠券？你如何衡量这个系统的性能？
问题 7 - 选择一个公司，然后描述你想应聘的该公司内的机器学习工程师职位（假设有）。现在，如果你已成功应聘，并从今天开始任职，在你看来，你的职位在下一年会有怎样的发展？你的长期职业目标是什么，这个职位如何帮助你实现长期目标？
13.3. 提交当你准备提交项目，点击下方“提交项目”，上传一份 PDF 文件。如果有任何与提交项目相关的疑问，或想查看提交状态，请向我们发送电子邮件询问（&#115;&#x75;&#x70;&#112;&#x6f;&#114;&#x74;&#x40;&#121;&#x6f;&#117;&#100;&#x61;&#120;&#117;&#101;&#46;&#x63;&#x6f;&#109;）。
13.4. 项目资源项目评审标准。优达学城的项目导师会依据此评审标准审阅你的项目。你可以在提交前自行检查自己是否已达到所有标准。职业资源中心。你可以在这里找到更多有关准备面试的资源和贴士。
13.5. 接下来面试练习并不是只在你求职时才需要做的事情， 在你的整个职业生涯中，你都需要继续提高自己的面试技巧。抓住每一个机会，思考和与人交流你的职业目标。在申请一个职位之前，请思考清楚自己为什么要申请——这将极大地帮助你为面试做好准备。
14. 书签Interview Guide: Machine Learninghttps://career-resource-center.udacity.com/interview-courses-and-guides/machine-learning
职业资源中心 - 简历https://career-resource-center.udacity.com/resume
职业资源中心 - 求职信https://career-resource-center.udacity.com/cover-letters
LinkedIn and GitHub Profileshttps://career-resource-center.udacity.com/linkedin-github-profiles
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>Kaggle</title>
    <url>/dev-kaggle/</url>
    <content><![CDATA[1. 前言本文转载自优达学城《机器学习工程师》
Kaggle 是一个极受欢迎的数据科学和机器学习挑战平台，有着极为活跃和不断壮大的社区。Kaggle 成员不仅可以争夺积分榜上的排名，公司和研究实验室在此提出的某些挑战题还设有丰厚奖金。
Kaggle 为您提供解决复杂现实问题的机会，您可通过解决这些问题磨练您的机器学习技巧，如果你的 Kaggle 个人页面中有积极参与和表现良好的记录，可以让你在申请工作时更具优势。


2. 开始准备好成为 Kaggle 的一员了吗？
1、前往 Kaggle 网站，并注册账号。看一下 进行中的竞赛，有没有你感兴趣的项目？
2、选择一个项目，根据指导说明获取数据，编写并运行您的算法，最后提交解决方案。
3、如果你觉得这些项目过于复杂，可以查看以下内容：

泰坦尼克号竞赛教程
数字识别
面部特征点检测
Julia 入门教程

3. Kaggle 脚本使用 Kaggle脚本，你无需设置你的电脑（或下载所有数据），便可轻松开始解决挑战问题。
3.1. 创建脚本选择一个竞赛或数据集，单击新脚本（New Script）或新记事本（New Notebook），选择你的语言偏好。
一般情况下，程序已为你提供了一些初始代码，教你如何下载所需数据。修改并运行代码，以浏览输出日志。你可直接在脚本中提交挑战的输出文件（格式通常为 .csv），很简单，对吧？
3.2. Fork 一个已有脚本Kaggle 成员通常会将自己的解决方案提供给其他参赛者浏览和参考，请在竞赛的控制面板（Dashboard）中寻找脚本。
例如，与数字识别挑战有关的所有脚本都可在这里找到。让我们选择一个来详细了解：在 Python 中实现的简单感知分类器（一个单层神经网络）。点击 Fork 脚本（Fork Script ）或 Fork 记事本（Fork Notebook）按钮来运行或修改脚本。
4. 社群成为 Kaggle 成员不仅意味着你可以乐此不疲地解决多种挑战，并为此付出每一份精力（我相信你的精力是源源不断的），你还会成为数据科学家和机器学习工程师社群的一员，为解决当今社会所面临的诸多实际问题贡献一份力量。
如果你在解决挑战中遇到困难或需要建议，请前往论坛寻找相关信息，论坛中也会有一些他人的问题你能够回答。其他资源可参考此 wiki 网页。
你可以在 Kaggle 的名为 no free hunch 的官方博客中了解到竞赛、新功能和平台变化等方面的最新情况。例如，这篇文章介绍了 Kaggle 的一位顶级成员。
5. 你的 Kaggle 个人主页你可以在你公开的 Kaggle 个人主页上查阅你在不同竞赛中的排名和表现： 

https://www.kaggle.com/&lt;输入你的 Kaggle 用户名&gt;

请在个人主页中填写你的准确信息，提供社交编码平台（如 GitHub）和专业网站（如领英）的链接，并定期更新。我们也欢迎你提供你的优达学城个人主页链接。
6. 工作Kaggle 有一个专门的求职版块，你可能会在这里找到令你心动的职位。在 Kaggle 竞赛中的良好表现能使你脱颖而出。你在纳米学位项目中取得进展的同时，请继续尝试解决各项挑战，构建良好的档案记录。如果你正在寻找工作， 在申请时不要忘了利用这个资源。
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>scikit-learn</title>
    <url>/dev-scikit-learn/</url>
    <content><![CDATA[1. 前言本文转载自优达学城《机器学习工程师》


2. scikit-learn 的安装检查您是否装有可用的 python。优达学城使用 python 2.7 作为示例代码和在浏览器中完成作业的代码。
我们会使用 pip 来安装一些程序包。首先，在此处获取并安装 pip。如果你使用Anaconda, 你可以用 conda 命令来安装包。
使用 pip 或 anaconda 来安装 scikit-learn：

打开 Terminal（mac 下是 Terminal， PC 是 cmd）用下列命令安装 sklearn
pip install scikit-learn 或者 conda install scikit-learn
如果你不用 pip 或者 conda，可以在这里找到安装说明。

3. 关于 scikit-learn 版本的重要通知scikit-learn 最近把稳定版升级到了 v0.18。这次升级改变了一些我们将要在课程中讲到的函数的调用方法，例如：train_test_split、gridSearchCV、ShuffleSplit 和 learning_curves。scikit-learn 网站上的文档已经更新到了 v0.18。但是 Katie 导师的讲解以及优达学城（Udacity）的练习，作业还是基于v0.17。如果你需要查询 scikit-learn 的文档，请查询 v0.17 的说明，而非 v0.18。近期我们会把内容统一升级成 v0.18。
这个论坛链接提供了更加详细的说明。如果你还有疑问，可以在论坛和微信群里提出。
4. Scikit-learn 代码在接下来的部分中，Katie 会演示如何将 scikit-learn（或 sklearn）文档与在“机器学习简介”课程中介绍的高斯朴素贝叶斯模型一起使用。对于本练习，您不必熟悉朴素贝叶斯或者 Katie 演示的代码，而是要熟悉 sklearn 的布局，以便之后能评估和验证任何数据模型。
在即将开始的“监督式机器学习”课程中，我们会更详细地介绍朴素贝叶斯以及其他有用的受监督模型，并运用我们在本课程中学到的知识评估每个模型的优缺点。
如果想提前了解一下朴素贝叶斯，请查看此链接。
5. sklearn使用入门在谷歌上搜索“sklearn naive bayes”即可。
6. 高斯朴素贝叶斯示例http://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.GaussianNB.html
7. 有关地形数据的高斯 NB 部署7.1. studentMain.py# -*- coding: UTF-8 -*-#!/usr/bin/python&quot;&quot;&quot; Complete the code in ClassifyNB.py with the sklearn    Naive Bayes classifier to classify the terrain data.        The objective of this exercise is to recreate the decision     boundary found in the lesson video, and make a plot that    visually shows the decision boundary &quot;&quot;&quot;from prep_terrain_data import makeTerrainDatafrom class_vis import prettyPicture, output_imagefrom ClassifyNB import classifyimport numpy as npimport pylab as plfeatures_train, labels_train, features_test, labels_test = makeTerrainData()### the training data (features_train, labels_train) have both &quot;fast&quot; and &quot;slow&quot; points mixed### in together--separate them so we can give them different colors in the scatterplot,### and visually identify themgrade_fast = [features_train[ii][0] for ii in range(0, len(features_train)) if labels_train[ii]==0]bumpy_fast = [features_train[ii][1] for ii in range(0, len(features_train)) if labels_train[ii]==0]grade_slow = [features_train[ii][0] for ii in range(0, len(features_train)) if labels_train[ii]==1]bumpy_slow = [features_train[ii][1] for ii in range(0, len(features_train)) if labels_train[ii]==1]# You will need to complete this function imported from the ClassifyNB script.# Be sure to change to that code tab to complete this quiz.clf = classify(features_train, labels_train)### draw the decision boundary with the text points overlaidprettyPicture(clf, features_test, labels_test)output_image(&quot;test.png&quot;, &quot;png&quot;, open(&quot;test.png&quot;, &quot;rb&quot;).read())

7.2. class_vis.py# -*- coding: UTF-8 -*-#!/usr/bin/python#from udacityplots import *import warningswarnings.filterwarnings(&quot;ignore&quot;)import matplotlib matplotlib.use(&#x27;agg&#x27;)import matplotlib.pyplot as pltimport pylab as plimport numpy as np#import numpy as np#import matplotlib.pyplot as plt#plt.ioff()def prettyPicture(clf, X_test, y_test):    x_min = 0.0; x_max = 1.0    y_min = 0.0; y_max = 1.0    # Plot the decision boundary. For that, we will assign a color to each    # point in the mesh [x_min, m_max]x[y_min, y_max].    h = .01  # step size in the mesh    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])    # Put the result into a color plot    Z = Z.reshape(xx.shape)    plt.xlim(xx.min(), xx.max())    plt.ylim(yy.min(), yy.max())    plt.pcolormesh(xx, yy, Z, cmap=pl.cm.seismic)    # Plot also the test points    grade_sig = [X_test[ii][0] for ii in range(0, len(X_test)) if y_test[ii]==0]    bumpy_sig = [X_test[ii][1] for ii in range(0, len(X_test)) if y_test[ii]==0]    grade_bkg = [X_test[ii][0] for ii in range(0, len(X_test)) if y_test[ii]==1]    bumpy_bkg = [X_test[ii][1] for ii in range(0, len(X_test)) if y_test[ii]==1]    plt.scatter(grade_sig, bumpy_sig, color = &quot;b&quot;, label=&quot;fast&quot;)    plt.scatter(grade_bkg, bumpy_bkg, color = &quot;r&quot;, label=&quot;slow&quot;)    plt.legend()    plt.xlabel(&quot;bumpiness&quot;)    plt.ylabel(&quot;grade&quot;)    plt.savefig(&quot;test.png&quot;)    import base64import jsonimport subprocessdef output_image(name, format, bytes):    image_start = &quot;BEGIN_IMAGE_f9825uweof8jw9fj4r8&quot;    image_end = &quot;END_IMAGE_0238jfw08fjsiufhw8frs&quot;    data = &#123;&#125;    data[&#x27;name&#x27;] = name    data[&#x27;format&#x27;] = format    data[&#x27;bytes&#x27;] = base64.encodestring(bytes)    print image_start+json.dumps(data)+image_end

7.3. prep_terrain_data.py# -*- coding: UTF-8 -*-#!/usr/bin/pythonimport randomdef makeTerrainData(n_points=1000):################################################################################## make the toy dataset    random.seed(42)    grade = [random.random() for ii in range(0,n_points)]    bumpy = [random.random() for ii in range(0,n_points)]    error = [random.random() for ii in range(0,n_points)]    y = [round(grade[ii]*bumpy[ii]+0.3+0.1*error[ii]) for ii in range(0,n_points)]    for ii in range(0, len(y)):        if grade[ii]&gt;0.8 or bumpy[ii]&gt;0.8:            y[ii] = 1.0### split into train/test sets    X = [[gg, ss] for gg, ss in zip(grade, bumpy)]    split = int(0.75*n_points)    X_train = X[0:split]    X_test  = X[split:]    y_train = y[0:split]    y_test  = y[split:]    grade_sig = [X_train[ii][0] for ii in range(0, len(X_train)) if y_train[ii]==0]    bumpy_sig = [X_train[ii][1] for ii in range(0, len(X_train)) if y_train[ii]==0]    grade_bkg = [X_train[ii][0] for ii in range(0, len(X_train)) if y_train[ii]==1]    bumpy_bkg = [X_train[ii][1] for ii in range(0, len(X_train)) if y_train[ii]==1]#    training_data = &#123;&quot;fast&quot;:&#123;&quot;grade&quot;:grade_sig, &quot;bumpiness&quot;:bumpy_sig&#125;#            , &quot;slow&quot;:&#123;&quot;grade&quot;:grade_bkg, &quot;bumpiness&quot;:bumpy_bkg&#125;&#125;    grade_sig = [X_test[ii][0] for ii in range(0, len(X_test)) if y_test[ii]==0]    bumpy_sig = [X_test[ii][1] for ii in range(0, len(X_test)) if y_test[ii]==0]    grade_bkg = [X_test[ii][0] for ii in range(0, len(X_test)) if y_test[ii]==1]    bumpy_bkg = [X_test[ii][1] for ii in range(0, len(X_test)) if y_test[ii]==1]    test_data = &#123;&quot;fast&quot;:&#123;&quot;grade&quot;:grade_sig, &quot;bumpiness&quot;:bumpy_sig&#125;            , &quot;slow&quot;:&#123;&quot;grade&quot;:grade_bkg, &quot;bumpiness&quot;:bumpy_bkg&#125;&#125;    return X_train, y_train, X_test, y_test#    return training_data, test_data

7.4. ClassifyNB.py# -*- coding: UTF-8 -*-def classify(features_train, labels_train):       ### import the sklearn module for GaussianNB    ### create classifier    ### fit the classifier on the training features and labels    ### return the fit classifier            ### your code goes here!    def classify(traindata,trainlabel):        from sklearn.naive_bayes import GaussianNB        classifier=GaussianNB()        classifier.fit(traindata, trainlabel)        return classifier

]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>转载</tag>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>Numpy和Pandas教程</title>
    <url>/dev-numpy-and-pandas/</url>
    <content><![CDATA[1. 前言本文转载自优达学城《机器学习工程师》
既然目前为止，你已学会了一些基本的统计学概念，现在让我们探讨一些 Python 库，它们允许您研究数据和处理大型数据集。
具体而言，在本阶段的课程中，我们将探讨 numpy，它允许您处理大量数值数据以及 panda 序列和数据框（它们允许你存储大型数据集和提取其中的信息）。我们将学习 numpy 和 panda.DataFrames，前者能够帮助你处理大量数值数据，而后者可以帮助你存储大型数据集以及从数据集中提取出来的信息。
Numpy 库文档： https://docs.scipy.org/doc/numpy-dev/user/quickstart.html
Pandas 库文档： http://pandas.pydata.org/pandas-docs/version/0.17.0/


2. Numpyimport numpy as np&#x27;&#x27;&#x27;The following code is to help you play with Numpy, which is a library that provides functions that are especially useful when you have towork with large arrays and matrices of numeric data, like doing matrix matrix multiplications. Also, Numpy is battle tested and optimized so that it runs fast, much faster than if you were workingwith Python lists directly.&#x27;&#x27;&#x27;&#x27;&#x27;&#x27;The array object class is the foundation of Numpy, and Numpy arrays are likelists in Python, except that every thing inside an array must be of thesame type, like int or float.&#x27;&#x27;&#x27;# Change False to True to see Numpy arrays in actionif False:    array = np.array([1, 4, 5, 8], float)    print array    print &quot;&quot;    array = np.array([[1, 2, 3], [4, 5, 6]], float)  # a 2D array/Matrix    print array&#x27;&#x27;&#x27;You can index, slice, and manipulate a Numpy array much like you would with aa Python list.&#x27;&#x27;&#x27;# Change False to True to see array indexing and slicing in actionif False:    array = np.array([1, 4, 5, 8], float)    print array    print &quot;&quot;    print array[1]    print &quot;&quot;    print array[:2]    print &quot;&quot;    array[1] = 5.0    print array[1]# Change False to True to see Matrix indexing and slicing in actionif False:    two_D_array = np.array([[1, 2, 3], [4, 5, 6]], float)    print two_D_array    print &quot;&quot;    print two_D_array[1][1]    print &quot;&quot;    print two_D_array[1, :]    print &quot;&quot;    print two_D_array[:, 2]&#x27;&#x27;&#x27;Here are some arithmetic operations that you can do with Numpy arrays&#x27;&#x27;&#x27;# Change False to True to see Array arithmetics in actionif False:    array_1 = np.array([1, 2, 3], float)    array_2 = np.array([5, 2, 6], float)    print array_1 + array_2    print &quot;&quot;    print array_1 - array_2    print &quot;&quot;    print array_1 * array_2# Change False to True to see Matrix arithmetics in actionif False:    array_1 = np.array([[1, 2], [3, 4]], float)    array_2 = np.array([[5, 6], [7, 8]], float)    print array_1 + array_2    print &quot;&quot;    print array_1 - array_2    print &quot;&quot;    print array_1 * array_2&#x27;&#x27;&#x27;In addition to the standard arthimetic operations, Numpy also has a range ofother mathematical operations that you can apply to Numpy arrays, such asmean and dot product.Both of these functions will be useful in later programming quizzes.&#x27;&#x27;&#x27;if False:    array_1 = np.array([1, 2, 3], float)    array_2 = np.array([[6], [7], [8]], float)    print np.mean(array_1)    print np.mean(array_2)    print &quot;&quot;    print np.dot(array_1, array_2)

3. Pandasimport pandas as pd&#x27;&#x27;&#x27;The following code is to help you play with the concept of Series in Pandas.You can think of Series as an one-dimensional object that is similar toan array, list, or column in a database. By default, it will assign anindex label to each item in the Series ranging from 0 to N, where N isthe number of items in the Series minus one.Please feel free to play around with the concept of Series and see what it does*This playground is inspired by Greg Reda&#x27;s post on Intro to Pandas Data Structures:http://www.gregreda.com/intro-to-pandas-data-structures/&#x27;&#x27;&#x27;# Change False to True to create a Series objectif False:    series = pd.Series([&#x27;Dave&#x27;, &#x27;Cheng-Han&#x27;, &#x27;Udacity&#x27;, 42, -1789710578])    print series&#x27;&#x27;&#x27;You can also manually assign indices to the items in the Series whencreating the series&#x27;&#x27;&#x27;# Change False to True to see custom index in actionif False:    series = pd.Series([&#x27;Dave&#x27;, &#x27;Cheng-Han&#x27;, 359, 9001],                       index=[&#x27;Instructor&#x27;, &#x27;Curriculum Manager&#x27;,                              &#x27;Course Number&#x27;, &#x27;Power Level&#x27;])    print series&#x27;&#x27;&#x27;You can use index to select specific items from the Series&#x27;&#x27;&#x27;# Change False to True to see Series indexing in actionif False:    series = pd.Series([&#x27;Dave&#x27;, &#x27;Cheng-Han&#x27;, 359, 9001],                       index=[&#x27;Instructor&#x27;, &#x27;Curriculum Manager&#x27;,                              &#x27;Course Number&#x27;, &#x27;Power Level&#x27;])    print series[&#x27;Instructor&#x27;]    print &quot;&quot;    print series[[&#x27;Instructor&#x27;, &#x27;Curriculum Manager&#x27;, &#x27;Course Number&#x27;]]&#x27;&#x27;&#x27;You can also use boolean operators to select specific items from the Series&#x27;&#x27;&#x27;# Change False to True to see boolean indexing in actionif False:    cuteness = pd.Series([1, 2, 3, 4, 5], index=[&#x27;Cockroach&#x27;, &#x27;Fish&#x27;, &#x27;Mini Pig&#x27;,                                                 &#x27;Puppy&#x27;, &#x27;Kitten&#x27;])    print cuteness &gt; 3    print &quot;&quot;    print cuteness[cuteness &gt; 3]

4. Pandas 数据框import numpy as npimport pandas as pd&#x27;&#x27;&#x27;The following code is to help you play with the concept of Dataframe in Pandas.You can think of a Dataframe as something with rows and columns. It issimilar to a spreadsheet, a database table, or R&#x27;s data.frame object.*This playground is inspired by Greg Reda&#x27;s post on Intro to Pandas Data Structures:http://www.gregreda.com/intro-to-pandas-data-structures/&#x27;&#x27;&#x27;&#x27;&#x27;&#x27;To create a dataframe, you can pass a dictionary of lists to the Dataframeconstructor:1) The key of the dictionary will be the column name2) The associating list will be the values within that column.&#x27;&#x27;&#x27;# Change False to True to see Dataframes in actionif False:    data = &#123;&#x27;year&#x27;: [2010, 2011, 2012, 2011, 2012, 2010, 2011, 2012],            &#x27;team&#x27;: [&#x27;Bears&#x27;, &#x27;Bears&#x27;, &#x27;Bears&#x27;, &#x27;Packers&#x27;, &#x27;Packers&#x27;, &#x27;Lions&#x27;,                     &#x27;Lions&#x27;, &#x27;Lions&#x27;],            &#x27;wins&#x27;: [11, 8, 10, 15, 11, 6, 10, 4],            &#x27;losses&#x27;: [5, 8, 6, 1, 5, 10, 6, 12]&#125;    football = pd.DataFrame(data)    print football&#x27;&#x27;&#x27;Pandas also has various functions that will help you understand some basicinformation about your data frame. Some of these functions are:1) dtypes: to get the datatype for each column2) describe: useful for seeing basic statistics of the dataframe&#x27;s numerical   columns3) head: displays the first five rows of the dataset4) tail: displays the last five rows of the dataset&#x27;&#x27;&#x27;# Change False to True to see these functions in actionif False:    data = &#123;&#x27;year&#x27;: [2010, 2011, 2012, 2011, 2012, 2010, 2011, 2012],            &#x27;team&#x27;: [&#x27;Bears&#x27;, &#x27;Bears&#x27;, &#x27;Bears&#x27;, &#x27;Packers&#x27;, &#x27;Packers&#x27;, &#x27;Lions&#x27;,                     &#x27;Lions&#x27;, &#x27;Lions&#x27;],            &#x27;wins&#x27;: [11, 8, 10, 15, 11, 6, 10, 4],            &#x27;losses&#x27;: [5, 8, 6, 1, 5, 10, 6, 12]&#125;    football = pd.DataFrame(data)    print football.dtypes    print &quot;&quot;    print football.describe()    print &quot;&quot;    print football.head()    print &quot;&quot;    print football.tail()


from pandas import DataFrame, Series################## Syntax Reminder:## The following code would create a two-column pandas DataFrame# named df with columns labeled &#x27;name&#x27; and &#x27;age&#x27;:## people = [&#x27;Sarah&#x27;, &#x27;Mike&#x27;, &#x27;Chrisna&#x27;]# ages  =  [28, 32, 25]# df = DataFrame(&#123;&#x27;name&#x27; : Series(people),#                 &#x27;age&#x27;  : Series(ages)&#125;)def create_dataframe():    &#x27;&#x27;&#x27;    Create a pandas dataframe called &#x27;olympic_medal_counts_df&#x27; containing    the data from the table of 2014 Sochi winter olympics medal counts.      The columns for this dataframe should be called     &#x27;country_name&#x27;, &#x27;gold&#x27;, &#x27;silver&#x27;, and &#x27;bronze&#x27;.      There is no need to  specify row indexes for this dataframe     (in this case, the rows will automatically be assigned numbered indexes).        You do not need to call the function in your code when running it in the    browser - the grader will do that automatically when you submit or test it.    &#x27;&#x27;&#x27;    countries = [&#x27;Russian Fed.&#x27;, &#x27;Norway&#x27;, &#x27;Canada&#x27;, &#x27;United States&#x27;,                 &#x27;Netherlands&#x27;, &#x27;Germany&#x27;, &#x27;Switzerland&#x27;, &#x27;Belarus&#x27;,                 &#x27;Austria&#x27;, &#x27;France&#x27;, &#x27;Poland&#x27;, &#x27;China&#x27;, &#x27;Korea&#x27;,                  &#x27;Sweden&#x27;, &#x27;Czech Republic&#x27;, &#x27;Slovenia&#x27;, &#x27;Japan&#x27;,                 &#x27;Finland&#x27;, &#x27;Great Britain&#x27;, &#x27;Ukraine&#x27;, &#x27;Slovakia&#x27;,                 &#x27;Italy&#x27;, &#x27;Latvia&#x27;, &#x27;Australia&#x27;, &#x27;Croatia&#x27;, &#x27;Kazakhstan&#x27;]    gold = [13, 11, 10, 9, 8, 8, 6, 5, 4, 4, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]    silver = [11, 5, 10, 7, 7, 6, 3, 0, 8, 4, 1, 4, 3, 7, 4, 2, 4, 3, 1, 0, 0, 2, 2, 2, 1, 0]    bronze = [9, 10, 5, 12, 9, 5, 2, 1, 5, 7, 1, 2, 2, 6, 2, 4, 3, 1, 2, 1, 0, 6, 2, 1, 0, 1]    # your code here    data = &#123;&#x27;country_name&#x27;:Series(countries),&#x27;gold&#x27;:Series(gold),&#x27;silver&#x27;:Series(silver),&#x27;bronze&#x27;:Series(bronze)&#125;    olympic_medal_counts_df = DataFrame(data,index=countries)    return olympic_medal_counts_df

5. 索引数据框import pandas as pd&#x27;&#x27;&#x27;You can think of a DataFrame as a group of Series that share an index.This makes it easy to select specific columns that you want from the DataFrame. Also a couple pointers:1) Selecting a single column from the DataFrame will return a Series2) Selecting multiple columns from the DataFrame will return a DataFrame*This playground is inspired by Greg Reda&#x27;s post on Intro to Pandas Data Structures:http://www.gregreda.com/intro-to-pandas-data-structures/&#x27;&#x27;&#x27;# Change False to True to see Series indexing in actionif True:    data = &#123;&#x27;year&#x27;: [2010, 2011, 2012, 2011, 2012, 2010, 2011, 2012],            &#x27;team&#x27;: [&#x27;Bears&#x27;, &#x27;Bears&#x27;, &#x27;Bears&#x27;, &#x27;Packers&#x27;, &#x27;Packers&#x27;, &#x27;Lions&#x27;,                     &#x27;Lions&#x27;, &#x27;Lions&#x27;],            &#x27;wins&#x27;: [11, 8, 10, 15, 11, 6, 10, 4],            &#x27;losses&#x27;: [5, 8, 6, 1, 5, 10, 6, 12]&#125;    football = pd.DataFrame(data)    print football[&#x27;year&#x27;]    print &#x27;&#x27;    print football.year  # shorthand for football[&#x27;year&#x27;]    print &#x27;&#x27;    print football[[&#x27;year&#x27;, &#x27;wins&#x27;, &#x27;losses&#x27;]]&#x27;&#x27;&#x27;Row selection can be done through multiple ways.Some of the basic and common methods are:   1) Slicing   2) An individual index (through the functions iloc or loc)   3) Boolean indexingYou can also combine multiple selection requirements through booleanoperators like &amp; (and) or | (or)&#x27;&#x27;&#x27;# Change False to True to see boolean indexing in actionif True:    data = &#123;&#x27;year&#x27;: [2010, 2011, 2012, 2011, 2012, 2010, 2011, 2012],            &#x27;team&#x27;: [&#x27;Bears&#x27;, &#x27;Bears&#x27;, &#x27;Bears&#x27;, &#x27;Packers&#x27;, &#x27;Packers&#x27;, &#x27;Lions&#x27;,                     &#x27;Lions&#x27;, &#x27;Lions&#x27;],            &#x27;wins&#x27;: [11, 8, 10, 15, 11, 6, 10, 4],            &#x27;losses&#x27;: [5, 8, 6, 1, 5, 10, 6, 12]&#125;    football = pd.DataFrame(data)    print football.iloc[[0]]    print &quot;&quot;    print football.loc[[0]]    print &quot;&quot;    print football[3:5]    print &quot;&quot;    print football[football.wins &gt; 10]    print &quot;&quot;    print football[(football.wins &gt; 10) &amp; (football.team == &quot;Packers&quot;)]


6. 向量化方法from pandas import DataFrame, Seriesimport numpydef avg_medal_count():    &#x27;&#x27;&#x27;    Compute the average number of bronze medals earned by countries who     earned at least one gold medal.          Save this to a variable named avg_bronze_at_least_one_gold. You do not    need to call the function in your code when running it in the browser -    the grader will do that automatically when you submit or test it.        HINT-1:    You can retrieve all of the values of a Pandas column from a     data frame, &quot;df&quot;, as follows:    df[&#x27;column_name&#x27;]        HINT-2:    The numpy.mean function can accept as an argument a single    Pandas column.         For example, numpy.mean(df[&quot;col_name&quot;]) would return the     mean of the values located in &quot;col_name&quot; of a dataframe df.    &#x27;&#x27;&#x27;    countries = [&#x27;Russian Fed.&#x27;, &#x27;Norway&#x27;, &#x27;Canada&#x27;, &#x27;United States&#x27;,                 &#x27;Netherlands&#x27;, &#x27;Germany&#x27;, &#x27;Switzerland&#x27;, &#x27;Belarus&#x27;,                 &#x27;Austria&#x27;, &#x27;France&#x27;, &#x27;Poland&#x27;, &#x27;China&#x27;, &#x27;Korea&#x27;,                  &#x27;Sweden&#x27;, &#x27;Czech Republic&#x27;, &#x27;Slovenia&#x27;, &#x27;Japan&#x27;,                 &#x27;Finland&#x27;, &#x27;Great Britain&#x27;, &#x27;Ukraine&#x27;, &#x27;Slovakia&#x27;,                 &#x27;Italy&#x27;, &#x27;Latvia&#x27;, &#x27;Australia&#x27;, &#x27;Croatia&#x27;, &#x27;Kazakhstan&#x27;]    gold = [13, 11, 10, 9, 8, 8, 6, 5, 4, 4, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]    silver = [11, 5, 10, 7, 7, 6, 3, 0, 8, 4, 1, 4, 3, 7, 4, 2, 4, 3, 1, 0, 0, 2, 2, 2, 1, 0]    bronze = [9, 10, 5, 12, 9, 5, 2, 1, 5, 7, 1, 2, 2, 6, 2, 4, 3, 1, 2, 1, 0, 6, 2, 1, 0, 1]        olympic_medal_counts = &#123;&#x27;country_name&#x27;:Series(countries),                            &#x27;gold&#x27;: Series(gold),                            &#x27;silver&#x27;: Series(silver),                            &#x27;bronze&#x27;: Series(bronze)&#125;    df = DataFrame(olympic_medal_counts)        # YOUR CODE HERE    broze_at_least_one_gold = df[&#x27;bronze&#x27;][df[&#x27;gold&#x27;] &gt;= 1]    avg_bronze_at_least_one_gold = numpy.mean(broze_at_least_one_gold)    return avg_bronze_at_least_one_gold

import numpyfrom pandas import DataFrame, Seriesdef avg_medal_count():    &#x27;&#x27;&#x27;    Using the dataframe&#x27;s apply method, create a new Series called     avg_medal_count that indicates the average number of gold, silver,    and bronze medals earned amongst countries who earned at     least one medal of any kind at the 2014 Sochi olympics.  Note that    the countries list already only includes countries that have earned    at least one medal. No additional filtering is necessary.        You do not need to call the function in your code when running it in the    browser - the grader will do that automatically when you submit or test it.    &#x27;&#x27;&#x27;    countries = [&#x27;Russian Fed.&#x27;, &#x27;Norway&#x27;, &#x27;Canada&#x27;, &#x27;United States&#x27;,                 &#x27;Netherlands&#x27;, &#x27;Germany&#x27;, &#x27;Switzerland&#x27;, &#x27;Belarus&#x27;,                 &#x27;Austria&#x27;, &#x27;France&#x27;, &#x27;Poland&#x27;, &#x27;China&#x27;, &#x27;Korea&#x27;,                  &#x27;Sweden&#x27;, &#x27;Czech Republic&#x27;, &#x27;Slovenia&#x27;, &#x27;Japan&#x27;,                 &#x27;Finland&#x27;, &#x27;Great Britain&#x27;, &#x27;Ukraine&#x27;, &#x27;Slovakia&#x27;,                 &#x27;Italy&#x27;, &#x27;Latvia&#x27;, &#x27;Australia&#x27;, &#x27;Croatia&#x27;, &#x27;Kazakhstan&#x27;]    gold = [13, 11, 10, 9, 8, 8, 6, 5, 4, 4, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]    silver = [11, 5, 10, 7, 7, 6, 3, 0, 8, 4, 1, 4, 3, 7, 4, 2, 4, 3, 1, 0, 0, 2, 2, 2, 1, 0]    bronze = [9, 10, 5, 12, 9, 5, 2, 1, 5, 7, 1, 2, 2, 6, 2, 4, 3, 1, 2, 1, 0, 6, 2, 1, 0, 1]        olympic_medal_counts = &#123;&#x27;country_name&#x27;:countries,                            &#x27;gold&#x27;: Series(gold),                            &#x27;silver&#x27;: Series(silver),                            &#x27;bronze&#x27;: Series(bronze)&#125;        df = DataFrame(olympic_medal_counts)        # YOUR CODE HERE    avg_medal_count = df[[&#x27;gold&#x27;,&#x27;silver&#x27;,&#x27;bronze&#x27;]].apply(numpy.mean)        return avg_medal_count


7. 矩阵乘法和Numpy Dotimport numpyfrom pandas import DataFrame, Seriesdef numpy_dot():    &#x27;&#x27;&#x27;    Imagine a point system in which each country is awarded 4 points for each    gold medal,  2 points for each silver medal, and one point for each     bronze medal.      Using the numpy.dot function, create a new dataframe called     &#x27;olympic_points_df&#x27; that includes:        a) a column called &#x27;country_name&#x27; with the country name        b) a column called &#x27;points&#x27; with the total number of points the country           earned at the Sochi olympics.               You do not need to call the function in your code when running it in the    browser - the grader will do that automatically when you submit or test it.    &#x27;&#x27;&#x27;    countries = [&#x27;Russian Fed.&#x27;, &#x27;Norway&#x27;, &#x27;Canada&#x27;, &#x27;United States&#x27;,                 &#x27;Netherlands&#x27;, &#x27;Germany&#x27;, &#x27;Switzerland&#x27;, &#x27;Belarus&#x27;,                 &#x27;Austria&#x27;, &#x27;France&#x27;, &#x27;Poland&#x27;, &#x27;China&#x27;, &#x27;Korea&#x27;,                  &#x27;Sweden&#x27;, &#x27;Czech Republic&#x27;, &#x27;Slovenia&#x27;, &#x27;Japan&#x27;,                 &#x27;Finland&#x27;, &#x27;Great Britain&#x27;, &#x27;Ukraine&#x27;, &#x27;Slovakia&#x27;,                 &#x27;Italy&#x27;, &#x27;Latvia&#x27;, &#x27;Australia&#x27;, &#x27;Croatia&#x27;, &#x27;Kazakhstan&#x27;]    gold = [13, 11, 10, 9, 8, 8, 6, 5, 4, 4, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]    silver = [11, 5, 10, 7, 7, 6, 3, 0, 8, 4, 1, 4, 3, 7, 4, 2, 4, 3, 1, 0, 0, 2, 2, 2, 1, 0]    bronze = [9, 10, 5, 12, 9, 5, 2, 1, 5, 7, 1, 2, 2, 6, 2, 4, 3, 1, 2, 1, 0, 6, 2, 1, 0, 1]     # YOUR CODE HERE    data = &#123;&#x27;country_name&#x27;:Series(countries),&#x27;gold&#x27;:Series(gold),&#x27;silver&#x27;:Series(silver),&#x27;bronze&#x27;:Series(bronze)&#125;    df = DataFrame(data)    medal_counts = df[[&#x27;gold&#x27;,&#x27;silver&#x27;,&#x27;bronze&#x27;]]    points = numpy.dot(medal_counts,[4,2,1])    olympic_points = &#123;&#x27;country_name&#x27;: Series(countries),&#x27;points&#x27;:Series(points)&#125;    olympic_points_df = DataFrame(olympic_points)        return olympic_points_df

8. 书签Pandas文档http://pandas.pydata.org/pandas-docs/stable/
Pandas IPython Notebook 教程https://bitbucket.org/hrojas/learn-pandas
numpy.dot — NumPy v1.12 Manualhttps://docs.scipy.org/doc/numpy/reference/generated/numpy.dot.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>转载</tag>
        <tag>机器学习</tag>
        <tag>numpy</tag>
        <tag>pandas</tag>
      </tags>
  </entry>
  <entry>
    <title>Jupyter notebook</title>
    <url>/dev-jupyter-notebook/</url>
    <content><![CDATA[1. 前言本文转载自优达学城《机器学习工程师》
Jupyter notebook 是 Web 文档，能让你将文本、图像和代码全部组合到一个文档中。它已经成为数据分析的标准环境。notebook 源自 2011 年的 IPython 项目，之后迅速流行起来。


2. Jupyter notebook 是什么？欢迎学习本课，即如何使用 Jupyter notebook。notebook 是一种 Web 应用，能让用户将说明文本、数学方程、代码和可视化内容全部组合到一个易于共享的文档中。例如，不久前我共享了我最爱的 notebook 之一，它分析了 LIGO 实验探测到的两个碰撞的黑洞所发出的引力波。你可以下载数据，运行 notebook 中的代码，重复整个分析，实际上等于你自己探测引力波！
Notebook 已迅速成为处理数据的必备工具。其已知用途包括数据清理和探索、可视化、机器学习和大数据分析。我为我的个人博客创建了一个 notebook 示例，它展示了 notebook 的许多特点。这项工作通常在终端中完成，也即使用普通的 Python shell 或 IPython 完成。可视化在单独的窗口中进行，而文字资料以及各种函数和类脚本包含在独立的文档中。但是，notebook 能将这一切集中到一处，让用户一目了然。
GitHub 上面也会自动提供 notebook。借助此出色的功能，你可以轻松共享工作。http://nbviewer.jupyter.org/ 也会提供 GitHub 代码库中的 notebook 或存储在其他地方的 notebook。
2.1. 文学化编程notebook 是 Donald Knuth 在 1984 年提出的文学化编程的一种形式。在文学化编程中，直接在代码旁写出叙述性文档，而不是另外编写单独的文档。用 Donald Knuth 的话来说：

让我们集中精力向人们解释我们希望计算机做什么，而不是设想我们的主要任务是指示计算机做什么。

归根到底，代码是写给人而不是计算机看的。notebook 恰恰提供了这种能力。你能够直接在代码旁写出叙述性文档。这不仅对阅读 notebook 的人很有用，而且对你将来回头分析代码也很有用。
说点题外话：最近，文学化编程这个概念已经发展成为一门完整的编程语言，即 Eve。
2.2. notebook 如何工作Jupyter notebook 源自 Fernando Perez 发起的 IPython 项目。IPython 是一种交互式 shell，与普通的 Python shell 相似，但具有一些很好的功能（例如语法高亮显示和代码补全）。最初，notebook 的工作方式是，将来自 Web 应用（你在浏览器中看到的 notebook）的消息发送给 IPython 内核（在后台运行的 IPython 应用程序）。内核执行代码，然后将代码发送回 notebook。当前架构与之相似，具体见下图（摘自Jupyter文档）。中心点是 notebook 服务器。你通过浏览器连接到该服务器，而 notebook 呈现为 Web 应用。你在 Web 应用中编写的代码通过该服务器发送给内核。内核运行代码并将代码发送回该服务器，之后，任何输出都会返回到浏览器中。保存 notebook 时，它作为 JSON 文件（文件扩展名为 .ipynb）写入到该服务器中。
此架构的一个优点是，内核无需运行 Python。由于 notebook 和内核分开，因此可以在两者之间发送任何语言的代码。例如，早期的两个非 Python 内核分别用于 R 语言和 Julia 语言。使用 R 内核时，用 R 编写的代码将发送给执行该代码的 R 内核，这与在 Python 内核上运行 Python 代码完全一样。IPython notebook 已被改名，因为 notebook 变得与编程语言无关。新的名称 Jupyter 由 Julia、Python 和 R 组合而成。如果有兴趣，不妨看看可用内核的列表。
另一个优点是，可以在任何地方运行服务器，并且可通过互联网访问服务器。通常，你会在存储所有数据和 notebook 文件的自有计算机上运行服务器。但是，你也可以在远程计算机或云实例（如 Amazon 的 EC2）上设置服务器。之后，可以在全球任何地方通过浏览器访问 notebook。
3. 安装 Jupyter notebook到目前为止，安装 Jupyter 的最简单方法是使用 Anaconda。该发行版自动附带了 Jupyter notebook。你能够在默认环境下使用 notebook。
要在 conda 环境中安装 Jupyter notebook，请使用 conda install jupyter notebook。
也可以通过 pip 使用 pip install jupyter notebook 来获得 Jupyter notebook。
4. 安装多版本内核1、查看已安装内核jupyter kernelspec list
2、安装内核假设默认环境是python2.7，那么切换到python3.6环境下，然后安装内核
activate py3conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/conda config --set show_channel_urls yesconda install ipykernel

3、把内核添加到jupyter
activate py3python -m ipykernel install

可以添加命令 --name kernelname 为kernel指定名字。
4、删除内核jupyter kernelspec remove kernelname
5. 启动 notebook 服务器要启动 notebook 服务器，请在终端或控制台中输入 jupyter notebook。服务器会在你运行此命令的目录中启动。这意味着任何 notebook 文件都会保存在该目录中。你通常希望在 notebook 所在的目录中启动服务器。不过，你可以在文件系统中导航到 notebook 所在的位置。
运行此命令时（请自己试一下！），服务器主页会在浏览器中打开。默认情况下，notebook 服务器的运行地址是 http://localhost:8888。如果你不熟悉该地址，其含义是：localhost 表示你的计算机，而 8888 是服务器的通信端口。只要服务器仍在运行，你随时都能通过在浏览器中输入 http://localhost:8888 返回到服务器。
如果启动其他服务器，新服务器会尝试使用端口 8888，但由于此端口已被占用，因此新服务器会在端口 8889 上运行。之后，可以通过 http://localhost:8889 连接到新服务器。每台额外的 notebook 服务器都会像这样增大端口号。
如果你尝试启动自己的服务器，它应类似以下所示：
你可能会看到上面列表中的一些文件和文件夹，具体取决于你在哪里启动服务器。
在右侧，你可以点击“New”（新建），创建新的 notebook、文本文件、文件夹或终端。“Notebooks”下的列表显示了你已安装的内核。由于我在 Python 3 环境中运行服务器，因此列出了 Python 3 内核。你在这里看到的可能是 Python 2。我还安装了用于 Scala 2.10 和 2.11 的内核，因此它们出现在列表中。
如果在 conda 环境中运行 Jupyter notebook 服务器，则你还能选择任何其他环境中的内核（见下图）。要创建新的 notebook，请点击你要使用的内核。
顶部的选项卡是 Files（文件）、Running（运行）和 Cluster（聚类）。Files（文件）显示当前目录中的所有文件和文件夹。点击 Running（运行）选项卡会列出所有正在运行的 notebook。可以在该选项卡中管理这些 notebook。
过去，在 Clusters（聚类）中创建多个用于并行计算的内核。现在，这项工作已经由 ipyparallel 接管，因此该选项卡如今用处不多。
如果在 conda 环境中运行 notebook 服务器，则你还能访问以下所示的“Conda”选项卡。可以通过该选项卡管理 Jupyter 中的环境。你可以执行多种操作，例如创建新的环境、安装包、更新包、导出环境。
6. 快速启动 notebook启动jupyter notebook时，每次都要切换文件路径，需要输入相关的命令，相对来说比较麻烦，有一个可以快速启动的小技巧。
在想要打开的目录下，创建一个 ipy.bat 文件，用记事本打开这个文件，输入如下内容并保存。
jupyter notebookpause
双击这个 ipy.bat 文件，就可以快速启动jupyter notebook，把这个 ipy.bat文件以快捷方式发送到桌面，启动很方便。
7. 关闭 Jupyter通过在服务器主页上选中 notebook 旁边的复选框，然后点击“Shutdown”（关闭），你可以关闭各个 notebook。但是，在这样做之前，请确保你保存了工作！否则，在你上次保存后所做的任何更改都会丢失。下次运行 notebook 时，你还需要重新运行代码。通过在终端中按两次 Ctrl + C，可以关闭整个服务器。再次提醒，这会立即关闭所有运行中的 notebook，因此，请确保你保存了工作！
8. notebook 界面创建新的 notebook 时，你会看到如下所示的界面：请随意尝试和四处浏览一下。
你会看到外框为绿色的一个小方框。它称为单元格。单元格是你编写和运行代码的地方。你也可以更改其类型，以呈现 Markdown（一种常用于编写 Web 内容的格式化语法）。我会在后面更详细地介绍 Markdown。在工具栏中点击“Code”，将其改为 Markdown，然后改回来。小型的播放按钮用于运行单元格，而向上和向下的箭头用于上下移动单元格。运行代码单元格时，单元格下方会显示输出。单元格还会被编号（左侧会显示 In [1]:）。这能让你知道运行的代码和运行顺序（如果运行了多个单元格的话）。在 Markdown 模式下运行单元格会将 Markdown 呈现为文本。
9. 工具栏从左侧开始，工具栏上的其他控件是：

落伍的软盘符号，表示“保存”。请记得保存 notebook！

按钮用于创建新的单元格


然后是用于剪切、复制和粘贴单元格的按钮。
运行、停止、重新启动内核
单元格类型：代码、Markdown、原始文本和标题
命令面板（见下文）
单元格工具栏，提供不同的单元格选项（例如将单元格用作幻灯片）

10. 命令面板小键盘符号代表命令面板。点击它会弹出一个带有搜索栏的面板，供你搜索不同的命令。这能切实帮助你加快工作速度，因为你无需使用鼠标翻查各个菜单。你只需打开命令面板，然后键入要执行的操作。例如，如果要合并两个单元格：
11. 更多事项顶部显示了标题。点击它可以将 notebook 重命名。
右侧是内核类型（在我的例子中是 Python 3），旁边是一个小圆形。在内核运行单元格时，会填充这个小圆形。对于大多数快速运行的操作，并不会填充它。它是一个小型指示器，让你知道实际运行的代码会运行较长时间。
工具栏包含了保存按钮，此外，notebook 也会定期自动保存。标题右侧会注明最近一次的保存。可以使用保存按钮手动进行保存，也可以按键盘上的 Esc，然后按 s。按 Esc 键会变为命令模式，而 s 是“保存”的快捷键。我会在后面介绍命令模式和快捷键。
在“File”（文件）菜单中，可以下载多种格式的 notebook。通常，你会希望将它作为 HTML 文件下载，以便与不使用 Jupyter 的其他人共享。也可以将 notebook 作为普通的 Python 文件下载，此时所有代码都会像平常一样运行。要在博客或文档中使用 notebook，Markdown 和 reST 格式很合适。
12. 代码单元格notebook 中的大部分工作均在代码单元格中完成。这是编写和执行代码的地方。在代码单元格中可以执行多种操作，例如编写任何代码、给变量赋值、定义函数和类、导入包。在一个单元格中执行的任何代码在所有其他单元格中均可用。
我创建了一个 notebook，你可以将它当作练习来完成。请在下面下载此 notebook (Working With Code Cells)，然后从你自己的 notebook 服务器运行它。（在你的终端中，转到包含此 notebook 文件的目录，然后输入 jupyter notebook）浏览器可能会尝试不下载就打开此 notebook 文件。如果是这样，请右击链接并选择“链接另存为…”。
辅助材料：Working With Code Cells
13. Markdown 单元格如前所述，单元格也可用于以 Markdown 编写的文本。Markdown 是格式化语法，可让你加入链接、将文本样式设为粗体或斜体和设置代码格式。像代码单元格一样，按 Shift + Enter 或 Ctrl + Enter 可运行 Markdown 单元格，这会将 Markdown 呈现为格式化文本。加入文本可让你直接在代码旁写出叙述性文档，以及为代码和代码中的思路编写文档。
你可以在此处查找文档，但我会提供简短的入门文档。
13.1. 标题要编写标题，可在文本前放置井号，即 #（英文读作 pound、hash 或 octothorpe）。一个 # 呈现为 h1 标题，两个 # 是 h2 标题，依此类推。类似以下所示：
# Header 1## Header 2### Header 3

13.2. 链接要在 Markdown 中添加链接，请在文本两侧加上方括号，并在 URL 两侧加上圆括号，例如：[Udacity&#39;s home page](https://www.udacity.com) 表示指向 Udacity’s home page的链接。
13.3. 强调效果可以使用星号或下划线（* 或 _）来表示粗体或斜体，从而添加强调效果。对于斜体，在文本两侧加上一个星号或下划线，例如 _gelato_ 或 *gelato* 会呈现为 gelato。
粗体文本使用两个符号，例如 **aardvark** 或 __aardvark__ 会呈现为 aardvark。
只要在文本两侧使用相同的符号，星号和下划线的作用都一样。
13.4. 代码可以通过两种不同的方式显示代码，一种是与文本内联，另一种是将代码块与文本分离。要将代码变为内联格式，请在文本两侧加上反撇号。例如，`string.punctuation` 会呈现为 string.punctuation。
要创建代码块，请另起一行并用三个反撇号将文本包起来：或者将代码块的每一行都缩进四个空格。
13.5. 数学表达式在 Markdown 单元格中，可以使用 LaTeX 符号创建数学表达式。notebook 使用 MathJax 将 LaTeX 符号呈现为数学符号。要启动数学模式，请在 LaTeX 符号两侧加上美元符号（例如 $y = mx + b$），以创建内联的数学表达式。对于数学符号块，请使用两个美元符号：
$$y = \frac&#123;a&#125;&#123;b+c&#125;$$

此功能的确很有用，因此，如果你没有用过 LaTeX，请阅读这篇入门文档，它介绍了如何使用 LaTeX 来创建数学表达式。
13.6. 小结在编写 Markdown 时，可以参考这个速查指南。我建议使用 Markdown 单元格，与使用一堆代码块相比，这使 notebook 变得更易于阅读。
14. 快捷键notebook 自带一组快捷键，能让你使用键盘与单元格交互，而无需使用鼠标和工具栏。熟悉这些快捷键需要花费一点时间，但如果能熟练掌握，将大大加快你在 notebook 中的工作速度。要详细了解这些快捷键和练习它们的用法，请在下面下载 notebook Keyboard Shortcuts。再次提醒，浏览器可能会尝试打开它，但请将它保存到计算机中。请右击链接并选择“链接另存为…”。辅助材料：Keyboard Shortcuts
15. Magic 关键字Magic 关键字是可以在单元格中运行的特殊命令，能让你控制 notebook 本身或执行系统调用（例如更改目录）。例如，可以使用 %matplotlib 将 matplotlib 设置为以交互方式在 notebook 中工作。
Magic 命令的前面带有一个或两个百分号（% 或 %%），分别对应行 Magic 命令和单元格 Magic 命令。行 Magic 命令仅应用于编写 Magic 命令时所在的行，而单元格 Magic 命令应用于整个单元格。
注意：这些 Magic 关键字是特定于普通 Python 内核的关键字。如果使用其他内核，这些关键字很有可能无效。
15.1. 代码计时有时候，你可能要花些精力优化代码，让代码运行得更快。在此优化过程中，必须对代码的运行速度进行计时。可以使用 Magic 命令 timeit 测算函数的运行时间，如下所示：
如果要测算整个单元格的运行时间，请使用 %%timeit，如下所示：

15.2. 在 notebook 中嵌入可视化内容如前所述，notebook 允许你将图像与文本和代码一起嵌入。这在你使用 matplotlib 或其他绘图包创建可视化内容时最为有用。可以使用 %matplotlib 将 matplotlib 设置为以交互方式在 notebook 中工作。默认情况下，图形呈现在各自的窗口中。但是，可以向命令传递参数，以选择特定的“后端”（呈现图像的软件）。要直接在 notebook 中呈现图形，应将内联后端与命令 %matplotlib inline 一起使用。

提示：在分辨率较高的屏幕（例如 Retina 显示屏）上，notebook 中的默认图像可能会显得模糊。可以在 %matplotlib inline 之后使用 %config InlineBackend.figure_format = &#39;retina&#39; 来呈现分辨率较高的图像。


15.3. 在 notebook 中进行调试对于 Python 内核，可以使用 Magic 命令 %pdb开启交互式调试器。出错时，你能检查当前命名空间中的变量。
在上图中，可以看到我尝试对字符串求和，这造成了错误。调试器指出了该错误，并提示你检查代码。
要详细了解 pdb，请阅读此文档。要退出调试器，在提示符中输入 q 即可。
15.4. 补充读物Magic 命令还有很多，我只是介绍了你将会用得最多的一些命令。要了解更多信息，请查看此列表，它列出了所有可用的 Magic 命令。
16. 转换 notebookNotebook 只是扩展名为 .ipynb 的大型 JSON 文件。
由于 notebook 是 JSON 文件，因此，可以轻松将其转换为其他格式。Jupyter 附带了一个名为 nbconvert 的实用程序，可将 notebook 转换为 HTML、Markdown、幻灯片等格式。
例如，要将 notebook 转换为 HTML 文件，请在终端中使用jupyter nbconvert --to html notebook.ipynb
要将 notebook 与不使用 notebook 的其他人共享，转换为 HTML 很有用。而要在博客和其他接受 Markdown 格式化的文本编辑器中加入 notebook，Markdown 很合适。
像平常一样，要详细了解 nbconvert，请阅读相关文档。
17. 创建幻灯片通过 notebook 创建幻灯片是我最爱的功能之一。此处有一个幻灯片示例，它介绍了用于处理数据的 Pandas。
在 notebook 中创建幻灯片的过程像平常一样，但需要指定作为幻灯片的单元格和单元格的幻灯片类型。在菜单栏中，点击“View”（视图）&gt;“Cell Toolbar”（单元格工具栏）&gt;“Slideshow”（幻灯片），以便在每个单元格上弹出幻灯片单元格菜单。
这会在每个单元格上显示一个下拉菜单，让你选择单元格在幻灯片中的显示方式。

Slides（幻灯片）是你从左向右移动的完整幻灯片。按向上或向下的箭头时，Sub-slides（子幻灯片）会出现在幻灯片中。Fragments（片段）最初是隐藏的，在你按下按钮时会出现。选择 Skip（忽略）会忽略幻灯片中的单元格，而选择 Notes（备注）会将单元格保留为演讲者备注。
18. 运行幻灯片要通过 notebook 文件创建幻灯片，需要使用 nbconvert：jupyter nbconvert notebook.ipynb --to slides
这只是将 notebook 转换为幻灯片必需的文件，你需要向其提供 HTTP 服务器才能真正看到演示文稿。
要转换它并立即看到它，请使用jupyter nbconvert notebook.ipynb --to slides --post serve这会在浏览器中打开幻灯片，让你可以演示它。
19. 恭喜你！这个简短的课程到此结束，它主要介绍了 Python 数据科学工作流程中的工具。充分利用 Anaconda 和 Jupyter notebook 不仅能提升你的工作效率，还会让你心情更愉快。要想充分发挥它们的作用，你还要学习很多东西（例如 Markdown 和 LaTeX），但很快你就会想知道为何要以其他方式进行数据分析。
再次恭喜你！祝你好运！
20. 书签Jupyter Notebook的27个秘诀，技巧和快捷键
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>转载</tag>
        <tag>jupyter</tag>
        <tag>anaconda</tag>
      </tags>
  </entry>
  <entry>
    <title>Anaconda</title>
    <url>/dev-anaconda/</url>
    <content><![CDATA[1. 前言本文转载自优达学城《机器学习工程师》
欢迎来到本课程！我叫 Mat Leonard，是数据分析师纳米学位的项目主管，也是这个简短课程的讲师。我会在课程中介绍两个对于数据分析师最为重要的工具，即 Anaconda 和 Jupyter notebook。
Anaconda 是一个包含数据科学常用包的发行版本。它是基于 conda ——一个包和环境管理器——衍生而来。你将使用 conda 创建环境，以便分隔使用不同 Python 版本和/或不同包的项目。你还将使用它在环境中安装、卸载和更新包。通过使用 Anaconda，使我处理数据的过程更加愉快。
Jupyter notebook 是 Web 文档，能让你将文本、图像和代码全部组合到一个文档中。它已经成为数据分析的标准环境。notebook 源自 2011 年的 IPython 项目，之后迅速流行起来。在本课程的第二节课中，你将获得使用 notebook 进行分析工作的经验。
让我们继续课程！首先学习 Anaconda。


2. Anaconda欢迎学习本课，即如何使用 Anaconda 来管理 Python 所用的包和环境。Anaconda 能让你轻松安装在数据科学工作中经常使用的包。你还将使用它创建虚拟环境，以便更轻松地处理多个项目。Anaconda 简化了我的工作流程，并且解决了我在处理包和多个 Python 版本时遇到的大量问题。
Anaconda 实际上是一个软件发行版，它附带了 conda、Python 和 150 多个科学包及其依赖项。应用程序 conda 是包和环境管理器。Anaconda 的下载文件比较大（约 500 MB），因为它附带了 Python 中最常用的数据科学包。如果只需要某些包，或者需要节省带宽或存储空间，也可以使用 Miniconda 这个较小的发行版（仅包含 conda 和 Python）。你仍可以使用 conda 来安装任何可用的包，它只是没有附带这些包而已。
conda 是一种只能通过命令行来使用的程序，因此，如果你觉得它很难用，请查看这个面向 Windows 的命令提示符教程，或者学习我们面向 OSX/Linux 的 Linux 命令行基础知识课程。
你可能已经安装了 Python，并且想知道为何还需要 Anaconda。首先，由于 Anaconda 附带了一大批数据科学包，因此你可以立即开始处理数据。其次，使用 conda 来管理包和环境能减少将来在处理你要使用的各种库时遇到的问题。
2.1. 管理包
包管理器用于在计算机上安装库和其他软件。你可能已经熟悉 pip，它是 Python 库的默认包管理器。conda 与 pip 相似，不同之处是可用的包以数据科学包为主，而 pip 适合一般用途。但是，conda 并非 像 pip 那样专门适用于 Python，它也可以安装非 Python 的包。它是适用于 任何 软件堆栈的包管理器。也就是说，并非所有的 Python 库都能通过 Anaconda 发行版和 conda 获得。在使用 conda 的同时，你仍可以并且仍将使用 pip 来安装包。
Conda 安装了预编译的包。例如，Anaconda 发行版附带了使用 MKL 库编译的 Numpy、Scipy 和 Scikit-learn，从而加快了各种数学运算的速度。这些包由发行版的贡献者维护，这意味着它们通常滞后于新版本。但是，由于有人需要为许多系统构建这些包，因此，它们往往更为稳定，而且更便于你使用。
2.2. 环境除了管理包之外，conda 还是虚拟环境管理器。它类似于另外两个很流行的环境管理器，即 virtualenv 和 pyenv。
环境能让你分隔你要用于不同项目的包。你常常要使用依赖于某个库的不同版本的代码。例如，你的代码可能使用了 Numpy 中的新功能，或者使用了已删除的旧功能。实际上，不可能同时安装两个 Numpy 版本。你要做的应该是，为每个 Numpy 版本创建一个环境，然后在适用于项目的环境中工作。
在应对 Python 2 和 Python 3 时，此问题也会常常发生。你可能会使用在 Python 3 中不能运行的旧代码，以及在 Python 2 中不能运行的新代码。同时安装两个版本可能会造成许多混乱和错误。而创建独立的环境会好很多。
也可以将环境中的包的列表导出为文件，然后将该文件与代码包括在一起。这能让其他人轻松加载代码的所有依赖项。pip 提供了类似的功能，即 pip freeze &gt; requirements.txt。
2.3. 接下来介绍的内容接下来，我会详细介绍 Anaconda 的用法。首先，我会介绍它的安装过程，然后介绍如何使用包管理器，最后介绍如何创建和管理环境。
3. 安装 AnacondaAnaconda 可用于 Windows、Mac OS X 和 Linux。可以在 https://www.continuum.io/downloads 上找到安装程序和安装说明。
如果计算机上已经安装了 Python，这不会有任何影响。实际上，脚本和程序使用的默认 Python 是 Anaconda 附带的 Python。
选择 Python 2.7 版本（你可以在以后安装 Python 3 版本）。此外，如果是 64 位操作系统，则选择 64 位安装程序，否则选择 32 位安装程序。继续并选择合适的版本，然后安装它。之后，继续进行！
完成安装后，会自动进入默认的 conda 环境，而且所有包均已安装完毕，如下面所示。可以在终端或命令提示符中键入 conda list，以查看你安装的内容。

在 Windows 上，会随 Anaconda 一起安装一批应用程序：

Anaconda Navigator，它是用于管理环境和包的 GUI
Anaconda Prompt 终端，它可让你使用命令行界面来管理环境和包
Spyder，它是面向科学开发的 IDE

为了避免报错，我推荐在默认环境下更新所有的包。打开 Anaconda Prompt （或者 Mac 下的终端），键入：conda upgrade --all
并在提示是否更新的时候输入y（Yes）以便让更新继续。初次安装下的软件包版本一般都比较老旧，因此提前更新可以避免未来不必要的问题。
在本课的余下部分，我会要求你在终端中使用命令。我强烈建议你以这种方式开始使用 Anaconda，之后再根据需要使用 GUI。
4. 管理包安装了 Anaconda 之后，管理包是相当简单的。要安装包，请在终端中键入 conda install package_name。例如，要安装 numpy，请键入 conda install numpy。你还可以同时安装多个包。类似 conda install numpy scipy pandas 的命令会同时安装所有这些包。还可以通过添加版本号（例如 conda install numpy=1.10）来指定所需的包版本。
Conda 还会自动为你安装依赖项。例如，scipy 依赖于 numpy，因为它使用并需要 numpy。如果你只安装 scipy (conda install scipy)，则 conda 还会安装 numpy（如果尚未安装的话）。
大多数命令都是很直观的。要卸载包，请使用 conda remove package_name。要更新包，请使用 conda update package_name。如果想更新环境中的所有包（这样做常常很有用），请使用 conda update --all。最后，要列出已安装的包，请使用前面提过的 conda list。
如果不知道要找的包的确切名称，可以尝试使用 conda search search_term 进行搜索。例如，我知道我想安装 Beautiful Soup，但我不清楚确切的包名称。因此，我尝试执行 conda search beautifulsoup。它返回可用的 Beautiful Soup 包的列表，并列出了相应的包名称 beautifulsoup4。
5. 管理环境如前所述，可以使用 conda 创建环境以隔离项目。要创建环境，请在终端中使用 conda create -n env_name list of packages。在这里，-n env_name 设置环境的名称（-n 是指名称），而 list of packages 是要安装在环境中的包的列表。例如，要创建名为 my_env 的环境并在其中安装 numpy，请键入 conda create -n my_env numpy。创建环境时，可以指定要安装在环境中的 Python 版本。这在你同时使用 Python 2.x 和 Python 3.x 中的代码时很有用。要创建具有特定 Python 版本的环境，请键入类似于 conda create -n py3 python=3 或 conda create -n py2 python=2 的命令。实际上，我在我的个人计算机上创建了这两个环境。我将它们用作与任何特定项目均无关的通用环境，以处理普通的工作（可轻松使用每个 Python 版本）。这些命令将分别安装 Python 3 和 2 的最新版本。要安装特定版本（例如 Python 3.3），请使用 conda create -n py python=3.3。
5.1. 进入环境创建了环境后，在 OSX/Linux 上使用 source activate my_env 进入环境。在 Windows 上，请使用 activate my_env。
进入环境后，你会在终端提示符中看到环境名称，它类似于 (my_env) ~ $。环境中只安装了几个默认的包，以及你在创建它时安装的包。可以使用 conda list 检查这一点。在环境中安装包的命令与前面一样：conda install package_name。不过，这次你安装的特定包仅在你进入环境后才可用。要离开环境，请键入 source deactivate（在 OSX/Linux 上）。在 Windows 上，请使用 deactivate。
5.2. 保存和加载环境共享环境这项功能确实很有用，它能让其他人安装你的代码中使用的所有包，并确保这些包的版本正确。可以使用 conda env export &gt; environment.yaml 将包保存为 YAML。第一部分 conda env export 写出环境中的所有包（包括 Python 版本）。上图可以看到列出了环境的名称和所有依赖项及其版本。导出命令的第二部分 &gt; environment.yaml 将导出的文本写入到 YAML 文件 environment.yaml 中。现在可以共享此文件，而且其他人能够创建和你用于项目相同的环境。
要通过环境文件创建环境，请使用 conda env create -f environment.yaml。这会创建一个新环境，而且它具有在 environment.yaml 中列出的同一库。
5.3. 列出环境如果忘记了环境的名称（我有时会这样），可以使用 conda env list 列出你创建的所有环境。你会看到环境的列表，而且你当前所在环境的旁边会有一个星号。默认的环境（即当你不在环境中时使用的环境）名为 root。
5.4. 删除环境如果你不再使用某些环境，可以使用 conda env remove -n env_name 删除指定的环境（在这里名为 env_name）。
6. 最佳做法6.1. 使用环境对我帮助很大的一点是，我的 Python 2 和 Python 3 具有独立的环境。我使用了 conda create -n py2 python=2 和 conda create -n py3 python=3 创建两个独立的环境，即 py2 和 py3。现在，我的每个 Python 版本都有一个通用环境。在所有这些环境中，我都安装了大多数标准的数据科学包（numpy、scipy、pandas 等）。
我还发现，为我从事的每个项目创建环境很有用。这对于与数据不相关的项目（例如使用 Flask 开发的 Web 应用）也很有用。例如，我为我的个人博客（使用 Pelican）创建了一个环境。
6.2. 共享环境在 GitHub 上共享代码时，最好同样创建环境文件并将其包括在代码库中。这能让其他人更轻松地安装你的代码的所有依赖项。对于不使用 conda 的人，我通常还会使用 pip freeze（在此处了解详情）将一个 pip requirements.txt 文件包括在内。
6.3. 了解更多信息要详细了解 conda 和它如何融入到 Python 生态系统中，请查看这篇由 Jake Vanderplas 撰写的文章：Conda myths and misconceptions（有关 conda 的迷思和误解）。此外，有空也可以参考这篇 conda 文档。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>conda</tag>
        <tag>转载</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python包管理工具Conda</title>
    <url>/dev-python-conda/</url>
    <content><![CDATA[1. Conda简介本文部分内容来自ChatGPT。
Conda是一个开源的跨平台包管理器，用于安装并管理不同编程语言的软件包。它最初是为Python编程语言设计的，但现已扩展到其他语言，如R、Ruby和Lua等。
conda的一个主要优势是它可以同时管理多个环境，使得在不同项目之间切换和隔离依赖变得容易。此外，conda还可以解决依赖关系，以确保软件包之间的兼容性。
相关文档：

github - conda
《Anaconda》
《Python包管理工具pip》
《Python版本管理器pyenv》



2. Anaconda和Miniconda2.1. AnacondaAnaconda是一个流行的Python和R语言的数据科学发行版，提供了包括conda在内的一整套工具和库。Anaconda预装了许多用于数据科学和机器学习的常用包，使得新手能够快速开始编写代码。然而，Anaconda的体积较大，可能会占用大量磁盘空间。
安装方法：访问anaconda官网，下载对应系统的安装包，执行安装。
2.2. MinicondaMiniconda是Anaconda的轻量级版本，只包含Python和conda。Miniconda没有预装任何其他包，但允许用户使用conda自定义安装所需的包。Miniconda是为那些对磁盘空间有限制或只需特定软件包的用户设计的。
安装方法：访问miniconda官网，下载对应系统的安装包或者脚本，执行安装。
Linux中安装方法：
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.shmkdir -p /data/miniconda3bash miniconda.sh -b -u -p /data/miniconda3/data/miniconda3/bin/conda init bash

MacOS中安装方法：
curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -o miniconda.shmkdir -p ~/miniconda3bash miniconda.sh -b -u -p ~/miniconda3~/miniconda3/bin/conda init bash~/miniconda3/bin/conda init zsh

Miniconda卸载方法：
rm -rf /data/miniconda3 # 根据真实安装路径修改# 注释或删除 .bash_profile 中关于conda的配置

3. Conda常用命令查看conda的版本
conda --version

更新conda至最新版本
conda update conda

创建一个新的环境，指定Python版本
conda create --name &lt;env_name&gt; python=&lt;python_version&gt;

激活指定的环境
conda activate &lt;env_name&gt;

停用当前环境
conda deactivate

在当前环境中安装指定的软件包
conda install &lt;package_name&gt;

列出当前环境中的所有软件包
conda list

更新指定的软件包
conda update &lt;package_name&gt;

从当前环境中删除指定的软件包
conda remove &lt;package_name&gt;

列出所有已创建的环境
conda env list# orconda info --envs

删除指定的环境
conda env remove --name &lt;env_name&gt;

不询问，直接删除指定的环境
conda env remove --name &lt;env_name&gt; -y

环境更换名称
conda env export --name old-name &gt; environment.ymlconda env create --file environment.yml --name new-nameconda env remove --name old-name

4. Conda创建环境示例使用conda创建一个名为my_env的Python 3.7.10环境，并安装requests、pytest和pytest-cov依赖。
conda create --name my_env python=3.7.10conda activate my_envconda install requests pytest pytest-cov

5. Conda常用配置conda自定义配置，可以使用 conda config 命令进行设置，也可以直接修改 ~/.condarc 文件。
新的 shell 不激活 base 环境
conda config --set auto_activate_base false

移除 conda 环境前缀，比如(base)
conda config --set changeps1 false

因为常用 conda activate ，因此最好配置一个alias在~/.bash_profile中
alias ca=&quot;conda activate&quot;

设置默认的python环境
echo &#x27;conda activate my_env&#x27; &gt;&gt; ~/.bashrc

6. Miniconda的重装方法如果误操作破坏了conda环境，那么可以通过重装conda进行恢复。

保留 envs 目录
mv /data/miniconda3/envs /data/
卸载miniconda
bash /data/miniconda3/uninstall.sh# 或者直接删除 miniconda3 目录
重装miniconda
bash miniconda.sh -b -u -p /data/miniconda3conda --help
恢复 envs 
mv /data/envs/* /data/miniconda3/envs/conda env list

7. conda vs pip7.1. 跨平台和跨语言支持conda：conda是一个跨平台的包管理器，支持Linux、macOS和Windows。虽然最初是为Python设计的，但conda现在也支持其他编程语言，如R、Ruby和Lua等。
pip：pip是Python的官方包管理器，专为Python设计。pip在各种平台上运行良好，但仅适用于Python包。
7.2. 环境管理conda：conda可以轻松创建和管理多个独立的环境，这使得在不同项目之间切换和隔离依赖变得容易。这对于处理具有复杂依赖关系或需要特定版本的包的项目特别有用。
pip：虽然pip本身不支持环境管理，但它可以与virtualenv或venv（Python 3.3+内置）等工具结合使用来创建和管理虚拟环境。
7.3. 二进制包和编译conda：conda提供的包通常是预编译的二进制格式，这意味着它们已经针对特定平台编译过，因此安装过程更快，不需要用户进行编译。这在处理需要编译的复杂库时非常有用，尤其是对于涉及复杂编译过程的本地扩展。
pip：pip从Python Package Index (PyPI)安装包，通常是源代码格式。虽然许多包可以直接安装，但某些包可能需要编译，这可能导致依赖关系问题或在特定平台上遇到安装困难。
7.4. 依赖解析conda：conda在安装软件包时解决依赖关系问题，确保所有依赖的包都兼容。这有助于避免版本冲突和库之间的潜在问题。
pip：pip在解决依赖关系方面的能力有限，可能导致版本冲突或不兼容。尽管如此，pip在最近的版本中已经取得了很大的进步。
7.5. 应用场景conda：conda特别适合于科学计算、数据科学和机器学习等领域，因为它可以轻松处理这些领域中常见的复杂依赖关系。Anaconda和Miniconda分别为这些领域的用户提供了全面和轻量级的解决方案。
pip：pip是Python生态系统的标准工具，适用于大多数项目。
8. conda vs pyenvConda 和 Pyenv 都可以管理 Python 环境，但它们的用途和功能略有不同。
Conda 是一个跨平台的 Python 包管理器和环境管理器。它可以在同一系统中管理多个 Python 版本和其它编程语言的环境。Conda 可以创建和管理 Python 环境，并安装、升级和删除软件包。Conda 还有一个非常好的特点就是可以轻松创建虚拟环境，每个虚拟环境可以拥有不同的 Python 版本和软件包。
Pyenv 是另一个 Python 版本管理器，它可以让您在同一系统中轻松安装和切换不同版本的 Python。Pyenv 不仅可以安装全局 Python 版本，而且还可以在不同的项目中使用不同的 Python 版本。此外，它还可以管理 Python 的虚拟环境。
综上所述，两者的区别在于 Conda 主要用于管理软件包和环境，而 Pyenv 主要用于管理 Python 版本。如果需要在同一系统中使用不同版本的 Python 或不同的软件包，则可以使用 Conda。如果仅仅需要切换 Python 版本，则可以使用 Pyenv。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>chatgpt</tag>
        <tag>conda</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>机器学习——泰坦尼克号生还者分析</title>
    <url>/dev-machine-learning-titanic/</url>
    <content><![CDATA[1. 前言实战项目：泰坦尼克号生还者分析
在这个可选项目中，你要依据乘客的一些特征，例如性别和年龄，来创造一个决策函数，预测1912年泰坦尼克号沉没事件中的生还者。从一个简单的算法开始，逐渐提高它的复杂性，直到你能准确预测给定数据中80%的乘客的生还情况。通过这个案例，我们将向你介绍机器学习纳米学位中会遇到的一些基本概念。


2. 项目概述在这个可选的项目中，您将创建决策函数，并根据1912年泰坦尼克号海难的乘客特征，如：性别、年龄等，对乘客生还结果进行预测。您可以从一个简单的算法入手，然后逐渐增加该算法的复杂度，直至您至少能精确地预测出所提供数据中80%的乘客的生还结果。通过该项目，您可在正式开始学习本纳米学位前，了解机器学习的一些概念。你还可以在论坛找到该题目在 Kaggle 的数据链接。
此外，请确保 Python 装有完成本项目所需的程序包。我们在本项目中将使用到的 Python 库有两个，即 numpy 和 pandas。现在不需担心它们如何运作——我们将在实战项目 1 中接触到它们。本项目还将让您熟悉项目的提交程序，项目提交是您在纳米学位课程中需要完成的内容。
3. 所需软件软件和库本项目采用以下软件和 Python 库：

Python 2.7
NumPy
pandas
matplotlib

你还需要安装和运行 Jupyter Notebook
对jupyter不熟悉的同学可以看一下这两个链接：
Jupyter使用视频教程为什么使用jupyter？如果您还未安装 Python，我们强烈推荐您安装 Python 发行版：Anaconda，其具备包括上述程序包在内的更多程序包。安装时，确保您选择的是 Python 2.7 安装程序，而不是 Python 3.x 安装程序。
如果您的计算机中已装有 Python 2.7，那么您可使用命令行上的 pip 安装 numpy， scikit-learn 和 Jupyter Notebook（之前叫’iPython’）。如果使用 pip 执行安装时出现问题，这个页面对 Windows 用户的某些程序包也是有用的。安装完 pip 之后，你可以执行下列命令安装所需要的包：
sudo pip install numpy pandas matplotlib jupyter scikit-learn
4. 开始项目要开始这个项目，你可以访问我们的GitHub页面，或者点击这里直接下载最新的项目所需文件。
projects/titanic_survival_exploration 文件夹包含三个文件：

Titanic_Survival_Exploration.ipynb: 这是最主要的文件，项目中的主要工作都将在这个文件上完成
titanic_data.csv: 项目数据表。您将需要把这个数据加载到 notebook 里。
titanic_visualizations.py: 这个 Python 脚本包含 helper 函数，可以让数据和存活结果可视化。

为了打开 jupyter notebook，需要完成以下几步。如果你使用 Windows 系统，你需要打开命令终端或 PowerShell；如果你使用 Mac 或者 Linux 系统，直接打开Terminal 终端即可。使用 cd 命令来打开项目文件夹。例如，在 Windows 上你可以使用 cd C:\Users\username\Documents\  （username 用自己的用户名替换）找到项目所在的文件夹；在 Mac 上，你可以使用 cd ~/Documents/ 。在 Windows 上你可以使用 dir 命令，在 Mac 或者 Linux 上用 ls命令列出当前目录中的文件和文件夹。如果发现进错目录，可以使用 cd .. 返回上一级目录。
一旦你进入包含项目文件的文件夹，您可以输入命令
jupyter notebook titanic_survival_exploration.ipynb
打开一个浏览器窗口，或者新建标签页，来使用你的 notebook。依照 notebook 上的指导回答每一个问题完成这个项目。我们还提供了随项目的 READEME 文档，上面也有关于这个项目的信息和指导。
5. 项目提交5.1. 评估你的项目会由优达学城项目评审师按照 泰坦尼克号探索项目要求进行评审。请确定你仔细阅读了该要求，并在项目提交前自我对检查。要求当中的所有条目都必须合格项目才能通过。
5.2. 提交文件当你准备好提交项目时，你可以把下列文件压缩成一个 zip 文件上传。或者，你可以提交你在 GitHub 的 Repo 。可以把文件夹命名为 titanic_survival_exploration 便于查找：

带有完整问题答案和代码的 titanic_survival_exploration.ipynb notebook 文件。
notebook 项目导出的 HTML 文件，命名为 report.html。

注意：所提交文件的文件名名，包括zip压缩包内的文件名，都不能含有中文及任何ASCII之外的字符，否则会造成提交失败。
如何导出HTML的说明在 notebook 的最下方。 你也许需要先在命令后通过 pip install mistune 命令安装 mistune 。当你准备好所有这些文件，并且依照项目要求核对过之后，就可以在下面的项目提交页面提交你的项目了。
如果你是第一次在优达学城提交项目，点击提交之后，要等1分钟左右才能打开提交页面。如果长时间打不开，可以刷新。如果依然无法打开项目提交页面，可以联系客服微信或者邮件至 &#115;&#x75;&#x70;&#112;&#111;&#x72;&#x74;&#64;&#121;&#x6f;&#x75;&#100;&#97;&#120;&#117;&#x65;&#x2e;&#x63;&#x6f;&#x6d;
6. 后记这个项目，是优达学城《机器学习工程师》课程提供的第一个实战项目。摘录了全文，方便查看。郝同学的项目地址：https://github.com/voidking/udaciyty-machine-learning/tree/master/projects/titanic_survival_exploration
7. 书签机器学习工程师（中/英）https://cn.udacity.com/course/machine-learning-engineer-nanodegree--nd009
数据科学入门https://cn.udacity.com/course/intro-to-data-science--ud359
如何把 Project 0 提交到 Kaggle 上http://discussions.youdaxue.com/t/project-0-kaggle/7032
Kaggle: Your Home for Data Sciencehttps://www.kaggle.com/
Titanic: Machine Learning from Disaster | Kagglehttps://www.kaggle.com/c/titanic
A Visual Introduction to Machine Learninghttp://www.r2d3.us/visual-intro-to-machine-learning-part-1/
numpy 1.12.0 : Python Package Indexhttps://pypi.python.org/pypi/numpy
pandas 0.19.2 : Python Package Indexhttps://pypi.python.org/pypi/pandas
window 下python2.7与python3.5两版本共存设置http://blog.csdn.net/u010004460/article/details/53410091
win7下python2.7安装 pip，setuptools的正确方法http://www.jincon.com/archives/213/
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>机器学习入门</title>
    <url>/dev-machine-learning-start/</url>
    <content><![CDATA[1. 前言机器学习标志着计算机科学、数据分析、软件工程和人工智能领域内的重大技术突破。AlphaGo 战胜人类围棋冠军、人脸识别、语音识别、图片识别、大数据挖掘、自动驾驶等等，都和机器学习密切相关。
机器学习其实很简单，而且很有趣。
机器学习是什么？我们生活在一个有人类和计算机的世界，人类和计算机的一大不同点是，人类能从过去的经验中学习，而计算机只能执行指令，它们需要被编程。现在的问题是，我们能让计算机从过去的经验中学习吗？答案是我们能这么做。这就是机器学习的目的，教会计算机利用过往的经验完成指定任务。当然，对计算机来说，过去的经验就是被记录的数据。


2. 先修条件和要求1、掌握中级编程知识，可以通过“编程入门”纳米学位、其他编程入门课程项目或其他软件开发实战经验获得，相关知识包括：

字符串、数值和变量
语句、操作符和表达式
列表、元组和字典
条件、循环
过程、对象、模块和库
故障诊断和调试
调研和文档
解决问题
算法和数据结构

2、掌握中级统计学知识，可以通过优达学城的统计学入门课程获得，相关知识包括：

总体，样本
均数、中间值、众数
标准误差
方差，标准差
正态分布
精度和准确度

3、掌握中级微积分和线性代数知识，这些知识可以通过线性代数复习课获得，相关知识包括:

导数
积分
级数展开
通过特征向量和特征值进行矩阵运算

我们为你准备了一些学习扩展资料，点击这里查看。
3. 决策树假设我们是苹果或谷歌应用商店，我们的目标是为用户推荐应用。对每个用户，我们向其推荐最有可能下载的应用。我们有一张用来制定推荐规则的数据表。我们有6个用户信息，包括性别、年龄和下载过的应用。从表中可以看出，年龄比性别更适合用来预测用户会下载什么应用。经过分析，我们可以得到一个决策树。现在我们一旦我们有了新用户，就可以将决策树应用于他们的数据，并向他们推荐决策树得出的应用。
显然，我们并不总能得到与数据良好匹配的决策树，我们将学习一个算法，用来帮助我们找到和数据最匹配的决策树。
4. 朴素贝叶斯我们再来看下一个例子，我们将搭建一个垃圾邮件检测分类器。我们有100封邮件，其中已经手工标记出了25封垃圾邮件，剩下的75封被称为非垃圾邮件。现在我们来思考一封垃圾有家可能会显示出什么特征，并对这些特征进行分析。例如，可能有一个特征包含单词“cheap”，认为一封包含了单词cheap的邮件可能是垃圾邮件，是合理的。我们来分析这一判定，我们发现25封垃圾邮件中有20封包含单词“cheap”，其余75封正常邮件的5封包含单词“cheap”。我们暂时不理会剩下的不含“cheap”的邮件，只关注那些包含cheap的邮件。请问，如果有一封邮件包含单词“cheap”，那么它是垃圾邮件的概率是多少？答：80%。即，如果一封邮件包含单词“cheap”，那么它的垃圾邮件的概率为80%，于是我们把这个特征的关联概率设为80%，并用它来标记新邮件是否为垃圾邮件。我们也可以检查其他特征，尝试找出它们的关联概率。比如我们查看邮件是否包含拼写错误，发现包含拼写错误的邮件为垃圾邮件的概率为70%。接着查看缺少标题的邮件，发现它们是垃圾邮件的概率为90%。当我们收到新邮件，我们可以结合这些特征来猜测新邮件是否是垃圾邮件，这个算法叫做朴素贝叶斯算法。
5. 梯度下降假如我们现在正身处山顶（珠穆朗玛峰），我们的目标是抵达山底，应该采取什么策略？我们可以一步一步来。我们查看四周可以走的许多选项，在其中找到下降距离最大的方向，下降一步。然后重复这个步骤，查看四周，找到下降距离最大的方向，下降一步。重复此步骤，直到完成下山。这个算法，叫做梯度下降。你可以把这座山想象成要解决的问题，把山底当做对问题的解答。我们解决这个问题的过程，就是总是顺着指向答案的方向小步前进。
6. 线性回归假设我们正在研究房地产市场，我们的任务是预测给定面积的房屋的价格。这里有一栋较小的房子售价为$70000，一栋较大的房子售价为$160000，我们希望估计一栋中等面积的房子的价格，应该如何来做？图中的蓝色数据点，是我们收集的过去的房屋数据。我们可以看到这些数据点似乎排成一条直线，我们可以画出一条对拟合数据的直线。根据这条直线，我们可以预测中等房子的价格为$120000。这个方法叫做线性回归。
你可以把线性回归想象成一个画家，他检查数据并画出穿过数据区域的最佳拟合直线。你或许会问，我们如何找到这条线？我们来看一个例子，我们尝试找出这三个点的最佳拟合直线。我们假设自己是计算机，因此不能直接用视觉来判断。我们随机地画一条直线，检查这条直线的效果如何，为此我们需要计算误差。我们将检查这三个点与拟合直线的距离，我们把这三个距离之和，称为误差。现在四处移动这条线，看是否能减小误差。移动后，再次计算误差。如果误差变大，则说明方向错误。如果误差变小，则说明方向正确。我们发现误差变小了，因此决定采用这次移动。如果重复几次这个步骤，我们总能继续减小误差，最终得到一条拟合曲线作为很好的解决方案。这种最小化误差的通用方法叫做梯度下降（gradient descent）。
在实际使用中，我们并不希望使用数值为负的距离，我们会使用数据点到拟合直线的距离的平方，而不是距离本身。这种方法叫做最小二乘法（least squares）。
我们再次回到珠穆朗玛峰顶，尝试找出到达山底的下降路径。在这个背景里，我们所处的位置越高，代表当前的误差越大，下降高度就意味着减小误差。我们该怎么做？我们查看四周，寻找能下降更多高度的路径，这等价于四处移动拟合直线来最大程度减小误差。举例来说，这个方向似乎就是高度下降最大的方向，我们朝着这个方向前进一步，这等价于沿着能够最大程度减小误差的方向移动拟合曲线，即移动直线让它更加接近这三个点。现在我们减小了误差，也就是更加靠近了山底。然后我们重复以上的过程，查看四周并判断能够最大程度下降的方向。或者等价地，能够使拟合直线更加靠近数据点的方向。然后我们不断地重复此过程，寻找高度下降最大的方向，或者使拟合直线更加靠近数据点的方向，不断减小误差直到其达到最小值，也就得到了最佳拟合直线。
7. 逻辑回归假设我们是一所大学的招生办公室，正在决定录取哪些学生。根据如下两条信息，我们将录取或拒绝他们。录取考试成绩，和他们的课程成绩。例如，学生1考试成绩为9，课程成绩为8，这名学生最终被录取了。还有一名学生2，考试成绩为3，课程成绩为4，最终没有被录取。现在有一位新提交申请的学生3，考试成绩为7，课程成绩为6，我们是否应该录取他？为此，我们首先把他们的数据画在坐标系中，x轴代表考试成绩，y轴代表课程成绩。为决定是否录取学生3，我们尝试在录取数据中寻找规律。为此我们检查曾经申请过的学生，被录取或拒绝的数据。录取的点代表过去被录取的学生，红色的点代表曾经被拒绝的学生。我们来仔细查看数据，红色的点和绿色的点似乎可以被一条线很好地分开。在这条线上方的大部分点是绿色的，而下方的大部分点是红色的，但有些数据点例外。我们以这条线为模型，每当接到新的学生申请，我们把他们的成绩画在坐标图上。如果数据点是在这条线的上方，那么预测他们会被录取；如果数据点在下方，则预测他们会被拒绝。学生3的数据坐标为(7,6)，位于直线上方，因此我们判断这个学生会被录取。这种方法叫做逻辑回归（Logistic Regression）。
现在的问题是，我们如何找到这条能最好地分割数据点的线？来看一个简单的例子，如何画出一条线以最好地区分绿色数据点和红色数据点？计算机无法依靠视觉来画出这条线，所以我们从画一条像这样随机的线开始。有了这条线，我们再随机地规定，位于线上方的点为绿色，下方的点为红色。然后就像线性那样，我们先计算这条线的效果。一个简单的测量误差的标准是出错的数目，即被错误归类的数据点的数量。这条线错判了两个点，一个红色的点和一个绿色的点。仍然和线性回归类似，我们移动这条线，通过梯度下降算法，最小化错误数量。实际使用中，为了正确地使用梯度下降算法，我们需要最小化的并不是错误数目。取而代之的是，能代表错误数目的对数损失函数（log loss function）。
最初6个数据，有4个被正确分类，它们是两个红色和两个绿色；2个被错误分类，它们是一个红色和一个绿色。误差函数会对这两个被错误归类的数据点施加很大的惩罚，而对四个被正确分类的数据点施加很小的惩罚。现在四处移动这条线以将误差降到最小。移动直线后，可以看到有些误差减小了，有些增加了，但总体上，误差之和变小了，因为我们正确归类了之前被错判的两个点。这个过程的意图是，找到能最小化误差函数的最佳拟合曲线。我们如何最小化误差函数？依旧是使用梯度下降算法。我们再次回到珠穆朗玛峰，我们所在的位置很高，因此此时有很大的误差，我们探索四周寻找下降最大的方向。或者等价的，寻找能通过移动直线最大程度减小误差的方向。我们决定沿着这个方向前进一步，减小了函数误差。重复这个步骤，沿着最大程度减小误差的方向前进一步。到达了山底，则说明我们已经将误差减少到了最小值。
8. 支持向量机SVM我们更进一步学习把数据一分为二的艺术。我们看这6个点，似乎有很多条线可以分开它们。例如，这条蓝色的和黄色的。请问，那条线可以把数据更好地分开？看这条蓝色的线，似乎它是失败的，它太靠近这两个点了。如果我们想要最小化误差，它会误分到一个或多个点。这条黄线似乎是一个更好的划分，考虑到它能够来自于所有的点进行划分。所以，似乎最好的线是这条黄色的。现在的问题是，我们如何能够找到这条黄色的线？在找这条黄色的线之前，让我们事实上来找一个度量方式，它可以告诉我们黄色的线更好。那么，第一件事情就是观察这些远离边界的点，它们对于我们的决定并不重要，所以让我们忘掉这些点，只考虑那些离边界近的点。事实上，我们真正要关注的是点到两条直线的距离。什么让我们知道了黄的线更好呢？这条黄色的线更好，是因为它距离这些点更远。那么，我们观察这四个点的距离，最小的距离就是我们度量点离直线到底有多近。这意味着我们必须要考虑的，就是这四个距离的最小值，那是我们度量线离点到底有多近的方式。我们称这个度量为距离。
所以我们得出结论，黄色的线更好。因为在黄线中，这些距离中最小的距离，比蓝线中最小的距离大。这就是我们需要处理的函数，这四个距离中的最小值。我们的目标是使这个最小值尽可能的大，换句话说，我们需要最大化这个距离。我们能够怎么最大化这个距离呢？没错，梯度下降。这里也有其他方法能够使用，之后的课上会学到。这个方法叫做支持向量机，支持，是因为靠近边界的点被称为支撑。
9. 神经网络我们更加仔细地观察这个模型，这个模型接受或者拒绝学生。比如有一个学生4，他在这次考试成绩中得到了9分，但他的课程成绩是1。根据我们的模型，这个学生应该被接受。因为他在这条线的上面，但是这似乎不对，因为这个学生有一个非常低的课程成绩，不应该被接受。所以想要用一条直线来区分这里的数据，已经不合适了。也许真实的数据，应该像那些低的考试成绩或者低的课程成绩的都不应该被接受，所以这条线已经不能分离这些点了。那么，我们该怎么做呢？也许是一个圆，也许是两条线。
让我们试一下两条线，我们能够怎么找到这两条线呢？我们可以通过梯度下降来最小化一个和之前相似的对数损失函数。这被称为神经网络（neural network）。
现在问题是，为什么这被称为神经网络？比如我们在一台非常慢的电脑上工作，一个时间只能处理一件事，请问这个区域的点被两条线区分开了吗？我们必须把它分成两个单独的问题。第一个问题是，这个点在蓝线之上吗？第二个问题是，这个点在黄线之上吗？第三个问题是，第一个问题和第二个问题的答案都是“yes”吗？假设我们把(1,8)作为输入，可以得到如下结果：我们把这三个问题合成到一起，就得到了一个神经网络。把“and”节点看成和前两个节点相同，可以得到最终神经网络的样子。左边，我们叫做输入层；中间，我们叫做隐藏层，会帮助我们计算；右边，我们叫做输出层。
这是一个简单的神经网络，但是能够看出如何在中间增加更多的节点或增加更多的层，帮助我们映射更加复杂的问题，甚至在三维空间或者更高维的空间。神经网络是非常强的机器学习算法，其被使用在大多数人工智能项目中，比如面部识别、语音识别、下棋和驾驶。我们称它为神经网络是因为这些节点像大脑里的神经元，神经元会把输入和其他神经元的输出以一种神经脉冲的形式，决定是否激活这个神经脉冲。在我们的情况中，神经元把输入和其他神经元的输出以数字的形式，决定返回一个0或1。
10. 核函数现在我们学习另外一种非常强大的方法，来分开平面上的点。假设我们有4个点像这样排列，我们想要分开它们，似乎一条线不能完成这个任务，因为它们已经在一条线上面。我们要跳出思维定式，一个方式是用一个曲线来分开它们，另外的方式事实上是在平面外考虑。想象一下这些点正在一个三维的空间中，有4个点在平面上，对于第三个维度，我们增加一个额外的z轴。如果我们能够找到一个方式来提升这两个绿色的点，那么我们就能够用平面分开它们。那么哪一个是更好的解决办法？在一个平面里用一条曲线分开这些点，还是在空间里用平面分开它们？实际上，这两个其实是相同的办法。这种方法被称为核方法（kernel method），在支持向量机中它被很好的使用。
我们接下来学习曲线办法的更多细节。首先我们把坐标放在点上，我们需要的是一种办法，能够从红色的点中区分出绿色的点。如果点的坐标是(x,y)，也许我们需要一个方程，变量是x和y。对于绿色的点，给我们一个大的值；对于红色的点，给我们一个小的值。或者是相反的情况。那么下面哪个方程能够解决这个问题呢？这不是一个简单的问题，我们先来做一个表格。这些等式中，哪一个能够分离绿色的点和红色的点？x+y对于每个点都是结果3，无法分离；x的平方，对于红色点的结果有大有小，绿色点的结果也有大有小，无法分离；xy对于红色点结果为0，绿色的点结果为2，可以分离。因为这是一个函数，红色的点给我们一个值，绿色的点又给我们另外一个值，这个函数能够区分开红色和绿色。这里我们有4个点，坐标是(x,y)，还有一个坐标的乘积xy。对于红色的点，我们有乘积xy=0；对于绿色的点，我们有乘积xy=2。什么能够分开0和2呢？那就是1，所以我们用式子xy=1就能分开它们。那么，xy=1是什么？这个和y=1/x是一样的。y=1/x的图，就是上面的双曲线。
接下来我们看看在三维空间中发生了什么。我们有4个点，有一个额外的z轴（用于表示高度）。我们把xy加入我们的核心需求中，换句话说，我们考虑一个二维到三维的映射，把平面上坐标是(x,y)的点映射到了空间中，坐标为(x,y,xy)。所以，以一个不同的方式，它仍然能够分离这些点，因为它把绿色的点升高到了2，把红色的点留在下面。现在什么可以区分这些点呢？一个平面。这就是在三维空间中可视化核方法的一种方式。
11. K均值聚类假设我们有一些披萨店，我们想把他们中的三个放到这个城市里。研究发现吃披萨的人大都住在这些地方，我们需要知道哪里是放置他们最优的地方。这些房子能够很好地分成三组，红色、蓝色和黄色。在这三个组里每个放一个披萨店似乎行得通，但是电脑并不知道怎么做这个，所以我们需要一个算法来做这件事。我们随机选择三个地方作为披萨店，它们在星标的地方。每一个房子都会去离他们最近的披萨店，也就是说，黄色房子会去黄色的披萨店，蓝色房子会去蓝色的披萨店，红色房子会去红色的披萨店。但是现在，黄色房子离黄色披萨店很远。如果移动黄色披萨店到它们的顾客中心，是有意义的，对于蓝色和红色披萨店也一样。那么，让我们移动每一个披萨店到它们的顾客中心。每一次移动披萨店后，离房子最近的披萨店随之发生变化，房子的颜色变成最近的披萨店的颜色。现在，我们有了一个优化的方案。迭代这个过程，我们从随机的解，逼近了理想的解，也就是把披萨店放在顾客住的房子的中心。这个算法叫做K均值聚类（K-Means Clustering）。当我们知道聚类数量的时候，K均值聚类非常实用。现在留下一个问题，如果我们不知道聚类数量，该怎么办？
12. 层次聚类这里有一个方式，让我们给房子分组，不需要提前知道聚类的详细数量。比如房子被排列成这样：如果两个房子很近，那么它们应该由共同的披萨店服务。按照这个理念，我们来分组这些房子。把最近两个房子分到一组，重复这个步骤，直到最近的两个房子的距离超过某个界限，这个界限将会控制我们想要这些聚类分开多远。这个算法叫做分层聚类。上面的例子可以看出，当我们不知道数目，但是知道想要这些聚类离得多远的时候，分层聚类很实用。
13. 后记我们学习了很多被用在机器学习中的主要算法，使用线性回归来预测房屋价格，使用朴素贝叶斯来检测垃圾邮件，使用决策树来推荐应用。我们学会了使用逻辑回归和支持向量机来创建一个模型，使用神经网络和核方法来改善模型。我们学会了使用聚类算法，在一个城市中如何定位一个披萨店。还有更多的算法，等待我们以后去学习。如果给你一个数据集，你怎么知道该挑选哪种算法呢？我们需要学习比较它们，基于运行时间、准确率等等。我们还能够把算法组合在一起使用。
14. 书签优达学城-机器学习工程师（中/英）https://cn.udacity.com/course/machine-learning-engineer-nanodegree--nd009
Udacity GitHub 机器学习项目https://github.com/udacity/machine-learning/
优达学城论坛https://discussions.youdaxue.com/c/nd009-machine-learning-engineer-cafe
Latest MLND: Café topics - Udacity Discussion Forumhttps://discussions.udacity.com/c/nd009-cafe
Coursera-机器学习https://www.coursera.org/learn/machine-learning/home/welcome
Coursera-机器学习基础：案例研究https://www.coursera.org/learn/ml-foundations/home/welcome
Machine Learning Examples - MATLAB &amp; Simulinkhttps://cn.mathworks.com/solutions/machine-learning/examples.html?s_eid=PSM_da
MATLAB数据分析与挖掘实战——图书配套资料下载http://www.tipdm.org/ts/578.jhtml
BdRace数睿思_数据挖掘竞赛平台http://www.tipdm.org/datarace/index.html
机器学习公开课汇总
微软“机器学习”系列文章（需翻墙）http://www.msra.cn/zh-cn/research/machine-learning-group/default.aspx
机器学习(Machine Learning)&amp;深度学习(Deep Learning)资料(Chapter 1)https://github.com/ty4z2008/Qix/blob/master/dl.md
从0到1：我是如何在一年内无师自通机器学习的？http://www.leiphone.com/news/201609/SJGulTsdGcisR8Wz.html
Deep Learning Tutorialshttp://deeplearning.net/tutorial/
]]></content>
      <categories>
        <category>engineering</category>
        <category>machinelearning</category>
      </categories>
      <tags>
        <tag>机器学习</tag>
      </tags>
  </entry>
  <entry>
    <title>matlab入门</title>
    <url>/dev-matlab-start/</url>
    <content><![CDATA[1. 前言
MATLAB是美国MathWorks公司出品的商业数学软件，用于算法开发、数据可视化、数据分析以及数值计算的高级技术计算语言和交互式环境，主要包括MATLAB和Simulink两大部分。

读研以来，耳边不时听到“matlab”，被老师和同学普遍推崇。今天，郝同学就来学习一下matlab的基础操作。


2. 基础设置显示类型计算圆面积时，面积的结果显示，默认是short类型（保留小数点后4位），我们可以通过设置为long类型，来显示更精确的结果。1）通过命令设置format long，这种设置方式临时性的。
2）通过界面设置File，Preferences，Command Window，Numeric format选择long，之后Apply即可。
清屏清屏命令：clc
清数据清数据命令：clear
修改变量在workspace中，双击变量，可以在图形化界面修改变量值。
绘图在workspace中，单击变量，然后点击plot。
查看变量信息who，whos，whos a
重新执行命令在command history中，找到一条命令，右键选择evaluate selection。
保存变量save a，保存变量到当前工作路径，文件名为a.mat。
加载数据文件load a或者load a.mat。
编辑器窗口edit，打开编辑窗口，新建matlab程序。
图像窗口figure
GUI窗口guide
添加搜索路径File，Set Path，Add Folder。
设置初始路径右键matlab快捷方式，在快捷方式选项卡中，修改起始位置。
查找函数路径which sin
3. 设置默认编码中文时，Matlab默认编码格式为GB2312。使用sublime打开文件，显示乱码，郝同学想把Mablab默认编码修改为UTF-8。
1、在Matlab安装目录下的bin目录下（例如D:\Program Files\MATLAB\R2010b\bin），找到lcdata.xml文件。
2、在matlab中，输入命令feature(&#39;locale&#39;)，查看当前使用的编码。
3、编辑lcdata.xml，找到如下一段：
&lt;locale name=&quot;zh_CN&quot; encoding=&quot;GB2312&quot; xpg_name=&quot;zh_CN.GB2312&quot;&gt;    &lt;alias name=&quot;zh-Hans&quot;/&gt;&lt;/locale&gt;

修改为：
&lt;locale name=&quot;zh_CN&quot; encoding=&quot;UTF-8&quot; xpg_name=&quot;zh_CN.UTF-8&quot;&gt;    &lt;alias name=&quot;zh-Hans&quot;/&gt;&lt;/locale&gt;

然而，修改后重启，matlab依然使用GBK编码。而且，把.m文件转换成UTF-8格式后，Matlab中会出现乱码。
无奈，从另一个方面入手，让sublime支持GBK编码文件，安装插件ConvertToUTF8即可。
4. Matlab语言基础4.1. 变量和常量matlab遵循弱类型语言的变量初始化和赋值语法，和python基本相同。
输入数据x = input(&#39;请输入数据&#39;)，输入数据后，数据存入x。
默认赋值如果输入数值，没有赋值给变量，那么默认赋值给内置的ans变量。
4.2. 基本数据结构输入行矩阵a = [1 2 3]或a = [1,2,3]
输入列矩阵b = [1 2 3]&#39;或b = [1,2,3]&#39;或b = [1;2;3]
输入2*2矩阵c = [1 2; 3 4]
输入特定值矩阵d(2,3) = 8，生成2*3的矩阵，第二行第三列的元素为8，其他元素为0。
内置函数生成矩阵ones(4)，生成4*4的矩阵，所有元素为1。
ones(4,3)，生成4*3的矩阵，所有元素为1。
zeros(4)，生成4*4的矩阵，所有元素为0。
zeros(4,3)，生成4*3的矩阵，所有元素为0。
eye(4)，生成4*4的单位矩阵。
eye(4,3)，生成4*3的矩阵，前三行前三列组成单位矩阵，第四行为0。
magic(4)，生成4*4的魔方数组。
冒号表达式生成矩阵3:9，生成3到9的行向量，增量为1。
3:2:9，生成3到9的行向量，增量为2。
(3:9)&#39;，生成3到9的列向量，增量为1。
读取矩阵数据a = [1 2 3]，a(2)，读取第二个数据。
c = [1 2; 3 4]，c(1,2)，读取第一行第二列的数据。
c(:,2)，读取二列的数据。
c(1,:)，读取第一行的数据。
k = [1 2 3;4 5 6;7 8 9;10 11 12]，k(2:4,2)，读取第二列中第二行到第四行的数据。
拼接矩阵m = [c,c]，横向拼接两个c矩阵。
n = [c:c]，纵向拼接两个c矩阵。
矩阵函数size(n)，返回行列数。
length(n)，返回行数和列数中的较大者。
4.3. 空数组和子数组空数组nullmatrix = []，创建空数组。
子数组magicmatrix = magic(4)，child = magicmatrix(3,[2,4])
child = magicmatrix(3,2:end)
magicmatrix(3,2) = 3，单个元素赋值。
等差数列linspace(1,99,50)，生成1到99的50个数，等差为2。
等比数列logspace(1,3,3)，生成10 100 1000三个数。
数组变形1:1:9，reshape(ans,3,3)，按列排列变形成为方数组。
4.4. 时间变量data，clock，year(now)，month(now)，day(now)，day(today)
4.5. 算术运算符标量运算正常加减乘除，和python基本相同。需要注意的是5/6代表5除以6，5\6代表6除以5。
5^2和power(5,2)都是代表5的平方。
矩阵运算A=[1 2 3; 4 5 6; 7 8 9]，B=magic(3)
A+B，A-B，A*B，A+3，A*3
inv(B)，求B矩阵的逆矩阵。
A/B和A*inv(B)相同。
A.*B，对应位置相乘。
4.6. 运算函数matlab的运算函数包括幂次方、指数与对数函数、三角与反三角函数等等。
幂次方幂次方的符号就是常用的^记号。指数部分可以是任意数。2^2，2^(-1)，2^(1/2)，2^(1.25) 
指数与对数科学与工程领域惯用「标准指数函数」，也就是以e为底的指数函数。其中，e是一个无理数，大约等于2.71828。Matlab并不提供e这个常数，而是以函数exp()来计算以e为底的指数函数。exp(1)
Matlab 分别提供三个函数 log()、log10()和log2()，分别表示以e为底的对数（自然对数），以10为底的对数（常用对数），以2为底的对数。log(exp(2))，log10(100)，log2(4)，答案都是2。
三角与反三角函数六个三角函数在Matlab 中对应的函数分别为：正弦：sin()余弦：cos()正切：tan()余切：cot()正割：sec()余割：csc()
六个反三角函数在Matlab 中对应的函数分别为：反正弦：asin()反余弦：acos()反正切：atan()反余切：acot()反正割：asec()反余割：acsc()
需要注意的就是使用三角函数时，角度的单位是“弧度”。
复数Matlab 的所有运算符号、所有函数，都懂得如何做复数计算。sqrt(-1)
abs(3+4i)
4.7. 微积分极限求极限是微积分的基础，求极限的函数limit。limit(f,x,a)，x趋近于a时，f 的极限。limit(f,x,a,&#39;left&#39;)，x左趋近于a时，f 的极限。limit(f,x,a,&#39;right&#39;)，x右趋近于a时，f的极限。
微分diff(f,t,n)，求f 对独立变量t的n次微分值。
积分int(f,&#39;t&#39;,a,b)，求f 对独立变量t 在积分区间[a，b]的积分值。
级数自变量v在[a，b]之间取值时，对通项s求和，用函数symsum(s,v,a,b)。
4.8. 方程求解代数方程solve(f)
solve(f,a)
常微分方程dsolve(&#39;常微分方程式&#39;,&#39;初始条件&#39;,&#39;自变量&#39;)
5. 可视化5.1. 二维平面图形5.1.1. 折线图plot(x,y)函数，x，y是维度相同的序列或向量。
x=[0 1 2];y=[0 1 0];plot(x,y);

用300段折线画出sin(x)在[-pi,pi]区间内的折线图。
x = linspace(-pi, pi, 301);plot(x, sin(x));

如果要画多条曲线，也可以用plot函数。
x=0:pi/10:2*pi;y1=sin(x);y2=cos(x);plot(x,y1,x,y2);plot(x,y1,&#x27;r + -&#x27;,x,y2,&#x27;k * :&#x27;);
图形是以公共的x元素为横坐标值，y1、y2为纵坐标值绘制曲线图的。如果想要图形更加完美，我们可以用一些特殊的图形函数对它进行修饰。
xlabel(&#x27;独立变量X&#x27;);ylabel(&#x27;变量Y&#x27;);title(&#x27;正弦和余弦曲线&#x27;);text(1.5,0.3,&#x27;cos(x)&#x27;);text(0,0,&#x27;sin(x)&#x27;);%axis([0 2*pi -0.9 0.9]);

如果只给plot()一个参数，例如plot(y)，而y是一个n维向量或列，则它的效果就相当于plot((1:n), y)。
y = [ 1 4 0 2 3 5];plot(y);

5.1.2. 多重折线图Matlab在一张图片上可以重复制图。基本上，画一张图的指令，将会自动清除前一张图。但是，如果下了指令hold on，将不会清除前一张图，而是重复画上去。下了hold on指令的所有图将会重迭在一张图片里，直到你下了hold off为止。
我们以 300 个折线段，在一张图片中，画出以下三个函数在[-pi, pi]区间内的曲线图：sin(x)，cos(x)，x。
x = linspace(0, 2*pi, 301);y = sin(x);plot(x, y, &#x27;r&#x27;);axis([0 2*pi -1.2 1.2]);hold ony = cos(x);plot(x, y, &#x27;g&#x27;);y = x; plot(x, y, &#x27;b&#x27;);hold off;

我们还可以采用图形窗口分割的方法，在同一个视图窗口中画出多个小图形。这时要用到subplot(n,m,k)。如果写subplot(2,2,1),即就是把图形窗口分割成2行2列，在第1个位置（第1行第1列）画图。
x = linspace(0, 2*pi, 301);y = sin(x);subplot(2,2,1);plot(x, y);y = cos(x);subplot(2,2,2);plot(x, y);
Matlab对数据是按列存储和计算的。
5.2. 三维立体图形5.2.1. 三维曲线图plot3函数调用格式：plot3(x1,y1,z1,x2,y2,z2,…)。其中x1，y1，z1，x2，y2，z2…等分别为维数相同的向量，分别存储着曲线的三个坐标值。
绘制方程在t=[0,2π]的空间方程。
\begin{cases}x=t \y=sin(t) \z=cos(t)\end{cases}
t=0:pi/10:2*pi;x=t;y=sin(t);z=cos(t);plot3(x,y,z,&#x27;r:p&#x27;);grid on;xlabel(&#x27;X&#x27;);ylabel(&#x27;Y&#x27;);zlabel(&#x27;Z&#x27;);title(&#x27;sine and cosine&#x27;);

5.2.2. 三维网格图和曲面图Matlab在绘制三维网格图与曲面图时，往往先将要绘制图形的定义区域分成若干网格，然后计算这些网格节点上的二元函数值，最后才能使用mesh和surf函数绘制相应的图形。生成网格矩阵使用meshgrid函数，其调用格式为：
[U, V]=meshgrid(x,y)

函数说明：利用向量x和y生成网格矩阵U和V，以便mesh和surf等函数用来绘图。其中x、y分别是长度为n和m升序排列的行向量。
生成的方法是将x复制n次生成网格矩阵U，将y转置成列向量后复制m次生成网格矩阵V。坐标(uij,vij)表示xoy平面上网格节点的坐标，第三维坐标zij=f(uij,vij)。
例：给定向量x=[1 2 3 4]，y=[10 11 12 13 14]，试由向量x、y生成网格矩阵。
x=[1 2 3 4]; %输入向量xy=[10 11 12 13 14]; %输入向量y[U,V]=meshgrid(x,y); %生成网格矩阵Z=peaks(U,V);mesh(U,V,Z); %绘制三维网格图

Matlab提供了一个peaks函数，可产生一个凹凸有致的曲面，包含了三个局部极大点及三个局部极小点。在matlab中输入peaks()、peaks(5)就可以看到效果。
例：在 -4&lt;x&lt;4，-4&lt;y&lt;4 上绘制 $z=x^2+y^2$ 的三维网格图。
[x,y]=meshgrid(-4:4, -4:4); %定义网格数据向量x,yz=x.^2+y.^2; %计算二元函数值mesh(x,y,z); %绘制三维网格图% surf(x,y,z); %绘制三维曲面图

5.2.3. 观察点函数view(azinmuth,elevation)azinmuth：方位角。观察点与坐标原点的连线在水平面上的投影和y轴负方向的夹角。（在水平面上）elevation：仰角。观察点与坐标原点的连线和水平面的夹角。（与水平面垂直）
使用循环和观察点设定来实现动画效果。
6. Matlab程序设计6.1. 命令文件Matlab提供两种源程序文件格式：命令文件和函数文件。这两种文件的扩展名相同，均为“.m”，又称为“M文件”。命令文件的执行方式：在提示符后键入命令文件的文件名。命令文件适合于用户做需要理解得到结果的小规模运算。
6.2. 函数文件函数文件由function语句引导。其格式为：
function [返回变量列表]=函数名(输入变量列表)

1、新建一个求阶乘的函数文件myFunc.m：
function value = myFunc(n);if n&lt;=1    value = 1;else    value = myFunc(n-1)*n;end

2、重写求阶乘的函数文件myFunc2.m：
function value = myFunc2(n);value = 1;while n &gt; 1    value = value*n;    n = n - 1;end

3、重写求阶乘的函数文件myFunc3.m：
function value = myFunc3(n);value = 1;for i=1:1:n    value = value*i;end%for i=n:-1:1%    value = value*i;%end

4、新建传入数值显示结果的函数文件showNum.m：
function showNum(input_var);switch input_var    case 1        disp(&#x27;1&#x27;);    case &#123;2,3,4&#125;        disp(&#x27;2 or 3 or 4&#x27;);    case 5        disp(&#x27;5&#x27;);    otherwise        disp(&#x27;something else&#x27;);end

7. 源码分享https://github.com/voidking/matlab-start.git
8. 书签Matlab视频教程http://www.51zxw.net/list.aspx?cid=456
我的学习资料http://pan.baidu.com/s/1bp1oyXT
Mathjax与LaTex公式简介http://mlworks.cn/posts/introduction-to-mathjax-and-latex-expression/
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>matlab</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python抓取百度百科数据</title>
    <url>/dev-python-crawler-baidu-baike/</url>
    <content><![CDATA[1. 前言本文整理自慕课网《Python开发简单爬虫》，将会记录爬取百度百科“python”词条相关页面的整个过程。


2. 抓取策略确定目标：确定抓取哪个网站的哪些页面的哪部分数据。本实例抓取百度百科python词条页面以及python相关词条页面的标题和简介。分析目标：分析要抓取的url的格式，限定抓取范围。分析要抓取的数据的格式，本实例中就要分析标题和简介这两个数据所在的标签的格式。分析要抓取的页面编码的格式，在网页解析器部分，要指定网页编码，然后才能进行正确的解析。编写代码：在网页解析器部分，要使用到分析目标得到的结果。执行爬虫：进行数据抓取。
3. 分析目标1、url格式进入百度百科python词条页面，页面中相关词条的链接比较统一，大都是/view/xxx.htm。
2、数据格式标题位于类lemmaWgt-lemmaTitle-title下的h1子标签，简介位于类lemma-summary下。
3、编码格式查看页面编码格式，为utf-8。
经过以上分析，得到结果如下：
4. 代码编写4.1. 项目结构在sublime下，新建文件夹baike-spider，作为项目根目录。新建spider_main.py，作为爬虫总调度程序。新建url_manger.py，作为url管理器。新建html_downloader.py，作为html下载器。新建html_parser.py，作为html解析器。新建html_outputer.py，作为写出数据的工具。最终项目结构如下图：
4.2. spider_main.py# coding:utf-8import url_manager, html_downloader, html_parser, html_outputerclass SpiderMain(object):    def __init__(self):        self.urls = url_manager.UrlManager()        self.downloader = html_downloader.HtmlDownloader()        self.parser = html_parser.HtmlParser()        self.outputer = html_outputer.HtmlOutputer()    def craw(self, root_url):        count = 1        self.urls.add_new_url(root_url)        while self.urls.has_new_url():            try:                new_url = self.urls.get_new_url()                print(&#x27;craw %d : %s&#x27; % (count, new_url))                html_cont = self.downloader.download(new_url)                new_urls, new_data = self.parser.parse(new_url, html_cont)                self.urls.add_new_urls(new_urls)                self.outputer.collect_data(new_data)                if count == 10:                    break                count = count + 1            except:                print(&#x27;craw failed&#x27;)        self.outputer.output_html()if __name__==&#x27;__main__&#x27;:    root_url = &#x27;http://baike.baidu.com/view/21087.htm&#x27;    obj_spider = SpiderMain()    obj_spider.craw(root_url)

4.3. url_manger.py# coding:utf-8class UrlManager(object):    def __init__(self):        self.new_urls = set()        self.old_urls = set()    def add_new_url(self, url):        if url is None:            return        if url not in self.new_urls and url not in self.old_urls:            self.new_urls.add(url)    def add_new_urls(self, urls):        if urls is None or len(urls) == 0:            return        for url in urls:            self.add_new_url(url)    def has_new_url(self):        return len(self.new_urls) != 0    def get_new_url(self):        new_url = self.new_urls.pop()        self.old_urls.add(new_url)        return new_url

4.4. html_downloader.py# coding:utf-8import urllib.requestclass HtmlDownloader(object):    def download(self, url):        if url is None:            return None        response = urllib.request.urlopen(url)        if response.getcode() != 200:            return None        return response.read()

4.5. html_parser.py# coding:utf-8from bs4 import BeautifulSoupimport refrom urllib.parse import urljoinclass HtmlParser(object):    def _get_new_urls(self, page_url, soup):        new_urls = set()        # /view/123.htm        links = soup.find_all(&#x27;a&#x27;, href=re.compile(r&#x27;/view/\d+\.htm&#x27;))        for link in links:            new_url = link[&#x27;href&#x27;]            new_full_url = urljoin(page_url, new_url)            # print(new_full_url)            new_urls.add(new_full_url)        #print(new_urls)        return new_urls    def _get_new_data(self, page_url, soup):        res_data = &#123;&#125;        # url        res_data[&#x27;url&#x27;] = page_url        # &lt;dd class=&quot;lemmaWgt-lemmaTitle-title&quot;&gt; &lt;h1&gt;Python&lt;/h1&gt;        title_node = soup.find(&#x27;dd&#x27;, class_=&#x27;lemmaWgt-lemmaTitle-title&#x27;).find(&#x27;h1&#x27;)        res_data[&#x27;title&#x27;] = title_node.get_text()        # &lt;div class=&quot;lemma-summary&quot; label-module=&quot;lemmaSummary&quot;&gt;        summary_node = soup.find(&#x27;div&#x27;, class_=&#x27;lemma-summary&#x27;)        res_data[&#x27;summary&#x27;] = summary_node.get_text()        # print(res_data)        return res_data    def parse(self, page_url, html_cont):        if page_url is None or html_cont is None:            return        soup = BeautifulSoup(html_cont, &#x27;html.parser&#x27;)        # print(soup.prettify())        new_urls = self._get_new_urls(page_url, soup)        new_data = self._get_new_data(page_url, soup)        # print(&#x27;mark&#x27;)        return new_urls, new_data

4.6. html_outputer.py# coding:utf-8class HtmlOutputer(object):    def __init__(self):        self.datas = []    def collect_data(self, data):        if data is None:            return        self.datas.append(data)    def output_html(self):        fout = open(&#x27;output.html&#x27;,&#x27;w&#x27;, encoding=&#x27;utf-8&#x27;)        fout.write(&#x27;&lt;html&gt;&#x27;)        fout.write(&#x27;&lt;body&gt;&#x27;)        fout.write(&#x27;&lt;table&gt;&#x27;)        for data in self.datas:            fout.write(&#x27;&lt;tr&gt;&#x27;)            fout.write(&#x27;&lt;td&gt;%s&lt;/td&gt;&#x27; % data[&#x27;url&#x27;])            fout.write(&#x27;&lt;td&gt;%s&lt;/td&gt;&#x27; % data[&#x27;title&#x27;])            fout.write(&#x27;&lt;td&gt;%s&lt;/td&gt;&#x27; % data[&#x27;summary&#x27;])            fout.write(&#x27;&lt;/tr&gt;&#x27;)        fout.write(&#x27;&lt;/table&gt;&#x27;)        fout.write(&#x27;&lt;/body&gt;&#x27;)        fout.write(&#x27;&lt;/html&gt;&#x27;)        fout.close()

4.7. 运行在命令行下，执行python spider_main.py。
4.8. 编码问题问题描述：UnicodeEncodeError: ‘gbk’ codec can’t encode character ‘\xa0’ in position … 
使用Python写文件的时候，或者将网络数据流写入到本地文件的时候，大部分情况下会遇到这个问题。网络上有很多类似的文章讲述如何解决这个问题，但是无非就是encode，decode相关的，这是导致该问题出现的真正原因吗？不是的。很多时候，我们使用了decode和encode，试遍了各种编码，utf8，utf-8，gbk，gb2312等等，该有的编码都试遍了，可是仍然出现该错误，令人崩溃。
在windows下面编写python脚本，编码问题很严重。将网络数据流写入文件时，我们会遇到几个编码：1、#encoding=’XXX’这里(也就是python文件第一行的内容)的编码是指该python脚本文件本身的编码，无关紧要。只要XXX和文件本身的编码相同就行了。比如notepad++”格式”菜单里面里可以设置各种编码，这时需要保证该菜单里设置的编码和encoding XXX相同就行了，不同的话会报错。
2、网络数据流的编码比如获取网页，那么网络数据流的编码就是网页的编码。需要使用decode解码成unicode编码。
3、目标文件的编码将网络数据流写入到新文件，写文件代码如下：
fout = open(&#x27;output.html&#x27;,&#x27;w&#x27;)fout.write(str)
在windows下面，新文件的默认编码是gbk，python解释器会用gbk编码去解析我们的网络数据流str，然而str是decode过的unicode编码，这样的话就会导致解析不了，出现上述问题。 解决的办法是改变目标文件的编码：
fout = open(&#x27;output.html&#x27;,&#x27;w&#x27;, encoding=&#x27;utf-8&#x27;)

4.9. 运行结果
5. 源码分享https://github.com/voidking/baike-spider
6. 书签Python开发简单爬虫http://www.imooc.com/learn/563
The Python Standard Libraryhttps://docs.python.org/3/library/index.html
Beautiful Soup 4.2.0 文档https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html
Python词条http://baike.baidu.com/view/21087.htmhttp://baike.baidu.com/item/Python
Python3.x爬虫教程：爬网页、爬图片、自动登录http://www.2cto.com/kf/201507/417660.html
使用python3进行优雅的爬虫（一）爬取图片http://www.jianshu.com/p/696922f268df
Python UnicodeEncodeError: ‘gbk’ codec can’t encode character 解决方法http://www.jb51.net/article/64816.htm
Scrapy documentationhttps://doc.scrapy.org/en/latest/
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
        <tag>爬虫</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python爬虫基础</title>
    <url>/dev-python-crawler-base/</url>
    <content><![CDATA[1. 前言Python非常适合用来开发网页爬虫，理由如下：1、抓取网页本身的接口相比与其他静态编程语言，如java，c#，c++，python抓取网页文档的接口更简洁；相比其他动态脚本语言，如perl，shell，python的urllib包提供了较为完整的访问网页文档的API。（当然ruby也是很好的选择）此外，抓取网页有时候需要模拟浏览器的行为，很多网站对于生硬的爬虫抓取都是封杀的。这是我们需要模拟user agent的行为构造合适的请求，譬如模拟用户登陆、模拟session/cookie的存储和设置。在python里都有非常优秀的第三方包帮你搞定，如Requests，mechanize
2、网页抓取后的处理抓取的网页通常需要处理，比如过滤html标签，提取文本等。python的beautifulsoap提供了简洁的文档处理功能，能用极短的代码完成大部分文档的处理。其实以上功能很多语言和工具都能做，但是用python能够干得最快，最干净。

Life is short, you need python.

PS：python2.x和python3.x有很大不同，本文只讨论python3.x的爬虫实现方法。


2. 爬虫架构2.1. 架构组成URL管理器：管理待爬取的url集合和已爬取的url集合，传送待爬取的url给网页下载器。网页下载器（urllib）：爬取url对应的网页，存储成字符串，传送给网页解析器。网页解析器（BeautifulSoup）：解析出有价值的数据，存储下来，同时补充url到URL管理器。
2.2. 运行流程
3. URL管理器3.1. 基本功能
添加新的url到待爬取url集合中。
判断待添加的url是否在容器中（包括待爬取url集合和已爬取url集合）。
获取待爬取的url。
判断是否有待爬取的url。
将爬取完成的url从待爬取url集合移动到已爬取url集合。

3.2. 存储方式1、内存（python内存）待爬取url集合：set()已爬取url集合：set()
2、关系数据库（mysql）urls(url, is_crawled)
3、缓存（redis）待爬取url集合：set已爬取url集合：set
大型互联网公司，由于缓存数据库的高性能，一般把url存储在缓存数据库中。小型公司，一般把url存储在内存中，如果想要永久存储，则存储到关系数据库中。
4. 网页下载器（urllib）将url对应的网页下载到本地，存储成一个文件或字符串。
4.1. 基本方法新建baidu.py，内容如下：
import urllib.requestresponse = urllib.request.urlopen(&#x27;http://www.baidu.com&#x27;)buff = response.read()html = buff.decode(&quot;utf8&quot;)print(html)
命令行中执行python baidu.py，则可以打印出获取到的页面。
4.2. 构造Request上面的代码，可以修改为：
import urllib.requestrequest = urllib.request.Request(&#x27;http://www.baidu.com&#x27;)response = urllib.request.urlopen(request)buff = response.read()html = buff.decode(&quot;utf8&quot;)print(html)

4.3. 携带参数新建baidu2.py，内容如下：
import urllib.requestimport urllib.parseurl = &#x27;http://www.baidu.com&#x27;values = &#123;&#x27;name&#x27;: &#x27;voidking&#x27;,&#x27;language&#x27;: &#x27;Python&#x27;&#125;data = urllib.parse.urlencode(values).encode(encoding=&#x27;utf-8&#x27;,errors=&#x27;ignore&#x27;)headers = &#123; &#x27;User-Agent&#x27; : &#x27;Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0&#x27; &#125;request = urllib.request.Request(url=url, data=data,headers=headers,method=&#x27;GET&#x27;)response = urllib.request.urlopen(request)buff = response.read()html = buff.decode(&quot;utf8&quot;)print(html)

4.4. 使用Fiddler监听数据我们想要查看一下，我们的请求是否真的携带了参数，所以需要使用fiddler。打开fiddler之后，却意外发现，上面的代码会报错504，无论是baidu.py还是baidu2.py。虽然python有报错，但是在fiddler中，我们可以看到请求信息，确实携带了参数。
经过查找资料，发现python以前版本的Request都不支持代理环境下访问https。但是，最近的版本应该支持了才对。那么，最简单的办法，就是换一个使用http协议的url来爬取，比如，换成http://www.csdn.net。结果，依然报错，只不过变成了400错误。
然而，然而，然而。。。神转折出现了！！！当我把url换成http://www.csdn.net/后，请求成功！没错，就是在网址后面多加了一个斜杠/。同理，把http://www.baidu.com改成http://www.baidu.com/，请求也成功了！神奇！！！
4.5. 添加处理器
import urllib.requestimport http.cookiejar# 创建cookie容器cj = http.cookiejar.CookieJar()# 创建openeropener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))# 给urllib.request安装openerurllib.request.install_opener(opener)# 请求request = urllib.request.Request(&#x27;http://www.baidu.com/&#x27;)response = urllib.request.urlopen(request)buff = response.read()html = buff.decode(&quot;utf8&quot;)print(html)print(cj)

5. 网页解析器（BeautifulSoup）从网页中提取出有价值的数据和新的url列表。
5.1. 解析器选择为了实现解析器，可以选择使用正则表达式、html.parser、BeautifulSoup、lxml等，这里我们选择BeautifulSoup。其中，正则表达式基于模糊匹配，而另外三种则是基于DOM结构化解析。
5.2. BeautifulSoup5.2.1. 安装测试1、安装，在命令行下执行pip install beautifulsoup4。2、测试
import bs4print(bs4)

5.2.2. 使用说明
5.2.3. 基本用法1、创建BeautifulSoup对象
import bs4from bs4 import BeautifulSoup# 根据html网页字符串创建BeautifulSoup对象html_doc = &quot;&quot;&quot;&lt;html&gt;&lt;head&gt;&lt;title&gt;The Dormouse&#x27;s story&lt;/title&gt;&lt;/head&gt;&lt;body&gt;&lt;p class=&quot;title&quot;&gt;&lt;b&gt;The Dormouse&#x27;s story&lt;/b&gt;&lt;/p&gt;&lt;p class=&quot;story&quot;&gt;Once upon a time there were three little sisters; and their names were&lt;a href=&quot;http://example.com/elsie&quot; class=&quot;sister&quot; id=&quot;link1&quot;&gt;Elsie&lt;/a&gt;,&lt;a href=&quot;http://example.com/lacie&quot; class=&quot;sister&quot; id=&quot;link2&quot;&gt;Lacie&lt;/a&gt; and&lt;a href=&quot;http://example.com/tillie&quot; class=&quot;sister&quot; id=&quot;link3&quot;&gt;Tillie&lt;/a&gt;;and they lived at the bottom of a well.&lt;/p&gt;&lt;p class=&quot;story&quot;&gt;...&lt;/p&gt;&quot;&quot;&quot;soup = BeautifulSoup(html_doc)print(soup.prettify())

2、访问节点
print(soup.title)print(soup.title.name)print(soup.title.string)print(soup.title.parent.name)print(soup.p)print(soup.p[&#x27;class&#x27;])

3、指定tag、class或id
print(soup.find_all(&#x27;a&#x27;))print(soup.find(&#x27;a&#x27;))print(soup.find(class_=&#x27;title&#x27;))print(soup.find(id=&quot;link3&quot;))print(soup.find(&#x27;p&#x27;,class_=&#x27;title&#x27;))

4、从文档中找到所有&lt;a&gt;标签的链接
for link in soup.find_all(&#x27;a&#x27;):    print(link.get(&#x27;href&#x27;))
出现了警告，根据提示，我们在创建BeautifulSoup对象时，指定解析器即可。
soup = BeautifulSoup(html_doc,&#x27;html.parser&#x27;)

5、从文档中获取所有文字内容
print(soup.get_text())

6、正则匹配
link_node = soup.find(&#x27;a&#x27;,href=re.compile(r&#x27;til&#x27;))print(link_node)


6. 后记python爬虫基础知识，至此足够，接下来，在实战中学习更高级的知识。
7. 书签Python开发简单爬虫http://www.imooc.com/learn/563
The Python Standard Libraryhttps://docs.python.org/3/library/index.html
Beautiful Soup 4.2.0 文档https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html
为什么python适合写爬虫？http://www.cnblogs.com/benzone/p/5854084.html
如何学习Python爬虫[入门篇]？https://zhuanlan.zhihu.com/p/21479334?refer=passer
你需要这些：Python3.x爬虫学习资料整理https://zhuanlan.zhihu.com/p/24358829?refer=passer
如何入门 Python 爬虫？https://www.zhihu.com/question/20899988
Python3.X 抓取网络资源http://www.open-open.com/lib/view/open1396062681294.html
python网络请求和”HTTP Error 504:Fiddler - Receive Failure”http://blog.csdn.net/guoguo527/article/details/50709244
怎么使用Fiddler抓取自己写的爬虫的包？https://www.zhihu.com/question/52614615
fiddler对python脚本抓取https包时发生了错误?https://www.zhihu.com/question/42104344?sort=created
HTTPS和HTTP的区别http://blog.csdn.net/whatday/article/details/38147103
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
        <tag>爬虫</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python模块和模块路径</title>
    <url>/dev-python-module/</url>
    <content><![CDATA[1. 什么是Python模块？Python中，每一个 .py 文件都是一个模块。包含 .py 文件的目录叫做包。
Python 模块(Module)，是一个 Python 文件，以 .py 结尾，包含了 Python 对象定义和Python语句。
模块让我们能够有逻辑地组织我们的 Python 代码段。把相关的代码分配到一个模块里能让我们的代码更好用，更易懂。
模块能定义函数，类和变量，模块里也能包含可执行的代码。
参考文档：Python 模块


2. 搜索路径当导入一个模块时，Python 解析器对模块位置的搜索顺序为：
1、当前目录。2、shell 变量 PYTHONPATH 中的每个目录。3、Python 安装路径。UNIX下，默认路径一般为/usr/local/lib/python/。
模块搜索路径存储在 system 模块的 sys.path 变量中。变量里包含当前目录，PYTHONPATH和由安装过程决定的默认目录。
关于模块路径的一些变量：
python -c &quot;import sys; print(sys.executable)&quot; # 当前使用的python解释器路径python -c &quot;import sys; print(sys.path)&quot; # 当前模块的搜索路径列表python -c &quot;import sys; print(sys.prefix)&quot; # 当前使用的 $path_prefix

参考文档：我们的 Python 包都装到哪了？
3. pip安装模块到哪里？pip 对应的python解释器，写在 pip 文件的 shebang 里的。一般情况下，如果 pip 路径是 $path_prefix/bin/pip，那么 Python 路径对应的就是 $path_prefix/bin/python。
pip 安装包和模块，默认自动安装到 $path_prefix/lib/pythonX.Y/site-packages
pip 中更改安装位置的选项：

–prefix PATH，替换 $path_prefix 为指定的值
–root ROOT_PATH，在 $path_prefix 前面加上 ROOT_PATH
–target TARGET，直接指定安装位置到 TARGET

PS：Shebang（也被称为 Hashbang）是一个由井号和感叹号构成的字符序列（#!），位于脚本文件的第一行的最前面。在Unix-like操作系统中，shebang可以用来指示操作系统使用哪个解释器来执行该脚本文件。对于pip文件，它的shebang通常是指向python解释器的路径。
关于pip的详细用法，参考文档：《Python包管理工具pip》
4. 导入模块4.1. 导入模块语法import module_name_1from package_name import module_name_2

4.2. 相同父目录时导入模块1、已知 name.py 内容为：
name=&#x27;voidking&#x27;

2、相同目录下，进入python shell
python

3、导入模块
&gt;&gt;&gt; import name&gt;&gt;&gt; print(name.name)

或者（推荐）：
&gt;&gt;&gt; from . import name&gt;&gt;&gt; print(name.name)

4.3. 不同目录时导入模块模块 a 路径为：/path/to/x/a.py模块 b 路径为：/path/to/y/b.py模块a和模块b父目录不同，怎样让模块a被模块b导入？
方法一：PYTHONPATH变量中，添加模块a的父路径
export PYTHONPATH=/path/to/x:$PYTHONPATH

方法二：使用 sys.path.append 函数。
sys.path.append(&#x27;/path/to/x&#x27;)# orsys.path.insert(0, &#x27;/path/to/x&#x27;)

PS：指定不搜索某个路径
sys.path.remove(&#x27;/path/to/x&#x27;)

4.4. 全局导入模块模块 a 路径为：/path/to/x/a.py怎样让模块a可以被全局导入？
方法一：PYTHONPATH变量中，添加模块a的父路径
export PYTHONPATH=/path/to/x:$PYTHONPATH

方法二：自定义模块的路径，写入到安装路径中的.pth文件中。
具体操作方法：1、进入 xxx/python3/lib/python3.6/site-packages 目录2、新建 yyy.pth 文件，写入自定义模块的路径
/path/to/x/

详情参考python之使用.pth文件导入自定义模块
方法三：安装模块法通过 setup.py 打包，并使用 pip install 进行安装。
5. 导入包5.1. 导入包语法import package_name

5.2. 相同父目录下导入包如果我们有一个包结构如下:
my_project/│├── main.py└── my_package/    ├── __init__.py    └── module1.py

在 main.py 中我们可以这样导入 my_package ，并使用其中的模块和函数。
import my_packagemy_package.module1.my_function()

5.3. 不同父目录时导入包如果我们的包不在我们的脚本所在的目录或子目录下，我们将无法直接导入。我们必须要让 Python 知道我们的包在哪。
比如我们有如下结构：
/├── path/│   └── to/│       └── my_project/│           └── main.py├── another/    └── path/        └── my_package/            ├── __init__.py            └── module1.py

此时，我们需要把包的路径添加到 PYTHONPATH 或者 sys.path 才可以从 main.py 中导入 my_package：
export PYTHONPATH=/another/path:$PYTHONPATH

或者在 main.py 内：
import syssys.path.insert(0, &quot;/another/path&quot;)import my_package

5.4. 全局导入包全局导入包的方法和全局导入模块的方法几乎完全相同，请参阅本文【全局导入模块】一节。
]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Python：Python基础</title>
    <url>/dev-python-start/</url>
    <content><![CDATA[1. Python简介
Bruce Eckel: Life is short, you need Python.

Python，是 Bruce Eckel 在1989年为了打发无聊的圣诞节而编写的一门编程语言，特点是优雅、明确、简单，现今拥有丰富的标准库和第三方库。
Python适合开发Web网站和各种网络服务，系统工具和脚本，作为“胶水”语言把其他语言开发的模块包装起来使用，科学计算等等。
本文中，我们就来学习一下Python的基础知识。
参考文档：

Python官网
Python 3.6.10 documentation
PEP 8 – Style Guide for Python Code
Python入门
如何学习Python爬虫？
你需要这些：Python3.x爬虫学习资料整理



2. 准备工作1、在Python官网下载安装喜欢的版本，郝同学使用的，是当前最新版本3.6.0。2、打开IDLE，这是Python的集成开发环境，尽管简单，但极其有用。IDLE包括一个能够利用颜色突出显示语法的编辑器、一个调试工具、Python Shell，以及一个完整的Python3在线文档集。
3. hello world3.1. 交互模式1、打开IDLE
2、输入想要执行的语句
print(&#x27;hello world&#x27;)

PS：语句末尾加不加分号;都可以
3、执行语句回车，即可执行语句。
3.2. 脚本模式1、新建文件hello.py，内容为：
print(&#x27;hello world&#x27;)

2、执行脚本
python hello.py

3.3. 脚本模式-指定解释器1、新建文件hello.py，内容为：
#!/usr/bin/env pythonprint(&#x27;hello world&#x27;)

2、添加执行权限
chmod a+x hello.py

3、执行脚本
./hello.py

4. 基础语法常用函数（print）、数据类型、表达式、变量、条件和循环、函数。和其他语言类似，下面选择一部分展开。
5. list链表数组1、定义数组myList = [&#39;Hello&#39;, 100, True]
2、输出数组print(myList)
3、输出数组元素print(myList[0])，print(myList[-1])
4、追加元素到末尾myList.append(&#39;voidking&#39;)
5、追加元素到头部myList.insert(0,&#39;voidking&#39;)
6、删除元素myList.pop()，myList.pop(0)
7、元素赋值myList[0]=&#39;hello666&#39;
6. tuple固定数组1、定义数组myTuple = (&#39;Hello&#39;, 100, True)错误定义：myTuple1=(1)，正确定义：myTuple=(1,)
2、输出数组print(myTuple)
3、输出数组元素print(myTuple[0])
4、tuple和list结合t = (&#39;a&#39;, &#39;b&#39;, [&#39;A&#39;, &#39;B&#39;])，t[2][0]=&#39;X&#39;
7. if语句7.1. ifscore = 75if score&gt;=60:    print &#x27;passed&#x27;

7.2. if-elseif score&gt;=60:    print(&#x27;passed&#x27;)else:    print(&#x27;failed&#x27;)

7.3. if-elif-elseif score&gt;=90:    print(&#x27;excellent&#x27;)elif score&gt;=80:    print(&#x27;good&#x27;)elif score&gt;=60:    print(&#x27;passed&#x27;)else:    print(&#x27;failed&#x27;)

8. 循环8.1. for循环L = [75, 92, 59, 68]sum = 0.0for score in L:       sum += scoreprint(sum / 4)

8.2. while循环sum = 0x = 1while x&lt;100:    sum += x    x = x + 1print(sum)

8.3. breaksum = 0x = 1while True:    sum = sum + x    x = x + 1    if x &gt; 100:        breakprint(sum)

8.4. continueL = [75, 98, 59, 81, 66, 43, 69, 85]sum = 0.0n = 0for x in L:    if x &lt; 60:        continue    sum = sum + x    n = n + 1print(sum/n)

8.5. 多重循环for x in [&#x27;A&#x27;, &#x27;B&#x27;, &#x27;C&#x27;]:    for y in [&#x27;1&#x27;, &#x27;2&#x27;, &#x27;3&#x27;]:        print(x + y)

9. dictdict的作用是建立一组 key和一组value的映射关系。
d = &#123;    &#x27;Adam&#x27;: 95,    &#x27;Lisa&#x27;: 85,    &#x27;Bart&#x27;: 59,    &#x27;Paul&#x27;: 75&#125;print(d)print(d[&#x27;Adam&#x27;])print(d.get(&#x27;Lisa&#x27;))d[&#x27;voidking&#x27;]=100print(d)for key in d:    print(key+&#x27;:&#x27;,d.get(key))

10. setset持有一系列元素，这一点和list很像，但是set的元素没有重复，而且是无序的，这点和dict的key很像。
s = set([&#x27;Adam&#x27;, &#x27;Lisa&#x27;, &#x27;Bart&#x27;, &#x27;Paul&#x27;])print(s)s = set([&#x27;Adam&#x27;, &#x27;Lisa&#x27;, &#x27;Bart&#x27;, &#x27;Paul&#x27;, &#x27;Paul&#x27;])print(s)len(s)print(&#x27;Adam&#x27; in s)print(&#x27;adam&#x27; in s)for name in s:    print(name)

s = set([(&#x27;Adam&#x27;, 95), (&#x27;Lisa&#x27;, 85), (&#x27;Bart&#x27;, 59)])for x in s:    print(x[0]+&#x27;:&#x27;,x[1])

s.add(100)print(s)s.remove((&#x27;Adam&#x27;,95))print(s)

11. 函数11.1. 自带函数L = [x*x for x in range(1,101)]print(sum(L))

11.2. 自定义函数def my_abs(x):    if x &gt;= 0:        return x    else:        return -xprint(my_abs(-100))

11.3. 引入函数库import mathdef quadratic_equation(a, b, c):    x = b * b - 4 * a * c    if x &lt; 0:        return none    elif x == 0:        return -b / (2 *a)    else:        return ((math.sqrt(x) - b ) / (2 * a)) , ((-math.sqrt(x) - b ) / (2 * a))print(quadratic_equation(2, 3, 0))print(quadratic_equation(1, -6, 5))

11.4. 可变参数def average(*args):    if args:        return sum(args)*1.0/len(args)    else:        return 0.0print(average())print(average(1, 2))print(average(1, 2, 2, 3, 4))

12. 切片12.1. list切片L = [&#x27;Adam&#x27;, &#x27;Lisa&#x27;, &#x27;Bart&#x27;, &#x27;Paul&#x27;]print(L[0:3])print(L[:3])print(L[1:3])print(L[:])print(L[::2])

12.2. 倒序切片print(L[-2:])print(L[-3:-1])print(L[-4:-1:2])

L = range(1, 101)print(L[-10:])print(L[4::5][-10:])
PS：range是有序的list，默认以函数形式表示，执行range函数，即可以list形式表示。
12.3. 字符串切片def firstCharUpper(s):    return s[0:1].upper() + s[1:]print(firstCharUpper(&#x27;hello&#x27;))

13. 迭代Python的for循环不仅可以用在list或tuple上，还可以作用在其他任何可迭代对象上。迭代操作就是对于一个集合，无论该集合是有序还是无序，我们用for循环总是可以依次取出集合的每一个元素。集合是指包含一组元素的数据结构，包括：

有序集合：list，tuple，str和unicode；
无序集合：set
无序集合并且具有key-value对：dict

for i in range(1,101):    if i%7 == 0:        print(i)

13.1. 索引迭代对于有序集合，元素是有索引的，如果我们想在for循环中拿到索引，怎么办？方法是使用enumerate()函数。
L = [&#x27;Adam&#x27;, &#x27;Lisa&#x27;, &#x27;Bart&#x27;, &#x27;Paul&#x27;]for index, name in enumerate(L):    print(index+1, &#x27;-&#x27;, name)myList = zip([100,20,30,40],L);for index, name in myList:    print(index, &#x27;-&#x27;, name)

13.2. 迭代dict的valued = &#123; &#x27;Adam&#x27;: 95, &#x27;Lisa&#x27;: 85, &#x27;Bart&#x27;: 59 &#125;print(d.values())for v in d.values():    print(v)

PS：Python3.x中，dict的方法dict.keys()，dict.items()，dict.values()不会再返回列表，而是返回一个易读的“views”。这样一来，k = d.keys();k.sort()不再有用，可以使用k = sorted(d)来代替。同时，dict.iterkeys()，dict.iteritems()，dict.itervalues()方法不再支持。
13.3. 迭代dict的key和valued = &#123; &#x27;Adam&#x27;: 95, &#x27;Lisa&#x27;: 85, &#x27;Bart&#x27;: 59 &#125;for key, value in d.items():    print(key, &#x27;:&#x27;, value)

14. 列表生成14.1. 一般表达式L = [x*(x+1) for x in range(1,100)]print(L)

14.2. 复杂表达式d = &#123; &#x27;Adam&#x27;: 95, &#x27;Lisa&#x27;: 85, &#x27;Bart&#x27;: 59 &#125;def generate_tr(name, score):    if score &gt;=60:        return &#x27;&lt;tr&gt;&lt;td&gt;%s&lt;/td&gt;&lt;td&gt;%s&lt;/td&gt;&lt;/tr&gt;&#x27; % (name, score)    else:        return &#x27;&lt;tr&gt;&lt;td&gt;%s&lt;/td&gt;&lt;td style=&quot;color:red&quot;&gt;%s&lt;/td&gt;&lt;/tr&gt;&#x27; % (name, score)tds = [generate_tr(name,score) for name, score in d.items()]print(&#x27;&lt;table border=&quot;1&quot;&gt;&#x27;)print(&#x27;&lt;tr&gt;&lt;th&gt;Name&lt;/th&gt;&lt;th&gt;Score&lt;/th&gt;&lt;tr&gt;&#x27;)print(&#x27;\n&#x27;.join(tds))print(&#x27;&lt;/table&gt;&#x27;)

14.3. 条件表达式L = [x * x for x in range(1, 11) if x % 2 == 0]print(L)

def toUppers(L):    return [x.upper() for x in L if isinstance(x,str)]print(toUppers([&#x27;Hello&#x27;, &#x27;world&#x27;, 101]))

14.4. 多层表达式L = [m + n for m in &#x27;ABC&#x27; for n in &#x27;123&#x27;]print(L)

L = [a*100+b*10+c for a in range(1,10) for b in range(0,10) for c in range(1,10) if a==c]print(L)



]]></content>
      <categories>
        <category>engineering</category>
        <category>python</category>
      </categories>
      <tags>
        <tag>python</tag>
        <tag>好好学Python</tag>
      </tags>
  </entry>
  <entry>
    <title>大数据知识结构</title>
    <url>/dev-big-data-structure/</url>
    <content><![CDATA[1. 大数据脑图


2. 大数据2.1. 云计算1、云服务

SaaS
PaaS
LaaS

2、Openstack
3、Docker
2.2. 分布式计算1、hadoop

HDFS（分布式文件系统）
Mapreduce（计算框架）
yarn（资源管理平台）
pig（piglatin语句到mapreduce的映射）
hive（数据仓库，提供SQL）
mahout（机器学习算法的mapreduce实现库）

2、spark

RDO
Spark SQL
Spark Streaming（流处理）
MLlib（用于机器学习）

3、storm

Topology
和KAFKA集合

2.3. 数据分析工具1、R语言2、matlab3、SAS
2.4. 算法1、机器学习

聚类
时间序列
推荐系统
回归分析
文本挖掘
决策树
支持向量机
贝叶斯分类
神经网络

2、一致性

paxos
raft
gossip

3、数据结构

栈、队列、链表
散列表
二叉树、红黑树、B树
面

4、常用算法

排序
最大子数组
最长公共子序列
最小生成树
最短路径
矩阵的存储和运算

3. 书签大数据资源整理http://www.toutiao.com/i6361620880396648962/
]]></content>
      <categories>
        <category>engineering</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>大数据</tag>
      </tags>
  </entry>
  <entry>
    <title>公交查询系统</title>
    <url>/dev-bus-search-system/</url>
    <content><![CDATA[1. 交互流程1、用户进入首页，看到热门公交线路列表。2、输入关键词，可以搜索到相关公交线路。3、单击选择公交线路，进入线路方向选择页面。4、单击选择线路方向，进入百度地图。5、在初始化百度地图时，传入线路方向。


2. 技术架构前端技术：jquery和art-template。后端技术：servlet、freemarker、jdbc和mysql。
3. 详细设计3.1. 前端3.1.1. 响应式本系统适用于移动端，为了适应不同的屏幕，所以前端使用响应式布局。
html&#123;font-size:10px&#125;@media screen and (min-width:321px) and (max-width:375px)&#123;html&#123;font-size:11px&#125;&#125;@media screen and (min-width:376px) and (max-width:414px)&#123;html&#123;font-size:12px&#125;&#125;@media screen and (min-width:415px) and (max-width:639px)&#123;html&#123;font-size:15px&#125;&#125;@media screen and (min-width:640px) and (max-width:719px)&#123;html&#123;font-size:20px&#125;&#125;@media screen and (min-width:720px) and (max-width:749px)&#123;html&#123;font-size:22.5px&#125;&#125;@media screen and (min-width:750px) and (max-width:799px)&#123;html&#123;font-size:23.5px&#125;&#125;@media screen and (min-width:800px)&#123;html&#123;font-size:25px&#125;&#125;*&#123;    margin: 0;    padding: 0;&#125;

3.1.2. 模板引擎搜索后，搜索结果要显示在页面上。这里，我们选用art-template模板引擎，把搜索请求得到的数据渲染到页面上。
&lt;script id=&quot;line-template&quot; type=&quot;text/html&quot;&gt;&lt;ul&gt;    &#123;&#123;each lineList as line&#125;&#125;    &lt;a href=&quot;$&#123;basePath&#125;/Direction?busName=&#123;&#123;line.busName&#125;&#125;&quot;&gt;        &lt;li&gt;&#123;&#123;line.fullName&#125;&#125;(&#123;&#123;line.firstStop&#125;&#125;-&#123;&#123;line.lastStop&#125;&#125;)&lt;/li&gt;    &lt;/a&gt;    &#123;&#123;/each&#125;&#125;&lt;/ul&gt;&lt;/script&gt;

$(&#x27;#search&#x27;).click(function(event) &#123;    var basePath = $(&#x27;#basePath&#x27;).val();    var key = $(&#x27;#key&#x27;).val();    $.ajax(&#123;        url: basePath+&#x27;/Search&#x27;,        type: &#x27;POST&#x27;,        dataType: &#x27;json&#x27;,        data: &#123;key: key&#125;,        success: function(data)&#123;            console.log(data);            var html = template(&#x27;line-template&#x27;, data);            $(&#x27;.content&#x27;).html(html);        &#125;,        error: function(xhr)&#123;            console.log(xhr);        &#125;    &#125;);&#125;);

3.1.3. 百度地图最终结果，以百度地图的方式展示，需要用到百度地图api。
&lt;script type=&quot;text/javascript&quot;&gt;    function  init() &#123;        window.busName = document.getElementById(&#x27;busName&#x27;).value;        window.firstStop = document.getElementById(&#x27;firstStop&#x27;).value;        // console.log(window.busName);        // console.log(window.firstStop);    &#125;    // 百度地图API功能    var map = new BMap.Map(&quot;l-map&quot;);            // 创建Map实例    map.centerAndZoom(new BMap.Point(125.434025,43.83246), 12);    var busline = new BMap.BusLineSearch(map,&#123;        renderOptions:&#123;map:map,panel:&quot;r-result&quot;&#125;,            onGetBusListComplete: function(result)&#123;                var directionArr = result.PA;                console.log(result);                if(result) &#123;                    var line = result.getBusListItem(0);                    var backLine = result.getBusListItem(1);                    console.log(line);                    var regx = /\([\u4E00-\u9FA5\uF900-\uFA2D]*-/;                    var str = line.name;                    console.log(str);                    var rs = str.match(regx)[0];                    rs = rs.slice(1,-1);                    console.log(rs);                    if(rs == window.firstStop)&#123;                        busline.getBusLine(line);                    &#125;else&#123;                        busline.getBusLine(backLine);                    &#125;                &#125;            &#125;    &#125;);    function busSearch()&#123;        busline.getBusList(window.busName);        //console.log(busline);    &#125;    init();    setTimeout(function()&#123;        busSearch();    &#125;,1500);&lt;/script&gt;

3.2. 后端3.2.1. servlet鉴于本系统业务简单，所以使用servlet就够用了。
package com.voidking.servlet;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@WebServlet(&quot;/Home&quot;)public class Home extends HttpServlet &#123;    public Home() &#123;        super();    &#125;    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException &#123;        // TODO Auto-generated method stub        response.getWriter().append(&quot;Served at: &quot;).append(request.getContextPath());    &#125;    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException &#123;        // TODO Auto-generated method stub        doGet(request, response);    &#125;&#125;

3.2.2. freemarker进入首页时，查询出的数据需要渲染到页面上。这里，我们要选择一个后端模板引擎。不习惯使用jsp，决定使用freemarker。
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException &#123;    // TODO Auto-generated method stub    //freemarker配置      Configuration config=new Configuration();    ServletContext context = request.getServletContext();    config.setServletContextForTemplateLoading(context, &quot;template&quot;);        //加载模板文件      Template template=config.getTemplate(&quot;home.ftl&quot;);         //创建数据模型      Map&lt;String,Object&gt; map=new HashMap&lt;String,Object&gt;();      map.put(&quot;basePath&quot;, request.getContextPath());    map.put(&quot;lineList&quot;, lineList);        response.setCharacterEncoding(&quot;utf8&quot;);    PrintWriter out = response.getWriter();    try &#123;        // 输出模板到页面上        template.process(map, out);        out.flush();        out.close();    &#125; catch (TemplateException e) &#123;        e.printStackTrace();    &#125;&#125;

3.2.3. json目前，前后端传输数据最常用的格式不外乎xml和json，本系统采用json作为数据传输格式。
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException &#123;    // TODO Auto-generated method stub    response.setCharacterEncoding(&quot;utf8&quot;);    PrintWriter pw = response.getWriter();        String key = request.getParameter(&quot;key&quot;);    ArrayList&lt;Line&gt; lineList = lineService.searchLine(key);    if(lineList.size() &gt; 0)&#123;        jsonObj = new JSONObject(&quot;&#123;&#x27;code&#x27;:&#x27;0&#x27;,&#x27;ext&#x27;:&#x27;success&#x27;&#125;&quot;);        jsonObj.put(&quot;lineList&quot;, lineList);    &#125;else&#123;        jsonObj = new JSONObject(&quot;&#123;&#x27;code&#x27;:&#x27;1&#x27;,&#x27;ext&#x27;:&#x27;error&#x27;&#125;&quot;);    &#125;        pw.println(jsonObj);&#125;

3.2.4. jdbc数据库连接和查询，使用jdbc。
public ArrayList&lt;Line&gt; getLines() &#123;    // 结果集合    ArrayList&lt;Line&gt; result = new ArrayList&lt;Line&gt;();    // Step 1: Claim Connection and Statement    Connection conn = null;    Statement stmt = null;    try &#123;        // STEP 2: Register JDBC driver        Class.forName(JDBC_DRIVER);        // STEP 3: Open a connection        System.out.println(&quot;Connecting to database...&quot;);        conn = DriverManager.getConnection(DB_URL, USER, PASS);        // STEP 4: Execute a query        System.out.println(&quot;Creating statement...&quot;);        stmt = conn.createStatement();        String sql;        sql = &quot;select * from line&quot;;        ResultSet rs = stmt.executeQuery(sql);        // STEP 5: Extract data from result set        while (rs.next()) &#123;            // Retrieve by column name            int id = rs.getInt(&quot;id&quot;);            String busName = rs.getString(&quot;bus_name&quot;);            String fullName = rs.getString(&quot;full_name&quot;);            String firstStop = rs.getString(&quot;first_stop&quot;);            String lastStop = rs.getString(&quot;last_stop&quot;);            Line line = new Line(id, busName, fullName, firstStop, lastStop);            result.add(line);        &#125;        // STEP 6: Clean-up environment        rs.close();        stmt.close();        conn.close();    &#125; catch (SQLException se) &#123;        // Handle errors for JDBC        se.printStackTrace();    &#125; catch (Exception e) &#123;        // Handle errors for Class.forName        e.printStackTrace();    &#125; finally &#123;        // finally block used to close resources        try &#123;            if (stmt != null)                stmt.close();        &#125; catch (SQLException se2) &#123;        &#125; // nothing we can do        try &#123;            if (conn != null)                conn.close();        &#125; catch (SQLException se) &#123;            se.printStackTrace();        &#125; // end finally try    &#125; // end try    System.out.println(&quot;Goodbye!&quot;);    return result;&#125;


3.3. 数据库3.3.1. 数据搜集长春公交有近200条公交线路，这里，我们从8684网站上搜集了常用的11条线路用来做测试。
3.3.2. 表设计CREATE TABLE `line` (  `id` int(4) NOT NULL AUTO_INCREMENT,  `bus_name` varchar(32) NOT NULL,  `full_name` tinytext NOT NULL,  `first_stop` varchar(32) DEFAULT NULL,  `last_stop` varchar(32) DEFAULT NULL,  PRIMARY KEY (`id`)) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

4. 效果演示
5. 源码分享https://github.com/voidking/bus-search
6. 后记为什么不做实时公交查询？因为拿不到数据。百度地图没有提供实时公交API，也拿不到公交公司的数据。
7. 书签Introducing JSONhttp://www.json.org/
JSON入门之二：org.json的基本用法http://blog.csdn.net/jediael_lu/article/details/25779087
JSON解析工具-org.json使用教程http://www.open-open.com/lib/view/open1381566882614.html
Amaze UI 模板中心http://tpl.amazeui.org/
（淘宝无限适配）手机端rem布局详解http://www.cnblogs.com/well-nice/p/5509589.html
长春公交查询http://changchun.8684.cn/
百度地图JavaScript 开源库http://lbsyun.baidu.com/index.php?title=open/library
百度地图API示例http://lbsyun.baidu.com/jsdemo.htm#a1_2
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>freemarker</tag>
      </tags>
  </entry>
  <entry>
    <title>微信小程序基础</title>
    <url>/dev-weapp-base/</url>
    <content><![CDATA[1. 前言
张小龙在朋友圈里这样解释到：小程序是一种不需要下载安装即可使用的应用，它实现了应用“触手可及”的梦想，用户扫一扫或者搜一下即可打开应用，也体现了“用完即走”的理念，用户不用关心是否安装太多应用的问题。应用将无处不在，随时可用，但又无需安装卸载。



老师帮助申请了一个小程序，接下来，郝同学决定利用小程序做一个应用。有以下五个想法：1、公交查询系统2、餐厅排号系统3、餐厅点餐系统4、书籍借阅交易系统5、小范围问答系统
无论做哪个，都需要对腾讯提供的小程序API进行一个较全面的了解，本文就记录一下学习过程中的重点。
2. 项目结构使用微信开发者工具，新建小程序项目，生成一些初始文件，结构如下。



文件
必填
作用



app.js
是
小程序逻辑


app.json
是
小程序公共设置


app.wxss
否
小程序公共样式表


| 文件类型 | 必填 | 作用 || js | 是 | 页面逻辑 || wxml | 是 | 页面结构 || wxss | 否 | 页面样式表 || json | 否 | 页面配置 |
app.json文件来对微信小程序进行全局配置，决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
以下是一个包含了所有配置选项的简单配置app.json ：
&#123;  &quot;pages&quot;: [    &quot;pages/index/index&quot;,    &quot;pages/logs/index&quot;  ],  &quot;window&quot;: &#123;    &quot;navigationBarTitleText&quot;: &quot;Demo&quot;  &#125;,  &quot;tabBar&quot;: &#123;    &quot;list&quot;: [&#123;      &quot;pagePath&quot;: &quot;pages/index/index&quot;,      &quot;text&quot;: &quot;首页&quot;    &#125;, &#123;      &quot;pagePath&quot;: &quot;pages/logs/logs&quot;,      &quot;text&quot;: &quot;日志&quot;    &#125;]  &#125;,  &quot;networkTimeout&quot;: &#123;    &quot;request&quot;: 10000,    &quot;downloadFile&quot;: 10000  &#125;,  &quot;debug&quot;: true&#125;

3. 加载过程由上图可以看出，加载过程为：1、app.js中的onLaunch和onShow函数被调用。2、按照app.json中的配置，注册pages参数中的页面。3、按照app.json中的配置，跳转到pages参数中的第一个页面（index页面）。4、index.js中的onLoad和onShow函数被调用。5、使用index.js的data初始化页面。6、index.js中onReady函数被调用。
4. 逻辑层4.1. 注册程序App() 函数用来注册一个小程序。接受一个 object 参数，其指定小程序的生命周期函数等。
object参数说明：



属性
类型
描述
触发时机



onLaunch
Function
生命周期函数–监听小程序初始化
当小程序初始化完成时，会触发 onLaunch（全局只触发一次）


onShow
Function
生命周期函数–监听小程序显示
当小程序启动，或从后台进入前台显示，会触发onShow


onHide
Function
生命周期函数–监听小程序隐藏
当小程序从前台进入后台，会触发 onHide


其他
Any
开发者可以添加任意的函数或数据到 Object 参数中，用 this 可以访问



4.2. 注册页面Page() 函数用来注册一个页面。接受一个 object 参数，其指定页面的初始数据、生命周期函数、事件处理函数等。
object 参数说明：



属性
类型
描述



data
Object
页面的初始数据


onLoad
Function
生命周期函数–监听页面加载


onReady
Function
生命周期函数–监听页面初次渲染完成


onShow
Function
生命周期函数–监听页面显示


onHide
Function
生命周期函数–监听页面隐藏


onUnload
Function
生命周期函数–监听页面卸载


onPullDownRefresh
Function
页面相关事件处理函数–监听用户下拉动作


onReachBottom
Function
页面上拉触底事件的处理函数


其他
Any
开发者可以添加任意的函数或数据到 object 参数中，在页面的函数中用 this 可以访问


详见小程序文档
4.3. 模块化我们可以将一些公共的代码抽离成为一个单独的 js 文件，作为一个模块。模块只有通过 module.exports 或者 exports 才能对外暴露接口。
需要注意的是：

exports 是 module.exports 的一个引用，因此在模块里边随意更改 exports 的指向会造成未知的错误。所以我们更推荐开发者采用 module.exports 来暴露模块接口，除非你已经清晰知道这两者的关系。
小程序目前不支持直接引入 node_modules , 开发者需要使用到 node_modules 时候建议拷贝出相关的代码到小程序的目录中。

// common.jsvar common = &#123;    sayHello: function()&#123;        console.log(&#x27;hello&#x27;);    &#125;,    sayGoodbye: function()&#123;        console.log(&#x27;goodbye&#x27;);    &#125;&#125;;module.exports = common;

在需要使用这些模块的文件中，使用 require(path) 将公共代码引入。
var common = require(&#x27;../../modules/common/common.js&#x27;);Page(&#123;  say: function() &#123;    common.sayHello();    common.sayGoodbye();  &#125;&#125;)

4.4. API小程序开发框架提供丰富的微信原生 API，可以方便的调起微信提供的能力，如获取用户信息，本地存储，支付功能等。详细介绍请参考API 文档
5. 视图层5.1. WXML&lt;!-- item.wxml --&gt;&lt;template name=&quot;item&quot;&gt;  &lt;text&gt;&#123;&#123;text&#125;&#125;&lt;/text&gt;&lt;/template&gt;

&lt;!--home.wxml--&gt;&lt;view class=&quot;container&quot;&gt;  &lt;view  bindtap=&quot;bindViewTap&quot; class=&quot;userinfo&quot;&gt;    &lt;image class=&quot;userinfo-avatar&quot; src=&quot;&#123;&#123;userInfo.avatarUrl&#125;&#125;&quot; background-size=&quot;cover&quot;&gt;&lt;/image&gt;    &lt;text class=&quot;userinfo-nickname&quot;&gt;&#123;&#123;userInfo.nickName&#125;&#125;&lt;/text&gt;  &lt;/view&gt;  &lt;button bindtap=&quot;toIndex&quot;&gt;跳转index&lt;/button&gt;  &lt;view wx:if=&quot;&#123;&#123;id==1&#125;&#125;&quot;&gt;第一条&lt;/view&gt;  &lt;view wx:elif=&quot;&#123;&#123;id==2&#125;&#125;&quot;&gt;第二条&lt;/view&gt;  &lt;view wx:else&gt;其他&lt;/view&gt;  &lt;!--hidden只用于text--&gt;  &lt;div hidden=&quot;&#123;&#123;true&#125;&#125;&quot;&gt;    这是个div  &lt;/div&gt;  &lt;text hidden=&quot;&#123;&#123;true&#125;&#125;&quot;&gt;这是个text&lt;/text&gt;  &lt;view wx:for=&quot;&#123;&#123;array&#125;&#125;&quot;&gt;    &#123;&#123;index&#125;&#125;:&#123;&#123;item.message&#125;&#125;  &lt;/view&gt;  &lt;!--定义模板--&gt;  &lt;template name=&quot;myTemplate&quot;&gt;    &lt;view&gt;      &lt;text&gt; &#123;&#123;index&#125;&#125;: &#123;&#123;msg&#125;&#125; &lt;/text&gt;      &lt;text&gt; Time: &#123;&#123;time&#125;&#125; &lt;/text&gt;    &lt;/view&gt;  &lt;/template&gt;  &lt;!--使用模板--&gt;  &lt;template is=&quot;myTemplate&quot; data=&quot;&#123;&#123;...myData&#125;&#125;&quot;/&gt;  &lt;template name=&quot;odd&quot;&gt;    &lt;view&gt; odd &lt;/view&gt;  &lt;/template&gt;  &lt;template name=&quot;even&quot;&gt;    &lt;view&gt; even &lt;/view&gt;  &lt;/template&gt;  &lt;block wx:for=&quot;&#123;&#123;[1, 2, 3, 4, 5]&#125;&#125;&quot;&gt;    &lt;template is=&quot;&#123;&#123;item % 2 == 0 ? &#x27;even&#x27; : &#x27;odd&#x27;&#125;&#125;&quot;/&gt;    &lt;template is=&quot;&#123;&#123;index % 2 == 0 ? &#x27;even&#x27; : &#x27;odd&#x27;&#125;&#125;&quot;/&gt;  &lt;/block&gt;  &lt;block wx:for=&quot;&#123;&#123;[1, 2, 3, 4, 5]&#125;&#125;&quot; wx:for-index=&quot;itemIndex&quot; wx:for-item=&quot;itemData&quot;&gt;    索引&#123;&#123;itemIndex&#125;&#125;，数值&#123;&#123;itemData&#125;&#125;  &lt;/block&gt;  &lt;view id=&quot;tapTest&quot; data-hi=&quot;WeChat&quot; bindtap=&quot;tapName&quot;&gt; Click me! &lt;/view&gt;  &lt;import src=&quot;item.wxml&quot;/&gt;  &lt;template is=&quot;item&quot; data=&quot;&#123;&#123;text: &#x27;forbar&#x27;&#125;&#125;&quot;/&gt;&lt;/view&gt;

/*home.js*/// 获取应用实例var app = getApp();var common = require(&#x27;../../modules/common/common.js&#x27;);Page(&#123;  data:&#123;    id: 4,    userInfo: &#123;&#125;,    array: [&#123;      message: &#x27;foo&#x27;,    &#125;, &#123;      message: &#x27;bar&#x27;    &#125;],    myData: &#123;      index: 1000,      msg: &#x27;模板&#x27;,      time: &#x27;16:08&#x27;    &#125;  &#125;,  onLoad:function(options)&#123;    // 页面初始化 options为页面跳转所带来的参数    var that = this;    //调用应用实例的方法获取全局数据    app.getUserInfo(function(userInfo)&#123;      //更新数据      that.setData(&#123;        userInfo:userInfo      &#125;)    &#125;);    common.sayHello();  &#125;,  onReady:function()&#123;    // 页面渲染完成  &#125;,  onShow:function()&#123;    // 页面显示    console.log(&#x27;这是home页的onShow方法&#x27;);  &#125;,  onHide:function()&#123;    // 页面隐藏  &#125;,  onUnload:function()&#123;    // 页面关闭  &#125;,  toIndex: function()&#123;    wx.navigateTo(&#123;      url: &#x27;../index/index&#x27;    &#125;);  &#125;,  tapName: function(event) &#123;    console.log(event)  &#125;&#125;)


5.2. WXSS当成css使用就好。
5.3. 组件组件文档
6. 后记小程序利弊各半，是否使用还是要看需求。
利：

不用安装，即开即用，用完就走。
省流量，省安装时间。
相较于原生应用，开发成本更低。
推广更容易更简单，更省成本。

弊：

寄生于微信，网页没有可移植性。
无法取代休闲娱乐办公类原生应用。

最后，引用iH5互动大师创始人孟智平的一段话：

真正的小程序的形式只能是H5。最轻（不用下载安装，用了就走）、最灵活（开发难度小，投放周期短，调整更容易）、最通用（标准的Web形态，有浏览器就能打开）。

7. 书签简易教程-小程序https://mp.weixin.qq.com/debug/wxadoc/dev/index.html
微信小程序全方位深度解析-课程学习-百度传课http://www.chuanke.com/v4702151-193232-1107660.html
为什么我反对微信小程序？https://www.huxiu.com/article/171880/1.html?f=myzakercom
我们真的需要网页版App吗？Google PWA的困局http://www.leiphone.com/news/201606/UEiart497WUzS62u.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>微信</tag>
        <tag>小程序</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo生成sitemap</title>
    <url>/dev-hexo-sitemap/</url>
    <content><![CDATA[1. 前言很久，都不再关心百度谷歌对自己博客的收录，只是把博客当做生活的记录。今天，突然想稍微搞下SEO，然后就有了这篇博文。


2. 过程1、生成站点地图
npm install hexo-generator-sitemap --savenpm install hexo-generator-baidu-sitemap --save

提示警告，不过可以忽略。
npm WARN optional Skipping failed optional dependency /chokidar/fsevents:npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.0.15......

在博客目录的_config.yml中添加如下代码(貌似不加也可以，反正我没加)
sitemap:path: sitemap.xmlbaidusitemap:path: baidusitemap.xml

2、验证网站Google验证网站的方式是文件验证；百度验证网站的方式有三种，文件验证、html标签验证、CNAME验证。郝同学采用文件验证方式，下载验证文件，放到hexo/source目录下，hexo g，hexo d，即可发布验证文件到服务器上。
3、添加站点地图分别登录Google Search Console，和百度站长平台，添加站点地图。
3. 问题由上面两个图可以看出，sitemap.xml没有问题，但是baidusitemap.xml解析会出错。访问http://www.voidking.com/baidusitemap.xml，看到详细报错。
xml解析器在遇到字符“&amp;”时，会把“&amp;”当做一个实体引用的开始，而去寻找这个实体引用的结束符号“;”，
在xml文档中，像“&amp;”这样的预定义的实体引用一共有5个，分别是“&amp;”、“&lt;”、“&gt;”、“’”、“””。
解决方法一：把实体引用编码后使用，即
把“&amp;” 编码为 “&amp;amp;” 把“&lt;” 编码为“&amp;lt;” 把“&gt;” 编码为 “&amp;gt;” 把“&#x27;” 编码为 “&amp;apos;” 把“&quot;” 编码为 “&amp;quot;” 

&lt;loc&gt;url&amp;mb=bt&lt;/loc&gt;
替换为：
&lt;loc&gt;url&amp;amp;mb=bt&lt;/loc&gt;

解决方法二：在标记CDATA中，所有的标记、实体引用都被忽略，而被“xml解析器”一视同仁地当做原始字符数据看待，CDATA的形式如下：针对本文上面的问题，即把原来代码中的：
&lt;loc&gt;url&amp;mb=bt&lt;/loc&gt;
替换为：
&lt;loc&gt;&lt;![CDATA[url&amp;mb=bt]]&gt;&lt;/loc&gt;
使用CDATA需要注意的两点，1、由于CDATA的结束符号是“]]&gt;”，所以CDATA中不能包含“]]&gt;”。2、由于CDATA中的所有标记、实体引用都被忽略，所以CDATA不能嵌套使用。
4. 后记然而，上面的问题并没有找到解决办法，那就留个坑吧。如果看到这篇文章的同学找到解决办法，希望能给我留言，在此谢过。
5. 书签站点信息_站长工具_站点重要数据概览_百度站长平台http://zhanzhang.baidu.com/dashboard/index
Search Console - 首页https://www.google.com/webmasters/tools/home?hl=zh-CN
Hexo生成sitemap站点地图的方法http://blog.kenai.cc/article/hexo/hexo-skills/
hexo干货系列：（六）hexo提交搜索引擎（百度+谷歌）http://www.jianshu.com/p/619dab2d3c08
关于xml特殊符号报错EntityRef: expecting ‘;’http://bbs.phome.net/showthread-13-330736-0.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：thinkphp中session跨域问题</title>
    <url>/dev-thinkphp-session-cross-domain/</url>
    <content><![CDATA[1. 问题描述《thinkphp实现短信验证注册》中，郝同学不止记录了短信验证码的实现方法，同时还记录了图片验证码的实现方法。本地使用，一切正常；后端项目和前端项目都部署到服务器，一切正常；后端项目部署到服务器，并设置允许跨域访问后，本地前端项目使用服务器上后端项目接口时，问题来了：首先，使用postman测试获取图片验证码接口和验证图片验证码接口，正常。然后，在html中使用获取图片验证码接口，正常；最后，在JS中使用验证图片验证码接口，出错！！！


2. 分析通过问题描述，我们看出，问题出现在跨域上。那么，有两种可能，一种是因为跨域设置不正确；一种是因为thinkphp本身的问题。
采用另外一种跨域配置，问题依然存在。那就是thinkphp本身的问题了，经查找资料，问题定位在thinkphp的session跨域上。
3. 跨子域解决办法其实不管是ThinkPHP还是php本身，在解决session跨域问题的时候都需要设置session.cookie_domain。针对session跨域这一问题的解决方法主要有以下几种：第一种情况：如果目录下没有.htaccess这个文件，也就是没有采取url伪静态的话，那么，在conf/config.php的第一行加上：
ini_set(&#x27;session.cookie_domain&#x27;,&quot;.domain.com&quot;);//跨域访问Session
这时如果你开启了调试，那么可以用！但关闭了调试，就不管用了！
第二种情况：如果你目录下有.htaccess这个文件，那么你在根目录，index.php的第一行加入：
&lt;?php ini_set(&#x27;session.cookie_domain&#x27;,&quot;.domain.com&quot;);//跨域访问Session// 应用入口文件?&gt;
这种方法不管开不开启调试都管用！
然而，我们的问题并不是跨子域的问题，而是完全跨域，所以上述方法无效。
4. 完全跨域解决办法4.1. 获取图片验证码请求查看获取图片验证码的请求信息，Request Headers为：
Accept:image/webp,image/*,*/*;q=0.8Accept-Encoding:gzip, deflate, sdchAccept-Language:zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4Connection:keep-aliveCookie:pma_lang=zh_CN; pma_collation_connection=utf8_unicode_ci; pma_iv-1=wnpO4gv0eQRW1AMHmGr2ww%3D%3D; pmaUser-1=weZPqS0%2BW7nzFUVHRdqcfA%3D%3DHost:api.voidking.comReferer:http://localhost/ajax/ajax.htmlUser-Agent:Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36

Response Headers为：
Access-Control-Allow-Origin:*Cache-Control:post-check=0, pre-check=0Cache-Control:private, max-age=0, no-store, no-cache, must-revalidateConnection:keep-aliveContent-Type:image/pngDate:Sun, 27 Nov 2016 12:10:44 GMTExpires:Thu, 19 Nov 1981 08:52:00 GMTPragma:no-cacheServer:nginxSet-Cookie:PHPSESSID=721t4sqanvsii550m1dk8gq1o3; path=/; domain=.voidking.comTransfer-Encoding:chunked

4.2. 验证验证码请求查看验证验证码的请求信息，Request Headers为：
Accept:application/json, text/javascript, */*; q=0.01Accept-Encoding:gzip, deflateAccept-Language:zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4Connection:keep-aliveContent-Length:9Content-Type:application/x-www-form-urlencoded; charset=UTF-8Host:api.voidking.comOrigin:http://localhostReferer:http://localhost/ajax/ajax.htmlUser-Agent:Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36

Response Headers为：
Access-Control-Allow-Origin:*Cache-Control:no-store, no-cache, must-revalidate, post-check=0, pre-check=0Connection:keep-aliveContent-Encoding:gzipContent-Type:text/html; charset=UTF-8Date:Sun, 27 Nov 2016 12:13:21 GMTExpires:Thu, 19 Nov 1981 08:52:00 GMTPragma:no-cacheServer:nginxSet-Cookie:PHPSESSID=149t0hhs2icqaaemvp39onkgp4; path=/; domain=.voidking.comTransfer-Encoding:chunkedVary:Accept-Encoding

4.3. 再次获取图片验证码请求Request Headers为：
Accept:image/webp,image/*,*/*;q=0.8Accept-Encoding:gzip, deflate, sdchAccept-Language:zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4Cache-Control:max-age=0Connection:keep-aliveCookie:pma_lang=zh_CN; pma_collation_connection=utf8_unicode_ci; pma_iv-1=wnpO4gv0eQRW1AMHmGr2ww%3D%3D; pmaUser-1=weZPqS0%2BW7nzFUVHRdqcfA%3D%3D; PHPSESSID=721t4sqanvsii550m1dk8gq1o3Host:api.voidking.comReferer:http://localhost/ajax/ajax.htmlUser-Agent:Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36

Response Headers为：
Access-Control-Allow-Origin:*Cache-Control:private, max-age=0, no-store, no-cache, must-revalidateCache-Control:post-check=0, pre-check=0Connection:keep-aliveContent-Type:image/pngDate:Sun, 27 Nov 2016 13:26:21 GMTExpires:Thu, 19 Nov 1981 08:52:00 GMTPragma:no-cacheServer:nginxTransfer-Encoding:chunked

4.4. 三次请求比较
第一次获取图片验证码请求，Cookie中没有PHPSESSID，所以，返回信息中有Set-Cookie。第二次获取图片验证码请求，Cookie中含有PHPSESSID，所以，返回信息中没有了Set-Cookie。而且第一次请求返回信息Set-Cookie中的PHPSESSID，和第二次请求请求信息Cookie中的PHPSESSID是相同的。
而验证图片验证码的ajax请求，没有Cookie，自然也没有PHPSESSID，所以，返回信息中也有Set-Cookie。
可见，我们需要在前端做一些修改，使之发送请求时带着Cookie。
4.5. 前端jquery设置&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;jquery&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;p&gt;        &lt;img src=&quot;http://api.voidking.com/owner-bd/index.php/Home/CheckCode/getPicCode&quot; alt=&quot;&quot;&gt;        &lt;input type=&quot;text&quot; id=&quot;picCode&quot;&gt;        &lt;input type=&quot;button&quot; id=&quot;send&quot; value=&quot;验证&quot;&gt;    &lt;/p&gt;&lt;script src=&quot;http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js&quot;&gt;&lt;/script&gt;&lt;script&gt;    $(function()&#123;        $(&#x27;#send&#x27;).click(function()&#123;            //console.log(document.cookie);            $.ajax(&#123;                url: &#x27;http://api.voidking.com/owner-bd/index.php/Home/CheckCode/checkPicCode&#x27;,                type: &#x27;POST&#x27;,                crossDomain: true,                xhrFields: &#123;                    withCredentials: true                &#125;,                dataType: &#x27;json&#x27;,                data: &#123;code: $(&#x27;#picCode&#x27;).val()&#125;,                success: function(data)&#123;                    console.log(data);                &#125;,                error: function(xhr)&#123;                    console.log(xhr);                &#125;            &#125;);        &#125;);    &#125;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

请求时报错如下：
A wildcard &#x27;*&#x27; cannot be used in the &#x27;Access-Control-Allow-Origin&#x27; header when the credentials flag is true. Origin &#x27;http://localhost&#x27; is therefore not allowed access. The credentials mode of an XMLHttpRequest is controlled by the withCredentials attribute.

出现了跨域报错，可见后端也需要做一些修改，使之可以接收跨域Cookie。
4.6. 后端nginx设置add_header Access-Control-Allow-Origin http://localhost;add_header Access-Control-Allow-Credentials true;
注意：服务器端Access-Control-Allow-Credentials参数为true时，Access-Control-Allow-Origin参数的值不能为*。
后端nginx设置后，jquery的ajax请求正常了，可以携带Cookie，后端正常接收数据并返回数据。
由于angular的ajax请求不同于jquery，所以，我们还需要研究一下angular怎么发送携带Cookie的跨域请求。
4.7. 前端angular设置&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;    &lt;meta charset=&quot;utf-8&quot;&gt;    &lt;title&gt;angular&lt;/title&gt;    &lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt;&lt;/head&gt;&lt;body ng-app=&quot;myApp&quot; &gt;    &lt;p ng-controller=&quot;myCtrl&quot;&gt;        &lt;img src=&quot;http://api.voidking.com/owner-bd/index.php/Home/CheckCode/getPicCode&quot; alt=&quot;&quot;&gt;        &lt;input type=&quot;text&quot; id=&quot;picCode&quot; ng-model=&quot;picCode&quot;&gt;        &lt;input type=&quot;button&quot; ng-click=&quot;send()&quot;  value=&quot;验证&quot;&gt;    &lt;/p&gt;&lt;script&gt;    var app = angular.module(&#x27;myApp&#x27;, []);    app.controller(&#x27;myCtrl&#x27;, function($scope, $http, $httpParamSerializer) &#123;        $scope.send = function()&#123;            $http(&#123;                method:&#x27;POST&#x27;,                url:&#x27;http://api.voidking.com/owner-bd/index.php/Home/CheckCode/checkPicCode&#x27;,                headers:&#123;                    &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;                &#125;,                withCredentials: true,                dataType: &#x27;json&#x27;,                data: $httpParamSerializer(&#123;code: $scope.picCode&#125;)            &#125;).then(function successCallback(response) &#123;                console.log(response.data);                $scope.username = response.data.username;            &#125;, function errorCallback(response) &#123;                console.log(response.data);            &#125;);        &#125;    &#125;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

5. nginx配置文件结合《thinkphp部署到nginx服务器》中nginx的配置，最终nginx配置配置文件nginx.conf文件内容如下：
error_log  logs/error.log  error ;pid logs/nginx.pid;user  www;worker_processes  auto;worker_rlimit_nofile 51200;events &#123;    use epoll;    worker_connections  51200;&#125;http &#123;    client_body_buffer_size 32k;    client_header_buffer_size 2k;    client_max_body_size 2m;    default_type application/octet-stream;    log_not_found off;    server_tokens off;    include       mime.types;    gzip on;    gzip_min_length  1k;    gzip_buffers     4 16k;    gzip_http_version 1.0;    gzip_comp_level 2;    gzip_types       text/plain text/css text/xml text/javascript application/x-javascript application/xml application/rss+xml application/xhtml+xml application/atom_xml;    gzip_vary on;    #error_page   500 502 503 504  /50x.html;     log_format  access  &#x27;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#x27;              &#x27;$status $body_bytes_sent &quot;$http_referer&quot; &#x27;              &#x27;&quot;$http_user_agent&quot; $http_x_forwarded_for&#x27;;    server &#123;        listen 80 default_server;        server_name localhost api.voidking.com;        root /home/wwwroot/;        index index.php index.html index.htm;            add_header Access-Control-Allow-Origin http://localhost;        add_header Access-Control-Allow-Credentials true;        location ~ \.php &#123;        root /home/wwwroot/;            fastcgi_pass   127.0.0.1:9000;            fastcgi_index  index.php;        fastcgi_split_path_info ^(.+\.php)(.*)$;            fastcgi_param PATH_INFO $fastcgi_path_info;            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;            fastcgi_param  PHP_VALUE        open_basedir=$document_root:/tmp/:/proc/;            include        fastcgi_params;        &#125;    &#125;    include vhost/*.conf;    &#125;

6. 后记至此，大功告成，session跨域问题完美解决。
7. 书签
ThinkPHP框架实现session跨域问题的解决方法
ThinkPHP二级域名session共享问题
php 跨域、跨子域，跨服务器读取session
跨服务器Session共享的四种方法
Angular通过CORS实现跨域方案

]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>nginx</tag>
        <tag>好好学PHP</tag>
        <tag>thinkphp</tag>
        <tag>session</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Nginx：ThinkPHP部署到Nginx服务器</title>
    <url>/dev-thinkphp-nginx/</url>
    <content><![CDATA[1. 前言nginx默认情况下不支持pathinfo模式，从而不能支持ThinkPHP。能访问的，只有首页，其他函数的路径，都无法访问。本文中，我们就来学习解决这个问题。
参考文档：

最完美ThinkPHP nginx 配置文件
Nginx跨域配置，支持DELETE,PUT请求
Nginx执行php显示no input file specified的处理方法
nginx指定404 No input file specified
简单配置nginx使之支持pathinfo



2. nginx配置支持pathinfo首先，查看nginx配置文件的位置，ps aux | grep nginx。然后，进入配置文件所在文件夹，备份配置文件cp nginx.conf nginx.conf_mybak。
原nginx.conf内容如下：
error_log  logs/error.log  error ;pid logs/nginx.pid;user  www;worker_processes  auto;worker_rlimit_nofile 51200;events &#123;    use epoll;    worker_connections  51200;&#125;http &#123;    client_body_buffer_size 32k;    client_header_buffer_size 2k;    client_max_body_size 2m;    default_type application/octet-stream;    log_not_found off;    server_tokens off;    include       mime.types;    gzip on;    gzip_min_length  1k;    gzip_buffers     4 16k;    gzip_http_version 1.0;    gzip_comp_level 2;    gzip_types       text/plain text/css text/xml text/javascript application/x-javascript application/xml application/rss+xml application/xhtml+xml application/atom_xml;    gzip_vary on;    #error_page   500 502 503 504  /50x.html;     log_format  access  &#x27;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#x27;              &#x27;$status $body_bytes_sent &quot;$http_referer&quot; &#x27;              &#x27;&quot;$http_user_agent&quot; $http_x_forwarded_for&#x27;;    server &#123;        listen 80 default_server;        server_name localhost;        root /home/wwwroot/;        index index.php index.html index.htm;        location ~ \.php$ &#123;            fastcgi_pass   127.0.0.1:9000;            fastcgi_index  index.php;            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;            fastcgi_param  PHP_VALUE        open_basedir=$document_root:/tmp/:/proc/;            include        fastcgi_params;        &#125;    &#125;    include vhost/*.conf;    &#125;

修改nginx.conf的server部分如下：
server &#123;    listen 80 default_server;    server_name localhost;    root /home/wwwroot/;    index index.html index.htm index.php;    error_page 404 /404.html;    location = /404.html &#123;        return 404 &#x27;Sorry, File not Found!&#x27;;    &#125;    error_page 500 502 503 504 /50x.html;    location = /50x.html &#123;        root /usr/local/nginx/html; # windows用户替换这个目录    &#125;    location / &#123;        try_files $uri @rewrite;    &#125;    location @rewrite &#123;        set $static 0;        if  ($uri ~ \.(css|js|jpg|jpeg|png|gif|ico|woff|eot|svg|css\.map|min\.map)$) &#123;            set $static 1;        &#125;        if ($static = 0) &#123;            rewrite ^/(.*)$ /index.php?s=/$1;        &#125;    &#125;    location ~ /Uploads/.*\.php$ &#123;        deny all;    &#125;    location ~ \.php/ &#123;       if ($request_uri ~ ^(.+\.php)(/.+?)($|\?)) &#123; &#125;       fastcgi_pass 127.0.0.1:9000;       include fastcgi_params;       fastcgi_param SCRIPT_NAME     $1;       fastcgi_param PATH_INFO       $2;       fastcgi_param SCRIPT_FILENAME $document_root$1;    &#125;    location ~ \.php$ &#123;        fastcgi_pass 127.0.0.1:9000;        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;        include fastcgi_params;    &#125;    location ~ /\.ht &#123;        deny  all;    &#125;&#125;

最后，重启nginx。cd /usr/local/nginx/sbin，./nginx -s reload。
访问http://192.168.56.101，提示No input file specified.。出错了？不过没有太大影响。
输入函数完整路径，http://192.168.56.101/thinkphp/index.php/Home/Index/getInfo，获取信息成功！至此，nginx配置支持pathinfo成功！
3. nginx配置支持跨域紧接着上面的nginx.conf配置文件修改，添加一行即可：
add_header Access-Control-Allow-Origin *;location / &#123;    try_files $uri @rewrite;&#125;


4. No input file specified紧接着上面的nginx.conf配置文件修改，添加一行即可：
http &#123;    # 其他配置省略    fastcgi_intercept_errors on;    include vhost/*.conf;&#125;

5. 后记至此，在虚拟机CentOS7上配置的nginx，已经支持pathinfo模式、支持跨域、404报错正常。
6. 阿里云问题同样的配置，放在阿里云的CentOS6.5上居然报错！无奈，寻找另外一种配置nginx支持pathinfo模式的方法。只需要在nginx.conf初始配置的基础上，修改四个地方即可：
location ~ \.php &#123; #去掉$    root /home/wwwroot/; #增加这一句    fastcgi_pass   127.0.0.1:9000;    fastcgi_index  index.php;    fastcgi_split_path_info ^(.+\.php)(.*)$; #增加这一句    fastcgi_param PATH_INFO $fastcgi_path_info; #增加这一句    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;    fastcgi_param  PHP_VALUE        open_basedir=$document_root:/tmp/:/proc/;    include        fastcgi_params;&#125;

添加跨域支持：
add_header Access-Control-Allow-Origin *;

域名绑定：
server_name localhost api.voidking.com;
然后，在万网添加域名解析A记录到阿里云主机ip地址。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>nginx</tag>
        <tag>好好学Nginx</tag>
        <tag>thinkphp</tag>
        <tag>pathinfo</tag>
      </tags>
  </entry>
  <entry>
    <title>Java网页爬虫</title>
    <url>/dev-java-crawler/</url>
    <content><![CDATA[1. 功能进阶1、Java网页爬虫，最基础的功能，是能爬取某个页面的html源码。2、图形化界面。3、爬取某个页面的html源码，以及页面需要的静态资源（图片、css和js）。4、爬取某个页面的html源码，以及页面中的链接指向的页面的html源码，并且不断地延伸爬取。
整个开发过程，需要用到网络编程、正则表达式、I/O流、图形界面编程、事件监听、多线程等。为了简化开发，还需要用到一些外部jar包，比如jsoup。


2. 模块划分1、获取页面模块：获取页面文档，以及页面文档的字符串。2、获取页面链接模块：获取页面中存在的各种链接，包括a标签、img标签、css链接、js链接等。3、获取静态文件模块：静态文件分为两种，一种是字符串类型，一种是字节类型。4、保存文件模块：保存页面文档和静态文件。5、界面模块：包括界面设计，事件监听处理。
3. 核心代码摘要3.1. 获取页面模块public class Page &#123;    Document doc = null;    public Page(String url) &#123;        try &#123;            doc = Jsoup.connect(url).timeout(5000).userAgent(&quot;Mozilla&quot;).get();        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;        public String getHtml()&#123;        return doc.html();    &#125;        public Document getDoc()&#123;        return doc;    &#125;&#125;

3.2. 获取页面链接模块public class UrlList &#123;    public Document doc = null;    public UrlList(String url) &#123;        try &#123;            doc = Jsoup.connect(url).timeout(5000).userAgent(&quot;Mozilla&quot;).get();        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;        public ArrayList&lt;Element&gt; getLinkList()&#123;        Elements links = doc.select(&quot;link[href]&quot;);        ArrayList&lt;Element&gt; resultList = new ArrayList&lt;Element&gt;();        for(Element link : links)&#123;            resultList.add(link);        &#125;        return resultList;    &#125;        public ArrayList&lt;Element&gt; getCssList()&#123;        Elements links = doc.select(&quot;link[href]&quot;);        ArrayList&lt;Element&gt; resultList = new ArrayList&lt;Element&gt;();        for(Element link : links)&#123;            if(&quot;stylesheet&quot;.equals(link.attr(&quot;rel&quot;)))&#123;                resultList.add(link);                           &#125;        &#125;        return resultList;    &#125;        public ArrayList&lt;Element&gt; getAList()&#123;        Elements links = doc.select(&quot;a[href]&quot;);        ArrayList&lt;Element&gt; resultList = new ArrayList&lt;Element&gt;();        for(Element link : links)&#123;            resultList.add(link);        &#125;        return resultList;    &#125;        public ArrayList&lt;Element&gt; getImgList()&#123;        Elements links = doc.select(&quot;img[src]&quot;);        ArrayList&lt;Element&gt; resultList = new ArrayList&lt;Element&gt;();        for(Element link : links)&#123;            resultList.add(link);        &#125;        return resultList;    &#125;        public ArrayList&lt;Element&gt; getJsList()&#123;        Elements links = doc.select(&quot;script[src]&quot;);        ArrayList&lt;Element&gt; resultList = new ArrayList&lt;Element&gt;();        for(Element link : links)&#123;            resultList.add(link);        &#125;        return resultList;    &#125;&#125; 

3.3. 获取静态文件public class Source &#123;    public String getString(String url) &#123; // 定义一个字符串用来存储网页内容        String result = &quot;&quot;;        // 定义一个缓冲字符输入流        BufferedReader in = null;        try &#123;            // 将string转成url对象            URL realUrl = new URL(url);            // 初始化一个链接到那个url的连接            URLConnection connection = realUrl.openConnection();            // 开始实际的连接            connection.connect();            // 初始化 BufferedReader输入流来读取URL的响应            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));            // 用来临时存储抓取到的每一行的数据            String line;            while ((line = in.readLine()) != null) &#123;                // 遍历抓取到的每一行并将其存储到result里面                result += line;            &#125;        &#125; catch (Exception e) &#123;            System.out.println(&quot;发送GET请求出现异常！&quot; + e);            e.printStackTrace();        &#125; finally &#123;            try &#123;                if (in != null) &#123;                    in.close();                &#125;            &#125; catch (Exception e2) &#123;                e2.printStackTrace();            &#125;        &#125;        return result;    &#125;&#125;

3.4. 保存文件模块public class SaveFile &#123;    public void saveFile(String path, String htmlStr) &#123;        File file = new File(path + &quot;\\爬取的文件\\index.html&quot;);        if (!file.getParentFile().exists()) &#123;            file.getParentFile().mkdirs();        &#125;        // 字节输出流        FileOutputStream fos = null;        try &#123;            fos = new FileOutputStream(file);            OutputStreamWriter writer = new OutputStreamWriter(fos, &quot;UTF-8&quot;);            writer.write(htmlStr);            writer.close();        &#125; catch (Exception e) &#123;            e.printStackTrace();        &#125; finally &#123;            try &#123;                fos.close();            &#125; catch (IOException e) &#123;                // TODO Auto-generated catch block                e.printStackTrace();            &#125;        &#125;    &#125;    public void saveCss(String path, ArrayList&lt;Element&gt; cssList) &#123;        for (int i = 0; i &lt; cssList.size(); i++) &#123;            if (!cssList.get(i).attr(&quot;abs:href&quot;).equals(cssList.get(i).attr(&quot;href&quot;))) &#123;                Source source = new Source();                String css = source.getString(cssList.get(i).attr(&quot;abs:href&quot;));                String paths[] = cssList.get(i).attr(&quot;href&quot;).split(&quot;\\?&quot;);                File file = new File(path + &quot;\\爬取的文件\\&quot; + paths[0]);                if (!file.getParentFile().exists()) &#123;                    file.getParentFile().mkdirs();                &#125;                // 字节输出流                FileOutputStream fos = null;                try &#123;                    fos = new FileOutputStream(file);                    OutputStreamWriter writer = new OutputStreamWriter(fos, &quot;UTF-8&quot;);                    writer.write(css);                    writer.close();                &#125; catch (Exception e) &#123;                    e.printStackTrace();                &#125; finally &#123;                    try &#123;                        fos.close();                    &#125; catch (IOException e) &#123;                        // TODO Auto-generated catch block                        e.printStackTrace();                    &#125;                &#125;            &#125;        &#125;    &#125;    public void saveImg(String path, ArrayList&lt;Element&gt; imgList) &#123;        for (int i = 0; i &lt; imgList.size(); i++) &#123;            if (!imgList.get(i).attr(&quot;abs:src&quot;).equals(imgList.get(i).attr(&quot;src&quot;))) &#123;                String paths[] = imgList.get(i).attr(&quot;src&quot;).split(&quot;\\?&quot;);                File file = new File(path + &quot;\\爬取的文件\\&quot; + paths[0]);                if (!file.getParentFile().exists()) &#123;                    file.getParentFile().mkdirs();                &#125;                try &#123;                    URL uri = new URL(imgList.get(i).attr(&quot;abs:src&quot;));                      InputStream in = uri.openStream();                      FileOutputStream fo = new FileOutputStream(file);                      byte[] buf = new byte[1024];                      int length = 0;                      while ((length = in.read(buf, 0, buf.length)) != -1) &#123;                          fo.write(buf, 0, length);                      &#125;                      in.close();                      fo.close();                  &#125; catch (Exception e) &#123;                      e.printStackTrace();                  &#125;                              &#125;        &#125;    &#125;        public void saveJs(String path, ArrayList&lt;Element&gt; jsList)&#123;        for (int i = 0; i &lt; jsList.size(); i++) &#123;            if (!jsList.get(i).attr(&quot;abs:src&quot;).equals(jsList.get(i).attr(&quot;src&quot;))) &#123;                Source source = new Source();                String css = source.getString(jsList.get(i).attr(&quot;abs:src&quot;));                String paths[] = jsList.get(i).attr(&quot;src&quot;).split(&quot;\\?&quot;);                File file = new File(path + &quot;\\爬取的文件\\&quot; + paths[0]);                if (!file.getParentFile().exists()) &#123;                    file.getParentFile().mkdirs();                &#125;                // 字节输出流                FileOutputStream fos = null;                try &#123;                    fos = new FileOutputStream(file);                    OutputStreamWriter writer = new OutputStreamWriter(fos, &quot;UTF-8&quot;);                    writer.write(css);                    writer.close();                &#125; catch (Exception e) &#123;                    e.printStackTrace();                &#125; finally &#123;                    try &#123;                        fos.close();                    &#125; catch (IOException e) &#123;                        // TODO Auto-generated catch block                        e.printStackTrace();                    &#125;                &#125;            &#125;        &#125;    &#125;&#125;

3.5. 界面模块代码略，来个图说明设计。
4. 后记未填的坑：1、最终设计做到了功能3，功能4未做。2、爬取文件的相对位置需要另做处理。
5. 源码分享https://github.com/voidking/java-crawler
6. 书签如何用Java写一个爬虫？https://www.zhihu.com/question/30626103
零基础写Java知乎爬虫之先拿百度首页练练手http://www.jb51.net/article/57193.htm
网页爬虫的设计与实现（Java版）http://www.aiuxian.com/article/p-2279197.html
网页爬虫系统的设计http://www.tuicool.com/articles/7JVzIza
专栏：使用JSOUP实现网络爬虫http://blog.csdn.net/column/details/jsoup.html
jsoup Cookbook(中文版)http://www.open-open.com/jsoup/
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
      </tags>
  </entry>
  <entry>
    <title>Hadoop单词计数</title>
    <url>/dev-hadoop-wordcount/</url>
    <content><![CDATA[1. 前言《在Ubuntu16.04上安装Hadoop》一文中，搭建好了hadoop平台。接下来，我们参照慕课网Kit_Ren的《Hadoop大数据平台架构与实践——基础篇》教程，跑一下单词计数程序。
要求：计算文件中出现每个单词的频数，输入结果按照字母顺序进行排序。输入：
hello world bye worldhello hadoop bye hadoopbye hadoop hello hadoop

输出：
bye     3hello   3hadoop  4world   2



2. MapReduce过程
3. 编译打包和运行1、上传 WordCount.java 文件到hadoop所在机器
2、编译java文件
javac -classpath /opt/hadoop-1.2.1/hadoop-core-1.2.1.jar:/opt/hadoop-1.2.1/lib/commons-cli-1.2.jar WordCount.java

3、打包三个.class文件为一个jar文件
jar -cvf wordcount.jar *.class

4、新建两个文件，file1和file2，内容分别为：
hello world bye worldhello hadoop bye hadoopbye hadoop hello hadoop

hello world bye worldsystem hello hadoop

5、把file1和file2放到hdfs中
hadoop fs -mkdir wordcount_inputhadoop fs -put file* wordcount_input/

6、执行jar包
hadoop jar wordcount.jar WordCount wordcount_input wordcount_output

7、查看结果
hadoop fs -ls wordcount_output`，`hadoop fs -cat wordcount_output/part-r-00000

]]></content>
      <categories>
        <category>engineering</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>大数据</tag>
        <tag>hadoop</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学MySQL：MySQL重置密码</title>
    <url>/dev-mysql-reset-password/</url>
    <content><![CDATA[1. mysql5.6设置密码1.1. 通过修改表数据设置密码一般mysql的root默认密码为空，如果之前没有设置过root密码，我们可以使用空密码登录后设置密码。
1、启动mysql
service start mysql

2、进入mysql控制台
mysql -u root

3、修改密码
use mysql;update mysql.user set password=password(&#x27;voidking&#x27;) where user=&#x27;root&#x27;;exit;

4、重启mysql服务
service mysqld restart

5、测试登录
mysql -u root -p



1.2. 通过mysqladmin修改密码1、启动mysql
2、重置密码
mysqladmin -u root -p password voidking

2. mysql5.6忘记密码1、停止或者kill掉mysql进程
service mysqld stop

2、以不检查权限的方式启动mysql
cd /usr/local/mysql/bin./mysqld_safe --skip-grant-tables &amp;

3、用空密码登录mysql
mysql -u root

4、修改root用户的密码
use mysql;select user, host from user;update user set password=password(&#x27;voidking&#x27;) where user=&#x27;root&#x27; and host=&#x27;localhost&#x27;;update user set password=password(&#x27;voidking&#x27;) where user=&#x27;root&#x27; and host=&#x27;%&#x27;;-- orgrant all privileges on *.* to root@&#x27;localhost&#x27; identified by &#x27;voidking&#x27; with grant option;grant all privileges on *.* to root@&#x27;%&#x27; identified by &#x27;voidking&#x27; with grant option;select host, user from user;flush privileges;quit;

PS：主机地址支持单个 IP 形式的地址，也支持填入%（表示不做 IP 范围限制）进行模糊匹配。示例1：填入%，表示不做 IP 范围限制，即允许所有 IP 地址的客户端使用该帐号访问数据库。示例2：填入10.5.10.%，表示允许 IP 范围在10.5.10.%内的客户端使用该帐号访问数据库。示例3：填入%.mysql-read.mysql.svc.cluster.local，表示允许主机名范围在%.mysql-read.mysql.svc.cluster.local内的客户端使用该帐号访问数据库。
5、重新启动mysql
ps -e | grep mysqlkill -KILL $mysqld_safe_pidkill -KILL $mysqld_pidservice mysqld start

6、测试登录mysql
mysql -u root -p

3. mysql5.7修改密码select user,host from mysql.user where user=&#x27;root&#x27;;set password for root@&#x27;localhost&#x27; = password(&#x27;voidking&#x27;);set password for root@&#x27;%&#x27; = password(&#x27;voidking&#x27;);

4. mysql8重置密码alter user &#x27;root&#x27;@&#x27;localhost&#x27; identified with mysql_native_password by &#x27;voidking&#x27;;flush privileges;

5. 允许远程访问1、确认配置了远程访问权限
use mysql;select user, host from user where user=&#x27;root&#x27;;grant all privileges on *.* to &#x27;root&#x27;@&#x27;%&#x27; with grant option;

2、开放所在主机端口
systemctl stop firewalld.service# orfirewall-cmd --zone=public --add-port=3306/tcp --permanentfirewall-cmd --reload

6. 书签
Mysql数据库中设置root密码的命令及方法
Access denied for user ‘root@localhost’ (using password:NO)

]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>mysql</tag>
        <tag>好好学MySQL</tag>
      </tags>
  </entry>
  <entry>
    <title>Hadoop入门篇</title>
    <url>/dev-hadoop-start/</url>
    <content><![CDATA[1. 什么是大数据？什么是大数据？正如字面意思，大量的数据。举个例子，Mysql的一张表里存了1万条数据，查询没问题；100万条数据，查询也没问题；那么，1亿条数据？100亿条数据？更大的数据？

大数据科学家JohnRauser提到一个简单的定义：大数据就是任何超过了一台计算机处理能力的庞大数据量。

为了处理大量的数据，我们必须找到更好的办法。谷歌经过研究，发表了一些关于大数据解决方案的论文，涉及MapReduce、BigTable、GFS等。但是，谷歌开发的大数据处理平台，并没有开源。一些勤奋的同学根据谷歌发表的论文，搞出了Hadoop平台，后来成为一个主流的大数据处理平台。
参考文档：

什么是大数据？
Hadoop大数据平台架构与实践–基础篇



2. 大数据分析1、可视化分析大数据分析的使用者有大数据分析专家，同时还有普通用户，但是他们二者对于大数据分析最基本的要求就是可视化分析，因为可视化分析能够直观的呈现大数据特点，同时能够非常容易被读者所接受，就如同看图说话一样简单明了。
2、数据挖掘算法大数据分析的理论核心就是数据挖掘算法，各种数据挖掘的算法基于不同的数据类型和格式才能更加科学的呈现出数据本身具备的特点，也正是因为这些被全世界统计学家所公认的各种统计方法（可以称之为真理）才能深入数据内部，挖掘出公认的价值。另外一个方面也是因为有这些数据挖掘的算法才能更快速的处理大数据，如果一个算法得花上好几年才能得出结论，那大数据的价值也就无从说起了。
3、预测性分析能力大数据分析最终要的应用领域之一就是预测性分析，从大数据中挖掘出特点，通过科学的建立模型，之后便可以通过模型带入新的数据，从而预测未来的数据。
4、数据质量和数据管理大数据分析离不开数据质量和数据管理，高质量的数据和有效的数据管理，无论是在学术研究还是在商业应用领域，都能够保证分析结果的真实和有价值。
3. Hadoop简介
Apache Hadoop (/həˈduːp/) is a collection of open-source software utilities that facilitates using a network of many computers to solve problems involving massive amounts of data and computation. 

Apache Hadoop是一个开源工具集合，能够通过网络利用多台计算机解决大量数据和大量计算的问题。
参考文档：

Apache Hadoop
Hadoop Tutorial: Getting Started with Hadoop
1.0 Hadoop 教程
Hadoop(一) 原理简介、基本构建

4. Hadoop架构未完待续
5. Hadoop安装参考文档《在Linux中安装Hadoop》
6. Hadoop常用命令6.1. 查看集群状态jps -ljps -l | grep hadoophdfs dfsadmin -reporthdfs haadmin -getAllServiceState

6.2. 单独启动namenodehdfs --daemon start namenode

6.3. 查看配置cd /usr/local/hadoop/hadoop-2.10.2/etc/hadoop/cat core-site.xmlcat hdfs-site.xmlcat log4j.properties...

6.4. 查看日志进入log4j.properties中配置的日志目录，查看日志。
tail -n 1000 xxx.log | grep xxx

6.5. 查看存储hdfs dfs -df -hhdfs dfs -ls /

6.6. 删除文件1、删除文件
hdfs dfs -rm -r -f /path/to/directory

回收站保留时间配置项fs.trash.interval，默认配置1440分钟，在core-site.xml中配置。
2、还原回收站
hdfs dfs -mv /user/root/.Trash/Current/path/to/directory /path/to/

3、清理回收站
hdfs dfs -rm -r -f /user/root/.Trash/Current/path/to/directory#hdfs dfs -expunge

4、删除文件，不经过回收站
hdfs dfs -rm -r -f /path/to/directory -skipTrash

注意：hdfs中删除文件后，系统磁盘删除文件非常慢。曾经删除过17T的数据，物理磁盘上删除了三天才完成。删除过程可以查看datanode的日志。
6.7. 清理损坏块hdfs fsck /path/to/directoryhdfs fsck /path/to/directory -delete

6.8. 找到十天前的文件now=$(date +%s)hdfs dfs -ls /wdc/result/tmp5 | grep &quot;^d&quot; | while read f; do dir_date=`echo $f | awk &#x27;&#123;print $6&#125;&#x27;` difference=$(( ( $now - $(date -d &quot;$dir_date&quot; +%s) ) / (24 * 60 * 60 ) )) if [ $difference -gt 10 ]; then   echo $f; fidone

参考文档How to find directories in HDFS which are older than N days?
]]></content>
      <categories>
        <category>engineering</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>大数据</tag>
        <tag>hadoop</tag>
      </tags>
  </entry>
  <entry>
    <title>Linux中安装配置Hadoop</title>
    <url>/dev-install-hadoop-on-linux/</url>
    <content><![CDATA[1. 前言Hadoop是一种分布式系统框架，组件包括HDFS、MapRedure和Yarn。本文中，我们学习在 Linux 系统中安装 Hadoop。
参考文档：

Ubutnu20.04+hadoop2.10.2+hive1.2.2实现完美安装详细教程
Hadoop 安装详细步骤

Hadoop下载地址：

Hadoop Download
Hadoop release archive
Hadoop release archive mirror site



2. 安装Hadoop1.2.13. 安装JDK1.8安装jdk-8u161，参考文档《全平台安装JDK》
3.1. 下载Hadoop安装包wget http://mirror.bit.edu.cn/apache/hadoop/common/hadoop-1.2.1/hadoop-1.2.1.tar.gztar -xzvf hadoop-1.2.1.tar.gz -C /opt/

3.2. 修改Hadoop配置1、修改hadoop-env.sh
cd /opt/hadoop-1.2.1/conf/vim hadoop-env.sh
修改JAVA_HOME。
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161

2、修改core-site.xml，内容如下：
&lt;?xml version=&quot;1.0&quot;?&gt;&lt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;configuration.xsl&quot;?&gt;&lt;!-- Put site-specific property overrides in this file. --&gt;&lt;configuration&gt;    &lt;property&gt;        &lt;name&gt;hadoop.tmp.dir&lt;/name&gt;        &lt;value&gt;/hadoop&lt;/value&gt;    &lt;/property&gt;    &lt;property&gt;        &lt;name&gt;dfs.name.dir&lt;/name&gt;        &lt;value&gt;/hadoop/name&lt;/value&gt;    &lt;/property&gt;    &lt;property&gt;        &lt;name&gt;fs.default.name&lt;/name&gt;        &lt;value&gt;hdfs://localhost:9000&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

3、修改hdfs-site.xml，内容如下：
&lt;?xml version=&quot;1.0&quot;?&gt;&lt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;configuration.xsl&quot;?&gt;&lt;!-- Put site-specific property overrides in this file. --&gt;&lt;configuration&gt;    &lt;property&gt;        &lt;name&gt;dfs.data.dir&lt;/name&gt;        &lt;value&gt;/hadoop/data&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

4、修改mapred-site.xml，内容如下：
&lt;?xml version=&quot;1.0&quot;?&gt;&lt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;configuration.xsl&quot;?&gt;&lt;!-- Put site-specific property overrides in this file. --&gt;&lt;configuration&gt;    &lt;property&gt;        &lt;name&gt;mapred.job.tracker&lt;/name&gt;        &lt;value&gt;localhost:9001&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

5、修改/etc/profile，修改PATH如下：
export HADOOP_HOME=/opt/hadoop-1.2.1export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$HADOOP_HOME/bin:$PATH

生效，source /etc/profile。
6、测试
hadoop
如果出现COMMAND提示，则表明安装配置成功。
如果出现：Warning: $HADOOP_HOME is deprecated.这是因为新版的hadoop废弃掉了HADOOP_HOME这个变量。若要除去这个警告，要么换用HADOOP_PREFIX，要么在hadoop-env.sh添加一行：
export HADOOP_HOME_WARN_SUPPRESS=1
详情参考文档：Warning: $HADOOP_HOME is deprecated.的原因以及解决方法
3.3. 启动Hadoop1、namenode格式化
hadoop namenode -format

2、启动hadoop
cd /opt/hadoop-1.2.1/binstart-all.sh

3、检查是否启动成功
jps
如果看到上图中的进程，则表明启动成功。
4、查看hadoop/hdfs中有哪些文件
hadoop fs -ls /

至此，hadoop安装完成。
4. 安装Hadoop2.10.25. 安装JDK1.8安装jdk-8u161，参考文档《全平台安装JDK》
5.1. 下载Hadoop安装包wget https://dlcdn.apache.org/hadoop/common/hadoop-2.10.2/hadoop-2.10.2.tar.gz --no-check-certificatemkdir -p /usr/local/hadoop/tar -xzvf hadoop-2.10.2.tar.gz -C /usr/local/hadoop/

5.2. Hadoop配置使用JDK1、修改hadoop-env.sh
cd /usr/local/hadoop/hadoop-2.10.2vim etc/hadoop/hadoop-env.sh
修改JAVA_HOME为绝对路径。
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_161

2、验证环境
./bin/hadoop version

5.3. 单机测试运行mkdir ./inputcp ./etc/hadoop/*.xml ./input./bin/hadoop jar ./share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar grep ./input ./output &#x27;dfs[a-z.]+&#x27;cat ./output/*

看到结果1  dfsadmin表明运行成功。
5.4. 配置HDFS伪分布式部署：

在一台机器上安装，使用的是分布式思想，即分布式文件系统，非本地文件系统。
Hdfs涉及到的相关守护进程(namenode,datanode,secondarynamenode)都运行在一台机器上，都是独立的java进程。
用途比 Standalone mode 多了代码调试功能，允许检查内存使用情况，HDFS输入输出，以及其他的守护进程交互。

1、修改etc/hadoop/core-site.xml
&lt;configuration&gt;    &lt;property&gt;        &lt;name&gt;hadoop.tmp.dir&lt;/name&gt;        &lt;value&gt;file:/usr/local/hadoop/hadoop-2.10.2/tmp&lt;/value&gt;    &lt;/property&gt;    &lt;property&gt;        &lt;name&gt;fs.defaultFS&lt;/name&gt;        &lt;value&gt;hdfs://localhost:9000&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

配置参考文档：hadoop2.10.2 core-default.xml
2、修改etc/hadoop/hdfs-site.xml
&lt;configuration&gt;    &lt;property&gt;        &lt;name&gt;dfs.replication&lt;/name&gt;        &lt;value&gt;1&lt;/value&gt;    &lt;/property&gt;    &lt;property&gt;        &lt;name&gt;dfs.namenode.name.dir&lt;/name&gt;        &lt;value&gt;file:/usr/local/hadoop/hadoop-2.10.2/tmp/dfs/name&lt;/value&gt;    &lt;/property&gt;    &lt;property&gt;        &lt;name&gt;dfs.datanode.data.dir&lt;/name&gt;        &lt;value&gt;file:/usr/local/hadoop/hadoop-2.10.2/tmp/dfs/data&lt;/value&gt;    &lt;/property&gt;&lt;/configuration&gt;

配置参考文档：hadoop2.10.2 hdfs-default.xml
3、格式化namenode
./bin/hdfs namenode -format

执行完成，没有报错，当前目录中出现了tmp目录，表明格式化成功。
4、启动hadoop服务
./sbin/start-dfs.sh
需要三次输入当前用户的密码。
5、查看hadoop进程
jps -l

6、浏览器访问浏览器访问 http://192.168.56.101:50070可以看到 NameNode 和 Datanode 的信息。
5.5. 配置免密启动启动hadoop时需要输入密码，这个比较麻烦，可以通过配置免密登录省去输入密码的步骤。
ssh-keygenssh-copy-id localhost

5.6. 停止hadoop服务./sbin/stop-all.sh






]]></content>
      <categories>
        <category>engineering</category>
        <category>bigdata</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>java</tag>
        <tag>大数据</tag>
        <tag>spark</tag>
        <tag>hadoop</tag>
      </tags>
  </entry>
  <entry>
    <title>开发者必备的开放平台注册应用大全</title>
    <url>/hobby-open-platform/</url>
    <content><![CDATA[所谓开放平台，就是首先提供一个基本的服务，然后通过开放自身的接口，使得第三方开发者得以通过运用和组装其接口以及其他第三方服务接口产生新的应用，并且使得该应用能够统一运行在这个平台之上，我们把这样的一种网络服务模式叫做开放平台。开放平台模式成功的要点在于，通过自身服务和第三方应用的互利互惠，提高用户对平台网站的粘性和使用程度，进而提高获利，同时，通过利益分摊，达到平台自身和第三方应用循环刺激而产生的滚雪球式的增长。麦布收集了最新最全的开放平台注册应用，用得着就收藏了吧。


1、新浪微博开放平台：http://open.weibo.com/2、QQ互联：http://connect.qq.com/3、百度开放平台：http://open.baidu.com/4、百度官网认证：http://guanwang.baidu.com/vcard/officialsite5、360官网认证：http://zhanzhang.so.com/?m=SiteCertification&amp;a=siteVerify6、Discuz! 开放平台：http://open.discuz.net/7、PHPCMS开放平台：http://open.phpcms.cn/8、搜搜论坛开放计划：http://open.soso.com/9、搜狗开放平台：http://open.sogou.com/10、QQ空间认证：http://page.opensns.qq.com/apply.html11、微游戏开放平台：http://open.weibo.com/game/12、腾讯微博开放平台：http://dev.open.t.qq.com/13、微信开放平台：http://open.weixin.qq.com/14、腾讯社区开放平台：http://opensns.qq.com/15、腾讯Q+平台：http://dev.qplus.com/16、拍拍网开放平台：http://pop.paipai.com/17、搜狗官网认证：http://help.sogou.com/renzheng/18、搜狗问问开放平台：http://open.wenwen.sogou.com/19、百度开发者中心：http://developer.baidu.com/20、百度知道开放平台：http://open.zhidao.baidu.com/21、人人网开放平台：http://dev.renren.com/22、网易微博开放平台：http://open.t.163.com/23、网易云阅读开放平台：http://open.yuedu.163.com/24、搜狐微博开放平台：http://open.t.sohu.com/25、搜狐新闻客户端全媒体平台：http://mp.k.sohu.com/26、搜狐博客开放平台：http://ow.blog.sohu.com/27、淘宝开放平台：http://open.taobao.com28、支付宝开放平台：http://bizpartner.alipay.com/denglu/index.htm29、阿里巴巴开放平台：http://open.1688.com/30、豆瓣API key：http://www.douban.com/service/apikey/apply31、UC优视开放平台：http://www.uc.cn/business/ucly.shtml32、天涯开放平台：http://open.tianya.cn/33、Google站长开发者：https://www.google.com/accounts/ManageDomains34、开心开放平台：http://open.kaixin001.com/35、天翼开放平台：http://open.189.cn/36、360软件开放平台：http://rz.360.cn/37、360应用开放平台：http://dev.app.360.cn/38、雅虎开放平台：https://developer.apps.yahoo.com/projects39、Twitter开放平台：https://dev.twitter.com/40、Facebook开放平台：https://developers.facebook.com/41、56视频开放平台：http://dev.56.com/42、亿起发–开放平台：http://open.yiqifa.com/43、PHPCMS开放平台：http://open.phpcms.cn/44、UC优视开放平台：http://www.uc.cn/business/ucly.shtml45、淘宝开放平台：http://open.taobao.com
转载自：http://mt.sohu.com/20160628/n456773156.shtml
]]></content>
      <categories>
        <category>computer</category>
      </categories>
  </entry>
  <entry>
    <title>好好学Linux：CentOS7安装SVN</title>
    <url>/dev-centos7-install-svn/</url>
    <content><![CDATA[1. SVN简介Apache Subversion 通常被缩写成 SVN，是一个开放源代码的版本控制系统，Subversion 在 2000 年由 CollabNet Inc 开发，现在发展成为 Apache 软件基金会的一个项目，拥有丰富的开发者和用户社区。
SVN相对于的RCS、CVS，采用了分支管理系统，它的设计目标是取代CVS。互联网上免费的版本控制服务大多基于Subversion。
本文中，在CentOS7中安装配置SVN。参考文档：CentOS 7搭建SVN服务器


2. 安装SVN1、安装SVN：
yum install subversionsvnserve --version

2、创建版本库：
mkdir -p /var/svn/svnrepossvnadmin create /var/svn/svnrepos

3、进入conf目录（该svn版本库配置文件），cd /var/svn/svnrepos/conf

authz文件是权限控制文件
passwd是帐号密码文件
svnserve.conf SVN服务配置文件

4、设置帐号密码，vi passwd在[users]块中添加用户和密码，格式：帐号=密码，如voidking=woaixuexi。
### This file is an example password file for svnserve.### Its format is similar to that of svnserve.conf. As shown in the### example below it contains one section labelled [users].### The name and password for each user follow, one account per line.[users]# harry = harryssecret# sally = sallyssecretvoidking=woaixuexi

5、设置权限，vi authz在末尾添加如下代码：
[/]voidking=rw
意思是版本库的根目录quwenzhe对其有读写权限。
6、修改svnserve.conf文件，vi svnserve.conf打开下面的几个注释：
anon-access = read #匿名用户可读auth-access = write #授权用户可写password-db = passwd #使用哪个文件作为账号文件authz-db = authz #使用哪个文件作为权限文件realm = /var/svn/svnrepos # 认证空间名，版本库所在目录

7、启动svn版本库
svnserve -d -r /var/svn/svnrepos

PS：停止svn命令为 killall svnserve
8、使用SVNWindows上右键，SVN Checkout。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>搭建自己的云盘服务</title>
    <url>/dev-build-owncloud/</url>
    <content><![CDATA[1. 前言
继华为、迅雷、新浪，115等网盘之后，近日360云盘也宣布关闭个人云存储服务。这场发起于2013年的个人网盘大战至今，排名靠前的常用网盘已经只剩下百度云盘和腾讯微云，真是令人唏嘘不已……

360云盘即将停止服务，所有用户数据将只保存到明年2月1日，可怜我的38T数据！！！首先，没有硬盘存储这么多数据，只能选择下载；其次，大家争相下载，360云盘限速，怎一个慢字了得！
考虑到别人的云盘不靠谱，郝同学决定自己搭建一个。


2. ubuntu安装owncloud1、安装lamp-server，tasksel install lamp-server，报错tasksel: apt-get failed (100)。就知道不会一帆风顺，先更新个软件源，apt-get update，然而，接着报错：E: Could not get lock /var/lib/dpkg/lock - open (11: Resource temporarily unavailable)E: Unable to lock the administration directory (/var/lib/dpkg/), is another process using it?
删除两个文件：
rm /var/cache/apt/archives/lockrm /var/lib/dpkg/lock

再次更新软件源，apt-get update，再次安装lamp-server，tasksel install lamp-server。
2、下载安装owncloud
wget -nv https://download.owncloud.org/download/repositories/9.1/Ubuntu_16.04/Release.key -O Release.keyapt-key add - &lt; Release.keysh -c &quot;echo &#x27;deb http://download.owncloud.org/download/repositories/9.1/Ubuntu_16.04/ /&#x27; &gt; /etc/apt/sources.list.d/owncloud.list&quot;apt-get updateapt-get install owncloud

3、访问owncloud访问http://192.168.56.102/owncloud，进入owncloud初始页面。
安装到这一步就完成了，已经可以通过服务器使用ownCloud的服务来存储同步分享文件了。
3. 绑定域名在/etc/apache2/sites-available文件夹下添加新的虚拟主机配置文件，备份000-default.conf文件，然后在其中添加：
&lt;VirtualHost *:80&gt;  ServerName yun.voidking.com  DocumentRoot /var/www/owncloud  &lt;IfModule mod_headers.c&gt;    Header always set Strict-Transport-Security &quot;max-age=15552000; includeSubDomains; preload&quot;  &lt;/IfModule&gt;&lt;/VirtualHost&gt;
然后域名解析yun.voidking.com到服务器ip地址。
4. 安装插件ubuntu安装ftp服务，apt-get install vsftpd。vi /etc/vsftpd.conf，启用如下配置：
write_enable=YESlocal_umask=022utf8_filesystem=YES
然后service vsftpd restart。
下载owncloud的插件，下载的压缩包解压到/var/www/owncloud/apps，然后在浏览器中打开owncloud，选择应用页面，启用插件。离线下载：https://apps.owncloud.com/content/show.php/ocDownloader+%28NG%29?content=169974
在线看视频：https://apps.owncloud.com/content/show.php/Video+Viewer+Plus?content=174666
5. 书签云盘一个个倒下怎么办？无需编码，手把手教你搭建至尊私享云盘https://zhuanlan.zhihu.com/p/23156514
天下没有免费的午餐！是时候搭建起自己的云盘服务了https://zhuanlan.zhihu.com/p/23179293
owncloud-fileshttp://download.owncloud.org/download/repositories/9.1/owncloud/
Installation Wizardhttps://doc.owncloud.org/server/9.1/admin_manual/installation/installation_wizard.html
ownCloud Applicationshttps://apps.owncloud.com/
客户端https://owncloud.org/install/#install-clients
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
      </tags>
  </entry>
  <entry>
    <title>Chrome插件开发基础</title>
    <url>/dev-chrome-extension-start/</url>
    <content><![CDATA[1. 前言前些天，小伙伴问我会不会开发chrome插件。突然意识到，这是个有趣的事情，那就搞一搞。
最开始想做一个批量下载图片插件，很快找到一个fatkun。做一个视频广告屏蔽插件？太难，放弃。要不做一个翻墙插件？更难，放弃。做一个划词翻译插件怎么样？已经有了Google翻译和划词翻译。做一个页面截图插件？已经有了捕捉网页截图 - FireShot的。做一个markdown编辑器？已经有了Markdown Here。
果然是前人种树，后人没地种哇！后来想做一个柯林斯词典翻译，然而没有平台提供这个词典的接口，唯一的办法是抓取百度翻译的页面，然后截取结果。太麻烦，还要搭个服务器。
最终，决定做一个获取公网IP的插件。开发工具：sublime；开发语言：html+css+js。


2. 官方入门demo2.1. 目录结构chrome-extention-start|-icon.png|-manifest.json|-popup.html|-popup.js
其中，icon.png是插件的图标；manifest.json是插件的配置文件；popup.html是单击插件图标后弹出的页面；popup.js是业务处理的js。
2.2. 启用插件访问chrome://extensions，勾选开发者模式，加载已解压的扩展程序，选择chrome-extention-start文件夹。
2.3. 试用官方给出的效果是这样的：
郝同学满怀期待打开YouTube，结果是这样的：不要在意这些细节，至少第一个插件已经跑起来了！
3. 查询公网IP3.1. manifest.json&#123;  &quot;manifest_version&quot;: 2,  &quot;name&quot;: &quot;Public IP&quot;,  &quot;description&quot;: &quot;This extension shows your public IP&quot;,  &quot;icons&quot;: &#123;    &quot;16&quot;: &quot;icon_16.png&quot;,    &quot;48&quot;: &quot;icon_48.png&quot;,    &quot;64&quot;: &quot;icon_64.png&quot;,    &quot;128&quot;: &quot;icon_128.png&quot;  &#125;,  &quot;version&quot;: &quot;1.0&quot;,  &quot;browser_action&quot;: &#123;    &quot;default_icon&quot;: &quot;icon_19.png&quot;,    &quot;default_title&quot;: &quot;查看公网IP&quot;,    &quot;default_popup&quot;: &quot;popup.html&quot;  &#125;,  &quot;permissions&quot;: [    &quot;http://ip.chinaz.com/getip.aspx&quot;  ]&#125;
需要注意的是，permissions中的url是允许跨域的。
3.2. popup.js$(function()&#123;  String.prototype.replaceAll  = function(s1,s2)&#123;         return this.replace(new RegExp(s1,&quot;gm&quot;),s2);       &#125;  $.ajax(&#123;    url: &#x27;http://ip.chinaz.com/getip.aspx&#x27;,    type: &#x27;GET&#x27;,    data: &#123;&#125;,    success: function(data)&#123;      console.log(data);      data = data.replace(&quot;ip&quot;,&quot;&#x27;ip&#x27;&quot;);      data = data.replace(&quot;address&quot;,&quot;&#x27;address&#x27;&quot;);      data = data.replaceAll(&quot;\&#x27;&quot;,&quot;\&quot;&quot;);      console.log(data);      var dataJson = JSON.parse(data);      $(&#x27;#ip&#x27;).val(dataJson.ip);      $(&#x27;#address&#x27;).val(dataJson.address);    &#125;,    error: function(xhr)&#123;      console.log(xhr);    &#125;  &#125;);  &#125;);



3.3. 效果
4. 发布插件的发布非常简单，上传项目的zip压缩包之后，填写一些信息即可。但是，想要获得发布插件的权限，非常麻烦，具体我会在后记部分吐槽。
5. 本地插件安装假设电脑被墙，又需要安装Chrome插件，那么下载crx文件，然后在Chrome地址栏输入chrome://extensions/，并将下载的crx拖入该页面即可。
6. 后记谷歌，你妹！这特么歧视也太明显了吧！大陆开发者，无法发布应用。要想发布，只能使用除了中国大陆外的信用卡支付！我*&amp;……%￥#@！
历经波折，终于拿到一张全球付的卡。然而由于激动，进错了页面，到了Google Play的支付页面，填写信息时居然有中国的选项，理所当然选了中国，填了真实信息。
后来，发现进错页面，回到开发者信息中心，却再也无法点出付款的页面！！！当我转到google payment center，却提示遇到了一个问题！What the fuck！这是摆明了不给修改的机会哇！
万般无奈，注册了小号，才支付成功。最后，附上插件链接：https://chrome.google.com/webstore/detail/public-ip/cdpgbfmmhnbmkdnhlgjpbjdgblggnhck
7. 书签Getting Started: Building a Chrome Extensionhttps://developer.chrome.com/extensions/getstarted
360极速浏览器应用开放平台http://open.chrome.360.cn/extension_dev/overview.html
Chrome扩展技术手册http://crxdoczh.readthedocs.io/en/latest/
Chrome扩展及应用开发（首发版）http://www.ituring.com.cn/book/1421
如何从零开始写一个 Chrome 扩展？https://www.zhihu.com/question/20179805
2016年最实用的七款chrome浏览器“神器”级别插件http://mt.sohu.com/20160621/n455512782.shtml
如何发布一款Chrome Apphttps://segmentfault.com/a/1190000000354014
发布了几个自己制作的Chrome插件http://www.jianshu.com/p/NpjteJ
Chrome插件（Extensions）开发攻略http://www.cnblogs.com/guogangj/p/3235703.html
Google Play Developer Consolehttps://play.google.com/apps/publish/signup/
开发者信息中心https://chrome.google.com/webstore/developer/dashboard/
Google payment centerhttps://payments.google.com/payments/home#settings
全球付 -国际购物-在线消费新体验https://www.globalcash.hk/index-welcome.do
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>chrome</tag>
        <tag>插件</tag>
      </tags>
  </entry>
  <entry>
    <title>thinkphp实现短信验证注册</title>
    <url>/dev-sms-verification-code/</url>
    <content><![CDATA[1. 前言小太阳项目最先做的模块，用户管理模块。业主端该模块分为三个功能：注册登录、个人信息修改、认证。注册时需要用到短信验证码，本文记录一下思路和具体实现。短信验证平台使用云片，短信验证码的生成使用thinkphp。


2. 思路1、用户输入手机号，请求获取短信验证码。2、thinkphp生成短信验证码，存储，同时和其他参数一起发送请求给云片。3、云片发送短信验证码到指定手机号。4、用户输入短信验证码。5、thinkphp根据验证码是否正确、验证码是否过期两个条件判断是否验证通过。
3. 代码实现3.1. 验证接口接口地址：https://sms.yunpian.com/v1/sms/send.json。使用postman，输入三个必须的参数apikey、mobile和text。
3.2. php发起http/https请求使用php的curl函数发起https请求，带入参数apikey、mobile和text。
// 获取短信验证码public function getSMSCode()&#123;    // create curl resource     $ch = curl_init();     // set url    $url = &#x27;https://sms.yunpian.com/v1/sms/send.json&#x27;;     curl_setopt($ch, CURLOPT_URL, $url);     // set param    $paramArr = array(        &#x27;apikey&#x27; =&gt; &#x27;******&#x27;,        &#x27;mobile&#x27; =&gt; &#x27;******&#x27;,        &#x27;text&#x27; =&gt; &#x27;【小太阳】您的验证码是1234&#x27;    );    $param = &#x27;&#x27;;    foreach ($paramArr as $key =&gt; $value) &#123;        $param .= urlencode($key).&#x27;=&#x27;.urlencode($value).&#x27;&amp;&#x27;;    &#125;    $param = substr($param, 0, strlen($param)-1);    curl_setopt($ch, CURLOPT_POSTFIELDS, $param);    curl_setopt($ch, CURLOPT_HEADER, 0);    curl_setopt($ch, CURLOPT_POST, 1);    //curl默认不支持https协议，设置不验证协议    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);     //return the transfer as a string     curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);     // $output contains the output string     $output = curl_exec($ch);     // close curl resource to free up system resources     curl_close($ch);     echo $output;&#125;


3.3. 生成随机短信验证码默认生成四位的随机短信验证码。
// 生成短信验证码public function createSMSCode($length = 4)&#123;    $min = pow(10 , ($length - 1));    $max = pow(10, $length) - 1;    return rand($min, $max);&#125;

3.4. 整合在数据库新建表sun_smscode：
DROP TABLE IF EXISTS `sun_smscode`;CREATE TABLE `sun_smscode` (  `id` int(8) NOT NULL AUTO_INCREMENT,  `mobile` varchar(11) NOT NULL,  `code` int(4) NOT NULL,  `create_at` datetime NOT NULL,  `update_at` datetime NOT NULL,  PRIMARY KEY (`id`)) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

thinkphp代码：
// 获取短信验证码public function getSMSCode()&#123;    // create curl resource     $ch = curl_init();     // set url    $url = &#x27;https://sms.yunpian.com/v1/sms/send.json&#x27;;     curl_setopt($ch, CURLOPT_URL, $url);     // set param    $mobile = $_POST[&#x27;mobile&#x27;];    $code = $this-&gt;createSMSCode();    $paramArr = array(        &#x27;apikey&#x27; =&gt; &#x27;******&#x27;,        &#x27;mobile&#x27; =&gt; $mobile,        &#x27;text&#x27; =&gt; &#x27;【小太阳】您的验证码是&#x27;.$code    );    $param = &#x27;&#x27;;    foreach ($paramArr as $key =&gt; $value) &#123;        $param .= urlencode($key).&#x27;=&#x27;.urlencode($value).&#x27;&amp;&#x27;;    &#125;    $param = substr($param, 0, strlen($param)-1);    curl_setopt($ch, CURLOPT_POSTFIELDS, $param);    curl_setopt($ch, CURLOPT_HEADER, 0);    curl_setopt($ch, CURLOPT_POST, 1);    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //不验证证书下同    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);     //return the transfer as a string     curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);     // $output contains the output string     $output = curl_exec($ch);     // close curl resource to free up system resources     curl_close($ch);     //$outputJson = json_decode($output);    $outputArr = json_decode($output, true);    //echo $outputJson-&gt;code;    //echo $outputArr[&#x27;code&#x27;];    if($outputArr[&#x27;code&#x27;] == &#x27;0&#x27;)&#123;        $data[&#x27;mobile&#x27;] = $mobile;        $data[&#x27;code&#x27;] = $code;        $smscode = D(&#x27;smscode&#x27;);        $smscodeObj = $smscode-&gt;where(&quot;mobile=&#x27;$mobile&#x27;&quot;)-&gt;find();        if($smscodeObj)&#123;            $data[&#x27;update_at&#x27;] = date(&#x27;Y-m-d H:i:s&#x27;);            $success = $smscode-&gt;where(&quot;mobile=&#x27;$mobile&#x27;&quot;)-&gt;save($data);            if($success !== false)&#123;                $result = array(                    &#x27;code&#x27; =&gt; &#x27;0&#x27;,                    &#x27;ext&#x27; =&gt; &#x27;修改成功&#x27;,                    &#x27;obj&#x27; =&gt; $smscodeObj                );            &#125;            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;else&#123;            $data[&#x27;create_at&#x27;] = date(&#x27;Y-m-d H:i:s&#x27;);            $data[&#x27;update_at&#x27;] = $data[&#x27;create_at&#x27;];            if($smscode-&gt;create($data))&#123;                $id = $smscode-&gt;add();                if($id)&#123;                    $smscode_temp = $smscode-&gt;where(&quot;id=&#x27;$id&#x27;&quot;)-&gt;find();                    $result = array(                        &#x27;code&#x27;=&gt; &#x27;0&#x27;,                        &#x27;ext&#x27;=&gt; &#x27;创建成功&#x27;,                        &#x27;obj&#x27;=&gt;$smscode_temp                    );                    echo json_encode($result,JSON_UNESCAPED_UNICODE);                &#125;            &#125;        &#125;            &#125;&#125;

3.5. 验证短信验证码验证短信验证码时间是否过期，验证短信验证码是否正确。
// 验证短信验证码是否有效public function checkSMSCode()&#123;    $mobile = $_POST[&#x27;mobile&#x27;];    $code = $_POST[&#x27;code&#x27;];    $nowTimeStr = date(&#x27;Y-m-d H:i:s&#x27;);    $smscode = D(&#x27;smscode&#x27;);    $smscodeObj = $smscode-&gt;where(&quot;mobile=&#x27;$mobile&#x27;&quot;)-&gt;find();    if($smscodeObj)&#123;        $smsCodeTimeStr = $smscodeObj[&#x27;update_at&#x27;];        $recordCode = $smscodeObj[&#x27;code&#x27;];        $flag = $this-&gt;checkTime($nowTimeStr, $smsCodeTimeStr);        if(!$flag)&#123;            $result = array(                &#x27;code&#x27; =&gt; &#x27;1&#x27;,                &#x27;ext&#x27; =&gt; &#x27;验证码过期，请刷新后重新获取&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);            return;        &#125;        if($code != $recordCode)&#123;            $result = array(                &#x27;code&#x27; =&gt; &#x27;2&#x27;,                &#x27;ext&#x27; =&gt; &#x27;验证码错误，请重新输入&#x27;            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);            return;        &#125;        $result = array(            &#x27;code&#x27; =&gt; &#x27;0&#x27;,            &#x27;ext&#x27; =&gt; &#x27;验证通过&#x27;        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;&#125;// 验证验证码时间是否过期public function checkTime($nowTimeStr,$smsCodeTimeStr)&#123;    //$nowTimeStr = &#x27;2016-10-15 14:39:59&#x27;;    //$smsCodeTimeStr = &#x27;2016-10-15 14:30:00&#x27;;    $nowTime = strtotime($nowTimeStr);    $smsCodeTime = strtotime($smsCodeTimeStr);    $period = floor(($nowTime-$smsCodeTime)/60); //60s    if($period&gt;=0 &amp;&amp; $period&lt;=20)&#123;        return true;    &#125;else&#123;        return false;    &#125;&#125;


4. 改进为了防止短信轰炸，在请求获取短信验证码时，需要加入图片验证码。
thinkphp提供了生成图片验证码的函数，下面我们来实现验证码的生成、刷新和验证。
4.1. 生成和刷新图片验证码// 获取图片验证码，刷新图片验证码public function getPicCode()&#123;    $config = array(        &#x27;fontSize&#x27;=&gt;30,    // 验证码字体大小        &#x27;length&#x27;=&gt;4,     // 验证码位数        &#x27;useNoise&#x27;=&gt;false, // 关闭验证码杂点        &#x27;expire&#x27;=&gt;600    );    $Verify = new \Think\Verify($config);    $Verify-&gt;entry(2333);//2333是验证码标志&#125;

假设，该函数的对应url为http://localhost/owner-bd/index.php/Home/CheckCode/getPicCode，那么，图片验证码的地址就是这个url，放入页面图片标签的src属性即可。
4.2. 验证图片验证码// 验证验证码是否正确public function checkPicCode($code)&#123;    $verify = new \Think\Verify();    if($verify-&gt;check($code, 2333))&#123;        $result = array(            &#x27;code&#x27; =&gt; &#x27;0&#x27;,            &#x27;ext&#x27; =&gt; &#x27;验证通过&#x27;        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;else&#123;        $result = array(            &#x27;code&#x27; =&gt; &#x27;1&#x27;,            &#x27;ext&#x27; =&gt; &#x27;验证码错误，请重新输入&#x27;        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;;&#125;
以上方法，我们利用了thinkphp提供的check方法，实现起来很简单。但是，如果想要得到验证细节，就没有办法了。比如，验证码错误，可能验证码超时，可能因为输入验证码错误，可能因为验证码已经使用过等等。必要的时候，可以重写thinkphp的验证码类，或者重写thinkphp的check方法。
5. 跑通前后端5.1. 后端修改验证图片验证码函数，改为被调用函数：
public function checkPicCode($picCode)&#123;    $verify = new \Think\Verify();    if($verify-&gt;check($picCode, 2333))&#123;        return true;    &#125;else&#123;        return false;    &#125;;&#125;

在获取短信验证码函数的最顶部，添加调用图片验证码函数，只有通过验证，才发送请求给云片。
// 获取短信验证码public function getSMSCode()&#123;    $picCode = $_POST[&#x27;picCode&#x27;];    if(!$this-&gt;checkPicCode($picCode))&#123;        $result = array(            &#x27;code&#x27; =&gt; &#x27;1&#x27;,            &#x27;ext&#x27; =&gt; &#x27;验证码错误，请重新输入&#x27;        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);        return;    &#125;    /*省略*/&#125;

5.2. 前端核心代码&lt;!--register.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot; ng-app=&quot;sunApp&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;注册&lt;/title&gt;&lt;/head&gt;&lt;body ng-controller=&quot;registerController&quot;&gt;    &lt;form action=&quot;&quot; class=&quot;register-form&quot; ng-show=&quot;isShow1&quot;&gt;        &lt;div class=&quot;input-group&quot;&gt;            &lt;input type=&quot;text&quot; class=&quot;mobile&quot; ng-model=&quot;mobile&quot; placeholder=&quot;手机号&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;input-group&quot;&gt;            &lt;input type=&quot;text&quot; class=&quot;pic-code&quot; ng-model=&quot;picCode&quot; placeholder=&quot;图片验证码&quot;&gt;            &lt;img class=&quot;img&quot; src=&quot;&#123;&#123;picCodeUrl&#125;&#125;&quot; alt=&quot;&quot; ng-click=&quot;refresh()&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;input-group&quot;&gt;            &lt;input type=&quot;text&quot; class=&quot;sms-code&quot; ng-model=&quot;SMSCode&quot; placeholder=&quot;短信验证码&quot;&gt;            &lt;button class=&quot;btn-sms&quot; ng-click=&quot;getSMSCode()&quot; ng-disabled=&quot;btnSMSDisabled&quot;&gt;&#123;&#123;btnSMSText&#125;&#125;&lt;/button&gt;        &lt;/div&gt;        &lt;button class=&quot;confirm-btn&quot; ng-click=&quot;next()&quot;&gt;下一步&lt;/button&gt;    &lt;/form&gt;    &lt;form action=&quot;&quot; class=&quot;register-form&quot; ng-show=&quot;isShow2&quot;&gt;        &lt;div class=&quot;input-group&quot;&gt;            &lt;input type=&quot;text&quot; class=&quot;mobile&quot; ng-model=&quot;mobile&quot; placeholder=&quot;手机号&quot; disabled=&quot;true&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;input-group&quot;&gt;            &lt;input type=&quot;password&quot; class=&quot;password&quot; ng-model=&quot;password&quot; placeholder=&quot;请输入密码&quot;&gt;            &lt;input type=&quot;password&quot; class=&quot;password&quot; ng-model=&quot;password2&quot; placeholder=&quot;请再次输入密码&quot;&gt;        &lt;/div&gt;        &lt;button class=&quot;confirm-btn&quot; ng-click=&quot;getSMSCode()&quot;&gt;注册&lt;/button&gt;    &lt;/form&gt;&lt;/body&gt;&lt;/html&gt;


// register.jsangular.module(&#x27;sunApp&#x27;).controller(&#x27;registerController&#x27;, function ($scope,$http,$httpParamSerializer,$state,$interval) &#123;     $scope.picCodeUrl = &#x27;/owner-bd/index.php/Home/CheckCode/getPicCode&#x27;;    $scope.isShow1 = true;    $scope.isShow2 = false;    $scope.btnSMSText = &#x27;获取验证码&#x27;;    $scope.btnSMSDisabled = false;    $scope.checkOver = false;    // 获取短信验证码    $scope.getSMSCode = function()&#123;        var param = &#123;            mobile: $scope.mobile,            picCode: $scope.picCode        &#125;;        $http(&#123;            method:&#x27;POST&#x27;,            url:&#x27;/owner-bd/index.php/Home/SMS/getSMSCode&#x27;,            //url: &#x27;/owner-fd/mock/common.json&#x27;,            headers:&#123;                &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;            &#125;,            dataType: &#x27;json&#x27;,            data: $httpParamSerializer(param)        &#125;).then(function successCallback(response) &#123;            console.log(response.data);            if(response.data.code == &#x27;0&#x27;)&#123;                $scope.checkOver = true;                $scope.btnSMSDisabled = true;                var time = 60;                var timer = null;                timer = $interval(function()&#123;                    time = time - 1;                    $scope.btnSMSText = time+&#x27;秒&#x27;;                    if(time == 0) &#123;                        $interval.cancel(timer);                        $scope.btnSMSDisabled = false;                        $scope.btnSMSText = &#x27;重新获取&#x27;;                    &#125;                &#125;, 1000);            &#125;        &#125;, function errorCallback(response) &#123;            console.log(response.data);        &#125;);    &#125;    // 验证短信验证码    $scope.next = function()&#123;        if(!$scope.checkOver)&#123;            console.log(&#x27;未通过验证&#x27;);            return;        &#125;        var param = &#123;            mobile: $scope.mobile,            code: $scope.SMSCode        &#125;;        $http(&#123;            method:&#x27;POST&#x27;,            url:&#x27;/owner-bd/index.php/Home/SMS/checkSMSCode&#x27;,            //url: &#x27;/owner-fd/mock/common.json&#x27;,            headers:&#123;                &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;            &#125;,            dataType: &#x27;json&#x27;,            data: $httpParamSerializer(param)        &#125;).then(function successCallback(response) &#123;            console.log(response.data);            if(response.data.code == &#x27;0&#x27;)&#123;                $scope.isShow1 = false;                $scope.isShow2 = true;            &#125;        &#125;, function errorCallback(response) &#123;            console.log(response.data);        &#125;);    &#125;    // 刷新图片验证码    $scope.refresh = function()&#123;        $scope.picCodeUrl = &#x27;/owner-bd/index.php/Home/CheckCode/getPicCode?&#x27;+Math.random();    &#125;&#125;);

5.3. 优化以上代码，安全性不是很好，我们可以利用工具绕过前端验证。为了避免这个问题，可以在checkPicCode和checkSMSCode函数中添加session值来标记。
$_SESSION[&#x27;checkPicCode&#x27;] = true;$_SESSION[&#x27;checkSMSCode&#x27;] = true;
在最后一步，向数据库中添加用户时，先验证一下两个session值是否都为true，都为true时再添加。
5.4. 成果
6. 后记以后也许有用的代码：
echo json_encode($_SESSION);// 打印出session中的数据echo session_id();// 打印当前session的id

7. 书签云片网https://www.yunpian.com/
cURL函数http://php.net/manual/zh/ref.curl.php
curl 基础例子http://php.net/manual/zh/curl.examples.php
在PHP语言中使用JSONhttp://www.ruanyifeng.com/blog/2011/01/json_in_php.html
thinkphp验证码http://document.thinkphp.cn/manual_3_2.html#verify
修改ThinkPHP的验证码类http://www.cnblogs.com/BTMaster/p/3547878.html
ThinkPHP 3.2版本 , 无法读取$_SESSION[‘verify_code’]http://www.cnblogs.com/lovezbs/p/4496117.html
LICEcap - Downloadhttp://licecap.en.softonic.com/
gif动态图局部加马赛克模糊广告文字http://www.leawo.cn/space-138176-do-thread-id-64000.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>angularjs</tag>
        <tag>php</tag>
        <tag>thinkphp</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：thinkphp和angularjs整合</title>
    <url>/dev-thinkphp-and-angularjs/</url>
    <content><![CDATA[1. 前言为了前后端分离的更彻底，便于前后端独立开发，小太阳项目，计划使用thinkphp+angular。后端专注写接口，前端负责页面渲染。
参考文档：

Php 5.6 Automatically populating $HTTP_RAW_POST_DATA is deprecated
PHP 5.6: Automatically populating $HTTP_RAW_POST_DATA is deprecated and will be removed in a future version.
angularjs 请求后端接口请求了两次
AngularJs + angular-ui-router + bootstrap 实现基础导航栏
Angular结合Bootstrap3的导航菜单



2. 项目分割项目分成三个子系统：业主端、物业端、CMS端。每个子系统分别有前端和后端，前端使用angular，后端使用thinkphp。自此，产生了六个子项目，分别命名为owner-fd、owner-bd、manager-fd、manager-bd、cms-fd和cms-bd。接下来，我们以cms-fd和cms-bd为例，来说明angular和thinkphp之间的交互。
3. 添加业主信息业务逻辑：在添加业主信息页面，填写业主信息的表单，填写完成后单击“确认添加”按钮，发送http请求给后端。后端把获取到的数据存到数据库中，并且返回值给前端，前端提示成功或失败。
3.1. angular部分1、入口html
&lt;!--cms-fd/index.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;link rel=&quot;stylesheet&quot; href=&quot;public/libs/bootstrap/dist/css/bootstrap.min.css&quot;&gt;    &lt;link rel=&quot;stylesheet&quot; href=&quot;public/libs/layer/skin/layer.css&quot;&gt;    &lt;link rel=&quot;stylesheet&quot; href=&quot;public/css/index.css&quot;&gt;    &lt;title&gt;CMS系统&lt;/title&gt;&lt;/head&gt;&lt;body ng-app=&quot;myApp&quot;&gt;    &lt;ul class=&quot;navigator nav nav-pills&quot; ng-controller=&quot;MainCtroller&quot;&gt;        &lt;li role=&quot;presentation&quot; ng-class=&quot;&#123;active:&#x27;home&#x27; == currentTab&#125;&quot;&gt;            &lt;a ui-sref=&quot;home&quot; ng-click=&quot;changeTab(&#x27;home&#x27;)&quot;&gt;首页&lt;/a&gt;        &lt;/li&gt;        &lt;li role=&quot;presentation&quot; ng-class=&quot;&#123;active:&#x27;ownerList&#x27; == currentTab&#125;&quot;&gt;            &lt;a ui-sref=&quot;ownerList&quot; ng-click=&quot;changeTab(&#x27;ownerList&#x27;)&quot;&gt;业主信息列表&lt;/a&gt;        &lt;/li&gt;        &lt;li role=&quot;presentation&quot; ng-class=&quot;&#123;active:&#x27;ownerAdd&#x27; == currentTab&#125;&quot;&gt;            &lt;a ui-sref=&quot;ownerAdd&quot; ng-click=&quot;changeTab(&#x27;ownerAdd&#x27;)&quot;&gt;添加业主信息&lt;/a&gt;        &lt;/li&gt;    &lt;/ul&gt;    &lt;div ui-view style=&quot;width: 500px;margin: 50px auto 0&quot;&gt;&lt;/div&gt;&lt;script src=&quot;public/libs/angular/angular.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;public/libs/angular-ui-router/release/angular-ui-router.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;public/libs/oclazyload/dist/ocLazyLoad.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;public/libs/jquery/dist/jquery.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;public/libs/layer/layer.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;public/js/index.js&quot;&gt;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

2、入口js
/* *cms-fd/public/js/index.js*/var myApp = angular.module(&#x27;myApp&#x27;,[&#x27;ui.router&#x27;,&#x27;oc.lazyLoad&#x27;]);myApp.config(function ($stateProvider,$urlRouterProvider) &#123;    $urlRouterProvider.when(&#x27;&#x27;,&#x27;/home&#x27;);    $stateProvider.state(&#x27;home&#x27;,&#123;        url:&#x27;/home&#x27;,        templateUrl: &#x27;views/home.html&#x27;,        resolve:&#123;            loadMyCtrl:[&#x27;$ocLazyLoad&#x27;,function ($ocLazyLoad) &#123;                return $ocLazyLoad.load(&#123;                    name:&#x27;homeController&#x27;,                    files:[&#x27;public/js/home.js&#x27;]                &#125;)            &#125;]         &#125;    &#125;);    $stateProvider.state(&#x27;ownerList&#x27;,&#123;        url:&#x27;/ownerList&#x27;,        templateUrl:&#x27;views/owner/list.html&#x27;,        resolve:&#123;            loadMyCtrl:function ($ocLazyLoad) &#123;                return $ocLazyLoad.load(&#123;                    name:&#x27;ownerListController&#x27;,                    files:[&#x27;public/js/owner/list.js&#x27;]                &#125;)            &#125;        &#125;    &#125;);    $stateProvider.state(&#x27;ownerAdd&#x27;,&#123;        url: &#x27;/ownerAdd&#x27;,        templateUrl: &#x27;views/owner/add.html&#x27;,        resolve:&#123;            loadMyCtrl:function ($ocLazyLoad) &#123;                return $ocLazyLoad.load(&#123;                    name:&#x27;ownerAddController&#x27;,                    files:[&#x27;public/js/owner/add.js&#x27;]                &#125;)            &#125;        &#125;    &#125;);    $stateProvider.state(&#x27;ownerEdit&#x27;,&#123;        url: &#x27;/owner/edit/:ownerId&#x27;,        templateUrl: &#x27;views/owner/edit.html&#x27;,        resolve:&#123;            loadMyCtrl:function ($ocLazyLoad) &#123;                return $ocLazyLoad.load(&#123;                    name:&#x27;ownerEditController&#x27;,                    files:[&#x27;public/js/owner/edit.js&#x27;]                &#125;)            &#125;        &#125;    &#125;);    &#125;);myApp.controller(&#x27;MainCtroller&#x27;,function($scope,$location)&#123;    //console.log($location.url());    var url = $location.url();    $scope.currentTab = url.substr(1);    $scope.changeTab = function(tabname)&#123;          $scope.currentTab = tabname;    &#125;;  &#125;);

3、添加业主信息页面
&lt;!--cms-fd/views/owner/add.html--&gt;&lt;div id=&quot;home&quot; ng-controller=&quot;ownerAddController&quot;&gt;    &lt;h2&gt;添加业主信息&lt;/h2&gt;    &lt;form role=&quot;form&quot;&gt;        &lt;div class=&quot;form-group&quot;&gt;            &lt;label for=&quot;&quot;&gt;用户名&lt;/label&gt;            &lt;input name=&quot;username&quot; type=&quot;text&quot; class=&quot;form-control username&quot; id=&quot;&quot; ng-model=&quot;username&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;form-group&quot;&gt;            &lt;label for=&quot;&quot;&gt;密码&lt;/label&gt;            &lt;input name=&quot;password&quot; type=&quot;password&quot; class=&quot;form-control password&quot; id=&quot;&quot; ng-model=&quot;password&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;form-group&quot;&gt;            &lt;label for=&quot;&quot;&gt;邮箱&lt;/label&gt;            &lt;input name=&quot;password&quot; type=&quot;email&quot; class=&quot;form-control email&quot; id=&quot;&quot; ng-model=&quot;email&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;form-group&quot;&gt;            &lt;label for=&quot;&quot;&gt;昵称&lt;/label&gt;            &lt;input name=&quot;nickname&quot; type=&quot;text&quot; class=&quot;form-control nickname&quot; id=&quot;&quot; ng-model=&quot;nickname&quot;&gt;        &lt;/div&gt;        &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary submit&quot; ng-click=&quot;submit()&quot;&gt;确认添加&lt;/button&gt;    &lt;/form&gt;&lt;/div&gt;

3、添加业主信息页面js
/* *cms-fd/public/js/owner/add.js*/angular.module(&#x27;myApp&#x27;).controller(&#x27;ownerAddController&#x27;, function ($scope,$http,$httpParamSerializer) &#123;     $scope.submit = function()&#123;        var param = &#123;            username: $scope.username,            password: $scope.password,            email: $scope.email,            nickname: $scope.nickname        &#125;;        $http(&#123;            method:&#x27;POST&#x27;,            url:&#x27;/cms-bd/index.php/Home/Owner/add&#x27;,            headers:&#123;                &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;            &#125;,            dataType: &#x27;json&#x27;,            data: $httpParamSerializer(param)        &#125;).then(function successCallback(response) &#123;            console.log(response.data);            layer.msg(response.data.ext);        &#125;, function errorCallback(response) &#123;            console.log(response.data);        &#125;);    &#125;&#125;);

3.2. thinkphp部分在cms-bd/Application/Home/Controller中新建OwnerController.class.php，编写add函数。
// 增加业主public function add()&#123;    if(!$_POST[&#x27;username&#x27;] || !$_POST[&#x27;password&#x27;] || !$_POST[&#x27;email&#x27;] || !$_POST[&#x27;nickname&#x27;])&#123;        $result = array(            &#x27;code&#x27; =&gt; &#x27;0&#x27;,            &#x27;ext&#x27; =&gt; &#x27;参数不足&#x27;         );        echo json_encode($result,JSON_UNESCAPED_UNICODE);        return;    &#125;    $data[&#x27;username&#x27;] = $_POST[&#x27;username&#x27;];    $data[&#x27;password&#x27;] = md5($_POST[&#x27;password&#x27;]);    $data[&#x27;email&#x27;] = $_POST[&#x27;email&#x27;];    $data[&#x27;nickname&#x27;] = $_POST[&#x27;nickname&#x27;];    $data[&#x27;create_at&#x27;] = date(&#x27;Y-m-d H:i:s&#x27;);    $data[&#x27;update_at&#x27;] = date(&#x27;Y-m-d H:i:s&#x27;);    $owner = D(&#x27;owner&#x27;);    if($owner-&gt;create($data))&#123;        $id = $owner-&gt;add();        if($id)&#123;            $owner_temp = $owner-&gt;where(&quot;id=&#x27;$id&#x27;&quot;)-&gt;find();            $result = array(                &#x27;code&#x27;=&gt; &#x27;0&#x27;,                &#x27;ext&#x27;=&gt; &#x27;success&#x27;,                &#x27;obj&#x27;=&gt;$owner_temp            );            echo json_encode($result,JSON_UNESCAPED_UNICODE);        &#125;    &#125;&#125;

4. 查看业主信息列表和删除业主信息业务逻辑：进入业主信息列表页时，发送http请求给后端，获取到业主信息列表，然后显示到业主信息列表页上。单击某条记录后面的“删除”按钮，弹出确认提示框。确认删除，则发送http请求给后端，获取返回值，如果删除成功，则从页面移除该条记录。
4.1. angular部分1、业主信息列表页面
&lt;!--cms-fd/views/owner/list.html--&gt;&lt;div id=&quot;owner-list&quot; ng-controller=&quot;ownerListController&quot;&gt;    &lt;h2&gt;业主列表&lt;/h2&gt;    &lt;ul&gt;        &lt;li ng-repeat=&quot;owner in ownerList&quot;&gt;            &lt;span&gt;用户名：&#123;&#123;owner.username&#125;&#125;，昵称：&#123;&#123;owner.nickname&#125;&#125;&lt;/span&gt;            &lt;button ng-click=&quot;edit(owner.id)&quot;&gt;修改&lt;/button&gt;            &lt;button ng-click=&quot;delete(owner.id,$index)&quot;&gt;删除&lt;/button&gt;        &lt;/li&gt;    &lt;/ul&gt; &lt;/div&gt;

2、业主信息列表页js
/* *cms-fd/public/js/owner/list.js*/angular.module(&#x27;myApp&#x27;).controller(&#x27;ownerListController&#x27;, function ($scope,$http,$httpParamSerializer,$state) &#123;     $http(&#123;        method: &#x27;POST&#x27;,        url: &#x27;/cms-bd/index.php/Home/Owner/listAll&#x27;,        headers:&#123;            &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;        &#125;,        dataType: &#x27;json&#x27;,        data: $httpParamSerializer(&#123;&#125;)    &#125;).then(function successCallback(response)&#123;        console.log(response.data);        $scope.ownerList = response.data;    &#125;, function errorCallback(response)&#123;        console.log(response.data);    &#125;);    $scope.edit = function(ownerId)&#123;        $state.go(&#x27;ownerEdit&#x27;, &#123;ownerId: ownerId&#125;);    &#125;    $scope.delete = function(ownerId,index)&#123;        var layerIndex = layer.confirm(&#x27;确认删除？&#x27;, &#123;            btn: [&#x27;是的&#x27;,&#x27;取消&#x27;] //按钮        &#125;, function()&#123;            $http(&#123;                method:&#x27;POST&#x27;,                url:&#x27;/cms-bd/index.php/Home/Owner/delete&#x27;,                headers:&#123;                    &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;                &#125;,                dataType: &#x27;json&#x27;,                data: $httpParamSerializer(&#123;ownerId: ownerId&#125;)            &#125;).then(function successCallback(response) &#123;                console.log(response.data);                if(response.data.code == &#x27;0&#x27;)&#123;                    $scope.ownerList.splice(index,1);                &#125;                layer.close(layerIndex);            &#125;, function errorCallback(response) &#123;                console.log(response.data);                layer.close(layerIndex);            &#125;);                    &#125;, function()&#123;            //layer.msg(&#x27;取消&#x27;);        &#125;);    &#125;&#125;);

4.2. thinkphp部分添加listAll函数和delete函数。
// 业主列表public function listAll()&#123;    $owner = D(&#x27;owner&#x27;);    $resultArr = $owner-&gt;where(&#x27;state=0&#x27;)-&gt;order(&#x27;update_at desc,id desc&#x27;)-&gt;select();    echo json_encode($resultArr,JSON_UNESCAPED_UNICODE);&#125;// 删除业主public function delete()&#123;    $ownerId = $_POST[&#x27;ownerId&#x27;];    $data[&#x27;state&#x27;] = 1;    $owner = D(&#x27;owner&#x27;);    $success = $owner-&gt;where(&quot;id=&#x27;$ownerId&#x27;&quot;)-&gt;save($data);    if($success)&#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;0&#x27;,            &#x27;ext&#x27;=&gt; &#x27;success&#x27;        );        echo json_encode($result);    &#125;else &#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;1&#x27;,            &#x27;ext&#x27;=&gt; &#x27;fail&#x27;        );        echo json_encode($result);    &#125;&#125;

5. 修改业主信息业务逻辑：在业主信息列表页，单击某条记录后的“修改”按钮，跳转到修改业主信息页面。修改完成后，单击“确认修改”按钮，跳转回业主信息列表页。
5.1. angular部分1、修改业主信息页面
&lt;!--cms-fd/views/owner/edit.html--&gt;&lt;div id=&quot;owner-edit&quot; ng-controller=&quot;ownerEditController&quot;&gt;    &lt;h2&gt;修改业主信息&lt;/h2&gt;    &lt;form role=&quot;form&quot;&gt;        &lt;div class=&quot;form-group&quot;&gt;            &lt;label for=&quot;&quot;&gt;用户名&lt;/label&gt;            &lt;input name=&quot;username&quot; type=&quot;text&quot; class=&quot;form-control username&quot; id=&quot;&quot; ng-model=&quot;owner.username&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;form-group&quot;&gt;            &lt;label for=&quot;&quot;&gt;密码&lt;/label&gt;            &lt;input name=&quot;password&quot; type=&quot;password&quot; class=&quot;form-control password&quot; id=&quot;&quot; ng-model=&quot;owner.password&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;form-group&quot;&gt;            &lt;label for=&quot;&quot;&gt;邮箱&lt;/label&gt;            &lt;input name=&quot;password&quot; type=&quot;email&quot; class=&quot;form-control email&quot; id=&quot;&quot; ng-model=&quot;owner.email&quot;&gt;        &lt;/div&gt;        &lt;div class=&quot;form-group&quot;&gt;            &lt;label for=&quot;&quot;&gt;昵称&lt;/label&gt;            &lt;input name=&quot;nickname&quot; type=&quot;text&quot; class=&quot;form-control nickname&quot; id=&quot;&quot; ng-model=&quot;owner.nickname&quot;&gt;        &lt;/div&gt;        &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary submit&quot; ng-click=&quot;submit()&quot;&gt;确认修改&lt;/button&gt;    &lt;/form&gt;&lt;/div&gt;

2、修改业主信息js
/* *cms-fd/public/js/owner/edit.js*/angular.module(&#x27;myApp&#x27;).controller(&#x27;ownerEditController&#x27;, function ($scope,$http,$httpParamSerializer,$stateParams,$state) &#123;     $http(&#123;        method:&#x27;POST&#x27;,        url:&#x27;/cms-bd/index.php/Home/Owner/findById&#x27;,        headers:&#123;            &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;        &#125;,        dataType: &#x27;json&#x27;,        data: $httpParamSerializer(&#123;ownerId: $stateParams.ownerId&#125;)    &#125;).then(function successCallback(response) &#123;        console.log(response.data);        $scope.owner = response.data.obj;    &#125;, function errorCallback(response) &#123;        console.log(response.data);    &#125;);    $scope.submit = function()&#123;        $http(&#123;            method:&#x27;POST&#x27;,            url:&#x27;/cms-bd/index.php/Home/Owner/edit&#x27;,            headers:&#123;                &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;            &#125;,            dataType: &#x27;json&#x27;,            data: $httpParamSerializer($scope.owner)        &#125;).then(function successCallback(response) &#123;            console.log(response.data);            $state.go(&#x27;ownerList&#x27;);        &#125;, function errorCallback(response) &#123;            console.log(response.data);        &#125;);    &#125;&#125;);

5.2. thinkphp部分添加findById和edit两个函数。
// 根据id查找业主public function findById()&#123;    $ownerId = $_POST[&#x27;ownerId&#x27;];    $owner = D(&#x27;owner&#x27;);    $ownerObj = $owner-&gt;where(&quot;id=&#x27;$ownerId&#x27;&quot;)-&gt;find();    if($ownerObj)&#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;0&#x27;,            &#x27;ext&#x27;=&gt; &#x27;success&#x27;,            &#x27;obj&#x27;=&gt; $ownerObj        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;else&#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;1&#x27;,            &#x27;ext&#x27;=&gt; &#x27;没有找到记录&#x27;        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;  &#125;// 修改业主public function edit()&#123;    if(!$_POST[&#x27;id&#x27;] || !$_POST[&#x27;username&#x27;] || !$_POST[&#x27;password&#x27;] || !$_POST[&#x27;email&#x27;] || !$_POST[&#x27;nickname&#x27;])&#123;        $result = array(            &#x27;code&#x27; =&gt; &#x27;0&#x27;,            &#x27;ext&#x27; =&gt; &#x27;参数不足&#x27;         );        echo json_encode($result,JSON_UNESCAPED_UNICODE);        return;    &#125;    $id = $_POST[&#x27;id&#x27;];    $data[&#x27;username&#x27;] = $_POST[&#x27;username&#x27;];    $data[&#x27;password&#x27;] = md5($_POST[&#x27;password&#x27;]);    $data[&#x27;email&#x27;] = $_POST[&#x27;email&#x27;];    $data[&#x27;nickname&#x27;] = $_POST[&#x27;nickname&#x27;];    $data[&#x27;update_at&#x27;] = date(&#x27;Y-m-d H:i:s&#x27;);    $owner = D(&#x27;owner&#x27;);    $success = $owner-&gt;where(&quot;id=&#x27;$id&#x27;&quot;)-&gt;save($data);    if($success)&#123;        $owner_temp = $owner-&gt;where(&quot;id=&#x27;$id&#x27;&quot;)-&gt;find();        $result = array(            &#x27;code&#x27;=&gt; &#x27;0&#x27;,            &#x27;ext&#x27;=&gt; &#x27;success&#x27;,            &#x27;obj&#x27;=&gt;$owner_temp        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;else &#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;1&#x27;,            &#x27;ext&#x27;=&gt; &#x27;用户不存在&#x27;        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;&#125;


6. thinkphp关闭右下角Trace信息thinkphp默认开启调试模式，返回值时总会跟着一个小图标，默认在页面的右下角小图标。在部署阶段，需要把它关闭。1、在入口文件index.php加入
define(&quot;APP_DEBUG&quot;, false);

2、在Application/Common/config.php 配置文件中加入
&#x27;SHOW_PAGE_TRACE&#x27; =&gt; false

3、删除Application下的Runtime文件夹。
7. 后记增删查改跑通，这个demo也算是比较完整了。在实际开发的时候，很多地方还要做调整。比如导航栏的一些坑、更好页面布局、更友好的提示和跳转、安全性校验、数据库表设计等等等等。
angular非常容易产生页面缓存，如果遇到很奇葩的坑，比如修改了某个页面，但是刷新无效。不要犹豫，先清下浏览器缓存。
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>angularjs</tag>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>thinkphp</tag>
      </tags>
  </entry>
  <entry>
    <title>AngularJS按需加载js</title>
    <url>/dev-angularjs-dynamic-js/</url>
    <content><![CDATA[1. 前言Angular是一个单页面应用，随着系统的迭代，首屏代码会越来越大，所以需要对《AngularJS入门》中的代码进行改造，实现AngularJS可以按需加载js和css。
实现这个需求，有三个方案：1、利用requirejs。requirejs并不是按照angular规范开发的第三方插件，后期估计会有很多坑，放弃。
2、利用ui-router和ocLazyLoad。

每次“页面跳转”都要额外请求js并加载，浪费带宽增加页面加载时间，基本抛弃了预加载。
每一个路由都需要配置resolve属性，太low。
模块化程度太低，不利于以后代码移植和维护。

3、自己写需要的组件。最好的方案，然而技术要求太高，放弃。
综上，第一种方案坑太多，放弃；第三种方案能力有限，放弃；第二种方案也不好，但是相对容易，而且是针对angular的插件，就它了。
参考文档：

RequireJS官方文档
Dynamically Loading Controllers and Views with AngularJS
angular应用如何实现按需加载
尝试通过AngularJS模块按需加载搭建大型应用（上）
尝试通过AngularJS模块按需加载搭建大型应用（下）
angularjs ocLazyLoad分步加载js文件,angularjs ocLazyLoad按需加载js
按需加载 AngularJS 的 Controller



2. 安装依赖bower install angular#1.5.8bower install angular-ui-routerbower install oclazyloadbower install bootstrap

3. 核心代码&lt;!--dynamic/index.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;link rel=&quot;stylesheet&quot; href=&quot;bower_components/bootstrap/dist/css/bootstrap.min.css&quot;&gt;     &lt;style&gt;        body&#123;            font-family: &quot;Microsoft Yahei&quot;;        &#125;        .navigator&#123;            width: 500px;margin: 0 auto        &#125;        .navigator li&#123;           color: #000;font-size: 14px;        &#125;    &lt;/style&gt;    &lt;title&gt;按需加载js&lt;/title&gt;&lt;/head&gt;&lt;body ng-app=&quot;myApp&quot;&gt;    &lt;ul class=&quot;navigator nav nav-pills&quot;&gt;        &lt;li role=&quot;presentation&quot; class=&quot;active&quot;&gt;&lt;a href=&quot;#home&quot; ng-click=&quot;isActive($event)&quot;&gt;主页&lt;/a&gt;&lt;/li&gt;        &lt;li role=&quot;presentation&quot; class=&quot;active&quot;&gt;&lt;a href=&quot;#page2&quot;&gt;Page2&lt;/a&gt;&lt;/li&gt;        &lt;li role=&quot;presentation&quot; class=&quot;active&quot;&gt;&lt;a href=&quot;#page3&quot; ng-click=&quot;isActive($event)&quot;&gt;Page3&lt;/a&gt;&lt;/li&gt;    &lt;/ul&gt;    &lt;div ui-view style=&quot;width: 500px;margin: 50px auto 0&quot;&gt;&lt;/div&gt;&lt;script src=&quot;bower_components/angular/angular.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;bower_components/angular-ui-router/release/angular-ui-router.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;bower_components/oclazyload/dist/ocLazyLoad.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;public/js/index.js&quot;&gt;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

/* *dynamic/public/js/index.js*/var myApp=angular.module(&quot;myApp&quot;,[&quot;ui.router&quot;,&quot;oc.lazyLoad&quot;]);myApp.config(function ($stateProvider,$urlRouterProvider) &#123;    $urlRouterProvider.when(&quot;&quot;,&quot;/home&quot;);    $stateProvider.state(&#x27;home&#x27;,&#123;        url:&quot;/home&quot;,        templateUrl: &#x27;views/homepage.html&#x27;,        controller: &#x27;homeController&#x27;,        resolve:&#123;            loadMyCtrl:[&#x27;$ocLazyLoad&#x27;,function ($ocLazyLoad) &#123;                return $ocLazyLoad.load(&#123;                    name:&quot;homeApp&quot;,                    files:[&quot;public/js/homepage.js&quot;]                &#125;)            &#125;]         &#125;    &#125;);    $stateProvider.state(&#x27;page2&#x27;,&#123;        url:&quot;/page2&quot;,        templateUrl:&#x27;views/page2.html&#x27;,        resolve:&#123;            loadMyCtrl:function ($ocLazyLoad) &#123;                return $ocLazyLoad.load(&#123;                    name:&#x27;page2App&#x27;,                    files:[&quot;public/js/page2.js&quot;]                &#125;)            &#125;        &#125;    &#125;)    $stateProvider.state(&#x27;page3&#x27;,&#123;        url:&quot;/page3&quot;,        templateUrl:&#x27;views/page3.html&#x27;,        resolve:&#123;            loadMyCtrl:function ($ocLazyLoad) &#123;                return $ocLazyLoad.load(&#123;                    name:&#x27;page3App&#x27;,                    files:[&quot;public/js/page3.js&quot;,&quot;public/js/page3-ext.js&quot;]                &#125;)            &#125;        &#125;    &#125;)    &#125;);

&lt;!--dynamic/views/homepage.html--&gt;&lt;div id=&quot;home&quot; ng-controller=&quot;homeController&quot;&gt;    &lt;h1&gt;首页&lt;/h1&gt;    &#123;&#123;content&#125;&#125;&lt;/div&gt;

/* *dynamic/public/js/homepage.js*/angular.module(&#x27;myApp&#x27;).controller(&#x27;homeController&#x27;, function ($scope) &#123;     $scope.content = &#x27;这是主页的内容&#x27;;&#125;);

完整代码github自取：https://github.com/voidking/angulardemo/tree/master/dynamic
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>angularjs</tag>
      </tags>
  </entry>
  <entry>
    <title>AngularJS入门篇</title>
    <url>/dev-angularjs-start/</url>
    <content><![CDATA[1. 前言AngularJS是一个JavaScript框架，它通过指令扩展了HTML，且通过表达式绑定数据到 HTML。
什么是框架？比如struts2、spring、hibernate、thinkphp、wordpress等等。什么是组件？比如jdbc、jquery、swiper、layer、arttemplate等等。
一般来说，那些可复用的、用于简化开发工作的代码集合，大的叫框架，小的叫组件。有人说jquery是框架？当然可以，大小并没有明确边界。不要太纠结于概念，如无必要，勿增实体。
本文，主要学习归纳一下Angular的各种特性，包括双向数据绑定、定义应用和控制器、优化模板渲染延迟、自定义指令、作用域、HTTP请求获取数据、自定义服务、依赖注入、路由控制等。最后，会给出一个综合实例。
参考文档：

AngularJS实战
AngularJS 教程 | 菜鸟教程
AngularJS中文网
AngularJS中文社区
图灵社区: 合集 : AngularJS入门教程
AngularJS: API: API Reference
ngCloak
AngularJS : Why ng-bind is better than 双括号 in angular?
Metronic3.3网页模板在线演示
框架到底是个什么东西？
理解AngularJS中的依赖注入
Hexo的一个小BUG(Template render error)



2. 双向数据绑定单向数据绑定的原理：模板+数据=&gt;视图。目前大多数前端框架都是单向数据绑定，比如jQueryUI、BackBone、Flex。
双向数据绑定原理：模板+数据=&gt;视图，模板+视图=&gt;数据。
Angular采用的，就是双向数据绑定。
&lt;!--helloworld.html--&gt;&lt;!DOCTYPE html&gt;&lt;html ng-app&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;双向数据绑定&lt;/title&gt;    &lt;script src=&quot;http://code.angularjs.org/angular-1.0.1.min.js&quot;&gt;&lt;/script&gt;&lt;/head&gt;&lt;body&gt;    Hello &#123;&#123;&#x27;World&#x27;&#125;&#125;!&lt;br/&gt;    Your name: &lt;input type=&quot;text&quot; ng-model=&quot;yourname&quot; placeholder=&quot;World&quot;&gt;    &lt;hr&gt;    Hello &#123;&#123;yourname || &#x27;World&#x27;&#125;&#125;!&lt;/body&gt;&lt;/html&gt;

3. 定义应用和控制器angular对象，是Angular的根对象。类似于express框架中的express对象，类似于seajs框架的seajs对象，类似于浏览器的window对象。如果说angular对象是Angular中的班主任，那么应用（或者叫模块，app）就是Angular中的班长！而班主任不常出没，管事的就是班长。控制器（controller），就是普通同学小明，负责控制Angular应用程序中的数据。
&lt;!--app.html--&gt;&lt;!DOCTYPE html&gt;&lt;html ng-app=&quot;myApp&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;定义应用和控制器&lt;/title&gt;    &lt;style&gt;        [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak &#123;          display: none !important;        &#125;    &lt;/style&gt;&lt;/head&gt;&lt;body&gt;    &lt;div ng-controller=&quot;myCtrl&quot;&gt;    名: &lt;input type=&quot;text&quot; ng-model=&quot;firstName&quot;&gt;&lt;br&gt;    姓: &lt;input type=&quot;text&quot; ng-model=&quot;lastName&quot;&gt;&lt;br&gt;    &lt;br&gt;    姓名: &lt;span&gt;&#123;&#123;firstName + &quot; &quot; + lastName&#125;&#125;&lt;/span&gt;&lt;br&gt;    姓名2: &lt;span class=&quot;ng-cloak&quot;&gt;&#123;&#123;fullName()&#125;&#125;&lt;/span&gt;&lt;br&gt;    姓名3: &lt;span ng-bind=&quot;fullName()&quot;&gt;&lt;/span&gt;    &lt;/div&gt;    &lt;script src=&quot;http://code.angularjs.org/angular-1.0.1.min.js&quot;&gt;&lt;/script&gt;    &lt;script&gt;        var app = angular.module(&#x27;myApp&#x27;, []);        app.controller(&#x27;myCtrl&#x27;, function($scope) &#123;            $scope.firstName= &quot;John&quot;;            $scope.lastName= &quot;Doe&quot;;            $scope.fullName = function() &#123;                return $scope.firstName + &quot; &quot; + $scope.lastName;            &#125;        &#125;);    &lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

4. 优化模板渲染延迟在定义应用和控制器的例子中，我们看到，页面上先出现了表达式，之后才出现我们期望的结果。解决这个问题，常用的有两个办法。一个是使用ng-bind，另一个是添加ng-cloak样式。
5. 自定义指令&lt;!--directive.html--&gt;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;自定义指令&lt;/title&gt;    &lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt; &lt;/head&gt;&lt;body ng-app=&quot;myApp&quot;&gt;    &lt;runoob-directive&gt;&lt;/runoob-directive&gt;    &lt;div runoob-directive&gt;&lt;/div&gt;    &lt;div class=&quot;runoob-directive&quot;&gt;&lt;/div&gt;    &lt;!-- 指令: runoob-directive --&gt;    &lt;script&gt;        var app = angular.module(&quot;myApp&quot;, []);        app.directive(&quot;runoobDirective&quot;, function() &#123;            return &#123;                //restrict : &quot;A&quot;,                //restrict : &quot;C&quot;,                //restrict : &quot;M&quot;,                //replace : true,                template : &quot;&lt;h1&gt;自定义指令!&lt;/h1&gt;&quot;            &#125;;        &#125;);    &lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

6. 作用域&lt;!--scope.html--&gt;&lt;!DOCTYPE html&gt;&lt;html ng-app=&quot;myApp&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;utf-8&quot;&gt;    &lt;title&gt;作用域&lt;/title&gt;    &lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt;&lt;/head&gt;&lt;body&gt;    &lt;div ng-controller=&quot;myCtrl&quot;&gt;        &lt;h1&gt;姓氏为 &#123;&#123;lastname&#125;&#125; 家族成员:&lt;/h1&gt;        &lt;ul&gt;            &lt;li ng-repeat=&quot;x in names&quot;&gt;&#123;&#123;x&#125;&#125; &#123;&#123;lastname&#125;&#125;&lt;/li&gt;        &lt;/ul&gt;    &lt;/div&gt;    &lt;script&gt;        var app = angular.module(&#x27;myApp&#x27;, []);        app.controller(&#x27;myCtrl&#x27;, function($scope, $rootScope) &#123;            $scope.names = [&quot;Emil&quot;, &quot;Tobias&quot;, &quot;Linus&quot;];            $rootScope.lastname = &quot;Refsnes&quot;;        &#125;);    &lt;/script&gt;    &lt;p&gt;注意 $rootScope 在循环对象内外都可以访问。&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

上面的例子中，$scope的作用域为myCtrl这个ng-controller的范围，$rootScope的作用域为myApp这个ng-app的范围。
7. HTTP请求获取数据7.1. 获取本地数据&lt;!--http.html--&gt;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;    &lt;meta charset=&quot;utf-8&quot;&gt;    &lt;title&gt;HTTP请求&lt;/title&gt;    &lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt;&lt;/head&gt;&lt;body ng-app=&quot;myApp&quot; &gt;    &lt;div ng-controller=&quot;myCtrl&quot;&gt;         &lt;h1&gt;欢迎你！&#123;&#123;username&#125;&#125;&lt;/h1&gt;    &lt;/div&gt;    &lt;p&gt; $http 服务向服务器请求信息，返回的值放入变量 &quot;username&quot; 中。&lt;/p&gt;&lt;script&gt;    var app = angular.module(&#x27;myApp&#x27;, []);    app.controller(&#x27;myCtrl&#x27;, function($scope, $http) &#123;      $http.get(&quot;http.json&quot;).then(function (response) &#123;          $scope.username = response.data.username;      &#125;);    &#125;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

http.json中的内容为：
&#123;    &quot;username&quot;:&quot;voidking&quot; &#125;

需要注意的是，本例需要在服务器中访问。因为Angular的HTTP请求封装了XMLHttpRequest，而XMLHttpRequest的使用需要服务器环境。
7.2. 获取服务器数据&lt;!--http2.html--&gt;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;    &lt;meta charset=&quot;utf-8&quot;&gt;    &lt;title&gt;HTTP请求服务器数据&lt;/title&gt;    &lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt;&lt;/head&gt;&lt;body ng-app=&quot;myApp&quot; &gt;    &lt;div ng-controller=&quot;myCtrl&quot;&gt;     &lt;h1&gt;欢迎你！&#123;&#123;username&#125;&#125;&lt;/h1&gt;    &lt;/div&gt;    &lt;p&gt; $http 服务向服务器请求信息，返回的值放入变量 &quot;username&quot; 中。&lt;/p&gt;&lt;script&gt;    var app = angular.module(&#x27;myApp&#x27;, []);    app.controller(&#x27;myCtrl&#x27;, function($scope, $http, $httpParamSerializer) &#123;        $http(&#123;            method:&#x27;POST&#x27;,            url:&#x27;/angulardemo/http.php&#x27;,            headers:&#123;                &#x27;Content-Type&#x27;:&#x27;application/x-www-form-urlencoded&#x27;            &#125;,            dataType: &#x27;json&#x27;,            data: $httpParamSerializer(&#123;username:&#x27;voidking&#x27;&#125;)        &#125;).then(function successCallback(response) &#123;            console.log(response.data);            $scope.username = response.data.username;        &#125;, function errorCallback(response) &#123;            console.log(response.data);        &#125;);;    &#125;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

新建http.php，内容如下：
&lt;?php     $username = $_POST[&#x27;username&#x27;];    $result = array(        &#x27;code&#x27; =&gt; &#x27;0&#x27;,        &#x27;ext&#x27; =&gt; &#x27;success&#x27;,        &#x27;username&#x27; =&gt; $username    );    echo json_encode($result);?&gt;


8. 自定义服务&lt;!--service.html--&gt;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;    &lt;meta charset=&quot;utf-8&quot;&gt;    &lt;title&gt;自定义Service&lt;/title&gt;    &lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt;&lt;/head&gt;&lt;body ng-app=&quot;myApp&quot; &gt;    &lt;div ng-controller=&quot;myCtrl&quot;&gt;        &lt;p&gt;自定义服务，用于转换16进制数：&lt;/p&gt;        &lt;p&gt;255 的16进制是:&lt;/p&gt;        &lt;h1&gt;&#123;&#123;hex&#125;&#125;&lt;/h1&gt;        &lt;hr&gt;        &lt;p&gt;在获取数组 [255, 251, 200] 值时使用过滤器:&lt;/p&gt;        &lt;ul&gt;          &lt;li ng-repeat=&quot;x in counts&quot;&gt;&#123;&#123;x | myFormat&#125;&#125;&lt;/li&gt;        &lt;/ul&gt;    &lt;/div&gt;&lt;script&gt;    var app = angular.module(&#x27;myApp&#x27;, []);    app.service(&#x27;hexafy&#x27;, function() &#123;        this.myFunc = function (x) &#123;            return x.toString(16);        &#125;    &#125;);    app.controller(&#x27;myCtrl&#x27;, function($scope, hexafy) &#123;        $scope.hex = hexafy.myFunc(255);        $scope.counts = [255, 251, 200];    &#125;);    app.filter(&#x27;myFormat&#x27;,[&#x27;hexafy&#x27;, function(hexafy) &#123;        return function(x) &#123;            return hexafy.myFunc(x);        &#125;;    &#125;]);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

当创建了自定义服务，并连接到应用上后，我们可以在控制器，指令，过滤器或其他服务中使用它。
9. 依赖注入AngularJS 提供很好的依赖注入机制。什么是依赖注入？wiki 上的解释是：依赖注入（Dependency Injection，简称DI）是一种软件设计模式，在这种模式下，一个或更多的依赖（或服务）被注入（或者通过引用传递）到一个独立的对象（或客户端）中，然后成为了该客户端状态的一部分。该模式分离了客户端依赖本身行为的创建，这使得程序设计变得松耦合，并遵循了依赖反转和单一职责原则。与服务定位器模式形成直接对比的是，它允许客户端了解客户端如何使用该系统找到依赖。
&lt;!--di.html--&gt;&lt;!DOCTYPE html&gt;&lt;html&gt; &lt;head&gt;   &lt;meta charset=&quot;utf-8&quot;&gt;   &lt;title&gt;AngularJS依赖注入&lt;/title&gt;&lt;/head&gt;   &lt;body ng-app=&quot;mainApp&quot; &gt;   &lt;h2&gt;AngularJS 简单应用&lt;/h2&gt;   &lt;div ng-controller=&quot;CalcController&quot;&gt;      &lt;p&gt;配置：&#123;&#123;constant&#125;&#125;&lt;/p&gt;      &lt;p&gt;输入一个数字: &lt;input type = &quot;number&quot; ng-model = &quot;number&quot; /&gt;&lt;/p&gt;      &lt;button ng-click = &quot;square()&quot;&gt;X&lt;sup&gt;2&lt;/sup&gt;&lt;/button&gt;      &lt;p&gt;结果: &#123;&#123;result&#125;&#125;&lt;/p&gt;   &lt;/div&gt;   &lt;hr&gt;   &lt;div ng-controller=&quot;CalcController2&quot;&gt;      &lt;p&gt;再输入一个数字: &lt;input type = &quot;number&quot; ng-model = &quot;number&quot; /&gt;&lt;/p&gt;      &lt;button ng-click = &quot;square()&quot;&gt;X&lt;sup&gt;2&lt;/sup&gt;&lt;/button&gt;      &lt;p&gt;结果: &#123;&#123;result&#125;&#125;&lt;/p&gt;   &lt;/div&gt;   &lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt;&lt;script&gt;   var mainApp = angular.module(&quot;mainApp&quot;, []);   mainApp.config(function($provide) &#123;      // 创建一个名叫MathService的provider      $provide.provider(&#x27;MathService&#x27;, function() &#123;         this.$get = function() &#123;            var factory = &#123;&#125;;                     factory.multiply = function(a, b) &#123;               return a * b;            &#125;            return factory;         &#125;;      &#125;);   &#125;);   // 创建一个名叫defaultInput的value   mainApp.value(&quot;defaultInput&quot;, 5);   // 创建一个名叫constant的constant value   mainApp.constant(&quot;constant&quot;, &quot;constant value&quot;);   // 将MathService、defaultInput、constant注入到控制器   mainApp.controller(&#x27;CalcController&#x27;, function($scope, MathService, defaultInput, constant) &#123;      $scope.number = defaultInput;      $scope.constant = constant;      $scope.result = MathService.multiply($scope.number,$scope.number);      $scope.square = function() &#123;         $scope.result = MathService.multiply($scope.number,$scope.number);      &#125;   &#125;);   /*--------以下是CalcController2的内容--------*/   // 创建一个名叫MathService2的factory   mainApp.factory(&#x27;MathService2&#x27;, function() &#123;      var factory = &#123;&#125;;            factory.multiply = function(a, b) &#123;         return a * b;      &#125;      return factory;   &#125;);      // 创建一个名叫CalcService2的service，并且注入MathService2   mainApp.service(&#x27;CalcService2&#x27;, function(MathService2)&#123;      this.square = function(a) &#123;         return MathService2.multiply(a,a);      &#125;   &#125;);   // 将CalcService2注入到控制器   mainApp.controller(&#x27;CalcController2&#x27;,function($scope,CalcService2)&#123;      $scope.number = 6;      $scope.result = CalcService2.square($scope.number);      $scope.square = function() &#123;         $scope.result = CalcService2.square($scope.number,$scope.number);      &#125;   &#125;);    &lt;/script&gt;   &lt;/body&gt;&lt;/html&gt;

provider()函数是用来创建provider对象的标准方法。
实际上，value()、constant()、factory()、service()全都是用来创建一个provider对象的方法，它们提供了一种方式来定义一个provider，而无需输入所有的复杂的代码。
10. 路由控制AngularJS 路由允许我们通过不同的 URL 访问不同的内容。通过 AngularJS 可以实现多视图的单页Web应用（single page web application，SPA）。通常我们的URL形式为http://runoob.com/first/page ，但在单页Web应用中AngularJS 通过 # + 标记 实现，例如：
http://runoob.com/#/firsthttp://runoob.com/#/secondhttp://runoob.com/#/third

当我们点击以上的任意一个链接时，向服务端请的地址都是一样的 (http://runoob.com/)。 因为 # 号之后的内容在向服务端请求时会被浏览器忽略掉。 所以我们就需要在客户端实现 # 号后面内容的功能实现。 AngularJS 路由 就通过 # + 标记 帮助我们区分不同的逻辑页面并将不同的页面绑定到对应的控制器上。

AngularJS 模块的 config 函数用于配置路由规则。通过使用 configAPI，我们请求把$routeProvider注入到我们的配置函数并且使用$routeProvider.whenAPI来定义我们的路由规则。$routeProvider 为我们提供了 when(path,object) &amp; otherwise(object) 函数按顺序定义所有路由，函数包含两个参数:第一个参数是 URL 或者 URL 正则规则。第二个参数是路由配置对象。
&lt;!--router.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;路由控制&lt;/title&gt;&lt;/head&gt;&lt;body ng-app=&quot;ngRouteExample&quot; class=&quot;ng-scope&quot;&gt;    &lt;div&gt;         &lt;div id=&quot;navigation&quot;&gt;          &lt;a href=&quot;#/home&quot;&gt;Home&lt;/a&gt;        &lt;a href=&quot;#/about&quot;&gt;About&lt;/a&gt;    &lt;/div&gt;          &lt;div ng-view=&quot;&quot;&gt;    &lt;/div&gt;&lt;script type=&quot;text/ng-template&quot; id=&quot;embedded.home.html&quot;&gt;    &lt;h1&gt; Home &lt;/h1&gt;&lt;/script&gt;&lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;http://apps.bdimg.com/libs/angular-route/1.3.13/angular-route.js&quot;&gt;&lt;/script&gt;&lt;script type=&quot;text/javascript&quot;&gt;    angular.module(&#x27;ngRouteExample&#x27;, [&#x27;ngRoute&#x27;])    .controller(&#x27;HomeController&#x27;, function ($scope, $route) &#123; $scope.$route = $route;&#125;)    .controller(&#x27;AboutController&#x27;, function ($scope, $route) &#123; $scope.$route = $route;&#125;)    .config(function ($routeProvider) &#123;        $routeProvider.        when(&#x27;/home&#x27;, &#123;            templateUrl: &#x27;embedded.home.html&#x27;,            controller: &#x27;HomeController&#x27;        &#125;).        when(&#x27;/about&#x27;, &#123;            templateUrl: &#x27;about.html&#x27;,            controller: &#x27;AboutController&#x27;        &#125;).        otherwise(&#123;            redirectTo: &#x27;/home&#x27;        &#125;);    &#125;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

&lt;!--about.html--&gt;&lt;h1&gt; About &lt;/h1&gt;

11. 综合&lt;!--complex.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;综合实例&lt;/title&gt;&lt;/head&gt;&lt;body ng-app=&quot;ngRouteExample&quot; class=&quot;ng-scope&quot;&gt;    &lt;div&gt;         &lt;div id=&quot;navigation&quot;&gt;          &lt;a href=&quot;#/page1&quot;&gt;Page1&lt;/a&gt;        &lt;a href=&quot;#/page2&quot;&gt;Page2&lt;/a&gt;    &lt;/div&gt;         &lt;div ng-view=&quot;&quot;&gt;    &lt;/div&gt;&lt;script src=&quot;http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;http://apps.bdimg.com/libs/angular-route/1.3.13/angular-route.js&quot;&gt;&lt;/script&gt;&lt;script type=&quot;text/javascript&quot;&gt;        var myApp = angular.module(&#x27;ngRouteExample&#x27;, [&#x27;ngRoute&#x27;]);    myApp.controller(&#x27;Page1Controller&#x27;, function ($scope, $route) &#123;         $scope.$route = $route;        $scope.content = &#x27;这是page1的内容&#x27;;    &#125;);    myApp.controller(&#x27;Page2Controller&#x27;, function ($scope, $route) &#123;         $scope.$route = $route;        $scope.content = &#x27;这是page2的内容&#x27;;    &#125;)    myApp.config(function ($routeProvider) &#123;        $routeProvider.        when(&#x27;/page1&#x27;, &#123;            templateUrl: &#x27;complex-page1.html&#x27;,            controller: &#x27;Page1Controller&#x27;        &#125;).        when(&#x27;/page2&#x27;, &#123;            templateUrl: &#x27;complex-page2.html&#x27;,            controller: &#x27;Page2Controller&#x27;        &#125;).        otherwise(&#123;            redirectTo: &#x27;/page1&#x27;        &#125;);    &#125;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

&lt;!--complex-page1.html--&gt;&lt;div id=&quot;page1&quot; ng-controller=&quot;Page1Controller&quot;&gt;    &lt;h1&gt;Page1&lt;/h1&gt;    &lt;p&gt;&#123;&#123;content&#125;&#125;&lt;/p&gt;&lt;/div&gt;

&lt;!--complex-page2.html--&gt;&lt;div id=&quot;page2&quot; ng-controller=&quot;Page2Controller&quot;&gt;    &lt;h1&gt;Page2&lt;/h1&gt;    &lt;p&gt;&#123;&#123;content&#125;&#125;&lt;/p&gt;&lt;/div&gt;

12. 后记至于输入验证、事件、动画、API等，本文不再讨论，用到时自行查阅文档。本文完整源码地址：https://github.com/voidking/angulardemo
记录一个hexo的坑：如果文中出现了双括号，而且双括号没有被代码块包含，那么解析会报错，无法生成页面。
查找到的解决办法：
&#123;% raw %&#125;内容&#123;% endraw %&#125;

经测试，无效，就用汉字代替好了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>angularjs</tag>
        <tag>hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>Web安全之漏洞扫描</title>
    <url>/dev-web-security-vulnerability-scan/</url>
    <content><![CDATA[1. 漏洞扫描概述漏洞扫描器可以快速帮助我们发现漏洞，例如，SQL注入漏洞（SQL injection）、跨站脚本攻击（cross site scripting）、缓冲区溢出（buffer overflow）。一个好的漏洞扫描器在渗透测试中是至关重要的，可以说是渗透成功或者失败的关键点。


2. 漏洞扫描工具一款优秀的漏洞扫描器会使渗透测试变得轻松，但对于一些漏洞，自动化软件是无法识别的，例如，逻辑性漏洞，及其隐蔽的XSS漏洞或者SQL注入漏洞。所以，在进行漏扫时，必须要与人工渗透相结合。
漏洞扫描也属于信息探测的一种，扫描器可以帮助我们发现非常多的问题。
本章主要介绍了几款工具的使用，郝同学更加喜欢在应用中学习，因此，不再摘抄书中内容，仅记录一下三款优秀的工具，具体用法自行百度。
2.1. Burp Suite在sectools.org Web扫描模块位居第一名。
2.2. AWVSAWVS（Acunetix Web Volunerability Scanner）是一个自动化的Web应用程序安全测试工具，它可以扫描任何可通过Web浏览器访问的和遵循HTTP/HTTPS规则的Web站点和Web应用程序。
也有很多人喜欢把AWVS改为WVS称呼，两者是同一款工具。
WVS可以快速扫描跨站脚本攻击（XXS）、SQL注入攻击、代码执行、代码执行、目录遍历攻击、文件入侵、脚本源代码泄漏、CRLF注入、PHP代码注入、XPath注入、LDAP注入、Cookie操纵、URL重定向、应用程序错误消息等。
官网：http://www.acunetix.com/
2.3. AppScanAppScan是IBM公司出品的一个领先的Web应用安全测试工具，曾以Watchfire AppScan的名称享誉业界。AppScan可自动化Web应用的安全漏洞评估工作，能扫描和检测所有常见的Web应用安全漏洞，例如，SQL注入、跨站点脚本攻击、缓冲区溢出及最新的Flash/Flex应用和Web2.0应用暴露等方面的安全漏洞扫描。
其他国外工具：HP Webinspect、Owasp Zap、Nikto、Owasp WebScarab、w3af、Netsparker。
其他国内工具：JSKY、Safe3 Web Vul Scanner、安恒信息明鉴Web应用弱点扫描器、极光远程安全评估系统。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>安全</tag>
        <tag>黑客</tag>
      </tags>
  </entry>
  <entry>
    <title>Web安全之信息探测</title>
    <url>/dev-web-security-info-detection/</url>
    <content><![CDATA[1. 信息探测在进行安全测试之前，最重要的一步就是信息探测。信息探测时应该搜集哪些资料呢？其实最主要的就是与服务器的配置信息和网站的信息，包括网站注册人、目标网站系统、目标服务器系统、目标网站相关子域名、目标服务器开放的端口和服务器存放网站等。


2. NmapNmap是一个开源的网络连接端扫描软件，用来扫描计算机开放的网络连接端，确定哪些服务运行在哪些连接端，并且推断计算机运行哪个操作系统。另外，它也用于评估网络系统安全。
2.1. 常用命令1、扫描主机列表nmap -sL 192.168.199.1/24
2、扫描特定主机上的特定端口nmap -p 80,21,23 192.168.199.1
3、探测主机操作系统nmap -O 192.168.199.1
4、全面的系统探测nmap -v -A 192.168.199.1
5、穿透防火墙的进行扫描nmap -Pn -A 192.168.199.1
6、使用文件。如果你有一个ip地址列表，将这个保存为一个txt文件，和命令行执行路径相同，扫描这个txt内的所有主机。nmap -iL target.txt
PS：target.txt内容
127.0.0.1www.baidu.com

7、保存扫描结果nmap -O 127.0.0.1 -oN result.txt
8、windows扫描本机nmap -sT 127.0.0.1
2.2. 脚本引擎在Nmap安装目录下存在Script文件夹，在Script文件夹中存在许多以“.nse”后缀结尾的文本文件，即Nmap自带的脚本引擎。
1、扫描Web敏感目录nmap -p 80 --script=http-enum.nse 192.168.199.1
2、扫描SqlInjectionnmap -p 80 --script=sql-injection.nse 192.168.199.1
3、使用通配符扫描nmap --script=&quot;http-*&quot; 192.168.199.1
4、使用所有的脚本进行扫描nmap --script all 192.168.199.1
3. DirBuster在渗透测试中，探测Web目录结构和隐藏的敏感文件是必不可少的一部分。通过探测可以了解网站的结构，获取管理员的一些敏感信息，比如网站的后台管理页面、文件上传界面，有事甚至可能扫描出网站的源代码。而DirBuster就是完成这些功能的一款优秀的资源探测工具。
图形化界面，熟悉下即可。
4. 指纹识别此处的指纹识别并非一些门禁指纹识别、财务指纹识别、汽车指纹识别等，而是针对计算机或计算机系统的某些服务的指纹识别。
Namp的nmap -O命令，御剑的指纹识别，AppPrint等。
5. 书签
Nmap: the Network Mapper - Free Security Scanner
DirBuster download | SourceForge.net
Nmap scan produces all “unknown”
Zenmap prits all states as unknown

]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>安全</tag>
        <tag>黑客</tag>
      </tags>
  </entry>
  <entry>
    <title>Cloudflare配置概述</title>
    <url>/dev-cloudflare-config/</url>
    <content><![CDATA[1. 添加域名
Create a Cloudflare account and add your domain.

Choose Plan.

Change your nameservers.




2. 快速配置快速配置地址：https://dash.cloudflare.com/&lt;xxx&gt;/voidking.com/recommendations其中 &lt;xxx&gt; 要改成Cloudflare自动生成的随机字符串。

提高安全性，自动HTTPS重写，始终使用HTTPS
优化性能，Brotli压缩
摘要

3. DNS记录配置Cloudflare的DNS记录配置，支持两种解析方式：

仅DNS：和其他域名服务器一样，域名解析到指定的地址；
代理：通过Cloudflare代理真实服务器的流量，也就是说域名解析到的地址是代理地址，真实IP地址会被隐藏。

解析方式是代理的情况下，就可以防DDoS了，因为代理服务器是防DDoS的，黑客攻击不到我们的真实IP地址。
注意：配置 Cloudflare DNS 代理模式时，源站（服务器）是否需要配置 SSL 证书，取决于我们在 Cloudflare 中选择的 SSL/TLS 加密模式，不同的模式对源站证书有不同要求。

当 SSL/TLS 模式选择关闭或者灵活时，源站不需要配置SSL证书，但是这样安全性就降低了。
建议选择完全（严格），此时使用CloudFlare的源服务器CA签发证书，配置到源站上。
还可以选择完全，此时可以使用自建CA签发证书，配置到源站上。具体做法可以参考《好好学Linux：Linux中安装使用mkcert签发域名证书》。
最后还可以选择严格，此时要求源站配置有权威机构CA签发的合法的证书。

4. Cloudflare IP地址范围如果在配置Cloudflare解析时，配置了Cloudflare作为代理，那么建议在源服务器上配置白名单，仅允许Cloudflare访问。配置了白名单后，万一我们的真实IP地址泄露了，攻击者也无法轻易攻击网站。
Cloudflare IP地址范围：

Cloudflare IPv4
Cloudflare IPv6

5. 配置域名重定向
登录 Cloudflare 控制台  

进入Rules，Page Rules

创建页面规则  



URL 字段输入旧域名的匹配模式，例如：olddomain.com/* 
Pick a Setting 选择 Forwarding URL
Select status code 选择 301 - Permanent Redirect
Enter destination URL 输入新域名，例如：https://newdomain.com/$1（$1 保留原路径）  


保存并部署（通常几分钟内生效）

确保DNS记录正确旧域名需在 DNS 中添加一条 A记录（如 @ 或 www），IP可随意（如 192.0.2.1或者 8.8.8.8），但必须开启 代理（橙色云图标）。


6. CloudFlare for SaaS配置6.1. CloudFlare for SaaS简介CloudFlare for SaaS允许未直接使用 Cloudflare NS 的域名仍可享受CloudFlare CDN 和安全服务，需要商业版或者企业版才能支持。
6.2. 添加域名
Create a Cloudflare account and add your domain.

For your Plan, choose Business or Enterprise.

Add your domain to Cloudflare. You should land on the Overview page.

Ignore the instructions to change your nameservers.

For Advanced Actions, click Convert to CNAME DNS Setup.


6.3. 验证域名Once you add your domain to Cloudflare, add the Verification TXT Record at your authoritative DNS provider. Cloudflare will verify the TXT record and send a confirmation email. This can take up to a few hours.
6.4. 添加解析
In Cloudflare, add an A, AAAA, or CNAME record.

At your authoritative DNS provider:


  a. Remove any existing A, AAAA, or CNAME records on the hostname you want to proxy to Cloudflare.
  b. Add a CNAME record for {your-hostname}.cdn.cloudflare.net.
  c. Repeat this process for each subdomain proxied to Cloudflare.
7. 问题记录7.1. 后端响应时间限制使用CloudFlare作为代理后，真实后端响应时间必须在100s以内，否则会报错524。解决办法是升级企业版，详情参考Troubleshooting Cloudflare 5XX errors – Cloudflare Help Center
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>cloudflare</tag>
      </tags>
  </entry>
  <entry>
    <title>Web安全之防御DDoS</title>
    <url>/dev-web-security-defend-ddos/</url>
    <content><![CDATA[1. DDoS简介DDoS(Distributed Denial of Service) 是一种攻击方式，中文翻译为分布式拒绝服务。DDoS攻击通过利用大量的计算机或设备向目标网络或服务器发送海量的请求，以消耗其带宽、处理器或存储等资源，从而使其无法正常服务。
相关文档：

WikiPedia - Denial-of-service attack
如何发动 DDoS 攻击 | DoS 和 DDoS 攻击工具



2. 常见防御方法
增加带宽：扩大带宽是一种最常见的DDoS攻击防御方法。增加带宽可以分摊攻击流量，从而减轻攻击对服务器的影响。
安装防火墙：防火墙可以监控和控制网络流量，过滤掉异常流量和攻击流量。防火墙可以根据一些策略进行配置，例如限制IP地址和端口号。
CDN加速：CDN（内容分发网络）可以将静态和动态内容缓存到全球各地的节点上，使用户能够更快地访问网站内容。CDN可以防止DDoS攻击，因为攻击者无法攻击所有节点。
防DDoS设备：防DDoS设备可以在攻击开始时自动检测并过滤攻击流量。这些设备可以使用各种算法进行攻击检测和流量分析，并对攻击流量进行过滤和阻止。
使用云服务：云服务提供商可以使用多个数据中心和高度可用的服务来防止DDoS攻击。通过使用云服务，网站管理员可以更好地分摊攻击流量，同时提高网站的可用性和稳定性。

3. 选择防御方法
增加带宽：不合适。LB本身就没有带宽限制。
安装防火墙：不合适。腾讯云WAF太贵（3880元/月 - 28880元/月），防止DDoS服务还要单独购买（10万元/年 - 300万元/年）；自己搭建DDoS防火墙，需要专职的安全人员开发，成本更高。
CDN加速：备选。配置简单，价格合适（还有免费版）。
防DDoS设备：不合适。价格昂贵（上百万一台）。
使用云服务：不合适。没有必要提供那么强的负载能力。

综上，选择CDN加速进行DDoS防护，是最优选。
4. CDN调研4.1. Cloudflare CDN4.1.1. Cloudflare CDN防护原理Cloudflare CDN加速防御DDoS攻击的基本原理：

分布式架构：Cloudflare使用分布式的CDN架构，在全球各地部署了大量的数据中心和服务器节点。这些节点可以缓存和分发网站的静态内容和动态内容，并提供基于云端的防御和安全服务。
基于行为的分析：Cloudflare使用基于行为的分析技术，对访问网站的流量进行实时分析和识别。当检测到异常的访问行为时，例如大量请求来自同一IP地址或同一地区的访问等，Cloudflare会自动封锁这些IP地址或地区的流量，以减轻DDoS攻击的影响。
基于IP地址的访问控制：Cloudflare可以通过设置IP白名单和黑名单，限制访问网站的IP地址范围。这可以帮助阻止一些恶意IP地址和流量，以保护网站的安全性和稳定性。
流量限制：Cloudflare可以限制访问网站的流量速率和数量，以避免过度的流量消耗和服务器崩溃。当检测到DDoS攻击的流量时，Cloudflare会自动限制流量速率和数量，以减轻攻击的影响。
DDoS防火墙：Cloudflare还提供了一些高级防御和防护功能，例如Web应用程序防火墙（WAF）和SSL证书等。WAF可以识别和阻止一些常见的攻击和漏洞，例如SQL注入、XSS等。SSL证书可以为网站提供HTTPS加密和身份认证功能，以保护用户的隐私和安全。

使用Cloudflare CDN之后，通过dig还能找到真实后端服务器吗？使用Cloudflare CDN加速后，通过dig命令（或其他查询DNS记录的工具）查询域名解析结果，通常只能查到Cloudflare的DNS记录，而无法直接查到真实的后端服务器。这是因为Cloudflare CDN使用了反向代理和缓存技术，将网站的内容缓存在Cloudflare的边缘节点上，并使用Cloudflare的IP地址作为前端入口，而后端的真实服务器地址通常不会暴露在外部。
4.1.2. Cloudflare CDN注意事项使用Cloudflare的CDN加速，可以实现防护DDoS。
Cloudflare收费标准：

免费版：支持DNS解析、DDoS防护、CDN、SSL证书，足够我们使用了。
付费Pro版：25刀/月
付费Business版：250刀/月
付费Enterprise版：250刀+/月

免费版和付费Pro版，添加站点时，最后一步会提示修改NS地址，也就是说该域名所有的解析都要托管到Cloudflare，这就很不友好了。对于付费Business版和Enterprise版，如果不想修改NS地址，那么建议使用Partial (CNAME) setup，配置方法参考《Cloudflare配置概述》。对于免费版和付费Pro版，如果不想修改NS地址，那么建议使用Cloudflare for SaaS，不过这种配置方式可能会被拦截，页面出现 警方提示疑似诈骗 联系电话：0592-96110。
参考文档：

安全、性能和可靠性 —— 一个软件包全部搞定
Partial (CNAME) setup
Cloudflare for SaaS
最新Cloudflare免费CNAME和IP接入教程-无需修改NS直接接入Cloudflare

4.2. 腾讯云CDN收费方式：按访问次数和流量收费是否防DDoS：想要防DDoS，需额外购买SCDN（最基础套餐3800元/月，支持20个域名，10Gbps防护）
结论：可防DDoS，价格不合适
参考文档：

全站加速网络 - 产品概述
全站加速网络 - 计费说明
安全加速 SCDN
SCND - 价格试算

4.3. 阿里云CDN收费方式：按流量计费是否防DDoS：想要防DDoS，需额外购买边缘DDoS（6万块10个域名/每月）
结论：可防DDoS，价格不合适
参考文档：

阿里云 - CDN
阿里云 - CDN计费概述
如何处理加速域名遭受DDoS或CC攻击问题？
阿里云 - 边缘DDoS防护

4.4. AWS CDN收费方式：每月免费1TB流量，1千万请求，超出部分收费是否防DDoS：防DDoS
结论：可防DDoS，备选
参考文档：

Amazon CloudFront
Amazon CloudFront 定价

4.5. Azure CDN收费方式：按流量、请求量和规则数量收费（较贵)是否防DDoS：防DDoS
结论：可防DDoS，价格不合适
参考文档

Azure 内容分发网络
内容分发网络定价

5. 小结综上，从价格方面考虑，最合适的选择是Cloudflare CDN，其次是AWS CDN。
6. 扩展阅读6.1. 腾讯云防护产品6.1.1. 腾讯云CLB腾讯云CLB依靠大禹分布式防御系统能够防御绝大多数网络攻击（例如 DDoS、Web 入侵），针对流量攻击实现秒级清洗，极大避免 IP 被封、带宽被占满等情况发生。CLB 自带的 synproxy 防攻击机制，避免了大禹系统生效之前后端 CVM 被攻击压垮，保护数据更安全稳定。CLB 对每个租户的流量进行严格隔离，提供主动 DDoS 防护能力，公网 CLB 默认支持 DDoS 基础防护。CLB 还支持 DDoS 高防包、DDoS 高防 IP、Web 应用防火墙 等多种安全产品，保障业务安全性。
价格：免费
6.1.2. 腾讯云WAF腾讯云 Web 应用防火墙（Web Application Firewall，WAF）是一款基于 AI 的一站式 Web 业务运营风险防护方案。通过 AI+规则双引擎识别恶意流量，保护网站安全，提高 Web 站点的安全性和可靠性。通过 BOT 行为分析，防御恶意访问行为，保护网站核心业务安全和数据安全。腾讯云 WAF 提供两种类型的云上 WAF，SaaS 型 WAF 和负载均衡型 WAF，两种 WAF 提供的安全防护能力基本相同，接入方式不同。

SaaS 型 WAF 通过 DNS 解析，将域名解析到 WAF 集群提供的 CNAME 地址上，通过 WAF 配置源站服务器 IP，实现域名恶意流量清洗和过滤，将正常流量回源到源站，保护网站安全。
负载均衡型 WAF 通过和腾讯云负载均衡集群进行联动，将负载均衡的 HTTP/HTTPS 流量镜像到 WAF 集群，WAF 进行旁路威胁检测和清洗，将用户请求的可信状态同步到负载均衡集群进行威胁拦截或放行，实现网站安全防护。

腾讯云 WAF 可以有效防御 SQL 注入、XSS 跨站脚本、木马上传、非授权访问等 OWASP 攻击。此外还可以有效过滤 CC 攻击、提供 0day 漏洞补丁、防止网页篡改等，通过多种手段全方位保护网站的系统以及业务安全。
价格：3880元/月 - 28880元/月
6.1.3. DDoS防护DDoS 基础防护是腾讯云免费为云服务器（Cloud Virtual Machine，CVM）、负载均衡（Cloud Load Balancer，CLB）等资源提供的基础 DDoS 防护能力，满足日常安全运营需求。普通用户默认可享受不超过2Gbps防护，VIP 用户 可享受不超过10Gbps防护，境外地区可享受不超过2Gbps防护。腾讯云会根据用户的安全信誉状态，动态调整封堵阈值。安全信誉状态与历史攻击情况、云上基础资源信息等相关。安全信誉过低会影响用户的免费防护能力，直到后续安全信誉恢复。DDoS 基础防护默认开启，实时监控网络流量，发现攻击立即清洗，为腾讯云上公网 IP 秒级开启防护。
除了基础防护，用户还可以按需选购轻量版高防包、DDoS 高防包、DDoS 高防 IP（中国大陆）、DDoS 高防 IP（境外）、DDoS 高防 IP（境外企业版）。
付费版价格：10万元/年 - 300万元/年
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>chatgpt</tag>
        <tag>cloudflare</tag>
        <tag>安全</tag>
        <tag>黑客</tag>
      </tags>
  </entry>
  <entry>
    <title>Web安全之HTTP请求抓包</title>
    <url>/dev-web-security-http-packetcapture/</url>
    <content><![CDATA[1. 前言最近在研读《Web安全深度剖析》和《Web安全深度剖析》，其中的一些实践很有意思，做下记录。


2. 发送HTTP请求2.1. 浏览器使用浏览器，输入网址访问网站，就发起了HTTP请求。
2.2. curl如果不借助浏览器，使用一些命令，也可以发起HTTP请求，比如curl命令。在Windows下，需要单独安装。下载地址：https://curl.haxx.se/download.html 。
安装成功后，我们发起一个HTTP请求：
curl www.baidu.com
返回的内容是乱码。是Windows的问题？换成Linux，依然是乱码。后来查到，从百度获取到的文件，实际上是经过压缩的，需要解压缩，换成命令：
curl www.baidu.com | gunzip
乱码问题解决，但是中文依然是乱码，先不管它。
2.3. telnet使用telnet命令也可以发起HTTP请求。
1、win+R，输入appwiz.cpl，回车，打开或关闭Windows功能，勾选Telnet客户端，确定。2、打开CMD运行框，输入telnet www.baidu.com 80，然后利用快捷键“ctrl+]”来打开telnet回显。3、按回车键，进入编辑状态。4、输入GET /index.html HTTP/1.1，按回车键，接着输入HOST:www.baidu.com，再连续两次按回车键。没错，Windows下中文依然是乱码，在CentOS7下试试：
3. HTTP抓包3.1. WireShark参考文档：《Wireshark入门篇》
3.2. FiddlerFiddler是一款优秀的Web调试工具，它可以记录所有的浏览器与服务器之间的通信信息（HTTP和HTTPS），并且允许你设置断点，修改输入/输出数据。官方下载地址：http://www.telerik.com/fiddler
1、监听HTTP请求安装完成后，无需任何设置，Fiddler就可以监听所有浏览器和其他应用（比如360、QQ）发出的HTTP请求和响应。
2、监听HTTPS请求Fiddler默认不记录HTTPS请求，选择Tools，Fiddler Options，HTTPS，勾选Decrypt HTTPS traffic复选框，单击OK。
3、设置断点方法一：通过Rules，Automatic Breakpoints，选择断点的插入点，三个选项分别是请求之前、响应之后和不拦截。插入断点之后，会应用到所有的请求和响应。
方法二：通过命令进行断点设置。例如，bpu www.baidu.com，会拦截所有发往www.baidu.com的请求；bpafter www.baidu.com，会拦截所有来自www.baidu.com的响应；bpu和bpa，会清除断点。
设置完断点后，我们就可以和使用Burp一样，来拦截HTTP请求并且修改请求信息了。
4、不要误滑滚轮我们可以利用Fiddler自己定义返回给浏览器的内容，非常方便。但是，只要点击“Break on Response”，并且滑动滚轮，自定义内容就默认设置为一张图片，你可以选择其他自定义返回的内容，但是，无法再正常返回。所以，切记不要误滑滚轮！！！
5、中文乱码在response信息中，经常可以看到乱码。因为Fiddler是默认按照UTF-8编码解码的，但是很多网站采用的是GB2312/GBK/GB18030编码，却没有在HEADER中指明编码方式。
解决办法：打开注册表编辑器，找到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fiddler2\，添加字符串值HeaderEncoding，value为GB18030，然后重启Fiddler。但是这样一来，UTF-8编码并且没有指明编码方式的文件，不就变成了乱码了么？所以，建议不要修改。
6、发起HTTP请求Fiddler，可以代替HttpRequester，用来测试接口。
3.3. Burp Suite ProxyBurp Suite是用于Web应用安全测试工具的集成平台，下载地址：https://portswigger.net/burp/download.html
1、配置网络代理。双击BurpLoader.jar，选择“Proxy”选项卡，然后选择“Options”选项卡，点击Add按钮，在“Bind to port”框中输入端口号6666，“Bind to address”选择“Loopback only”，然后单击“OK”按钮。
打开火狐浏览器，选项，高级，网络，设置，手动配置代理。在HTTP代理框中输入127.0.0.1，端口号为6666，其他不用配置，单击“确定”。
2、查看拦截信息。在火狐浏览器中，输入www.baidu.com，回车后发现服务器很久没有回应信息，原因是Burp把HTTP请求拦截了，此时浏览器会处于阻塞状态。查看Burp的Intercept选项卡，可以看到请求信息。
3、绕过前端验证。启动wamp服务器，在火狐浏览器访问我们自己写的登录页面。发现请求页面并没有被拦截，修改火狐代理配置。再次访问登录页面，页面被拦截，在Burp中点击“Forword”，跳转到下一步。当我们输入的用户名或密码为空时，前端会出现提示。当我们正常输入用户名和密码时，在Burp中拦截信息。然后修改信息为：点击“Forward”，前端出现登录结果。这个请求，就绕过了前端验证，发送了空的用户名和密码给后端。
4、拦截响应如果我们想要拦截服务器的响应信息，那么，在Proxy，Options选项卡中，勾选Intercept response based on the following rules即可。
3.4. WinSock ExpertWinsock Expert是一个用来监视和修改网络发送和接收数据的程序，可以用来帮助渗透测试人员调试网络应用程序。
4. 书签
FreeBuf.COM | 关注黑客与极客
漏洞盒子 | 互联网安全测试平台
Fiddler 高级用法：Fiddler Script 与 HTTP 断点调试
Fiddler工具使用
web debugger fiddler 使用小结
《Wireshark协议分析从入门到精通》

]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>安全</tag>
        <tag>黑客</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Docker：Docker容器的网络命名空间</title>
    <url>/dev-docker-network-namespace/</url>
    <content><![CDATA[1. 什么是网络命名空间？在Linux系统中，网络命名空间（Network Namespace）是一种内核特性，允许我们有多个独立的网络实例存在于同一个物理系统上。这相当于在单一操作系统中复制了多个网络栈，实现了网络隔离。每个网络命名空间都拥有自己的网络资源，例如IP地址、路由规则和端口号集合。应用程序可以被绑定到特定的网络命名空间中运行，从而被限制仅在该空间内的网络环境中通信。


2. Docker容器与网络命名空间Docker通过容器化技术，让应用能够在隔离的环境中运行。每个 Docker 容器都会自动创建一组命名空间，包括主机名命名空间（UTS）、用户命名空间（USER）、挂载命名空间（MNT）、网络命名空间（NET）、进程命名空间（PID）、进程间通信命名空间（IPC）。其中的网络命名空间允许每个容器拥有独立的网络栈，从而实现网络的隔离和管理。这为容器之间的通信、容器与宿主机之间的通信，以及容器访问外部网络带来了极大的灵活性和控制。
3. Docker的网络模式Docker支持几种网络模式，用于定义容器应该如何与网络交互：

bridge：这是默认的网络模式。在这种模式下，Docker会为所有需要网络通信的容器创建一个虚拟桥接器。容器将通过这个桥接器与宿主机以及其他容器交互。
none：在这个模式下，容器拥有自己的网络命名空间，但不会配置任何网络接口，这意味着容器与外界隔离。
host：容器将不会获得自己的网络命名空间，而是直接使用宿主机的网络栈。
overlay：适用于跨多个宿主机的容器集群，它们通过使用overlay网络互连，而不是依赖于每个宿主机的物理网络。

4. 手动管理网络命名空间4.1. 查看指定容器的网络命令空间通过容器详情查看网络信息
docker inspect xxx
查看 NetworkSettings 和 Networks 部分，包括其IP地址、网关和DNS设置等，但是不包含网络命名空间。
通过容器的进程查看网络命令空间
ls -l /proc/$(docker inspect --format &#x27;&#123;&#123;.State.Pid&#125;&#125;&#x27; xxx)/ns/net

4.2. 查看所有容器的网络命名空间我们可以通过查看 /var/run/docker/netns/ 目录来了解 Docker 创建了哪些网络命名空间：
ls -l /var/run/docker/netns/

注意，不是所有的 Docker 安装都会将这些文件放在这个目录下。有时，为了安全考虑，这个目录可能并不容易直接访问。
4.3. 进入容器的网络命名空间1、获取容器的PID
pid=$(docker inspect --format &#x27;&#123;&#123;.State.Pid&#125;&#125;&#x27; xxx)

2、进入指定容器的网络命名空间
nsenter --target $pid --net# ornsenter --net=/proc/$pid/ns/net

3、执行网络相关命令
ip addr

4、退出容器的网络命名空间
exit





]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>network</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>docker</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：route命令</title>
    <url>/dev-linux-route/</url>
    <content><![CDATA[1. route命令简介本文内容主要来自ChatGPT。
route命令用于在Linux操作系统中显示和操作IP路由表。路由表是一个存储在操作系统内核中的表格，用于确定数据包应该如何从一个网络接口发送到另一个网络接口。route命令可以帮助我们查看、添加、删除和修改路由表中的条目。这对于网络配置和故障排除非常有用。
参考文档：

linux 路由表设置之 route 指令详解
《iptables入门篇》



2. route命令语法route 命令的基本语法如下：
route [选项] [参数]

以下是一些常用的选项：

-n: 以数字形式显示IP地址和路由信息。
-e: 以类似netstat的格式显示路由信息。
-A &lt;地址类型&gt;: 指定地址类型，如 inet（IPv4）或 inet6（IPv6）。
-v: 显示详细信息。

以下是一些常用的参数：

add: 添加一条新的路由。
del: 删除一条现有的路由。
flush: 清空所有路由。

3. route常用命令3.1. 查看当前路由表route -n

3.2. 添加/删除静态路由已知主机A的IP为172.16.0.2和192.168.0.2，默认网关A为172.16.0.1。主机A同时和网关B 192.168.0.1连通，网关B连通 192.168.0.0/24 和 192.168.1.0/24。
添加一条静态路由，使主机A连通 192.168.1.0/24
route add -net 目标网络地址 netmask 子网掩码 gw 网关地址 dev 网络设备route add -net 192.168.1.0 netmask 255.255.255.0 gw 192.168.0.1 dev eth0

删除一条静态路由，使主机A不连通 192.168.1.0/24
route del -net 目标网络地址 netmask 子网掩码 gw 网关地址 dev 网络设备route del -net 192.168.1.0 netmask 255.255.255.0 gw 192.168.0.1 dev eth0

3.3. 添加/删除默认路由添加默认路由
route add default gw 网关地址route add default gw 192.168.0.1route add default gw 192.168.0.1 dev eth0

删除默认路由，常用来断网（出流量）
route del default gw 网关地址route del default gw 192.168.0.1

4. route 和 iptablesroute和iptables是Linux系统中两个不同的网络配置工具，它们有以下区别：

route主要用于处理IP路由表，即决定数据包应该如何从一个网络接口发送到另一个网络接口。路由表用于控制不同网络之间的数据包传输。
iptables是Linux系统中的防火墙工具，用于控制和管理数据包在网络层的访问控制。iptables可以根据源IP、目的IP、协议等信息对数据包进行过滤、转发或修改。

尽管route和iptables的作用不同，但它们在网络配置和故障排除方面是相辅相成的。以下是一些route和iptables在实际应用中的例子：

负载均衡：您可以使用iptables将特定的数据流量转发到不同的服务器以实现负载均衡，同时使用route来配置服务器之间的网络路径。
网络隔离：在需要对不同子网进行访问控制时，可以使用route为不同的子网配置不同的路由，然后使用iptables来限制或允许特定子网之间的通信。
VPN 配置：在配置VPN时，route用于设置VPN服务器与客户端之间的路由，而iptables用于配置防火墙规则，以便正确处理加密和解密的数据包。
网络安全：route和iptables可以共同实现网络安全策略。例如，route用于配置数据包发送的路径，而iptables则用于检查数据包的内容，阻止恶意流量进入网络。

总之，route和iptables虽然分别处理路由表和防火墙规则，但它们在实际应用中需要相互配合，共同实现更加高效、安全和稳定的网络环境。
5. MacOS中的route命令MacOS中的route命令与Linux中的route命令具有相似的功能，但它们之间存在一些语法和行为差异。以下是在MacOS上配置路由规则的一些常用操作。
5.1. 显示当前路由表netstat -nrsudo route get default

5.2. 添加一条静态路由sudo route add -net 目标网络地址/子网掩码 网关地址sudo route add -net 192.168.1.0/24 192.168.0.1

5.3. 删除一条静态路由sudo route delete -net 目标网络地址/子网掩码 网关地址sudo route delete -net 192.168.1.0/24 192.168.0.1

5.4. 添加默认路由sudo route add default 网关地址sudo route add default 192.168.0.1

5.5. 修改默认路由sudo route change default 网关地址sudo route change default 192.168.0.1

5.6. 删除默认路由sudo route delete default 192.168.0.1








]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>linux</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>好好学Linux</tag>
        <tag>网络</tag>
        <tag>chatgpt</tag>
        <tag>macos</tag>
      </tags>
  </entry>
  <entry>
    <title>路由器抓包方法</title>
    <url>/dev-router-packet-capture/</url>
    <content><![CDATA[1. 前言路由器是一个网关设备，局域网内所有设备上网的流量都会经过它。因此从路由器抓包，可以获取到整个局域网的网络数据，以便进行分析。
一般来说，路由器抓包方法有两种：

路由器支持抓包功能
路由器支持端口镜像

此外，还有一种方法叫做抓包设备替代网关。这种方法不属于路由器抓包方法，不过可以作为路由器抓包方法的一种替代。
相关文档：

《Wireshark入门篇》



2. 路由器支持抓包功能一些功能强大的路由器，自带的固件就是具备抓包功能的。WebUI上点抓包，然后导出到本地，使用Wireshare分析即可。
有些路由器自带的固件可能不支持抓包，但是支持刷OpenWrt固件。对于喜欢折腾的同学，可以刷一个OpenWrt固件，然后使用tcpdump命令进行抓包。
3. 路由器支持端口镜像一些路由器是支持端口镜像的。路由器端口镜像，也可称为端口复制，是一种监控网络流量的方法。
在此方法中，路由器或交换机中的一个端口的流量会被复制或”镜像”到另一个端口。这使得网络管理员可以使用一个与网络连接的单独设备（例如，运行着数据包捕获软件的计算机）来观察和分析从原始端口经过的流量。
端口镜像并不会影响网络的正常工作，因为它所建立的是原始流量的副本，而不是中断或者重定向这些流量。
4. 抓包设备替代网关如果我们只是想抓取局域网中某台设备（设备A）的网络流量，并且具有该设备的网络设置权限，那么可以使用抓包设备（设备B）替换网关的方法进行抓包。
方法一：开WIFI假设设备A可以连接WIFI，设备B可以开热点，那么设备A连接到设备B的热点，在设备B上就可以抓包了。
方法二：指定网关1、设备B配置SNAT，配置方法参考《好好学Linux：Linux配置SNAT上网》2、设备A上的网络设置为固定IP，其中网关的IP配置为设备B的IP地址3、在设备B上抓包
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>wireshark</tag>
        <tag>tshark</tag>
        <tag>tcpdump</tag>
      </tags>
  </entry>
  <entry>
    <title>Linux虚拟网络设备tun/tap和veth pair</title>
    <url>/dev-tun-tap-vethpair/</url>
    <content><![CDATA[1. Linux虚拟网络设备简介本文内容主要来自ChatGPT。
Linux虚拟网络设备有多种类型，它们在不同场景下被用于创建和管理虚拟网络连接。以下是一些常见的Linux虚拟网络设备：

TUN/TAP：TUN（网络层虚拟设备）和TAP（数据链路层虚拟设备）设备用于在用户空间和内核空间之间创建虚拟网络设备。它们通常用于实现VPN、虚拟机等场景。
veth pair：veth pair是一对虚拟以太网设备，它们在内核空间中相互连接。当一个veth设备接收到数据包时，它会将数据包传递给与之配对的另一个veth设备。veth pair常用于连接不同的网络命名空间或虚拟机。
bridge：虚拟桥接器用于将多个网络设备连接在一起，以便它们可以在同一个局域网中相互通信。虚拟桥接器通常用于连接虚拟机、容器或其他虚拟网络设备。
macvlan：macvlan设备允许在同一个物理网络接口上创建多个虚拟接口，每个接口都有自己的MAC地址。这种设备类型在虚拟化和容器环境中非常有用，因为它可以让多个虚拟设备共享同一个物理网络接口。
ipvlan：ipvlan与macvlan类似，但它在同一个物理网络接口上创建多个虚拟接口，这些接口共享相同的MAC地址但具有独立的IP地址。ipvlan在某些场景下可能比macvlan更高效，因为它避免了ARP广播风暴。
VXLAN：虚拟可扩展局域网（VXLAN）是一种在数据中心网络中创建可扩展的虚拟局域网的技术。VXLAN使用UDP封装将二层以太网帧封装到IP数据包中，从而实现跨数据中心和广域网的虚拟网络连接。
GRE：通用路由封装（GRE）是一种封装协议，可以在不同网络协议之间创建点对点连接。在Linux中，可以使用GRE隧道在两个网络节点之间创建虚拟网络连接。

VXLAN（Virtual Extensible LAN）和GRE（Generic Routing Encapsulation）并非严格意义上的网络设备，而是网络隧道协议。然而，在Linux系统中，它们可以被视为一种特殊类型的虚拟网络设备，因为它们可以创建和管理虚拟网络连接。
本文中，我们主要学习tun/tap和veth pair。
参考文档：

TUN/TAP设备浅析(一) – 原理浅析
Linux 虚拟网络设备 tun/tap veth pair



2. tun/tap原理tun/tap是Linux内核中的虚拟网络设备驱动，用于在用户空间和内核空间之间创建虚拟网络设备。tun设备用于处理三层数据包（IP数据包），而tap设备用于处理二层数据包（以太网帧）。
tun/tap设备允许用户空间程序与内核空间虚拟网络设备进行通信。当用户空间程序往tun/tap设备写入数据时，内核将数据包作为虚拟设备的输入进行处理。同样，当内核向用户空间程序发送数据包时，程序可从tun/tap设备读取数据。这使得用户空间程序可以模拟一个完整的网络设备，用于实现VPN、虚拟机等功能。
3. 使用tun/tap设备3.1. 使用tap设备1、安装iproute2和uml-utilities软件包
apt-get install iproute2 uml-utilities

2、创建一个tap设备
ip tuntap add dev tap0 mode tap

3、为tap设备分配IP地址并启用设备
ip addr add 192.168.100.1/24 dev tap0ip link set tap0 up

4、测试设备的连通性
ping 192.168.100.1

3.2. 使用tun设备同样地，创建一个tun设备的命令如下：
ip tuntap add dev tun0 mode tunip addr add 10.0.0.1/24 dev tun0ip link set tun0 up

4. veth pair原理veth pair（Virtual Ethernet Pair）是一对虚拟以太网设备，它们在内核空间中相互连接。当一个veth设备接收到数据包时，它会将数据包传递给与之配对的另一个veth设备。veth pair常用于连接不同的网络命名空间或虚拟机。
veth pair在内核空间中表现为一对连接在一起的虚拟网络设备。当一个veth设备接收到数据包时，数据包会立即传递给与之配对的另一个veth设备。这种机制使得veth pair可以用于连接不同的网络命名空间，例如Docker容器或Linux容器（LXC）中的网络。这种连接方式类似于将两台物理主机通过网线连接起来，但在这种情况下，连接发生在虚拟环境中。
5. 使用veth pair5.1. 相同网络命名空间内1、创建一对veth设备
ip link add veth0 type veth peer name veth1

2、为veth设备分配IP地址并启用设备
ip addr add 192.168.200.1/24 dev veth0ip link set veth0 upip addr add 192.168.200.2/24 dev veth1ip link set veth1 up

3、测试设备之间的连通性
ping 192.168.200.2 -c 4 -I veth0

5.2. 不同网络命名空间内如有需要，可以将veth pair与不同的网络命名空间关联。
1、创建两个网络命名空间
ip netns add ns1ip netns add ns2

2、将veth0移动到ns1命名空间
ip link set veth0 netns ns1

3、将veth1移动到ns2命名空间
ip link set veth1 netns ns2

4、在网络命名空间内分配IP地址并启用veth设备
ip netns exec ns1 ip addr add 192.168.200.1/24 dev veth0ip netns exec ns1 ip link set veth0 upip netns exec ns2 ip addr add 192.168.200.2/24 dev veth1ip netns exec ns2 ip link set veth1 up

5、在命名空间之间测试设备的连通性
ip netns exec ns1 ping 192.168.200.2 -c 4





]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>chatgpt</tag>
      </tags>
  </entry>
  <entry>
    <title>Wireshark入门篇</title>
    <url>/dev-wireshark-start/</url>
    <content><![CDATA[1. Wireshark简介
Wireshark: The world’s most popular network protocol analyzer.

Wireshark最初是一个名为Ethereal的项目，在2006年更名为Wireshark。Wireshark是一个免费且开源的网络协议分析工具，可以用于捕获和分析网络数据包。它支持超过2,000种协议，并且可以深入分析每个协议的细节。
参考文档：

《Wireshark网络分析就是这么简单》
《Wireshark网络分析的艺术》
Wireshark官网
《K8S中Pod抓包》
wireshark抓包教程详解
Wireshark数据包分析三板斧



2. 安装抓包工具2.1. 安装Wireshark1、安装wireshark访问Wireshark下载页，下载对应系统版本的Wireshark，双击安装。
2、配置命令行启动wiresharkmacos中，wireshark默认安装路径为/Applications/Wireshark.app/Contents/MacOS/Wireshark想要在命令行中直接启动wireshark，最好是配置一个软链
ln -s /Applications/Wireshark.app/Contents/MacOS/Wireshark /usr/local/bin/wiresharkwhich wiresharkwireshark

2.2. 安装TsharkTshark是Wireshark的命令行版本，可以在终端中使用。它可以用于捕获和分析网络数据包。
1、安装 epel 扩展源
yum install -y epel-release

2、安装wireshark
yum install -y wireshark
wireshark包含了tshark工具。
3、验证安装
tshark -v

参考文档：Tshark - INSTALL
2.3. 安装tcpdumptcpdump是另一个命令行网络协议分析工具，可以用于捕获和分析网络数据包。
1、安装tcpdump
yum install -y tcpdump

2、验证安装
tcpdump --version

3. 抓取数据包3.1. Wireshark抓包场景：访问www.voidking.com，抓取数据包。
1、打开Wireshark，选择要捕获的接口。
2、在“Capture Options”对话框中，可以选择要捕获的协议和过滤器。本例中，选择host过滤器，对应输入host www.voidking.com本步骤可以省略，直接抓取所有的流量，然后在抓包后进行过滤。
3、点击“Start”按钮开始捕获数据包。
4、浏览器访问http://www.voidking.com
5、Wireshark中捕获到指定域名的数据包，可以进行分析了。捕获到的数据包，可以再次进行过滤，例如使用http过滤器，对应输入http。
我们可以点击任意一个 HTTP 数据包来查看其详细信息，如请求方法、请求头、响应状态码、响应头、响应内容等。也可以右键点击一个数据包，并选择“Follow &gt; HTTP Stream”来查看整个 HTTP 会话的内容。
请注意，如果使用HTTPS协议进行通信，Wireshark将无法直接解密和查看通信内容。但是，Wireshark仍然可以捕获加密的数据包，包括TLS握手和加密的应用数据，可以通过分析这些数据包来获取有关通信的一些信息。
3.2. Tshark命令抓包场景：访问www.voidking.com，抓取数据包。
1、查看可以抓包的网络接口
tshark -D

2、捕获数据包
tshark -i eth0 -f &quot;host www.voidking.com&quot; -w stream.pcap

3、再开一个终端 curl www.voidking.com
4、读取数据包
tshark -r stream.pcaptshark -r stream.pcap -Vtshark -r stream.pcap -Y &quot;http&quot; -V

参考文档：Tshark - Capture Pcap
3.3. tcpdump命令抓包场景：访问www.voidking.com，抓取数据包。
1、查看可以抓包的网络接口
tcpdump -D

2、查看www.voidking.com域名主机
ping www.voidking.com

主机为：8.136.13.58
3、捕获数据包
tcpdump -i eth0 host 8.136.13.58 -w stream.pcap

4、读取数据包
tcpdump -r stream.pcaptcpdump -r stream.pcap -vtcpdump -r stream.pcap -vvvtshark -r stream.pcap -V

参考文档：在 Linux 命令行中使用 tcpdump 抓包
4. 查看数据包1、Wireshark打开捕获到的数据包文件（.pcap文件）。
2、Wireshark将显示捕获到的所有数据包。
3、可以使用过滤器过滤出特定的流量。
4、点击任何数据包以查看其详细信息，包括协议、源和目标地址、时间戳和数据内容等。
5. Wireshark过滤器表达式Wireshark过滤器表达式是用来过滤和搜索数据包的一种机制。这些表达式是基于BPF过滤器的语法，并支持大量的协议和字段过滤器。
5.1. 逻辑运算符Wireshark支持逻辑运算符“and”、“or”和“not”。可以使用这些运算符将多个过滤条件组合起来。例如，“tcp and port 80”表示同时过滤TCP流量和端口为80的流量。
5.2. 比较运算符Wireshark支持比较运算符“==”、“!=”、“&gt;”、“&lt;”、“&gt;=”和“&lt;=”。可以使用这些运算符比较数据包中的字段值。例如，“ip.src == 192.168.1.1”表示源IP地址为192.168.1.1的数据包。
5.3. 字段过滤器Wireshark支持许多字段过滤器，这些过滤器用于过滤和搜索特定的协议字段。例如，“tcp.port”表示TCP端口，而“http.request.uri”表示HTTP请求的URI字段。可以在过滤器表达式中使用这些字段过滤器来定位特定的数据包。
5.4. 协议过滤器Wireshark支持多种协议过滤器，这些过滤器用于过滤特定的协议流量。例如，“tcp”表示TCP流量，“udp”表示UDP流量，“http”表示HTTP流量等。
5.5. 限制表达式Wireshark支持一些限制表达式，这些表达式用于限制过滤器的搜索结果。例如，“ip.src == 192.168.1.1 limit 10”表示仅显示源IP地址为192.168.1.1的前10个数据包。
6. Wireshark与TCP报文参考文档：工具：Wireshark介绍及抓包分析
7. Wireshark视图7.1. 时间显示格式视图，时间显示格式，选择方便阅读的格式。
8. Wireshark统计8.1. 已解析地址统计，已解析地址，查看IP地址对应的域名。
8.2. 会话统计，会话，查看会话的通信量。
8.3. I/O图表统计，I/O图表，查看整个会话周期或者特定时间段内的数据包情况。
8.4. 流量图统计，流量图，查看会话的流量情况。能够直观看到TCP/IP握手、挥手和数据传输，适合分析丢包、重传、重复ACK等情况。
8.5. HTTP分组计数器统计，HTTP，分组计数器，查看HTTP请求的应答情况。能够直观看到HTTP会话的应答情况，比如说200 OK的数量，404的数量等。
8.6. TCP流图形统计，TCP流图形。能够看到整个会话周期或者特定时间段内会话的吞吐量、往返时间、窗口尺寸等情况。
9. Wireshark分析9.1. 专家信息分析，专家信息。专家信息可展示出数据包所有分组的问题，并自动进行问题分级，便于查看数据包的错误情况。
找到相应的错误后，右击”作为过滤器应用”-&gt;”选中”，即可过滤出该类型的所有数据包。
9.2. 追踪流在分析数据包时，找到了有问题的一帧，想看看此帧所属的TCP流的情况。分析，追踪流。或者，鼠标落在此帧上右击，找到”追踪流”，选中”TCP流”即可过滤出此帧所属的TCP会话。
10. 后记多学，多试，多用，熟能生巧！
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>wireshark</tag>
        <tag>tshark</tag>
        <tag>tcpdump</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：把UEditor整合进ThinkPHP</title>
    <url>/dev-ueditor-in-thinkphp/</url>
    <content><![CDATA[1. 前言
UEditor是由百度web前端研发部开发所见即所得富文本web编辑器，具有轻量，可定制，注重用户体验等特点，开源基于MIT协议，允许自由使用和修改代码…



巾帼志愿者项目，需要发布一些带图片的文章，使用UEditor很合适。同时，UEditor还可以当做图片上传插件使用。下面，我们把UEditor整合进ThinkPHP框架中。
2. 准备1、登录UEditor官网，下载PHP版本（UTF-8版）的代码。2、解压文件，重命名文件夹utf8-php为ueditor。3、拷贝ueditor到项目中的Public/libs文件夹下。4、修改ueditor/php/config.json里的路径。
3. 编辑器3.1. 新建表在数据库中新建表volun_culture，content字段的类型选择longtext。
3.2. Controller在Application/Admin/Controller/TestController.class.php中，添加函数ueditor、cultureAdd和ueditorshow。
public function ueditor()&#123;    $this-&gt;display();&#125;public function cultureAdd($title, $content)&#123;    $data[&#x27;title&#x27;] = $title;    $data[&#x27;content&#x27;] = $content;    $culture = D(&#x27;culture&#x27;);    if($culture-&gt;create($data))&#123;        $id = $culture-&gt;add();        if($id)&#123;            $data = array(                &#x27;code&#x27;=&gt;&#x27;0&#x27;,                &#x27;id&#x27;=&gt;$id            );            echo json_encode($data);        &#125;    &#125;&#125;public function ueditorshow()&#123;    $culture = D(&#x27;culture&#x27;);    $cultureArr = $culture-&gt;select();    $this-&gt;assign(&#x27;cultureArr&#x27;,$cultureArr);    $this-&gt;display();&#125;


3.3. 页面在Application/Admin/View/Test文件夹中，新建文件ueditor.html和ueditorshow.html。
&lt;!--ueditor.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;UEditor&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;!-- 加载编辑器的容器 --&gt;    &lt;script id=&quot;container&quot; name=&quot;content&quot; type=&quot;text/plain&quot;&gt;        这里写你的初始化内容    &lt;/script&gt;    &lt;button id=&quot;getContent&quot;&gt;获取内容&lt;/button&gt;    &lt;button id=&quot;saveContent&quot;&gt;保存&lt;/button&gt;    &lt;script src=&quot;/volunteer/Public/libs/jquery/jquery.min.js&quot;&gt;&lt;/script&gt;    &lt;!-- 配置文件 --&gt;    &lt;script type=&quot;text/javascript&quot; src=&quot;/volunteer/Public/libs/ueditor/ueditor.config.js&quot;&gt;&lt;/script&gt;    &lt;!-- 编辑器源码文件 --&gt;    &lt;script type=&quot;text/javascript&quot; src=&quot;/volunteer/Public/libs/ueditor/ueditor.all.js&quot;&gt;&lt;/script&gt;    &lt;!-- 实例化编辑器 --&gt;    &lt;script type=&quot;text/javascript&quot;&gt;        $(function()&#123;            var ue = UE.getEditor(&#x27;container&#x27;);            $(&#x27;#getContent&#x27;).click(function()&#123;                var html = ue.getContent();                alert(html);            &#125;);            $(&#x27;#saveContent&#x27;).click(function()&#123;                var html = ue.getContent();                var param = &#123;                    title: &#x27;测试&#x27;,                    content: html                &#125;;                $.ajax(&#123;                    url: &#x27;/volunteer/index.php/Admin/Test/cultureAdd&#x27;,                    type: &#x27;POST&#x27;,                    dataType: &#x27;json&#x27;,                    data: param,                    success: function(data)&#123;                        console.log(data);                    &#125;,                    error: function(xhr)&#123;                        console.log(xhr);                    &#125;                &#125;);                            &#125;);        &#125;);    &lt;/script&gt;&lt;/body&gt;&lt;/html&gt;
显示效果如下：点击“获取内容”：我们可以看到，上传的图片路径为/volunteer/Public/libs/ueditor/php/upload/image/*，这个路径是我们在准备工作的第四步中配置的。查看一下该路径，果然可以找到上传的图片。点击“保存”：
&lt;!--ueditorshow.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;文章展示&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;volist name=&quot;cultureArr&quot; id=&quot;item&quot;&gt;        &lt;div class=&quot;article&quot;&gt;            &lt;h2&gt;&#123;$item.title&#125;&lt;/h2&gt;            &#123;$item.content&#125;        &lt;/div&gt;        &lt;hr/&gt;    &lt;/volist&gt;&lt;/body&gt;&lt;/html&gt;

显示效果如下：
4. 图片上传插件4.1. Controller在Application/Admin/Controller/TestController.class.php中，添加函数imgupload。
public function imgupload()&#123;    $this-&gt;display();&#125;

4.2. 页面在Application/Admin/View/Test文件夹中，新建文件imgupload.html。
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;图片上传&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;tr&gt;        &lt;th&gt;上传图片&lt;/th&gt;        &lt;td&gt;            &lt;input type=&quot;text&quot; id=&quot;path&quot; /&gt;            &lt;input type=&quot;button&quot; id=&quot;addPic&quot; value=&quot;上传图片&quot;/&gt;        &lt;/td&gt;    &lt;/tr&gt;    &lt;div id=&quot;myeditor&quot; style=&quot;display: none;&quot;&gt;&lt;/div&gt;&lt;js file=&quot;__ROOT__/Public/libs/ueditor/ueditor.config.js&quot;/&gt;&lt;js file=&quot;__ROOT__/Public/libs/ueditor/ueditor.all.js&quot;/&gt;&lt;js file=&quot;__ROOT__/Public/libs/jquery/jquery.min.js&quot;/&gt;&lt;script&gt;    $(function()&#123;        var editor = UE.getEditor(&#x27;myeditor&#x27;);        editor.ready(function () &#123;            //editor.setDisabled();            editor.hide();            editor.addListener(&#x27;beforeInsertImage&#x27;, function (t, arg) &#123;                $(&quot;#path&quot;).val(arg[0].src);                //$(&quot;#preview&quot;).attr(&quot;src&quot;, arg[0].src);                console.log(arg);            &#125;);        &#125;);        $(&#x27;#addPic&#x27;).click(function()&#123;            var myImage = editor.getDialog(&quot;insertimage&quot;);            myImage.open();        &#125;);    &#125;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

5. UEditor优化5.1. 上传图片弹框延迟问题描述：在最新版Chrome浏览器中，点击上传图片时，等待非常久，大概10秒左右，才弹出选择图片的对话框。
解决方案：打开ueditor.all.js，找到：
accept=&quot;image/*&quot;
修改为：
accept=&quot;image/jpg,image/jpeg,image/png,image/gif&quot;

5.2. 点击选择图片弹框延迟问题描述：和上传图片弹框延迟基本相同，但是，点击的按钮有所差别。
解决方案：打开dialogs/images/images.js，找到：
accept: &#123;    title: &#x27;Images&#x27;,    extensions: acceptExtensions,    mimeTypes: &#x27;image/*&#x27;&#125;
修改为：
accept: &#123;    title: &#x27;Images&#x27;,    extensions: acceptExtensions,    mimeTypes: &#x27;image/jpg,image/jpeg,image/png,image/gif&#x27;&#125;

6. 后记为什么我会知道修改哪个js文件？两个原因：
第一，查找资料，资料指明了修改ueditor.all.js（ueditor.all.min.js）和webuploader.js（webuploader.min.js）。
第二，按照资料给的方法，并没有解决我的问题。仔细研读资料，关键在于accept参数的修改。问题没有解决，极大的可能是accept参数没有修改正确。检查后，确信accept参数已经修改正确。清掉缓存，确保修改后的代码生效。然而，问题依然没有解决。那么，只剩下一种可能，版本问题，accept参数也许移动到其他js中了。
右键“单击选择图片”按钮，检查，选择Event Listener，便可以定位到相关的js。
7. 书签UEditor - 首页http://ueditor.baidu.com/website/
UEditor Docshttp://fex-team.github.io/ueditor/#start-config
Web Uploaderhttp://fex.baidu.com/webuploader/
WebUploader UEditor chrome 点击上传文件选择框会延迟几秒才会显示 反应很慢http://www.cnblogs.com/liangjiang/p/5799984.html
FAQ · fex-team/ueditor Wiki · GitHubhttps://github.com/fex-team/ueditor/wiki/FAQ
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>thinkphp</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：ThinkPHP登录状态记录</title>
    <url>/dev-thinkphp-login-state/</url>
    <content><![CDATA[1. 前言登录状态的记录，两种思路：一种是在前端cookie中存一个token，每次请求都把token传给后端，用于验证用户身份；另一种是建立session，在session中保存用户信息。
本次开发中，我们采用第二种思路，下面实现一个简单的逻辑：1、在地址栏输入后台首页地址，如果管理员未登录，则显示后台首页（登录页面）；如果管理员已登录，则跳转到内容管理页面。2、在地址栏输入内容管理页面地址，如果管理员未登录，则跳转到后台首页；如果管理员已登录，则显示内容管理页面。


2. php代码// 后台首页public function index()&#123;    if(isset($_SESSION[&#x27;admin&#x27;]))&#123;        $url = &#x27;http://localhost/volunteer/index.php/Admin/manage&#x27;;        header(&quot;location: $url&quot;);    &#125;else&#123;        $this-&gt;display();    &#125;&#125;// 登录函数public function login($name, $password)&#123;    // $name = $_POST[&#x27;name&#x27;];    // $password = $_POST[&#x27;password&#x27;];    $admin = M(&#x27;admin&#x27;);    $data = $admin-&gt;where(&quot;name=&#x27;$name&#x27; AND password=&#x27;$password&#x27;&quot;)-&gt;find();    if($data)&#123;        $_SESSION[&#x27;admin&#x27;]=$name;        $result = array(            &#x27;code&#x27; =&gt; &#x27;0&#x27;,            &#x27;ext&#x27; =&gt; &#x27;登录成功&#x27;,            &#x27;adminName&#x27; =&gt; $_SESSION[&#x27;admin&#x27;]        );        echo json_encode($result);    &#125;else&#123;        $result = array(            &#x27;code&#x27; =&gt; &#x27;1&#x27;,            &#x27;ext&#x27; =&gt; &#x27;用户名或密码错误&#x27;        );        echo json_encode($result);    &#125;&#125;// 管理页面public function manage()&#123;    if(!isset($_SESSION[&#x27;admin&#x27;]) || !$_SESSION[&#x27;admin&#x27;])&#123;        $url = &#x27;http://localhost/volunteer/index.php/Admin/Index&#x27;;        header(&quot;location: $url&quot;);    &#125;else&#123;        $this-&gt;display();    &#125;&#125;

3. js代码$(function()&#123;    $(&#x27;#confirm&#x27;).click(function(e)&#123;        e.preventDefault();        var name = $(&#x27;#name&#x27;).val();        var password = $(&#x27;#password&#x27;).val();        $.ajax(&#123;            url: &#x27;/volunteer/index.php/Admin/Index/login&#x27;,            type: &#x27;POST&#x27;,            dataType: &#x27;json&#x27;,            data: &#123;                name: name,                password: password            &#125;,            success: function(data)&#123;                console.log(data);                if(data.code == 0)&#123;                    window.location = &#x27;/volunteer/index.php/Admin/Index/manage&#x27;;                &#125;            &#125;,            error: function(xhr)&#123;                console.log(xhr);            &#125;        &#125;);    &#125;); &#125;);

4. 书签起步 · Bootstrap v3 中文文档http://v3.bootcss.com/getting-started/#examples
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>thinkphp</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：ThinkPHP实战</title>
    <url>/dev-thinkphp-in-action/</url>
    <content><![CDATA[1. 前言本着“需要什么就学习什么”的原则，花了几天时间，实践了一下thinkphp框架的使用。整理了一下必须掌握的三个部分：路由控制、模板渲染、增删改查。


2. 准备在《ThinkPHP开发环境搭建》中，我们已经准备好了thinkphp的开发环境。在此基础上，复制Application中的Home文件夹，重命名为Admin。然后，修改Admin/IndexController.class.php文件的namespace为：
namespace Admin\Controller;
好了，可以进入真正的开发了！
3. 路由控制路由控制，在thinkphp开发手册中对应“架构&gt;URL模式”。thinkphp提供了多种URL模式，本次开发采用标准URL格式。
http://serverName/index.php/模块/控制器/操作

比如，我们在Admin/IndexController.class.php文件中添加函数：
public function firstFunc()&#123;    echo &#x27;第一个函数&#x27;;&#125;
那么，这个函数的访问路径为http://localhost/thinkphp/index.php/Admin/Index/firstFunc。访问该路径，我们可以在页面上看到返回值“第一个函数”。
4. 模板渲染模板渲染，在thinkphp开发手册中对应“视图&gt;模板赋值和模板渲染”以及“模板”。在Admin/View/Index文件夹下，新建文件template.html。在Admin/IndexController.class.php文件中添加函数：
public function template()&#123;    $data = &#x27;测试数据&#x27;;    $dataArr = array(        array(&#x27;name&#x27;=&gt;&#x27;郝锦&#x27;,&#x27;age&#x27;=&gt;&#x27;24&#x27;),        array(&#x27;name&#x27;=&gt;&#x27;小帅&#x27;,&#x27;age&#x27;=&gt;&#x27;22&#x27;),        array(&#x27;name&#x27;=&gt;&#x27;小飞&#x27;,&#x27;age&#x27;=&gt;&#x27;22&#x27;)    );    $dataObj = new userInfo();    $this-&gt;assign(&#x27;data2&#x27;,$data);    $this-&gt;assign(&#x27;dataArr&#x27;,$dataArr);    $this-&gt;assign(&#x27;dataObj&#x27;,$dataObj);    $this-&gt;display();&#125;
同时，在文件最后添加一个userInfo类：
class userInfo&#123;    public $name = &#x27;郝锦&#x27;;    public $age = &#x27;24&#x27;;    function show()&#123;        echo &#x27;一个函数&#x27;;    &#125;&#125;
那么，$data、$dataArr和$dataObj都会通过assign函数传送到template.html页面。template.html页面代码如下：
&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;模板&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h1&gt;模板&lt;/h1&gt;    &lt;p&gt;&#123;$data2&#125;&lt;/p&gt;    &lt;p&gt;&#123;$dataObj:name&#125;&lt;/p&gt;    &lt;p&gt;&#123;$dataObj:age&#125;&lt;/p&gt;    &lt;volist name=&#x27;dataArr&#x27; id=&quot;item&quot;&gt;        &#123;$item.name&#125;&amp;nbsp;&#123;$item.age&#125;&lt;br/&gt;    &lt;/volist&gt;&lt;/body&gt;&lt;/html&gt;

最终效果如下：
注意，函数名和页面名字是相同的，一一对应的。
5. 增删查改增删查改，在thinkphp开发手册中对应“模型&gt;GURD操作”和“专题&gt;数据分页”。
5.1. 数据库配置利用navicat等工具连接到本地mysql数据库，创建数据库volunteer，在数据库中创建表volun_admin(int id, varchar name, vachar password)和volun_project(int id, varchar title, varchar content)。注意，编码格式选择utf8。
5.2. php配置在进行数据库增删查改之前，我们需要先连接到数据库。打开Application/Common/Conf/config.php文件，修改内容如下：
&lt;?phpreturn array(    //&#x27;配置项&#x27;=&gt;&#x27;配置值&#x27;    &#x27;DB_TYPE&#x27;=&gt;&#x27;mysql&#x27;, //数据库类型    &#x27;DB_HOST&#x27;=&gt;&#x27;localhost&#x27;, //服务器地址    &#x27;DB_NAME&#x27;=&gt;&#x27;volunteer&#x27;, //数据库名    &#x27;DB_USER&#x27;=&gt;&#x27;root&#x27;, //用户名    &#x27;DB_PWD&#x27;=&gt;&#x27;&#x27;, //密码    &#x27;DB_PORT&#x27;=&gt;3306, //端口    &#x27;DB_PREFIX&#x27;=&gt;&#x27;volun_&#x27;, //数据库表前缀    &#x27;SHOW_PAGE_TRACE&#x27; =&gt;true,);?&gt;

5.3. project表的增删查改// 志愿项目增删查改public function projectAdd($title, $content)&#123;    $project = D(&#x27;project&#x27;);    $data[&#x27;title&#x27;] = $title;    $data[&#x27;content&#x27;] = $content;    if($project-&gt;create($data))&#123;        $id = $project-&gt;add();        if($id)&#123;            $result = array(                &#x27;code&#x27;=&gt; &#x27;0&#x27;,                &#x27;ext&#x27;=&gt; &#x27;success&#x27;            );            echo json_encode($result);        &#125;    &#125;&#125;public function projectEdit($id, $title, $content)&#123;    $project = D(&#x27;project&#x27;);    $data[&#x27;title&#x27;] = $title;    $data[&#x27;content&#x27;] = $content;    $success = $project-&gt;where(&quot;id=&#x27;$id&#x27;&quot;)-&gt;save($data);    if($success)&#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;0&#x27;,            &#x27;ext&#x27;=&gt; &#x27;success&#x27;        );        echo json_encode($result);    &#125;else &#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;1&#x27;,            &#x27;ext&#x27;=&gt; &#x27;fail&#x27;        );        echo json_encode($result);    &#125;&#125;public function projectDelete($id)&#123;    $project = D(&#x27;project&#x27;);    $success = $project-&gt;where(&quot;id=&#x27;$id&#x27;&quot;)-&gt;delete();    if($success)&#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;0&#x27;,            &#x27;ext&#x27;=&gt; &#x27;success&#x27;        );        echo json_encode($result);    &#125;else &#123;        $result = array(            &#x27;code&#x27;=&gt; &#x27;1&#x27;,            &#x27;ext&#x27;=&gt; &#x27;fail&#x27;        );        echo json_encode($result);    &#125;&#125;public function projectList()&#123;    $project = D(&#x27;project&#x27;);    $resultArr = $project-&gt;select();    echo json_encode($resultArr,JSON_UNESCAPED_UNICODE);&#125;public function projectPage($pageSize,$pageNum)&#123;    $project = D(&#x27;project&#x27;);    $total = $project-&gt;count();    $totalPage = $total%$pageSize ? (int)($total/$pageSize)+1 : (int)($total/$pageSize);        $list = $project-&gt;page($pageNum.&#x27;,&#x27;.$pageSize)-&gt;select();    if($list)&#123;        $resultArr = array(            &#x27;totalPage&#x27;=&gt; $totalPage,            &#x27;pageNum&#x27;=&gt; $pageNum,            &#x27;projectList&#x27;=&gt; $list        );        $result = array(            &#x27;code&#x27;=&gt; &#x27;0&#x27;,            &#x27;ext&#x27;=&gt; &#x27;success&#x27;,            &#x27;obj&#x27;=&gt; $resultArr        );        echo json_encode($result,JSON_UNESCAPED_UNICODE);    &#125;&#125;

以上方法，在js中，可以使用GET方式请求，也可以使用POST方式请求。
路由控制、模板渲染、增删改查，至此全部跑通，可以进行简单的开发了。至于thinkphp提供的其他更加强大的功能，在需要时查查文档就好。
6. 接口测试上次在给大家演示HttpRequester的时候，验证登录接口，一直出现错误。猜测有四种可能：1、HttpRequester的问题2、thinkphp的问题3、php的问题4、apache的问题
6.1. HttpRequester的问题以projectAdd方法为例，在js中使用ajax请求该方法，无论type是GET还是POST，都能成功拿到返回值&#123;&quot;code&quot;:&quot;0&quot;,&quot;ext&quot;:&quot;success&quot;&#125;。
在HttpRequester中，使用GET方式请求该方法，可以拿到返回值；但是，使用POST方式请求，拿不到返回值，提示“参数错误或者未定义:title”，因为projectAdd方法没有拿到我们传过去的参数。
修改projectAdd方法为：
public function projectAdd()&#123;    $title = $_POST[&#x27;title&#x27;];    $content = $_POST[&#x27;content&#x27;];    $project = D(&#x27;project&#x27;);    $data[&#x27;title&#x27;] = $title;    $data[&#x27;content&#x27;] = $content;    if($project-&gt;create($data))&#123;        $id = $project-&gt;add();        if($id)&#123;            $result = array(                &#x27;code&#x27;=&gt; &#x27;0&#x27;,                &#x27;ext&#x27;=&gt; &#x27;success&#x27;            );            echo json_encode($result);        &#125;    &#125;&#125;

再次使用HttpRequester请求projectAdd方法，这次换了个错误：
Column &#x27;content&#x27; cannot be null [ SQL语句 ] : INSERT INTO `volun_project` (`title`,`content`) VALUES (NULL,NULL)

显然，错误的原因依然是projectAdd方法没有拿到我们传过去的参数，导致title和content为空。
使用ajax的POST方式请求该方法，成功拿到返回值。可见，这是HttpRequester的问题，它无法模拟ajax的POST请求。
那么问题来了，ajax的POST和HttpRequester的POST到底有什么区别？很遗憾，暂时没有找到答案。
6.2. thinkphp的问题在公司开发时，一直使用HttpRequester来验证接口，能用ajax请求的接口，都可以使用HttpRequester来模拟请求。这样看来，就是接口的问题，也就是thinkphp框架的问题！或者，就是php的问题！
6.3. php的问题在thinkphp文件下，新建了一个test.php文件：
&lt;?php    $title = $_POST[&#x27;title&#x27;];    $array = array(        &#x27;title&#x27;=&gt;$title    );    echo json_encode($array);?&gt;
在ajax中，带入参数&#123;title:&#39;测试标题&#39;&#125;，POST请求http://localhost/thinkphp/test.php，成功拿到返回值。
在HttpRequester中，带入参数&#123;title:&#39;测试标题&#39;&#125;，POST请求http://localhost/thinkphp/test.php，提示错误“Notice: Undefined index: title in D:\Server\wamp64\www\thinkphp\test.php on line 2”。
修改test.php文件为：
&lt;?php    $title = $_GET[&#x27;title&#x27;];    $array = array(        &#x27;title&#x27;=&gt;$title    );    echo json_encode($array);?&gt;
在ajax中，带入参数&#123;title:&#39;测试标题&#39;&#125;，GET请求http://localhost/thinkphp/test.php，成功拿到返回值。
在HttpRequester中，带入参数&#123;title:&#39;测试标题&#39;&#125;，GET请求http://localhost/thinkphp/test.php，成功拿到返回值。
由此排除thinkphp的问题。
6.4. apache的问题还有一种可能，apache的问题，于是把test.php文件放到nginx下，经测试，接口同样拿不到参数。由此排除apache的问题。
6.5. 小结综上，HttpRequester无法测试thinkphp的POST接口问题，有两个可能，一个是HttpRequester存在缺陷，另一个是php存在缺陷。
7. 后记无论哪种原因，总之，无法使用HttpRequester的POST请求来验证我们的thinkphp接口了。需要验证接口时，暂时使用GET请求，因为我们的接口两种请求方式都可以拿到返回值。
注意，本应该是POST请求的接口，接口文档中的请求类型依然写POST，ajax的type也使用POST，保证规范性。
8. 2016.09.14更新Thinkphp3.2.3 接收不到json数据https://segmentfault.com/q/1010000004980405
原来，造成HttpRequester无法测试接口的原因，是Content-Type没有选对。
由于PHP默认只识别Content-Type: application/x-www.form-urlencoded标准的数据类型，因此，对型如application/json的内容无法解析为$_POST数组，故保留原型，交给$GLOBALS[&#39;HTTP_RAW_POST_DATA&#39;]来接收。
file_get_contents(&#39;php://input&#39;)允许读取 POST 的原始数据，和$GLOBALS[&#39;HTTP_RAW_POST_DATA&#39;]比起来，它给内存带来的压力较小，并且不需要任何特殊的php.ini设置。file_get_contents(&#39;php://input&#39;)不能用于enctype=&quot;multipart/form-data&quot;。
9. 书签序言 - ThinkPHP3.2完全开发手册http://document.thinkphp.cn/manual_3_2.html
ThinkPHP框架教程 - 猿团http://edu.yuantuan.com/course/explore/thinkphp
PHP: 语言参考 - Manualhttp://php.net/manual/zh/langref.php
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
        <tag>thinkphp</tag>
      </tags>
  </entry>
  <entry>
    <title>开源中国团队协作开发平台</title>
    <url>/dev-oschina-teamwork/</url>
    <content><![CDATA[1. 前言在公司时，使用wiki来写文档，使用jira来记录工作进度和bug，非常方便。马上要进行项目管理，本来打算自己搭建wiki和jira系统，后来想到开源中国有一个团队协作开发平台。试用了一下，基本满足要求，nice。


2. 写文档本平台内置markdown编辑器，建议学习一下markdown写法。
2.1. 普通文档1、点击左侧导航栏的“文档”，然后点击“新文档”。
2、输入文档信息。
3、之后点击文档名，进入编辑页面。
4、编辑完成，可以进行预览。
2.2. 接口文档接口文档，在普通文档的基础上，规定结构如下：
下面是一个示例：http://doc.oschina.net/interfacedemo
PS：接口文档后端负责编写。后端在开发前，先写好接口文档，然后把文档地址发到讨论组里，前端就可以自己模拟数据独立进行开发。后端在开发好接口之后，自己使用HttpRequester测试一下，确保接口没有问题，然后在讨论组里声明哪些接口已经可以使用。
2.3. 渲染文档thinkphp框架的前后端分离做的不彻底，默认架构为后端路由+后端渲染+后端接口，适合全栈开发者使用。理想的前后端分离框架，应该是由前端来负责路由控制和前端渲染，后端专注于接口，也就是前端路由+前端渲染+后端接口。
因为thinkphp框架中的路由控制由后端来控制，页面渲染也属于后端渲染，所以页面渲染的工作理论上应该归属于后端开发者。但是，后端开发者负责页面渲染的话，前后端就混到了一起，加大了开发的复杂度。
相较而言，让前端开发者负责页面渲染的更好一些。那么，前端开发者就需要拿到页面地址，以及用来页面渲染的数据。页面地址和页面渲染数据需要后端提供，因此需要一个文档来说明。
更多关于前后端渲染的内容，请参考《JavaScript模板引擎》。
规定渲染文档结构如下：
下面是一个示例：http://doc.oschina.net/interfacedemo2
PS：渲染文档同样由后端负责编写。后端渲染虽然不便于前后端分离，但是也有好处。因为后端渲染的话，页面加载的速度会更快些。如果变更架构为前端渲染+后端路由+后端接口，也很简单，只要在前端加一个模板引擎即可。
3. 记录工作进度3.1. 新建任务1、每次开发前，点击左侧导航栏的“任务”，然后点击“新建任务”。
2、选择项目，标题输入当天的任务（或一段时间的任务），任务标签选择功能（feature），指派成员选自己，选择开始时间和结束时间，其他可选。
3、点击确认创建。
3.2. 关闭任务当任务完成后，变更任务状态为已完成或验收通过。
3.3. 生成周报每周日，点击左侧导航栏的“周报”，然后点击“提交周报”，自动生成周报。
4. 记录bug和记录工作进度类似，只不过任务标签选择缺陷（bug），指派成员选择模块的开发者。并且，在任务描述中添加一些bug的描述信息。
5. 代码托管5.1. 获取项目git clone https://git.oschina.net/voidking/volunteer.git

5.2. 上传项目git pullgit add .git commit -m &quot;message&quot;git push

6. 后记有任何问题，或者好的建议，都可以提出来，希望大家在这个团体中共同进步，成为更加牛逼的自己。
7. 书签
卓音工作室—Team@OSC团队协作开发平台
码云平台帮助文档_V1.2
码云 - 开源中国

]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>jira</tag>
        <tag>wiki</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：在CentOS7上配置PHP运行环境</title>
    <url>/dev-centos-php-envirenment/</url>
    <content><![CDATA[1. 前言阿里云到期了，穷人买不起服务器，以后只能在虚拟机中练手了。VMware和VirtualBox都很好用，VMware可以安装MacOS，VirtualBox更轻量。
CentOS7安装到了VirtualBox中，下面，来学习配置一下PHP的运行环境。假设采用桥接，CentOS7的IP地址为192.168.1.114。



2. php环境配置常用的PHP环境为LAMP和LNMP，这次郝同学选择LNMP，也就是Linux、Nginx、Mysql和PHP。听说EZHTTP可以简化安装配置过程，尝试一下。
2.1. 安装screen(可选)由于编译安装Nginx Apache PHP MySQL等软件会花费比较长的时间，难免会出现由于网络意外中断而导致安装也中断了，所以为了避免此问题，可以用screen来安装。yum install -y screen
2.2. 安装unzip和wget执行ezhttp安装程序，至少需要unzip及wget工具。yum install -y wget unzip
2.3. 安装gityum install -y git
2.4. 安装EZHTTPgit clone https://github.com/centos-bz/ezhttp.gitcd ezhttpchmod +x start.sh./start.sh
软件选择问题，参见书签中《使用EZHTTP安装LNMP(Nginx MySQL PHP) 》。网络条件良好的话，一个小时左右就可以安装完成。
2.5. 测试在浏览器地址栏输入http://192.168.1.114，可以看到，EZHTTP已经安装成功。
2.6. 上传thinkphp1、下载thinkphp，解压到thinkphp_3.2.3_full。2、重命名thinkphp_3.2.3_full文件夹为thinkphp。3、利用xftp上传thinkphp文件夹到/home/wwwroot目录下。
2.7. 测试在浏览器栏输入http://192.168.1.114/thinkphp/，可以看到，thinkphp已经可以访问。
3. 后记再次启动centos，利用ip add命令查看到ip地址，在浏览器地址栏输入该ip地址。然后，神奇的事情发生了——网站无法访问！打开命令提示符，ping该ip，提示“无法访问目标主机”。
解决办法：关闭centos防火墙，执行命令systemctl stop firewalld.service。
4. 书签在CentOS上搭建PHP服务器环境
EZHTTP使用教程
EZHTTP安装前准备工作
使用EZHTTP安装LNMP(Nginx MySQL PHP)
EZHTTP相关进程管理及目录位置
LAMP一键安装包
LNMP一键安装包
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>php</tag>
        <tag>好好学PHP</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux系统配置静态IP</title>
    <url>/dev-linux-static-ip/</url>
    <content><![CDATA[1. 前言在Linux系统中，静态IP的配置是一个非常重要的维护任务，因为它帮助服务器在网络中保持固定的IP地址。本文中，我们会学习Linux系统（CentOS7 和 Ubuntu16）的静态IP配置方法。


2. CentOS7配置静态IP在CentOS 7中，静态IP的配置主要涉及到修改 /etc/sysconfig/network-scripts 目录下的 ifcfg-xxx 文件。
1、查看网卡名称
ip add

假设看到的网卡为 ifcfg-enp0s3
2、修改网卡配置文件，设置固定IP
vim /etc/sysconfig/network-scripts/ifcfg-enp0s3

ifcfg-enp0s3 原配置示例：
TYPE=EthernetBOOTPROTO=dhcpDEFROUTE=yesPEERDNS=yesPEERROUTES=yesIPV4_FAILURE_FATAL=noIPV6INIT=yesIPV6_AUTOCONF=yesIPV6_DEFROUTE=yesIPV6_PEERDNS=yesIPV6_PEERROUTES=yesIPV6_FAILURE_FATAL=noNAME=enp0s3UUID=459b17a4-fd16-4ea7-8a4b-9509b6e899e7DEVICE=enp0s3ONBOOT=yes

ifcfg-enp0s3 配置静态IP示例：
TYPE=EthernetNAME=enp0s3DEVICE=enp0s3ONBOOT=yesBOOTPROTO=staticIPADDR=192.168.56.101NETMASK=255.255.255.0GATEWAY=192.168.56.1DNS1=180.76.76.76DNS2=8.8.8.8

3、重启网络服务
service network restart

3. Ubuntu16配置静态IP在Ubuntu 16中，静态IP的配置涉及到修改 /etc/network/interfaces 文件。
1、查看网卡名称
ip add

假设看到的网卡为 ifcfg-enp0s3
2、修改网卡配置文件，设置固定IP
vim /etc/network/interfaces

interfaces 原配置示例：
# This file describes the network interfaces available on your system# and how to activate them. For more information, see interfaces(5).source /etc/network/interfaces.d/*# The loopback network interfaceauto loiface lo inet loopback# The primary network interfaceauto enp0s3iface enp0s3 inet dhcp

interfaces 配置静态IP示例：
# This file describes the network interfaces available on your system# and how to activate them. For more information, see interfaces(5).source /etc/network/interfaces.d/*# The loopback network interfaceauto loiface lo inet loopback# The primary network interfaceauto enp0s3iface enp0s3 inet staticaddress 192.168.56.102netmask 255.255.255.0gateway 192.168.56.1dns-nameservers 180.76.76.76 8.8.8.8

如果有多张网卡，那么可以继续在 /etc/network/interfaces 中添加。
3、重启网络服务
/etc/init.d/networking restart

或者重启单个网卡：
ifdown enp0s3ifup enp0s3

如果报错：RTNETLINK answers: File exists那就需要先执行一下 ip addr flush dev enp0s3
4. Ubuntu18参考文档：《好好学Linux：Ubuntu18配置静态IP》
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
        <tag>网络</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：VirtualBox网络适配器</title>
    <url>/dev-virtualbox-network-adapter/</url>
    <content><![CDATA[1. 为什么需要学习网络适配器？理解VirtualBox中的网络适配器和其工作方式是非常重要的，原因如下：

安全性：不同的网络连接方式会对虚拟机的安全性产生影响。例如，如果我们将虚拟机设置为桥接模式，虚拟机将会在我们的网络中设备中显示，这样可能会增加安全风险。如果我们不希望虚拟机暴露在我们的网络中，我们可能需要选择NAT或主机模式。
网络功能：不同的网络适配器类型将决定虚拟机可以访问哪些网络资源。如果我们需要在虚拟机上运行某些特定的网络服务或应用，我们需要确保我们选择了正确的网络适配器类型以便让这些服务或应用可以正常工作。
虚拟机间通信：如果我们在同一台主机上运行多个虚拟机，不同的网络适配器类型可以决定这些虚拟机是否可以彼此通信以及如何通信。
故障排除：如果我们在虚拟机上遇到网络问题，理解不同的网络适配器和其工作方式可以帮助你更快地找出问题的原因并解决问题。

本文中，我们以VirtualBox为例学习网络适配器。对于其他虚拟机软件的网络适配器，原理也是相通的。
为便于描述，下文中会使用一些名词，这里提前声明一下它们的含义：

虚拟机：虚拟软件虚拟出的机器，实际不存在。
宿主机：运行虚拟机软件的机器，一般是物理机，也可能是虚拟机（嵌套虚拟化）。
宿主机局域网：宿主机所在的二层网络。
其他主机：和宿主机在同一个二层网络的主机。

参考文档：

快速理解VirtualBox的四种网络连接方式
VirtualBox + CentOS 虚拟机网卡配置
centOS7在VirtualBox中装好后的网络连接问题



2. VirtualBox中的网络适配器Oracle VM VirtualBox提供了多种网络适配器类型供虚拟机使用：

不连接（None）：网络适配器存在但未连接到宿主机或其他网络。这种情况下，虚拟机不能访问任何网络。
网络地址转换（NAT）：使用NAT模式可以使虚拟机分享宿主机的IP地址，虚拟机可以访问宿主机局域网，但宿主机局域网不能直接访问虚拟机。这就像你的电脑连在一个路由器后面，你可以访问互联网，但互联网上的其他人不能直接访问你的电脑。
桥接适配器（Bridged networking）：桥接模式使虚拟机能够与宿主机在同一网络。在虚拟机内部看来，网络适配器是连接到其自己的物理网络，独立于宿主机的网络连接。这就像虚拟机和宿主机都连在同一个交换机上。
内部网络（Internal networking）：虚拟机可以彼此进行通信，但不能与外界通信。这就像虚拟机都连在一个交换机上，但这个交换机没连在任何其他网络上。
主机模式（Host-only networking）：与内部网络类似，但网络也连接到主机，允许在主机上运行的所有虚拟机与主机本身之间进行通信。

3. 网络适配器详解3.1. 不连接（None）不连接（None），网络适配器存在但未连接到宿主机或其他网络。这种情况下，虚拟机不能访问任何网络。
3.2. 网络地址转换（NAT）网络地址转换（NAT）是最简单的实现虚拟机上网的方式。
虚虚拟机可以访问宿主机能访问到的所有网络，但是对于宿主机和其他主机，虚拟机又是不可见的，甚至宿主机也访问不到虚拟机。
虚拟机网络关系说明：

虚拟机与宿主机的关系：只能单向访问，虚拟机可以访问到宿主机，宿主机无法访问到虚拟机。
虚拟机与其他主机的关系：只能单向访问，虚拟机可以访问到其他主机，其他主机不能访问到虚拟机。
虚拟机与虚拟机的关系：相互不能访问，虚拟机与虚拟机各自完全独立，相互间无法通过网络访问彼此。

3.3. 桥接适配器（Bridged networking）桥接适配器（Bridged networking）通过宿主机网卡，架设了一条桥，直接连入到宿主机局域网中。这种连接方式使得虚拟机能被分配到宿主机局域网中独立的IP，所有网络功能完全和在网络中的真实主机一样。
网桥模式下的虚拟机，它的网络表现和真实主机相同。
虚拟机网络关系说明：

虚拟机与宿主机的关系：可以相互访问，因为虚拟机在真实网络段中有独立IP，宿主机与虚拟机处于同一网络段中，彼此可以通过各自IP相互访问。
虚拟机于其他主机的关系：可以相互访问，同样因为虚拟机在真实网络段中有独立IP，虚拟机与所有网络其他主机处于同一网络段中，彼此可以通过各自IP相互访问。
虚拟机与虚拟机的关系：如果两台虚拟机都是桥接，那么可以相互访问；如果一台是桥接，那么把桥接的那台虚拟机视为网络中的其他主机。

3.4. 内部网络（Internal networking）内部网络（Internal networking）实现虚拟机和虚拟机之间的内部网络通信，与宿主机不通，与宿主机局域网不通。
虚拟机网络关系说明：

虚拟机与宿主机的关系：不能相互访问，彼此不属于同一个网络，无法相互访问。
虚拟机与其他主机的关系：不能相互访问，彼此不属于同一个网络，无法相互访问。
虚拟机与虚拟机的关系：可以相互访问，前提是在设置网络时，两台虚拟机设置同一网络名称。

3.5. 主机模式（Host-only networking）主机模式（Host-only networking）与内部网络类似，但网络也连接到主机，允许在主机上运行的所有虚拟机与主机本身之间进行通信。
宿主机中模拟出一张专供虚拟机使用的网卡，所有虚拟机都连接到该网卡上，宿主机本身也连接到该网卡上。我们可以通过设置这张网卡来实现上网及其他很多功能，比如（网卡共享、网卡桥接等）。
虚拟机网络关系说明：

虚拟机与主机的关系：默认可以相互访问，host-only网卡默认IP段为 192.168.56.0/24 ，创建的虚拟机默认分配到这个网段。
虚拟机与其他主机的关系：默认不能相互访问，通过设置，可以实现相互访问。
虚拟机与虚拟机的关系：默认可以相互访问，都是同处于一个网段。

4. 虚拟机网络设计比较简单的设计，是添加两张网卡。一张选择仅主机（Host-Only）适配器，用于宿主机和虚拟机之间的通讯，使宿主机可以访问虚拟机的服务；一张选择网络地址转换（NAT），用于分享宿主机网络给虚拟机，使虚拟机可以访问外网。
5. VirtualBox虚拟机网络配置方法选中虚拟机，设置，网络，连接方式选择桥接网卡或者其他网络适配器。如果想要给虚拟机设置静态IP，可以参考文档：《好好学Linux：Linux系统配置静态IP》
PS：虚拟机启动后，利用 右ctrl 键来切换宿主机和虚拟机的鼠标，利用 右ctrl+F 来切换虚拟机全屏状态。
6. 后记以后不玩图形界面的Linux了，而是专注于通过命令行操作Linux，加油！
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>好好学Linux</tag>
        <tag>网络</tag>
        <tag>virtualbox</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学PHP：ThinkPHP开发环境搭建</title>
    <url>/dev-thinkphp-environment/</url>
    <content><![CDATA[1. 前言8月12号，抵达东北师范，开启了闭关修炼模式。入手的第一个项目，小太阳，有意思的名字。这是一个物业管理系统，分为业主端、物业端、CMS端，计划使用ThinkPHP框架来开发。PHP，好久不见，倍感亲切。


2. PHP运行环境PHP运行，需要两个条件，一个是Apache容器，另一个是PHP程序。很多情况下，还需要Mysql数据库。本次环境搭建中，我们使用集成环境WAMPServer，本环境包含了以上三个软件。省却了很多配置，很方便。其中，W代表Windows，A代表Apache，M代表Mysql，P代表PHP。
2.1. 下载WAMPServerWAMPSever官网：http://www.wampserver.com/不翻墙的话，可能下载不下来，这里提供一个360网盘的下载链接：https://yunpan.cn/c6CkFLeumMZVv  访问密码 38f7
2.2. MSVCR110运行WAMPServer，报错：
如果是64位系统，下载安装vcredist_x64.exe，即可解决该错误，32位同理下载vcredist_x86.exe。微软官网下载：http://www.microsoft.com/zh-CN/download/details.aspx?id=30679
360网盘的下载链接：https://yunpan.cn/c6C2FxHGXTabm  访问密码 f641
2.3. 查看运行效果成功运行WAMPSever后，我们发现桌面右下角多了一个WAMPSever的logo。这时在浏览器地址栏中输入http://localhost，即可看到WAMP的一些信息。
2.4. phpmyadmin在浏览器地址栏中输入http://localhost/phpmyadmin/，可以看到phpmyadmin的登录界面。默认用户名为root，密码为空。
3. 下载安装ThinkPHP3.1. 下载ThinkPHP官网：http://www.thinkphp.cn/360网盘的下载链接：https://yunpan.cn/c6CvrUtM6C6X4  访问密码 ebd1
3.2. 安装1、解压到thinkphp_3.2.3_full。2、重命名thinkphp_3.2.3_full文件夹为thinkphp。3、剪切thinkphp文件夹到WAMP安装目录下的www文件夹中。比如我的WAMP安装目录为D:\Server\wamp64，那么就把thinkphp放到D:\Server\wamp64\www中。
3.3. 查看运行结果在浏览器地址栏中输入http://localhost/thinkphp，即可看到thinkphp的一些信息。
4. 后记至此，ThinkPHP的开发环境搭建成功，接下来可以愉快地开发了！
附上另外几款常见的PHP集成环境：1、AppServ2、WAMPP3、ServKit（原名PHPnow）4、UPUPW
5. 书签PHP入门篇http://www.imooc.com/learn/54
ThinkPHP3.2完全开发手册http://document.thinkphp.cn/manual_3_2.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>php</tag>
        <tag>好好学PHP</tag>
      </tags>
  </entry>
  <entry>
    <title>微信公众号支付流程</title>
    <url>/dev-weixinpay-process/</url>
    <content><![CDATA[1. 前言花费了一天时间，调通了微信公众号支付。作下记录，方便以后再次填坑。先声明，微信公众号支付，不同于微信H5支付，这点在本文结束时再详细说明。


2. 微信配置2.1. 设置测试目录在微信公众平台设置，栏目见下图。支付测试状态下，设置测试目录，测试人的微信号添加到白名单，发起支付的页面目录必须与设置的精确匹配。并将支付链接发到对应的公众号会话窗口中才能正常发起支付测试。注意正式目录一定不能与测试目录设置成一样，否则支付会出错。
2.2. 设置正式支付目录根据图中栏目顺序进入修改栏目，勾选JSAPI网页支付开通该权限，并配置好支付授权目录，该目录必须是发起支付的页面的精确目录，子目录下无法正常调用支付。具体界面如图所示：
3. 交互流程3.1. 业务流程时序图首先看下微信官方给出的交互流程：
3.2. 服务器交互微信公众号支付的流程，简而言之只有两步。第一步，下单，拿到prepay_id等信息；第二步，利用第一步拿到的prepay_id等信息进行支付。
4. 代码4.1. Node端// 微信支付exports.weixinpay = function(req, res)&#123;  var orderId = req.params.orderId;  var selectSum = req.params.selectSum;  var token = req.params.token;  // 获取code  if(!req.query.code)&#123;    var r_url = &#x27;http://&#x27; +config.host+&#x27;/artist/weixinpay/&#x27;+orderId+&#x27;/&#x27;+selectSum+&#x27;/&#x27;+token;    var url = &#x27;https://open.weixin.qq.com/connect/oauth2/authorize?appid=&#x27;+config.weixin.appid+&#x27;&amp;redirect_uri=&#x27;+urlencode(r_url)+&#x27;&amp;response_type=code&amp;scope=snsapi_userinfo&amp;state=111#wechat_redirect&#x27;;    res.redirect(url);  &#125;else&#123;    var code = req.query.code;    var state = req.query.state;    var ep = new eventproxy();    ep.all(&#x27;userInfo&#x27;,function(userInfoData)&#123;      var userInfoData = JSON.parse(userInfoData);      res.render(&#x27;artist/weixinpay&#x27;,&#123;        // 其他数据        openid: userInfoData.openid,        orderId: orderId,        selectSum: selectSum,        token: token      &#125;);    &#125;);    // 获取用户信息userInfo  &#125;&#125;exports.weixinpay2 = function(req, res)&#123;  var wxParamUrl = config.apihost + &#x27;/order/getH5SubOrderforWechat&#x27;;  var param = &#123;    orderId: req.query.orderId,    selectSum: req.query.selectSum,    accessToken: req.query.token,    openId: req.query.openid  &#125;;  request.post(&#123;url: wxParamUrl,form: JSON.stringify(param)&#125;,function(error, response, body)&#123;        res.render(&#x27;artist/weixinpay2&#x27;,&#123;      // 其他数据      wxParam: JSON.parse(response.body)    &#125;);  &#125;);&#125;

上面的代码看起来很奇怪，为什么需要一个weixinpay2函数？代码全部放在weixinpay函数里面，然后在weixinpay.html里调用微信支付接口，不是很好么？这个，就涉及到支付目录的问题了。weixinpay函数中，微信授权获取用户信息后的回调url，目录太深，而且不确定，因为我们是通过目录来传递参数的。这样，就无法在微信公众号上面设置支付授权目录。所以，我们不能在weixinpay.html这个页面发起支付请求，只能把参数转发给下一个weixinpay2.html页面，在weixinpap2.html中发起支付请求。
那么，为什么获取用户信息后的回调url目录太深？直接把orderId等信息当做参数放在回调url后面不就可以了吗？很遗憾，回调url无法带入你的自定义参数。因此，只能把参数当做回调url的一部分，也就是目录的一部分。
综上，weixinpay函数负责获取用户的openid，weixinpay.html负责跳转weixinpay2.html；weixinpay2函数负责获取调用微信JSAPI的参数，weixinpay2.html负责调用微信JSAPI。
4.2. html&lt;!--weixinpay.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;微信支付&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;a id=&quot;weixin-link&quot; href=&quot;/artist/weixinpay2?orderId=&lt;%= orderId%&gt;&amp;selectSum=&lt;%= selectSum%&gt;&amp;token=&lt;%= token%&gt;&amp;openid=&lt;%= openid%&gt;&quot; style=&quot;display: none&quot;&gt;微信支付&lt;/a&gt;&lt;script&gt;    var link = document.getElementById(&#x27;weixin-link&#x27;);    link.click();&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

&lt;!--weixinpay2.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;zh&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;微信支付&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;input id=&quot;appId&quot; type=&quot;hidden&quot; value=&quot;&lt;%= wxParam.obj.appid%&gt;&quot;&gt;    &lt;input id=&quot;timeStamp&quot; type=&quot;hidden&quot; value=&quot;&lt;%= wxParam.obj.timestamp%&gt;&quot;&gt;    &lt;input id=&quot;nonceStr&quot; type=&quot;hidden&quot; value=&quot;&lt;%= wxParam.obj.noncestr%&gt;&quot;&gt;    &lt;input id=&quot;package&quot; type=&quot;hidden&quot; value=&quot;&lt;%= wxParam.obj.packagestr%&gt;&quot;&gt;    &lt;input id=&quot;signType&quot; type=&quot;hidden&quot; value=&quot;MD5&quot;&gt;    &lt;input id=&quot;paySign&quot; type=&quot;hidden&quot; value=&quot;&lt;%= wxParam.obj.sign%&gt;&quot;&gt;&lt;script&gt;function onBridgeReady()&#123;    var appId = document.getElementById(&#x27;appId&#x27;).value;    var timeStamp = document.getElementById(&#x27;timeStamp&#x27;).value;    var nonceStr = document.getElementById(&#x27;nonceStr&#x27;).value;    var package = document.getElementById(&#x27;package&#x27;).value;    var signType = document.getElementById(&#x27;signType&#x27;).value;    var paySign = document.getElementById(&#x27;paySign&#x27;).value;    WeixinJSBridge.invoke(        &#x27;getBrandWCPayRequest&#x27;, &#123;            &quot;appId&quot;: appId,     //公众号名称，由商户传入                 &quot;timeStamp&quot;:timeStamp,         //时间戳，自1970年以来的秒数                 &quot;nonceStr&quot;: nonceStr, //随机串                 &quot;package&quot;: package,                 &quot;signType&quot;: signType,         //微信签名方式：                 &quot;paySign&quot;: paySign //微信签名         &#125;,        function(res)&#123;            WeixinJSBridge.log(res.err_msg);            //alert(res.err_code + res.err_desc + res.err_msg);            if (res.err_msg == &quot;get_brand_wcpay_request:ok&quot;) &#123;                  window.location.href = &#x27;/artist/alipayresult?trade_status=TRADE_SUCCESS&#x27;;                // 执行跳转页面....              &#125; else if (res.err_msg == &quot;get_brand_wcpay_request:cancel&quot;) &#123;                  alert (&quot;用户取消支付!&quot;);              &#125; else &#123;                  alert (&quot;支付失败!&quot;);              &#125;                      &#125;    ); &#125;if (typeof WeixinJSBridge == &quot;undefined&quot;)&#123;    if( document.addEventListener )&#123;        document.addEventListener(&#x27;WeixinJSBridgeReady&#x27;, onBridgeReady, false);    &#125;else if (document.attachEvent)&#123;        document.attachEvent(&#x27;WeixinJSBridgeReady&#x27;, onBridgeReady);         document.attachEvent(&#x27;onWeixinJSBridgeReady&#x27;, onBridgeReady);    &#125;&#125;else&#123;    onBridgeReady();&#125;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

为了加快执行速度，这里的两个页面中，都使用原生的js来实现页面逻辑。
5. 错误5.1. 支付验证签名失败很明显，支付验证签名失败。怎么判断算出的签名是否正确？使用微信支付接口签名校验工具。校验方式选择自定义参数，把需要签名的参数名称和参数值输入页面，点击生成签名即可。
5.2. 当前页面的URL未注册不好意思，这个错误忘记截图了。借来了一张图，凑合着看。这个问题，是由于调用支付接口的url不对！那么，怎样是对的呢？举个栗子：如果调用支付接口的url为http://wx.voidking.com/pay/weixinpay，那么微信公众平台上的支付授权目录应该设置为http://wx.voidking.com/pay/。也就是说，支付授权目录应该设置为调用支付接口的url的上一级目录。
6. 支付成功
7. 后记微信公众号支付，两个缺点，一是必须在微信浏览器使用，二是必须有一个拉取用户信息的步骤。但是我们发现，很多网站，在其他浏览器也可以使用微信支付，也不需要拉取用户信息，这是怎么回事？
因为，这是两种不同的支付方式！今天我们讨论的，叫做微信公众号支付；而在其他浏览器中调用微信支付，叫做微信H5支付！
更多关于微信H5支付的内容，请参考微信H5支付官方文档。
8. 书签微信支付开发文档——公众号支付https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1
微信支付接口签名校验工具https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1
非微信内置浏览器中的网页调起微信支付的方案研究http://blog.csdn.net/ahence/article/details/51317814
微信支付|商户平台开发者文档https://pay.weixin.qq.com/wiki/doc/api/wap.php?chapter=15_1
【微信支付V2.0】H5支付实例http://wxpay.weixin.qq.com/pub_v2/pay/wap.v2.php
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>微信</tag>
      </tags>
  </entry>
  <entry>
    <title>H5跳转微信公众平台</title>
    <url>/dev-h5-jump-wechat-public-platform/</url>
    <content><![CDATA[微信公众平台发布了一篇文章，单击公众平台名称，能够直接跳转到微信公众平台的关注页面。就像这样：



请问，如果我们自己写了一篇文章，不是发布在微信公众平台上，能不能跳转到微信公众平台关注页面呢？答案是，不能，但是我们有替代方案。
显然，这样关注也是很方便的，关键点在于“艾佳生活志”的链接怎么搞出来。有个网站贴心的给出了方案：微信公众号一键关注页面生成器。将公众号发过的图文链接粘贴进去，点击生成即可得到我们需要的链接。
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>微信</tag>
      </tags>
  </entry>
  <entry>
    <title>外包平台</title>
    <url>/dev-outsourcing-platform/</url>
    <content><![CDATA[1. 前言计划10号离职，12号前往吉林长春。无论是出于自尊心，还是出于一个男人的责任，都不打算再向家里要钱。没有了收入来源，那就开源！想来想去，外包也许是最好的选择。从知乎上摘录了一些外包平台，备忘。


2. 平台1、程序员客栈程序员客栈-程序员的经纪人|网站建设APP开发制作远程技术中心https://www.proginn.com/
2、快码众包快码众包-让互联网产品开发更快速！认准域名Kuai.mahttp://www.kuai.ma/
3、Coding码市Coding 码市 - 云端众包，可靠交付，远程工作，全程担保！https://mart.coding.net/
4、猿团猿团-YuanTuan.Com_成为创业者的专业技术合伙人http://www.yuantuan.com/
5、英选英选 | 从0到1，打造你的第一版产品https://www.linktion.cn/
6、开源中国众包平台开源中国众包平台 - 专业IT软件众包平台，快速交付，安全可靠-开源中国众包平台https://zb.oschina.net/
7、码易码易-高质量软件交付服务平台http://www.mayigeek.com/
8、小圆桌首页 - 创业与人才的社区|小圆桌http://www.xyuanzhuo.com/
9、项目软件交易网软件项目交易网－中国最早的软件外包服务平台，专注实现中国程序员价值http://www.sxsoft.com/
10、人人开发人人开发 - 集可视化开发，应用市场，威客众包，PaaS云于一体的企业级应用服务平台http://rrkf.com/
11、极客邦极客邦 专注提供技术服务的在线平台http://www.looip.cn/
12、开发邦开发邦-专业互联网软件开发与咨询服务http://www.kaifabang.com/
3. 书签国内有类似Freelancer的网站？https://www.zhihu.com/question/26478092
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
  </entry>
  <entry>
    <title>css雪碧图</title>
    <url>/dev-css-sprite/</url>
    <content><![CDATA[1. 前言CSS雪碧 即CSS Sprite，也有人叫它CSS精灵，是一种CSS图像合并技术，该方法是将小图标合并到一张图片上，然后利用css的背景定位来显示需要显示的图片部分。
优点：减少加载网页图片时对服务器的请求次数，提高页面的加载速度，减少鼠标滑过的一些bug。


2. 制作雪碧图的制作，可以使用PS，也可以使用专门的雪碧图制作工具。制作时，最好制作成一列或者一行，定位时会方便一些。下面这个工具挺好用，分享给大家：https://yunpan.cn/cMKygj2hnrBBe  访问密码 f516
css雪碧图简单制作工具（源码）https://github.com/iwangx/sprite
如果要制作svg雪碧图，推荐使用AI。
3. 定位3.1. 位置雪碧图定位的关键，在于background-position。诀窍在于“调试”，在页面控制背景图上下左右移动，很快就定位好了。
以上面的雪碧图为例，假设我们要显示微博的图标，那么scss代码如下：
.icon-weibo&#123;    width: 20px;    height: 20px;    background: url(../../img/test/index/icon.png) no-repeat;    background-position: 0px -60px;&#125;

3.2. 大小假设我们的要显示的图标比雪碧图大，或者比雪碧图小，该怎么办？background-size。
.icon-weibo&#123;    width: 40px;    height: 40px;    background: url(../../img/test/index/icon.png) no-repeat;    background-position: -2px -116px;    background-size: 118%;&#125;

3.3. 移动端很多时候，我们并不使用px作为单位，而是rem或者百分比，这时候，该怎么控制雪碧图的位置和大小？利用svg图片。
&lt;span class=&quot;i i_menu_0&quot;&gt;&lt;/span&gt;

.i &#123;    width: 0.8rem; height: 0.8rem;    background: url(../images/ico_global.svg) no-repeat;    display: inline-block;    background-size: 1100%;&#125;.i_menu_0 &#123; background-position: 0% 0%; &#125;.i_menu_1 &#123; background-position: 10% 0%; &#125;.i_menu_2 &#123; background-position: 20% 0%; &#125;.i_menu_3 &#123; background-position: 30% 0%; &#125;.i_menu_4 &#123; background-position: 40% 0%; &#125;.i_menu_5 &#123; background-position: 50% 0%; &#125;

4. 后记至于PS和AI的使用，在慕课网和网易云课堂上有很多优秀教程，不要错过。
5. 书签CSS雪碧图的实现方法（即背景定位）http://www.suixin8.com/59.html
CSS3技术-雪碧图自适应缩放http://www.imooc.com/wiki/detail/id/183
利用动态viewport+rem制作一张自适应的svg雪碧图iconhttp://www.open-open.com/lib/view/open1452229325136.html
SVG的用法http://www.webhek.com/svg/
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>css</tag>
        <tag>前端</tag>
        <tag>sprite</tag>
      </tags>
  </entry>
  <entry>
    <title>用div模拟select</title>
    <url>/dev-simulate-select-by-div/</url>
    <content><![CDATA[1. 前言四不四傻？有select不用，干嘛要用div来模拟select呢？下面来看一个问题：
请问不使用chosen等插件，也不使用div模拟select，通过html和css，有没有办法限制select下拉框的高度。默认显示20条option，我想改成5条该怎么处理？
答案是，无解！使用插件？找了十几款selectbox插件，都不满意！要么封装起来麻烦，要么根本不提供限制下拉框长度的功能。
评估了一下，还是自己模拟一个select选择框更靠谱。


2. select日期选择2.1. html&lt;div class=&quot;birthday&quot;&gt;    &lt;label&gt;生日：&lt;/label&gt;    &lt;select name=&quot;month&quot; id=&quot;month&quot;&gt;        &lt;option value=&quot;1&quot;&gt;1&lt;/option&gt;        &lt;option value=&quot;2&quot;&gt;2&lt;/option&gt;        &lt;option value=&quot;3&quot;&gt;3&lt;/option&gt;        &lt;option value=&quot;4&quot;&gt;4&lt;/option&gt;        &lt;option value=&quot;5&quot;&gt;5&lt;/option&gt;        &lt;option value=&quot;6&quot;&gt;6&lt;/option&gt;        &lt;option value=&quot;7&quot;&gt;7&lt;/option&gt;        &lt;option value=&quot;8&quot;&gt;8&lt;/option&gt;        &lt;option value=&quot;9&quot;&gt;9&lt;/option&gt;        &lt;option value=&quot;10&quot;&gt;10&lt;/option&gt;        &lt;option value=&quot;11&quot;&gt;11&lt;/option&gt;        &lt;option value=&quot;12&quot;&gt;12&lt;/option&gt;    &lt;/select&gt;月    &lt;select name=&quot;day&quot; id=&quot;day&quot;&gt;    &lt;/select&gt;日&lt;/div&gt;

2.2. js$(&#x27;#month&#x27;).change(function()&#123;    changeDay();&#125;);function changeDay()&#123;    var $day = $(&#x27;#day&#x27;);    var month = $(&#x27;#month&#x27;).val();    var data = [];              if(month==2)&#123;        for (var i = 1; i &lt;= 29; i++) &#123;            data.push(i);        &#125;    &#125;else if(month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12)&#123;        for (var i = 1; i &lt;= 31; i++) &#123;            data.push(i);        &#125;    &#125;else if(month==4 || month==6 || month==9 || month==11)&#123;        for (var i = 1; i &lt;= 30; i++) &#123;            data.push(i);        &#125;    &#125;    var html = &#x27;&#x27;;    data.forEach( function(element, index) &#123;        html += &#x27;&lt;option value=&#x27;+element+&#x27;&gt;&#x27;+element+&#x27;&lt;/option&gt;&#x27;;    &#125;);    $day.html(html);    $day.find(&#x27;option&#x27;).first().checked;&#125;

3. div日期选择3.1. html&lt;div class=&quot;birthday2&quot;&gt;    &lt;label&gt;生日：&lt;/label&gt;    &lt;div class=&quot;simulate-select&quot; id=&quot;month-box&quot;&gt;        &lt;input class=&quot;input&quot; name=&quot;month2&quot; type=&quot;hidden&quot; value=&quot;1&quot;&gt;        &lt;div class=&quot;check&quot;&gt;1&lt;/div&gt;月        &lt;ul class=&quot;items&quot;&gt;            &lt;li&gt;1&lt;/li&gt;            &lt;li&gt;2&lt;/li&gt;            &lt;li&gt;3&lt;/li&gt;            &lt;li&gt;4&lt;/li&gt;            &lt;li&gt;5&lt;/li&gt;            &lt;li&gt;6&lt;/li&gt;            &lt;li&gt;7&lt;/li&gt;            &lt;li&gt;8&lt;/li&gt;            &lt;li&gt;9&lt;/li&gt;            &lt;li&gt;10&lt;/li&gt;            &lt;li&gt;11&lt;/li&gt;            &lt;li&gt;12&lt;/li&gt;        &lt;/ul&gt;    &lt;/div&gt;    &lt;div class=&quot;simulate-select&quot; id=&quot;day-box&quot;&gt;        &lt;input class=&quot;input&quot; name=&quot;day2&quot; type=&quot;hidden&quot; value=&quot;1&quot;&gt;        &lt;div class=&quot;check&quot;&gt;1&lt;/div&gt;日        &lt;ul class=&quot;items&quot;&gt;        &lt;/ul&gt;    &lt;/div&gt;&lt;/div&gt;

3.2. scss.simulate-select&#123;    position: relative;    display: inline-block;    .check&#123;        display: inline-block;        width: 40px;        text-align: center;        border: 1px solid #333;    &#125;    .items&#123;        display: none;        list-style: none;        position: absolute;        top: 100%;        left: 0;        height: 100px;        overflow-y: auto;        margin: -1px 0 0 0;        padding: 0;        width: 40px;        text-align: center;        border: 1px solid #333;        background: #fff;        li&#123;            &amp;.active&#123;                background: #eee;                color: #fff;            &#125;        &#125;        &amp;.active&#123;            display: block;        &#125;    &#125;&#125;

3.3. jsselectMonth2();changeDay2();selectDay2();function selectMonth2()&#123;    var self = this;    var $month = $(&#x27;#month-box&#x27;);    $(&#x27;#month-box&#x27;).on(&#x27;click&#x27;,&#x27;.check&#x27;,function(event)&#123;        $(&#x27;#day-box&#x27;).find(&#x27;.items&#x27;).removeClass(&#x27;active&#x27;);        if($month.find(&#x27;.items&#x27;).hasClass(&#x27;active&#x27;))&#123;            $month.find(&#x27;.items&#x27;).removeClass(&#x27;active&#x27;);        &#125;else&#123;            $month.find(&#x27;.items&#x27;).addClass(&#x27;active&#x27;);        &#125;        event.stopPropagation();    &#125;);    $(&#x27;#month-box&#x27;).on(&#x27;click&#x27;,&#x27;li&#x27;,function()&#123;        var month = $(this).html();        $month.find(&#x27;.input&#x27;).val(month);        $month.find(&#x27;.check&#x27;).html(month);        $month.find(&#x27;.items&#x27;).removeClass(&#x27;active&#x27;);        changeDay2();    &#125;);    $(document).click(function()&#123;        $month.find(&#x27;.items&#x27;).removeClass(&#x27;active&#x27;);    &#125;);&#125;function changeDay2()&#123;    var $month = $(&#x27;#month-box&#x27;);    var $day = $(&#x27;#day-box&#x27;);    var month = $month.find(&#x27;.input&#x27;).val();    var data = [];          if(month==2)&#123;        for (var i = 1; i &lt;= 29; i++) &#123;            data.push(i);        &#125;    &#125;else if(month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12)&#123;        for (var i = 1; i &lt;= 31; i++) &#123;            data.push(i);        &#125;    &#125;else if(month==4 || month==6 || month==9 || month==11)&#123;        for (var i = 1; i &lt;= 30; i++) &#123;            data.push(i);        &#125;    &#125;    var html = &#x27;&#x27;;    data.forEach( function(element, index) &#123;        html += &#x27;&lt;li&gt;&#x27;+element+&#x27;&lt;/li&gt;&#x27;;    &#125;);    $day.find(&#x27;.items&#x27;).html(html);    $day.find(&#x27;.check&#x27;).html(&#x27;1&#x27;);&#125;function selectDay2()&#123;    var $day = $(&#x27;#day-box&#x27;);    $(&#x27;#day-box&#x27;).on(&#x27;click&#x27;,&#x27;.check&#x27;,function(event)&#123;        $(&#x27;#month-box&#x27;).find(&#x27;.items&#x27;).removeClass(&#x27;active&#x27;);        if($day.find(&#x27;.items&#x27;).hasClass(&#x27;active&#x27;))&#123;            $day.find(&#x27;.items&#x27;).removeClass(&#x27;active&#x27;);        &#125;else&#123;            $day.find(&#x27;.items&#x27;).addClass(&#x27;active&#x27;);        &#125;        event.stopPropagation();    &#125;);    $(&#x27;#day-box&#x27;).on(&#x27;click&#x27;,&#x27;li&#x27;,function()&#123;        var day = $(this).html();        $day.find(&#x27;.input&#x27;).val(day);        $day.find(&#x27;.check&#x27;).html(day);        $day.find(&#x27;.items&#x27;).removeClass(&#x27;active&#x27;);    &#125;);    $(document).click(function()&#123;        $day.find(&#x27;.items&#x27;).removeClass(&#x27;active&#x27;);    &#125;);&#125;

3.4. 效果图
4. 源码https://github.com/voidking/nodebase/blob/master/views/weixin/love.htmlhttps://github.com/voidking/nodebase/blob/master/public/scss/weixin/love.scsshttps://github.com/voidking/nodebase/blob/master/public/js/weixin/love.js
5. 后记我相信，一定有更简单的方法实现限制select下拉框的高度。如果在调试时，可以捕捉到下拉框，就可以设置该下拉框的高度了。但是，尝试了各种办法都没有捕捉到，奇怪。不管了，暂时这样，以后发现了更好的解决办法再补上。
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>css</tag>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：使用Node.js读写文件</title>
    <url>/dev-nodejs-read-and-write-file/</url>
    <content><![CDATA[1. 前言七夕将近，公司要整一个活动：七夕礼物测试。大致需求是，用户选择自己的性别、生日和情感状态，点击“惊喜一下”，出现礼物详情。根据性别、生日和情感状态的不同，给出不同的礼物详情，一共有37种不同的礼物。同时，添加统计，统计有多少人参加过测试。


2. 设计这次活动，不打算让后端参与，所以要Node端自己负责数据的存取。
读取文件有三个方案：1、数据存在代码中（运行时直接加载到内存）。2、利用request模块读取。3、利用fs模块读取。
写入文件有一个方案：利用fs模块写入。
在之前的文章中，已经讨论过使用request模块读取本地文件的方法，本文主要探讨一下使用fs模块读写文件的方法。
3. 代码3.1. 数据文件在/public/data文件下，新建文件count.json。内容如下：
&#123;&quot;count&quot;:100&#125;

3.2. fs读取文件var fs = require(&#x27;fs&#x27;);var path = require(&#x27;path&#x27;);fs.readFile(path.join(__dirname,&#x27;../public/data/count.json&#x27;),&#123;encoding:&#x27;utf-8&#x27;&#125;,function(error, data)&#123;    console.log(data);    var countData = JSON.parse(data);    console.log(count.countData.count);&#125;);
需要解释一下的，是__dirname，在任何模块文件内部，可以使用__dirname变量获取当前模块文件所在目录的完整绝对路径。
path.join([path1],[path2],[…])函数，将多个参数组合成一个path。
3.3. fs写入文件var fs = require(&#x27;fs&#x27;);var path = require(&#x27;path&#x27;);fs.readFile(path.join(__dirname,&#x27;../public/data/count.json&#x27;),&#123;encoding:&#x27;utf-8&#x27;&#125;,function(error, data)&#123;    //console.log(data);    var count = parseInt(JSON.parse(data).count);    count++;    var countData = &#123;        count: count    &#125;;    fs.writeFile(path.join(__dirname,&#x27;../public/data/count.json&#x27;),JSON.stringify(countData),function(error)&#123;        console.log(&#x27;success&#x27;);    &#125;);&#125;);

3.4. 完整代码https://github.com/voidking/nodebase/blob/master/controllers/weixin.js
4. 书签简单的nodejs 文件系统（fs）读写例子http://www.2cto.com/kf/201411/351586.html
Node.js读取文件内容http://blog.csdn.net/zk437092645/article/details/9231787
node 的文件操作http://blog.chinaunix.net/uid-26672038-id-4139323.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>好好学Node.js</tag>
        <tag>nodejs</tag>
      </tags>
  </entry>
  <entry>
    <title>Cookie、localStorage和sessionStorage</title>
    <url>/dev-cookie-localstorage-and-sessionstorage/</url>
    <content><![CDATA[1. 前言在《使用accessToken记录登录状态》一文中，已经讨论了Cookie的增删查改。本文详细探讨一下Cookie、localStorage和sessionStorage的概念差别，以及localStorage的用法。


2. CookieCookie的特点是小，只有4k，常用来存储辨别用户信息的数据，比如accessToken。而且，Cookie有数量限制，每个特定的域名下，最多生成50个Cookie（IE7+）。最大的优势是几乎所有的浏览器都支持。
3. localStoragelocalStorage 是 HTML5 标准中新加入的技术，拥有5M的存储空间，主流浏览器都支持。
3.1. 存储if(window.localStorage)&#123;    var x = window.localStorage.userInfo? JSON.parse(window.localStorage.userInfo):&#123;&#125;;    if((x.unionid == undefined) || (x.unionid == &quot;undefined&quot;))&#123;        var temp = &#123;            unionid: $(&#x27;#unionid&#x27;).val(),            openid: $(&#x27;#openid&#x27;).val(),            nickname: $(&#x27;#nickname&#x27;).val(),            headimgurl:$ (&#x27;#headimgurl&#x27;).val()        &#125;;        window.localStorage.userInfo = JSON.stringify(temp);    &#125;&#125;

3.2. 读取var temp = (window.localStorage &amp;&amp; window.localStorage.userInfo)?JSON.parse(window.localStorage.userInfo):&#123;&#125;var unionid = temp.unionid;console.log(unionid);

4. sessionStoragesessionStorage和localStorage非常相似，最主要的差别，是生命周期。localStorage除非被清除，否则永久保存；sessionStorage关闭页面或浏览器后被清除。
5. 异同


特性
Cookie
localStorage
sessionStorage



数据的生命期
可设置失效时间，默认是关闭浏览器后失效
除非被清除，否则永久保存
仅在当前会话下有效，关闭页面或浏览器后被清除


存放数据大小
4K左右
一般为5MB
一般为5MB


与服务器端通信
每次都会携带在HTTP头中，如果使用cookie保存过多数据会带来性能问题
仅在客户端（即浏览器）中保存，不参与和服务器的通信
仅在客户端（即浏览器）中保存，不参与和服务器的通信


易用性
需要程序员自己封装，源生的Cookie接口不友好
源生接口可以接受，亦可再次封装来对Object和Array有更好的支持
源生接口可以接受，亦可再次封装来对Object和Array有更好的支持


6. 书签详说 Cookie, LocalStorage 与 SessionStoragehttps://segmentfault.com/a/1190000002723469
HTML5 localStorage本地存储实际应用举例
谈谈本地存储利弊Cookie、localStorage、sessionStoragehttp://www.tuicool.com/articles/fM32ier
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>cookie</tag>
        <tag>localstorage</tag>
        <tag>sessionstorage</tag>
      </tags>
  </entry>
  <entry>
    <title>支付宝签名与验签</title>
    <url>/dev-alipay-sign-and-checksign/</url>
    <content><![CDATA[1. 前言支付宝提供了DSA、RSA、MD5三种签名方式，本文仅讨论RSA签名。

RSA公钥加密算法是1977年由罗纳德·李维斯特（Ron Rivest）、阿迪·萨莫尔（Adi Shamir）和伦纳德·阿德曼（Leonard Adleman）一起提出的。1987年首次公布，当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。



2. RSA理解以下内容转载自21aspnet的《公钥，私钥和数字签名这样最好理解》，原作者举的例子通俗易懂，郝同学偷了过来，大家不要告诉他。
2.1. 公钥加密假设一下，我找了两个数字，一个是1，一个是2。我喜欢2这个数字，就保留起来，不告诉你们(私钥），然后我告诉大家，1是我的公钥。我有一个文件，不能让别人看，我就用1加密了。别人找到了这个文件，但是他不知道2就是解密的私钥啊，所以他解不开，只有我可以用数字2，就是我的私钥，来解密。这样我就可以保护数据了。我的好朋友x用我的公钥1加密了字符a，加密后成了b，放在网上。别人偷到了这个文件，但是别人解不开，因为别人不知道2就是我的私钥，只有我才能解密，解密后就得到a。这样，我们就可以传送加密的数据了。
2.2. 私钥签名如果我用私钥加密一段数据（当然只有我可以用私钥加密，因为只有我知道2是我的私钥），结果所有的人都看到我的内容了，因为他们都知道我的公钥是1，那么这种加密有什么用处呢？但是我的好朋友x说有人冒充我给他发信。怎么办呢？我把我要发的信，内容是c，用我的私钥2，加密，加密后的内容是d，发给x，再告诉他解密看是不是c。他用我的公钥1解密，发现果然是c。这个时候，他会想到，能够用我的公钥解密的数据，必然是用我的私钥加的密。只有我知道我得私钥，因此他就可以确认确实是我发的东西。这样我们就能确认发送方身份了。这个过程叫做数字签名。当然具体的过程要稍微复杂一些。用私钥来加密数据，用途就是数字签名。
2.3. 小结总结：公钥和私钥是成对的，它们互相解密。公钥加密，私钥解密。私钥数字签名，公钥验证。
3. 支付宝签名与验签3.1. 请求参数签名签名的目的：为了支付宝能够确定请求是由开发者发出的。
1、筛选获取所有请求参数，不包括字节类型参数，如文件、字节流，剔除sign与sign_type参数。
2、排序将筛选的参数按照第一个字符的键值ASCII码递增排序（字母升序排序），如果遇到相同字符则按照第二个字符的键值ASCII码递增排序，以此类推。
3、拼接将排序后的参数与其对应值，组合成“参数=参数值”的格式，并且把这些参数用&amp;字符连接起来，此时生成的字符串为待签名字符串。
4、签名MD5签名的商户需要将key的值拼接在字符串后面，调用MD5算法生成sign；RSA签名的商户将待签名字符串和商户私钥带入SHA1算法中得出sign。
3.2. 返回参数验证签名验签的目的：为了开发者能够确定请求是由支付宝发出的。
1、筛选获取所有支付宝返回的参数，不包括字节类型参数，如文件、字节流。验签参数剔除sign与sign_type参数。
2、排序将筛选的参数按照第一个字符的键值ASCII码递增排序（字母升序），如果遇到相同字符则按照第二个字符的键值ASCII码递增排序，以此类推。
3、拼接将排序后的参数与其对应值，组合成“参数=参数值”的格式，并且把这些参数用&amp;字符连接起来，此时生成的字符串为待签名字符串。
4、验签MD5：把MD5密钥（Key）拼接在待验证签名的字符串尾部，然后使用各自语言对应的MD5加密函数进行加密，得到的加密值与支付宝返回的参数sign做“相等”判断，同时对返回参数中的notify_id进行验证，如果这两个数据同时为true，则验证通过。
啊嘞，为什么没有RSA验签的说明？沈晨帅哥解释说，人家支付宝就只给你一种MD5！发送请求可选的签名方式有三种，接受请求只有一种签名方式，很合理。
3.3. 问题假设支付宝返回参数的签名方式也是RSA，那么怎么验证签名？猜测解：筛选、排序、拼接后，利用RSA公钥对待签名字符串进行加密，得到的加密值与支付宝返回的参数sign做“相等”判断，同时对返回参数中的notify_id进行验证，如果这两个数据同时为true，则验证通过。
前提是，对同一个字符串进行加密和签名，得到的sign是一样的！比如，利用公钥对“a”进行加密得到sign1，利用私钥对“a”进行签名得到sign2，那么，sign1=sign2！
那么，这个前提对不对呢？不好意思，不对！
公钥加密，通过私钥解密可以得到明文；私钥加密，通过公钥解密可以得到明文。然而，对于同一个字符串，公钥加密和私钥加密得到的密文是不同的！
3.4. 对称加密对称加密算法中，使用的密钥只有一个，发收信双方都使用这个密钥对数据进行加密和解密。
MD5加密算法就属于对称加密算法，因为无法进行解密，所以MD5加密算法也属于单向加密算法。常用于用户密码的存储和签名验签。
3.5. 非对称加密不对称加密算法使用两把完全不同但又是完全匹配的一把钥匙——公钥和私钥。在使用不对称加密算法加密文件时，只有使用匹配的一对公钥和私钥，才能完成对明文的加密和解密过程。
RSA加密算法就属于非对称加密算法，常用于加密解密和git服务器验签。
4. 书签签名与验签
公钥，私钥和数字签名这样最好理解http://blog.csdn.net/21aspnet/article/details/7249401
RSA算法原理（一）http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html
RSA算法原理（二）http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>支付宝</tag>
        <tag>rsa</tag>
      </tags>
  </entry>
  <entry>
    <title>支付宝手机网站支付流程</title>
    <url>/dev-alipay-process/</url>
    <content><![CDATA[1. 前言公司M站要接入支付宝，借机研究了一下支付宝的支付流程。毕竟，只有公司才能拿到支付接口权限。
主要参考文档：https://doc.open.alipay.com/doc2/detail?treeId=60&amp;articleId=103564&amp;docType=1https://b.alipay.com/order/productDetail.htm?productId=2015110218008816


2. 手机网站支付接口2.1. 产品简介手机网站支付主要应用于手机、掌上电脑等无线设备的网页上，通过网页跳转或浏览器自带的支付宝快捷支付实现买家付款的功能，资金即时到账。
2.2. 申请条件1、您申请前必须拥有企业支付宝账号（不含个体工商户账号），且通过支付宝实名认证审核。2、如果您有已经建设完成的无线网站（非淘宝、天猫、诚信通网店），网站经营的商品或服务内容明确、完整（古玩、珠宝等奢侈品、投资类行业无法申请本产品）。3、网站必须已通过ICP备案，备案信息与签约商户信息一致。
3. 配置假设我们已经成功申请到手机网站支付接口，在进行开发之前，需要使用公司账号登录支付宝开放平台。
3.1. 查看PID1、开发者登录开放平台，点击右上角的“账户及密钥管理”。
2、选择“合作伙伴密钥”，即可查询到合作伙伴身份（PID），以2088开头的16位纯数字。
3.2. 配置密钥支付宝提供了DSA、RSA、MD5三种签名方式，本次开发中，我们使用RSA签名和加密，那就只配置RSA密钥就好了。
关于RSA加密的详解，参见《支付宝签名与验签》。
3.3. 应用环境本节可以忽略，本节可以忽略，本节可以忽略！因为官方文档并没有提及应用环境配置的问题。
进入管理中心，对应用进行设置。上图是我的应用配置选项，公司账号也许会有所不同。具体哪些参数需要配置？请参照接口参数说明，需要什么就配置什么。
4. 交互4.1. Node端发起支付请求我们公司采用的，就是这种方式，步骤3中Node端获取到的支付宝参数，包括sign。其实，sign的计算工作也可以放在Node端，只不过支付宝没有给出Node的demo，实现起来需要耗费多一点时间。
4.2. 后端发起支付请求这种方式也很好，而且，步骤4中后端获取到支付页面，也可以不传给Node端，自己显示出来。这样，整个流程就更加简单。
4.3. return_url和notify_urlreturn_url，支付完成后的回调url；notify_url，支付完成后通知的url。支付宝发送给两个url的参数是一样的，只不过一个是get，一个是post。
以上两种发起请求的方式中，return_url在Node端，notify_url在后端。我们也可以根据需要，把两个url都放在后端，或者都放在Node端，改变相应业务逻辑即可。
5. Node端详解Node端发起支付请求有两种选择，一种是获取到后端给的参数后，通过request模块发起get请求，获取到支付宝返回的支付页面，然后显示到页面上；另一种是获取到后端给的参数后，把参数全部输出到页面中的form表单，然后通过js自动提交表单，获取到支付宝返回的支付页面（同时显示出来）。
5.1. request发起请求// 通过orderId向后端请求获取支付宝支付参数alidatavar alipayUrl = &#x27;https://mapi.alipay.com/gateway.do?&#x27;+ alidata;request.get(&#123;url: alipayUrl&#125;,function(error, response, body)&#123;    res.send(response.body);&#125;);

理论上完全正确的请求，然而，获取到的支付页面，输出到页面上，却是乱码。没错，还是一个错误提示页面。
神奇的地方在于，在刷新页面多次后，正常了！！！啊嘞，这是什么鬼？

先解决乱码问题，看看报什么错！
request.get(&#123;url: alipayUrl&#125;,function(error, response, body)&#123;    var str = response2.body;    str = str.replace(/gb2312/, &quot;utf-8&quot;);    res.setHeader(&#x27;content-type&#x27;, &#x27;text/html;charset=utf-8&#x27;);    res.send(str);&#125;);

很遗憾，无效！乱码依然是乱码。。。和沈晨帅哥讨论很久，最终决定换一种方案——利用表单提交。
5.2. 表单提交请求5.2.1. Node端// node端// 通过orderId向后端请求获取支付宝支付参数alidatafunction getArg(str,arg) &#123;  var reg = new RegExp(&#x27;(^|&amp;)&#x27; + arg + &#x27;=([^&amp;]*)(&amp;|$)&#x27;, &#x27;i&#x27;);  var r = str.match(reg);  if (r != null) &#123;      return unescape(r[2]);  &#125;  return null;&#125;var alipayParam = &#123;  _input_charset: getArg(alidata,&#x27;_input_charset&#x27;),  body: getArg(alidata,&#x27;body&#x27;),  it_b_pay: getArg(alidata, &#x27;it_b_pay&#x27;),  notify_url: getArg(alidata, &#x27;notify_url&#x27;),  out_trade_no: getArg(alidata, &#x27;out_trade_no&#x27;),  partner: getArg(alidata, &#x27;partner&#x27;),  payment_type: getArg(alidata, &#x27;payment_type&#x27;),  return_url: getArg(alidata, &#x27;return_url&#x27;),  seller_id: getArg(alidata, &#x27;seller_id&#x27;),  service: getArg(alidata, &#x27;service&#x27;),  show_url: getArg(alidata, &#x27;show_url&#x27;),  subject: getArg(alidata, &#x27;subject&#x27;),  total_fee: getArg(alidata, &#x27;total_fee&#x27;),  sign_type: getArg(alidata, &#x27;sign_type&#x27;),  sign: getArg(alidata, &#x27;sign&#x27;),  app_pay: getArg(alidata, &#x27;app_pay&#x27;)&#125;;res.render(&#x27;artist/alipay&#x27;,&#123;  // 其他参数  alipayParam: alipayParam&#125;);

5.2.2. html&lt;!--alipay.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;支付宝支付&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;form id=&quot;ali-form&quot; action=&quot;https://mapi.alipay.com/gateway.do&quot; method=&quot;get&quot;&gt;           &lt;input type=&quot;hidden&quot; name=&quot;_input_charset&quot; value=&quot;&lt;%= alipayParam._input_charset%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;body&quot; value=&quot;&lt;%= alipayParam.body%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;it_b_pay&quot; value=&quot;&lt;%= alipayParam.it_b_pay%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;notify_url&quot; value=&quot;&lt;%= alipayParam.notify_url%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;out_trade_no&quot; value=&quot;&lt;%= alipayParam.out_trade_no%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;partner&quot; value=&quot;&lt;%= alipayParam.partner%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;payment_type&quot; value=&quot;&lt;%= alipayParam.payment_type%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;return_url&quot; value=&quot;&lt;%= alipayParam.return_url%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;seller_id&quot; value=&quot;&lt;%= alipayParam.seller_id%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;service&quot; value=&quot;&lt;%= alipayParam.service%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;show_url&quot; value=&quot;&lt;%= alipayParam.show_url%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;subject&quot; value=&quot;&lt;%= alipayParam.subject%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;total_fee&quot; value=&quot;&lt;%= alipayParam.total_fee%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;sign_type&quot; value=&quot;&lt;%= alipayParam.sign_type%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;sign&quot; value=&quot;&lt;%= alipayParam.sign%&gt;&quot;&gt;        &lt;input type=&quot;hidden&quot; name=&quot;app_pay&quot; value=&quot;&lt;%= alipayParam.app_pay%&gt;&quot;&gt;    &lt;/form&gt;&lt;% include ../bootstrap.html %&gt;&lt;script type=&quot;text/javascript&quot; src=&quot;&lt;%= dist %&gt;/js/business-modules/artist/alipay.js?v=2016052401&quot;&gt;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

5.2.3. js// alipay.jsseajs.use([&#x27;zepto&#x27;],function($)&#123;    var index = &#123;        init:function()&#123;            var self = this;            this.bindEvent();        &#125;,        bindEvent:function()&#123;            var self = this;            $(&#x27;#ali-form&#x27;).submit();        &#125;    &#125;    index.init();&#125;);

5.2.4. 小结选择支付宝支付后，成功跳转到了支付宝支付页面，nice！看来这种方案很靠谱。
开始时，打算把alidata直接输出到form表单的action中接口的后面，因为这样似乎最简便。但是，提交表单时，后面的参数全部被丢弃了。所以，不得不得把所有参数放在form表单中。Node端拆分了一下参数，组装成了一个alipayParam对象，这个工作也可以交给后端来做。
显然，request模拟表单提交和真实表单提交结果的不同，得出的结论是，request并不能完全模拟表单提交。或者，request可以模拟，而我不会-_-|||。
5.3. 错误排查值得点赞的是，支付宝给的错误代码很明确，一看就懂。上面这个错误，签名不对。因为我给alipayParam私自加了个app_pay属性，没有经过签名。
6. 微信屏蔽支付宝6.1. 问题描述以上，大功告成？不！还有一个坑要填，因为微信屏蔽了支付宝！在电脑上，跳转支付宝支付页面正常，很完美！然而，在微信浏览器中测试时，却没有跳转，而是出现如下信息。
6.2. 完美解决办法微信端支付宝支付，iframe改造http://www.cnblogs.com/jiqing9006/p/5584268.html
该办法的核心在于：把微信屏蔽的链接，赋值给iframe的src属性。
6.2.1. Node端res.render(&#x27;artist/alipay&#x27;,&#123;  alipayParam: alipayParam,  param: urlencode(alidata) &#125;);

6.2.2. html&lt;input type=&quot;hidden&quot; id=&quot;param&quot; value=&quot;&lt;%= param%&gt;&quot;&gt;&lt;iframe id=&quot;payFrame&quot; name=&quot;mainIframe&quot; src=&quot;&quot; frameborder=&quot;0&quot; scrolling=&quot;auto&quot; &gt;&lt;/iframe&gt;

6.2.3. jsvar iframe = document.getElementById(&#x27;payFrame&#x27;);var param = $(&#x27;#param&#x27;).val();iframe.src=&#x27;https://mapi.alipay.com/gateway.do?&#x27;+param;

6.2.4. 报错然而，在改造时，先是报错ILLEGAL_SIGN，于是利用urlencode处理了字符串。接着，又报错ILLEGAL_EXTERFACE，没有找到解决办法。
暂时放弃，以后如果有了解决办法再补上。
6.3. 官方解决办法关于微信公众平台无法使用支付宝收付款的解决方案说明https://cshall.alipay.com/enterprise/help_detail.htm?help_id=524702
该方法的核心在于：确认支付时，提示用户打开外部系统浏览器，在系统浏览器中支付。
6.3.1. html&lt;!--alipay.html--&gt;&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;  &lt;meta charset=&quot;UTF-8&quot;&gt;  &lt;title&gt;支付宝支付&lt;/title&gt;&lt;/head&gt;&lt;body&gt;  &lt;form id=&quot;ali-form&quot; action=&quot;https://mapi.alipay.com/gateway.do&quot; method=&quot;get&quot;&gt;     &lt;input type=&quot;hidden&quot; name=&quot;_input_charset&quot; value=&quot;&lt;%= alipayParam._input_charset%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;body&quot; value=&quot;&lt;%= alipayParam.body%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;it_b_pay&quot; value=&quot;&lt;%= alipayParam.it_b_pay%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;notify_url&quot; value=&quot;&lt;%= alipayParam.notify_url%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;out_trade_no&quot; value=&quot;&lt;%= alipayParam.out_trade_no%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;partner&quot; value=&quot;&lt;%= alipayParam.partner%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;payment_type&quot; value=&quot;&lt;%= alipayParam.payment_type%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;return_url&quot; value=&quot;&lt;%= alipayParam.return_url%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;seller_id&quot; value=&quot;&lt;%= alipayParam.seller_id%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;service&quot; value=&quot;&lt;%= alipayParam.service%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;show_url&quot; value=&quot;&lt;%= alipayParam.show_url%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;subject&quot; value=&quot;&lt;%= alipayParam.subject%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;total_fee&quot; value=&quot;&lt;%= alipayParam.total_fee%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;sign_type&quot; value=&quot;&lt;%= alipayParam.sign_type%&gt;&quot;&gt;    &lt;input type=&quot;hidden&quot; name=&quot;sign&quot; value=&quot;&lt;%= alipayParam.sign%&gt;&quot;&gt;    &lt;div class=&quot;wrapper buy-wrapper&quot; style=&quot;display: none;&quot;&gt;        &lt;a href=&quot;javascript:void(0);&quot;           class=&quot;J-btn-submit btn mj-submit btn-strong btn-larger btn-block&quot;&gt;确认支付&lt;/a&gt;    &lt;/div&gt;  &lt;/form&gt;&lt;input type=&quot;hidden&quot; id=&quot;param&quot; value=&quot;&lt;%= param%&gt;&quot;&gt;&lt;% include ../bootstrap.html %&gt;&lt;script type=&quot;text/javascript&quot; src=&quot;&lt;%= dist %&gt;/js/business-modules/artist/ap.js&quot;&gt;&lt;/script&gt;&lt;script&gt;    var btn = document.querySelector(&quot;.J-btn-submit&quot;);    btn.addEventListener(&quot;click&quot;, function (e) &#123;        e.preventDefault();        e.stopPropagation();        e.stopImmediatePropagation();        var queryParam = &#x27;&#x27;;        Array.prototype.slice.call(document.querySelectorAll(&quot;input[type=hidden]&quot;)).forEach(function (ele) &#123;            queryParam += ele.name + &quot;=&quot; + encodeURIComponent(ele.value) + &#x27;&amp;&#x27;;        &#125;);        var gotoUrl = document.querySelector(&quot;#ali-form&quot;).getAttribute(&#x27;action&#x27;) + &#x27;?&#x27; + queryParam;        _AP.pay(gotoUrl);        return false;    &#125;, false);    btn.click();&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

该页面会自动跳转到同一文件夹下的pay.htm，该文件官方已经提供，把其中的引入ap.js的路径修改一下即可。最终效果如下：
7. 后记支付宝的支付流程和微信的支付流程，有很多相似之处。沈晨指出一点不同：支付宝支付完成后有return_url和notify_url；微信支付完成后只有notify_url。
研读了一下微信支付的开发文档，确实如此。微信支付完成后的通知也分成两路，一路通知到notify_url，另一路返回给调用支付接口的JS，同时发微信消息提示。也就是说，跳转return_url的工作我们需要自己做。
最后，感谢沈晨帅哥提供的思路和帮助，感谢体超帅哥辛苦改后端。
8. 书签支付宝开放平台https://openhome.alipay.com/platform/home.htm
支付宝开放平台-手机网站支付-文档中心https://doc.open.alipay.com/doc2/detail?treeId=60&amp;articleId=103564&amp;docType=1
支付宝WAP支付接口开发http://blog.csdn.net/tspangle/article/details/39932963
wap h5手机网站支付接口唤起支付宝钱包付款
商家服务 - 支付宝 知托付！https://b.alipay.com/order/techService.htm
微信支付-开发文档https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1
微信端支付宝支付，iframe改造http://www.cnblogs.com/jiqing9006/p/5584268.html
微信如何突破支付宝的封锁http://blog.csdn.net/lee_sire/article/details/49530875
JavaScript专题（二）：深入理解iframehttp://www.cnblogs.com/fangjins/archive/2645631.html
关于微信公众平台无法使用支付宝收付款的解决方案说明https://cshall.alipay.com/enterprise/help_detail.htm?help_id=524702
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>node</tag>
        <tag>支付宝</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：利用Node.js的request模块访问本地数据</title>
    <url>/dev-nodejs-request-local-data/</url>
    <content><![CDATA[1. 前言最近公司系统架构调整，数据还没有迁移完毕，很多接口出现了问题，联调成了一件困难的事。开发还要继续，牛逼的前端自力更生！


2. 获取地址以获取用户的地址信息为例，对代码进行改造。
2.1. 原代码// 获取用户地址信息var queryAddressUrl = config.apihost + &#x27;/address/selAddress&#x27;;var param = &#123;  accessToken: accessToken&#125;;request.post(&#123;url: queryAddressUrl, form:JSON.stringify(param)&#125;,function(error, response, body)&#123;  console.log(response.body);  if(!error &amp;&amp; response.statusCode == 200)&#123;    ep.emit(&#x27;address&#x27;,response.body);  &#125;&#125;);

2.2. 改造后代码// 获取用户地址信息// var queryAddressUrl = config.apihost + &#x27;/address/selAddress&#x27;;var queryAddressUrl = config.host + &#x27;/data/address.json&#x27;;var param = &#123;  accessToken: accessToken&#125;;// post改为getrequest.get(&#123;url: queryAddressUrl, form:JSON.stringify(param)&#125;,function(error, response, body)&#123;  console.log(response.body);  if(!error &amp;&amp; response.statusCode == 200)&#123;    ep.emit(&#x27;address&#x27;,response.body);  &#125;&#125;);
两段代码对比，可以发现，请求的url由远程接口地址改为了本地文件地址，请求的type由post改为了get。
2.3. address.json因为public文件夹的虚拟路径为网站根目录，且为静态文件目录，所以在/public/data文件夹下，新建文件address.json，内容如下：
&#123;    &quot;code&quot;: 1,    &quot;ext&quot;: &#123;        &quot;msg&quot;: &quot;成功&quot;    &#125;,    &quot;obj&quot;: &#123;        &quot;pcdAddress&quot;: &quot;江苏省 南京市 雨花台区&quot;,        &quot;purchaserName&quot;: &quot;郝锦&quot;,        &quot;purchaserTel&quot;: &quot;15195892217&quot;,        &quot;street&quot;: &quot;华博智慧园&quot;,        &quot;areaId&quot;: 0,        &quot;defaultAddress&quot;: false    &#125;&#125;
至此，改造成功，可以愉快地获取数据了！
3. 后记有些需要计算的数据并不能模拟，比如支付宝的签名字符串。
利用ajax也可以获取本地数据，对于前端开发者来说同样很实用。用法很简单，一看就会，详情自行百度。
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>好好学Node.js</tag>
        <tag>nodejs</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：Node.js中exports和module.exports的区别和联系</title>
    <url>/dev-nodejs-exports-and-module-exports/</url>
    <content><![CDATA[1. 前言
exports只是module.exports的辅助方法。你的模块最终返回module.exports给调用者，而不是exports。exports所做的事情是收集属性，如果module.exports当前没有任何属性的话，exports会把这些属性赋予module.exports。如果module.exports已经存在一些属性的话，那么exports中所用的东西都会被忽略。


最初时，exports是module.exports的一个引用。(exports=global.exports)=(self.exports=module.exports)



2. exports正常导出// caculate.jsexports.add = function (a, b) &#123;    return a+b;&#125;

// main.jsvar caculate = require(&#x27;./caculate&#x27;);exports.runAdd = function(req, res)&#123;    var num = caculate.add(5,4);    console.log(num);&#125;
在main模块中，可以正常调用caculate模块中的add函数。因为exports是module.exports的一个引用，所以exports.add=function()&#123;&#125;这个操作，把add函数加到了module.exports指向的对象（函数也是对象）中。而main模块中获取到的，就是caculate的module.exports指向的对象，自然也获取到了add函数。
3. exports错误导出// caculate.jsexports = function (a, b) &#123;    return a+b;&#125;

// main.jsvar caculate = require(&#x27;./caculate&#x27;);exports.runAdd = function(req, res)&#123;    console.log(caculate);    //var num = caculate(5, 4);    //console.log(num);&#125;
如上的导出方法是错误的，我们可以看到打印出的caculate是一个空对象&#123;&#125;。因为exports原本是module.exports的一个引用，后来指向了一个函数。而module.exports的指向的对象，初始值就是空对象&#123;&#125;，自始至终都没有这个空对象添加属性或函数。所以，当main模块中获取到caculate的module.exports指向的对象时，依然是一个空对象&#123;&#125;。
4. module.exports导出// caculate.jsexports.add = function (a, b) &#123;    return a+b;&#125;var caculate = &#123;    delete: function(a, b)&#123;        return a-b;    &#125;,    multiple: function(a, b)&#123;        return a*b;    &#125;&#125;;module.exports = caculate;

// main.jsvar caculate = require(&#x27;./caculate&#x27;);exports.runDelete = function(req, res)&#123;    //var num = caculate.add(5, 4);    var num = caculate.delete(5, 4);    console.log(num);&#125;
如上导出方法，在caculate的module.exports指向的对象中，不会包含add函数。虽然exports.add=function()&#123;&#125;向module.exports指向的空对象&#123;&#125;中添加了一个add函数，但是紧接着，module.exports指向的对象改变了！变成了caculate对象。
5. seajs中module.exports和returnseajs中的exports和module.exports的关系，和Node中相同。
在进行插件CMD模块化时，发现module.exports和return基本相同。下面写个小例子：
// plugin.jsdefine(function(require, exports, module)&#123;    module.exports = function(jQuery)&#123;        // require(&#x27;another-plugin&#x27;)(jQuery);        // 依赖jQuery的插件代码    &#125;&#125;)

// plugin.jsdefine(function(require, exports, module)&#123;    return function(jQuery)&#123;        // require(&#x27;another-plugin&#x27;)(jQuery);        // 依赖jQuery的插件代码    &#125;&#125;)

seajs.use([&#x27;jquery&#x27;,&#x27;plugin&#x27;],function($,plugin)&#123;    plugin($);//初始化&#125;);

6. 书签Modules Node.js v6.3.0 Manual &amp; Documentationhttps://nodejs.org/api/modules.html#modules_the_module_object
module.exports 还是 exports？http://zihua.li/2012/03/use-module-exports-or-exports-in-node/
nodejs中export与module.export的区别
SeaJS 中的 exports 和模块加载http://www.tuicool.com/articles/Y3qmAj
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>好好学Node.js</tag>
        <tag>nodejs</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：使用Node.js处理微信自定义分享时的URL</title>
    <url>/dev-nodejs-weixin-share-url/</url>
    <content><![CDATA[1. 前言程序员的工作就是填坑，有时是填别人挖的坑，有时是填自己挖的坑。


2. 微信自定义分享在使用node开发的时候，经常会用到微信自定义分享。自定义分享的步骤，大致如下：
2.1. 微信配置登录微信公众平台，进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
2.2. node端1、设置分享数据（TKD）。2、获取权限验证配置数据。3、获取用户信息（非必要），因为需要在分享页面显示分享用户的昵称。
var config = require(&#x27;../config&#x27;);var urlencode = require(&#x27;urlencode&#x27;);var eventproxy = require(&#x27;eventproxy&#x27;);var request = require(&#x27;request&#x27;);var WechatAPI = require(&#x27;wechat-api&#x27;);var WXapi = new WechatAPI(config.weixin.appid, config.weixin.appsecret);// 微信exports.home = function(req, res)&#123;    // 获取code    var subscribe = 1;    if(!req.query.code)&#123;        var r_url = config.host+&#x27;/weixin/home&#x27;;        var url = &#x27;https://open.weixin.qq.com/connect/oauth2/authorize?appid=&#x27;+config.weixin.appid+&#x27;&amp;redirect_uri=&#x27;+urlencode(r_url)+&#x27;&amp;response_type=code&amp;scope=snsapi_userinfo&amp;state=111#wechat_redirect&#x27;;        res.redirect(url);    &#125;else&#123;        var code = req.query.code;        var state = req.query.state;                var ep = new eventproxy();        ep.all(&#x27;shareConfig&#x27;,&#x27;userInfo&#x27;,function(shareConfigData,userInfoData)&#123;            console.log(shareConfigData);            console.log(userInfoData);            // 设置分享数据（TKD）            var shareData = &#123;                enable: true,                title: &#x27;分享&#x27;,                icon: &#x27;http://cdn.voidking.com/img/head.jpg&#x27;,                desc: &#x27;没见过这么拉风的分享描述吧！&#x27;,                mUrl: config.host+&#x27;/weixin/userinfo?openid=000&amp;nickname=voidking&amp;num=10&#x27;            &#125;;            res.render(&#x27;./weixin/home&#x27;,&#123;                title: &#x27;微信&#x27;,                host: config.host,                shareConfigData: shareConfigData,                shareData: shareData,                userInfoData: JSON.parse(userInfoData)            &#125;);        &#125;);        var param = &#123;            debug: false,            jsApiList: [&#x27;onMenuShareTimeline&#x27;, &#x27;onMenuShareAppMessage&#x27;],            url: req.protocol+&quot;://&quot;+req.hostname+req.originalUrl        &#125;;             WXapi.getJsConfig(param, function(err,result)&#123;            // result就是权限配置验证配置数据            ep.emit(&#x27;shareConfig&#x27;,result);            &#125;);        // 通过code换取网页授权access_token        var params = &#123;            appid:config.weixin.appid,            secret:config.weixin.appsecret,            code:code,            grant_type:&#x27;authorization_code&#x27;        &#125;;        var getAccessTokenUrl = &#x27;https://api.weixin.qq.com/sns/oauth2/access_token?appid=&#x27;+params.appid+&#x27;&amp;secret=&#x27;+params.secret+&#x27;&amp;code=&#x27;+params.code+&#x27;&amp;grant_type=&#x27;+params.grant_type;        request.get(getAccessTokenUrl,function(error, response, body)&#123;            if (!error &amp;&amp; response.statusCode == 200) &#123;                var re = JSON.parse(response.body);                console.log(re);                var getuserinfo = &#x27;https://api.weixin.qq.com/sns/userinfo?access_token=&#x27;+re.access_token+&#x27;&amp;openid=&#x27;+re.openid+&#x27;&amp;lang=zh_CN&#x27;;                request.get(getuserinfo,function(error2, response2, body2)&#123;                    if (!error2 &amp;&amp; response2.statusCode == 200) &#123;                        ep.emit(&#x27;userInfo&#x27;,response2.body);                    &#125;                &#125;);            &#125;        &#125;);    &#125;&#125;

2.3. 前端1、前端页面中引入微信的js文件http://res.wx.qq.com/open/js/jweixin-1.0.0.js。2、微信权限验证数据配置。3、分享数据配置。
&lt;script type=&quot;text/javascript&quot;&gt;    wx.config(&#123;       debug:  false,  //调式模式，设置为ture后会直接在网页上弹出调试信息，用于排查问题       appId: &#x27;&lt;%= shareConfigData.appId%&gt;&#x27;,       timestamp: &#x27;&lt;%= shareConfigData.timestamp%&gt;&#x27;,       nonceStr: &#x27;&lt;%= shareConfigData.nonceStr%&gt;&#x27;,       signature: &#x27;&lt;%= shareConfigData.signature%&gt;&#x27;,       jsApiList: [  //需要使用的网页服务接口           &#x27;checkJsApi&#x27;,  //判断当前客户端版本是否支持指定JS接口           &#x27;onMenuShareTimeline&#x27;, //分享给好友           &#x27;onMenuShareAppMessage&#x27;, //分享到朋友圈           &#x27;onMenuShareQQ&#x27;,  //分享到QQ           &#x27;onMenuShareWeibo&#x27; //分享到微博       ]     &#125;);     wx.ready(function () &#123;   //ready函数用于调用API，如果你的网页在加载后就需要自定义分享和回调功能，需要在此调用分享函数。//如果是微信游戏结束后，需要点击按钮触发得到分值后分享，这里就不需要调用API了，可以在按钮上绑定事件直接调用。因此，微信游戏由于大多需要用户先触发获取分值，此处请不要填写如下所示的分享API        wx.onMenuShareTimeline(&#123;  //例如分享到朋友圈的API             title: &#x27;&lt;%= shareData.title%&gt;&#x27;, // 分享标题           link: &#x27;&lt;%= shareData.mUrl%&gt;&#x27;, // 分享链接           imgUrl: &#x27;&lt;%= shareData.icon%&gt;&#x27;, // 分享图标           desc:&#x27;&lt;%= shareData.desc%&gt;&#x27;,           success: function () &#123;               // 用户确认分享后执行的回调函数           &#125;,           cancel: function () &#123;               // 用户取消分享后执行的回调函数           &#125;        &#125;);        wx.onMenuShareAppMessage(&#123;  //例如分享到朋友圈的API             title: &#x27;&lt;%= shareData.title%&gt;&#x27;, // 分享标题           link: &#x27;&lt;%= shareData.mUrl%&gt;&#x27;, // 分享链接           imgUrl: &#x27;&lt;%= shareData.icon%&gt;&#x27;, // 分享图标           desc:&#x27;&lt;%= shareData.desc%&gt;&#x27;,           success: function () &#123;               // 用户确认分享后执行的回调函数           &#125;,           cancel: function () &#123;               // 用户取消分享后执行的回调函数           &#125;        &#125;);        wx.onMenuShareQQ(&#123;  //例如分享到朋友圈的API             title: &#x27;&lt;%= shareData.title%&gt;&#x27;, // 分享标题           link: &#x27;&lt;%= shareData.mUrl%&gt;&#x27;, // 分享链接           imgUrl: &#x27;&lt;%= shareData.icon%&gt;&#x27;, // 分享图标           desc:&#x27;&lt;%= shareData.desc%&gt;&#x27;,           success: function () &#123;               // 用户确认分享后执行的回调函数           &#125;,           cancel: function () &#123;               // 用户取消分享后执行的回调函数           &#125;        &#125;);        wx.onMenuShareWeibo(&#123;  //例如分享到朋友圈的API             title: &#x27;&lt;%= shareData.title%&gt;&#x27;, // 分享标题           link: &#x27;&lt;%= shareData.mUrl%&gt;&#x27;, // 分享链接           imgUrl: &#x27;&lt;%= shareData.icon%&gt;&#x27;, // 分享图标           desc:&#x27;&lt;%= shareData.desc%&gt;&#x27;,           success: function () &#123;               // 用户确认分享后执行的回调函数           &#125;,           cancel: function () &#123;               // 用户取消分享后执行的回调函数           &#125;        &#125;);    &#125;);     wx.error(function (res) &#123;        // alert(res.errMsg);  //打印错误消息。及把 debug:false,设置为debug:ture就可以直接在网页上看到弹出的错误提示    &#125;);&lt;/script&gt;

2.4. 源码https://github.com/voidking/nodebase/blob/master/controllers/weixin.jshttps://github.com/voidking/nodebase/blob/master/views/weixin/home.html
3. &amp;转&amp;amp;以上，已经完成了微信自定义分享的工作。然而，还存在一些坑！！！
如上，我们的分享页面url为http://wx.voidking.com/weixin/userinfo?openid=000&amp;nickname=voidking&amp;num=10，参数是写死的，实际生产环境中需要使用真实数据拼接出来。
该分享页面的url，如果输出到页面上，就显示正常，比如使用隐藏的input接收。
&lt;input type=&quot;hidden&quot; id=&quot;url&quot; value=&quot;&lt;%= shareData.url%&gt;&quot;&gt;
显示结果为
&lt;input type=&quot;hidden&quot; id=&quot;url&quot; value=&quot;http://wx.voidking.com/weixin/userinfo?openid=000&amp;nickname=voidking&amp;num=10&quot;&gt;

但是，如果输出到微信分享的js中，url中的&amp;就会变成&amp;amp;。
wx.onMenuShareTimeline(&#123;  //例如分享到朋友圈的API      link: &#x27;&lt;%= shareData.mUrl%&gt;&#x27;, // 分享链接    // 其他分享信息省略&#125;);
显示结果为
wx.onMenuShareTimeline(&#123;  //例如分享到朋友圈的API      link: &#x27;http://wx.voidking.com/weixin/userinfo?openid=000&amp;amp;nickname=voidking&amp;amp;num=10&#x27;, // 分享链接    // 其他分享信息省略&#125;);

啊嘞，&amp;转义成了&amp;amp;！！！没错，这就是第一个坑。因为分享出去的url参数不是以常规&amp;间隔，而是&amp;amp;，所以增加了我们截取参数的难度。
有两种解决方案：1、前端解决。输入内容到隐藏的input，然后获取值，添加到微信分享的js中。经测试，行不通。2、node端解决。在跳转分享页面时，改变获取参数的方式，代码如下。
exports.userinfo = function(req, res)&#123;        function getArg(str,arg) &#123;        var reg = new RegExp(&#x27;(^|&amp;)&#x27; + arg + &#x27;=([^&amp;]*)(&amp;|$)&#x27;, &#x27;i&#x27;);        var r = str.match(reg);        if (r != null) &#123;            return unescape(r[2]);        &#125;        return null;    &#125;    String.prototype.replaceAll  = function(s1,s2)&#123;             return this.replace(new RegExp(s1,&quot;gm&quot;),s2);         &#125;     var str = decodeURI(req.url.split(&#x27;?&#x27;)[1]);    console.log(str);    str = str.replaceAll(&#x27;&amp;amp%3B&#x27;,&#x27;&amp;&#x27;);     str = str.replaceAll(&#x27;&amp;amp;&#x27;,&#x27;&amp;&#x27;);    console.log(str);    var name = getArg(str,&#x27;name&#x27;);    console.log(name);    res.render(&#x27;weixin/userinfo&#x27;,&#123;        title: &#x27;用户信息&#x27;,        host: config.host    &#125;);&#125;

4. ;转%3B以上，我们解决了微信分享中&amp;转义的问题，在电脑端或者安卓端查看分享都是正常的。然而，在IOS端查看会出现错误！！！
因为链接http://wx.voidking.com/weixin/userinfo?openid=000&amp;amp;nickname=voidking&amp;amp;num=10在使用IOS访问的时候，变成了http://wx.voidking.com/weixin/userinfo?openid=000&amp;amp%3Bnickname=voidking&amp;amp%3Bnum=10，;转义成了%3B！！！
解决办法，参考&amp;转&amp;amp;一节node端代码，替换字符串即可。
5. 从url中截取中文参数以上，url属性中name的值是voidking，没有什么问题。然而，当我把name的值换为“帅哥”时，控制台打印出了乱码。啊哈，这就对了，中文肯定要处理一下嘛！
解决办法，使用decodeURI函数解码编码过的uri，参考&amp;转&amp;amp;一节node端代码。
6. 书签
微信公众平台开发者文档——微信JS-SDK说明文档
微信公共平台Node库 API
wechat-api Documentation

]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>微信</tag>
        <tag>好好学Node.js</tag>
        <tag>nodejs</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：在Node.js项目中使用Sea.js</title>
    <url>/dev-seajs-in-nodejs/</url>
    <content><![CDATA[1. 前言Sea.js 追求简单、自然的代码书写和组织方式，具有以下核心特性：

简单友好的模块定义规范：Sea.js遵循CMD规范，可以像Node.js一般书写模块代码。
自然直观的代码组织方式：依赖的自动加载、配置的简洁清晰，可以让我们更多地享受编码的乐趣。

Sea.js还提供常用插件，非常有助于开发调试和性能优化，并具有丰富的可扩展接口。


2. 配置2.1. 下载seajsseajs官方下载：http://seajs.org/docs/#downloads
2.2. 项目结构
2.3. 概念说明下面明确几个概念，方便接下来的描述：1、/（网站根目录），是项目目录nodebase。2、虚拟路径，是node把某个文件（夹）转化之后，该文件（夹）的相对于/的路径。3、访问路径，是通过浏览器url访问的路径。

虚拟路径不一定是访问路径，访问路径不一定是虚拟路径。因为，只有我们指定的资源、指定的路径是开放给用户访问的。

2.4. 静态资源配置// 把文件夹中的内容添加到网站根目录下，静态文件务必添加，否则访问不到app.use(express.static(path.join(__dirname, &#x27;bower_components&#x27;)));app.use(express.static(path.join(__dirname, &#x27;public&#x27;)));
在node项目的入口文件app.js中，加入如上配置，那么，bower_components、public两个文件夹的虚拟路径都变成了/，访问路径都变成了/。
// 静态文件目录var staticDir = path.join(__dirname, &#x27;public&#x27;);// 虚拟目录app.use(&#x27;/dist&#x27;, express.static(staticDir));app.use(&#x27;/public&#x27;, express.static(staticDir));
在node项目的入口文件app.js中，加入如上配置，那么，public文件夹的虚拟路径就变成了/dist和/public，访问路径就变成了/dist和/public。
通过上面两个配置，可以看出，静态资源的虚拟路径和访问路径是相同的。
2.5. sea-config.jsseajs.config(&#123;    //base: &#x27;../&#x27;,    base: window.host + &#x27;/&#x27;,    alias: &#123;        &#x27;jquery&#x27;: &#x27;seajs/jquery/jquery.min.js&#x27;,        &#x27;layer&#x27;: &#x27;seajs/layer/layer.js&#x27;,        &#x27;swiper&#x27;: &#x27;Swiper/dist/js/swiper.min.js&#x27;           &#125;&#125;);
如果base的值使用相对路径../，那么页面引入依赖的js文件的时候，就是相对于页面文件的虚拟路径。
app.set(&#x27;views&#x27;,&#x27;./views&#x27;);// 页面目录配置
举个例子，在app.js中使用了页面目录配置，views文件夹虚拟路径变成了/。注意，views文件夹依然没有访问路径。
views文件夹的目录结构如下：
views|-weixin| |-home.html|-index.html

在路由转发中，index.html和home.html的虚拟路径分别如下：
res.render(&#x27;index&#x27;,&#123;    title: &#x27;index&#x27;&#125;);res.render(&#x27;weixin/home&#x27;,&#123;    title: &#x27;home&#x27;&#125;);
在index.html中引入sea-config.js，在加载依赖的js文件的时候，会在index.html的基础上向上一层寻找。因为index.html所在的虚拟路径为/，没有上一层，所以肯定找不到js文件。
而home.html所在的虚拟路径为/weixin/，向上一层寻找，是虚拟路径/。而静态文件的虚拟路径都是/，刚好可以找到需要的js文件。

鉴于相对路径的差异问题，base最好使用绝对路径，配置在全局config.js中。

2.6. home.jsseajs.use([&#x27;jquery&#x27;,&#x27;layer&#x27;],function($,layer)&#123;    var index = &#123;        init:function()&#123;            this.saveData();            this.bindEvent();        &#125;,        saveData: function()&#123;            if(window.localStorage)&#123;                var x = window.localStorage.userInfo? JSON.parse(window.localStorage.userInfo):&#123;&#125;;                if((x.unionid == undefined) || (x.unionid == &quot;undefined&quot;))&#123;                    var temp = &#123;                        unionid: $(&#x27;#unionid&#x27;).val(),                        openid: $(&#x27;#openid&#x27;).val(),                        nickname: $(&#x27;#nickname&#x27;).val(),                        headimgurl:$ (&#x27;#headimgurl&#x27;).val()                    &#125;;                    window.localStorage.userInfo = JSON.stringify(temp);                &#125;            &#125;        &#125;,        bindEvent:function()&#123;            layer.alert(&#x27;layer&#x27;);        &#125;    &#125;    index.init();&#125;);

2.7. 引入js文件&lt;script&gt;    window.host = &#x27;&lt;%= host%&gt;&#x27;;&lt;/script&gt;&lt;script src=&quot;/seajs/seajs-1.3.1/dist/sea.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;/seajs/sea-config.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;/js/weixin/home.js&quot;&gt;&lt;/script&gt;

3. CMD模块化3.1. 不封装因为seajs遵循CMD规范，所以遵循CMD规范的插件，比如swiper，引入后就可以使用。
3.2. jquery封装jquery遵循AMD规范，需要封装一下，使其符合CMD规范才可以使用，否则会报错。
define(function(require, exports, module) &#123;    // 模块化jquery源码&#125;);

3.3. 依赖jquery的插件封装有些插件，是在jquery的基础上的开发的，需要先引入jquery。
define(function(require, exports, module) &#123;    var $ = require(&#x27;jquery&#x27;);    // 依赖jquery的模块化代码&#125;);

另外一种方法：
define(function(require, exports, module) &#123;    return function($)&#123;        // 依赖jquery的模块化代码    &#125; &#125;);
这种方法在使用模块前需要先传入jquery进行初始化。
3.4. 普通插件封装如果是未模块化的插件（普通js代码），需要暴露对应的接口。
define(function(require, exports, module) &#123;    var $ = require(&#x27;jquery&#x27;);    var voidking = &#123;        init: function()&#123;            // 未模块化代码            var height = $(document).height();            console.log(height);        &#125;    &#125;;    module.exports = voidking;&#125;);
在使用模块前需要调用init方法初始化。
4. layer的坑4.1. 不封装不对layer.js进行封装，直接引入时，会报错jQuery is not defined。需要说明的是，在使用layer-mobile.js的时候，不需要封装就可以正常使用。
4.2. 封装由上面的报错，可以看出，layer.js需要依赖jquery，那就封装一下，引入jquery吧！
define(function(require, exports, module) &#123;    var jQuery = require(&#x27;jquery&#x27;);    // layer.js代码&#125;);
然后，错误就变成了layer.alert is not a function。在控制台输入layer，可以看到layer是一个对象。啊嘞，layer对象明明包含了一个alert方法！！！为啥不能调用捏？
不甘心的郝同学，又尝试了很多其他的封装方法，依然无法使用。
4.3. 不完美解决办法layer的作者贤心说，layer是CMD规范的。那么，理论上直接引入就可以使用。但是，错误当道，jQuery is not defined。那么问题的关键，就是给layer.js引入jquery。
使用封装的办法，引入jquery，经过测试，行不通。
如果直接把jquery源码粘贴到layer.js文件中，是否可以呢？经过测试，确实可以！！！
好吧，这个坑总算是填上了。虽然不够完美，但是确实是个有效的方法！如果有更好的解决的办法，希望能留言告诉我，谢谢。
5. seajs模块加载顺序1、从seajs.use方法入口，开始加载use到的模块。2、use到的模块这时mod缓存当中一定是不存在的，seajs创建一个新的mod，赋予一些初始的状态。3、执行mod.load方法。4、一堆逻辑之后走到seajs.request方法，请求模块文件。模块加载完成之后，执行define方法。5、define方法分析提取模块的依赖模块，保存起来，缓存factory但不执行。6、模块的依赖模块再被加载，如果继续有依赖模块，则继续加载，直至所有被依赖的模块都加载完毕。7、所有的模块加载完毕之后，执行use方法的callback。8、模块内部逻辑从callback开始执行，require方法在这个过程当中才被执行。PS: define方法纯粹只是分析模块、存储模块，并没有执行模块。require方法就是根据id在define定义存储的模块缓存中找到相应的模块，并执行它，获得模块定义返回的方法。
6. 后记也许，参加Sea.js的社区维护也是一件很好玩的事情。又多了很多许多需要学习的东西，路漫漫其修远兮！
7. 书签
Seajs简易文档
快速上手seajs——简单易用Seajs
seajs base配置
jQuery 插件的模块化
seajs 加载Jquery的遇到问题?
如何改造现有文件为 CMD 模块
CMD 模块定义规范
Sea.js社区
前端模块化开发那点历史
如何参与开发
Develop A Package
高富帅seajs使用示例及spm合并压缩工具露脸
Hello Sea.js
从零开始编写自己的JavaScript框架（一）
Javascript模块化编程（一）：模块的写法
JavaSript模块规范 - AMD规范与CMD规范介绍
该如何理解AMD ，CMD，CommonJS规范
(function($){…})(jQuery)是什么意思
深入探寻seajs的模块化与加载方式
seajs模块加载机制
SeaJS中jQuery插件模块化及其调用方式
seajs模块化jQuery与jQuery插件
Sea.js 手册与文档

]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>好好学Node.js</tag>
        <tag>nodejs</tag>
        <tag>seajs</tag>
      </tags>
  </entry>
  <entry>
    <title>微信获取用户信息</title>
    <url>/dev-weixin-get-user-info/</url>
    <content><![CDATA[1. 前言在微信公众平台开发中，经常会需要获取用户信息。获取用户信息的方法很简单，传入ACCESS_TOKEN和OPENID，直接请求微信给出的接口就可以了。


本文中，参照方倍工作室的分类方法，把access_token分为两种。一种是使用AppID和AppSecret获取的access_token，一种是OAuth2.0授权中产生的access_token，方倍工作室分别称为全局access_token和授权access_token。
2. 全局access_token2.1. 获取全局access_token传入APPID和APPSECRET，请求微信给出的接口，就可以获取到全局access_token。
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&amp;appid=APPID&amp;secret=APPSECRET

返回结果：
&#123;    &quot;access_token&quot;: &quot;NU7Kr6v9L9TQaqm5NE3OTPctTZx797Wxw4Snd2WL2HHBqLCiXlDVOw2l-Se0I-WmOLLniAYLAwzhbYhXNjbLc_KAA092cxkmpj5FpuqNO0IL7bB0Exz5s5qC9Umypy-rz2y441W9qgfnmNtIZWSjSQ&quot;,    &quot;expires_in&quot;: 7200&#125;

2.2. 获取openid用户关注以及回复消息的时候，均可以获得用户的OpenID。
&lt;xml&gt;    &lt;ToUserName&gt;&lt;![CDATA[gh_b629c48b653e]]&gt;&lt;/ToUserName&gt;    &lt;FromUserName&gt;&lt;![CDATA[ollB4jv7LA3tydjviJp5V9qTU_kA]]&gt;&lt;/FromUserName&gt;    &lt;CreateTime&gt;1372307736&lt;/CreateTime&gt;    &lt;MsgType&gt;&lt;![CDATA[event]]&gt;&lt;/MsgType&gt;    &lt;Event&gt;&lt;![CDATA[subscribe]]&gt;&lt;/Event&gt;    &lt;EventKey&gt;&lt;![CDATA[]]&gt;&lt;/EventKey&gt;&lt;/xml&gt;
其中的FromUserName就是OpenID。
2.3. 获取用户信息有了access_token和openid，请求如下接口：
https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&amp;openid=OPENID&amp;lang=zh_CN

获取到的用户信息数据格式如下：
&#123;    &quot;subscribe&quot;: 1,     &quot;openid&quot;: &quot;o6_bmjrPTlm6_2sgVt7hMZOPfL2M&quot;,     &quot;nickname&quot;: &quot;Band&quot;,     &quot;sex&quot;: 1,     &quot;language&quot;: &quot;zh_CN&quot;,     &quot;city&quot;: &quot;广州&quot;,     &quot;province&quot;: &quot;广东&quot;,     &quot;country&quot;: &quot;中国&quot;,     &quot;headimgurl&quot;: &quot;http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0&quot;,     &quot;subscribe_time&quot;: 1382694957,    &quot;unionid&quot;: &quot;o6_bmasdasdsad6_2sgVt7hMZOPfL&quot;    &quot;remark&quot;: &quot;&quot;,    &quot;groupid&quot;: 0&#125;

3. 授权access_token3.1. 微信配置假设M站的网址为http://wx.voidking.com，那么在微信公众平台上，JS接口安全域名修改为wx.voidking.com，网页授权获取用户基本信息修改为wx.voidking.com。
3.2. 获取code在确保微信公众账号拥有授权作用域（scope参数）的权限的前提下（服务号获得高级接口后，默认拥有scope参数中的snsapi_base和snsapi_userinfo），引导关注者打开如下页面：
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&amp;redirect_uri=REDIRECT_URI&amp;response_type=code&amp;scope=SCOPE&amp;state=STATE#wechat_redirect
若提示“该链接无法访问”，请检查参数是否填写错误，是否拥有scope参数对应的授权作用域权限。
3.3. 获取授权access_token和openid获取code后，请求接口：
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&amp;secret=SECRET&amp;code=CODE&amp;grant_type=authorization_code

返回数据如下：
&#123;   &quot;access_token&quot;:&quot;ACCESS_TOKEN&quot;,   &quot;expires_in&quot;:7200,   &quot;refresh_token&quot;:&quot;REFRESH_TOKEN&quot;,   &quot;openid&quot;:&quot;OPENID&quot;,   &quot;scope&quot;:&quot;SCOPE&quot;,   &quot;unionid&quot;: &quot;o6_bmasdasdsad6_2sgVt7hMZOPfL&quot;&#125;

3.4. 获取用户信息获取code时，如果网页授权作用域为snsapi_userinfo，则此时开发者可以通过access_token和openid拉取用户信息了。请求接口如下：
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&amp;openid=OPENID&amp;lang=zh_CN

返回数据如下：
&#123;    &quot;openid&quot;:&quot;OPENID&quot;,    &quot;nickname&quot;:&quot;NICKNAME&quot;,    &quot;sex&quot;:&quot;1&quot;,    &quot;province&quot;:&quot;PROVINCE&quot;    &quot;city&quot;:&quot;CITY&quot;,    &quot;country&quot;:&quot;COUNTRY&quot;,    &quot;headimgurl&quot;:&quot;http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46&quot;,     &quot;privilege&quot;:[        &quot;PRIVILEGE1&quot;,        &quot;PRIVILEGE2&quot;    ],    &quot;unionid&quot;: &quot;o6_bmasdasdsad6_2sgVt7hMZOPfL&quot;&#125;

3.5. Node端源码https://github.com/voidking/nodebase/blob/master/controllers/weixin.js
4. 两种方法的对比4.1. 接口全局access_token获取用户信息接口为https://api.weixin.qq.com/cgi-bin/user/info。授权access_token获取用户信息接口为https://api.weixin.qq.com/sns/userinfo。
4.2. 返回数据全局access_token获取用户信息返回数据为：
授权access_token获取用户信息返回数据为：
4.3. 小结通过授权access_token无法得知用户是否关注了公众号，必须通过全局access_token。
5. 后记有问题，看文档。经历了四年发展的微信公众平台，文档已经越来越完善。
6. 书签微信公众平台开发(76) 获取用户基本信息http://www.cnblogs.com/txw1958/p/weixin76-user-info.html
微信公众平台开发者文档——获取用户基本信息http://mp.weixin.qq.com/wiki/14/bb5031008f1494a59c6f71fa0f319c66.html
微信公众平台开发者文档——网页授权获取用户基本信息http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
微信公共平台Node库 APIhttp://doxmate.cool/node-webot/wechat-api/api.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>微信</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：Node.js基础开发框架</title>
    <url>/dev-nodejs-base-framework/</url>
    <content><![CDATA[1. 前言测试新功能，开发新项目，按照本人的懒惰程度推算，八成会在原有项目的基础上开发。既然如此，抽出来一个node基础框架，似乎是一个很好的想法。本框架保留了毕设的主体代码，删除了一些无关代码，并且会继续增减代码，逐渐完善。


2. nodebasea node framework designed for myselfversion: v1.0.0url: https://github.com/voidking/nodebase.git  
3. 作者相关author: haojin(voidking)e-mail: &#x76;&#111;&#105;&#x64;&#x6b;&#105;&#110;&#103;&#x40;&#113;&#x71;&#46;&#99;&#x6f;&#x6d;site: http://www.voidking.com   
4. 开发环境说明1、安装5.6.0以上Node2、安装mongodb，不要配置密码3、安装sass4、全局安装bower5、全局安装supervisor6、全局安装node-inspector7、全局安装puer8、运行前执行命令
npm installbower install


5. node实时调试命令node-inspector --web-port=8888node --debug app.js

6. 运行命令node app.js，访问http://localhost
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>好好学Node.js</tag>
        <tag>nodejs</tag>
      </tags>
  </entry>
  <entry>
    <title>微信本地调试</title>
    <url>/dev-weixin-localhost-debug/</url>
    <content><![CDATA[1. 前言在微信开发的时候，需要填写与微信服务器相连接的url，这个url必须是公网域名。也就是说我们需要在这个公网域名对应的公网服务器上做开发，而没办法本地开发调试。
理想的解决办法，是把公网域名绑定到本地服务器，那就可以本地开发调试了。而本地主机是没有公网IP的，怎么办？动态域名解析。


2. ngrok
I want to expose a local server behind a NAT or firewall to the internet.我想发布一个能够被公网访问的本地服务。

ngrok官网上的这句话，也正是我们的需求。
按照官方教程，郝同学测试了一下。ngrok会分配给我们一个动态域名，也可以通过公网访问这个域名（也就访问到了本地服务器），nice。但是，如果想要使用固定域名，必须付费。基础版5dollar一个月，约合人民币33.4元，值得。
3. natapp
开启您的内网穿透之旅。

natapp，实际上是中国的ngrok。收费更便宜，基础版5元一个月，真心实惠，强烈推荐。
值得一提的是，natapp的服务人员态度很好。由于官方文档还不是特别完善，郝同学遇到了一个解决不了的问题：付费后依然无法使用固定域名。给服务人员发邮件，很快就回复，还帮忙远程。最后确定是win8.1系统的问题，无法使用配置文件，必须手输参数。
win8.1下natapp启动步骤：在natapp.exe所在的文件夹，鼠标右键+shift，在此处打开命令窗口，输入以下命令即可。
natapp -authtoken=xxxxxx

4. 花生壳花生壳的业务有点多，其中一个是提供动态域名解析。郝同学花了5元在花生壳买了个固定域名，不过没有继续测试，感兴趣的小伙伴自行百度。
5. 后记最终，郝同学决定长期使用natapp。natapp官网分配的固定域名为voidking.vip.natapp.cn，郝同学绑定了自己的域名wx.voidking.com。
在微信公众平台测试号上，JS接口安全域名修改为wx.voidking.com，网页授权获取用户基本信息修改为wx.voidking.com。
完成了以上步骤，就可以进行微信公众号网站的本地开发了。如果要做微信公众号后台，接口配置信息也需要修改。
6. 书签
微信开发如何做本地调试
ngrok - secure introspectable tunnels to localhost
NATAPP 基于ngrok高速内网穿透服务
花生壳官网
微信公众平台开发者文档
微信公众平台接口调试工具
微信web开发者工具

]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>内网穿透</tag>
        <tag>代理</tag>
        <tag>微信</tag>
        <tag>调试</tag>
      </tags>
  </entry>
  <entry>
    <title>css阴影效果</title>
    <url>/dev-css-box-shadow/</url>
    <content><![CDATA[有阴影的图，看上去高大上些？不管怎样，UI设计了阴影，咱就照做好了。
语法：
box-shadow: h-shadow v-shadow blur spread color inset;



解释：



值
描述



h-shadow
必需。水平阴影的位置。允许负值。


v-shadow
必需。垂直阴影的位置。允许负值。


blur
可选。模糊距离。


spread
可选。阴影的尺寸。


color
可选。阴影的颜色。请参阅 CSS 颜色值。


inset
可选。将外部阴影 (outset) 改为内部阴影。


常见用法：
div&#123;    box-shadow: 10px 10px 5px #0cc;&#125;
四个值分别是水平阴影位置、垂直阴影位置、模糊距离、颜色。
实际案例：
&lt;a href=&quot;&quot; class=&quot;confirm&quot;&gt;    &lt;span&gt;马上去抢2G流量&lt;/span&gt;&lt;/a&gt;

.confirm&#123;    display: inline-block;    box-shadow: 0 .5rem 1.5rem #0cc;    border-radius: 5px;    margin-top: 12%;    width: 98%;    height: 17%;    background: url(../../img/flowrate/blue.jpg) no-repeat;    background-size: 100% 100%;    color: #fff;    span&#123;        display: inline-block;        margin-top: 3%;        font-size: 1.5rem;    &#125;&#125;

最终效果如下：
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>css</tag>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>绝对定位居中布局</title>
    <url>/dev-absolute-center-layout/</url>
    <content><![CDATA[1. 前言绝对定位并且居中显示，在开发中经常用到。总结了一下这种布局的三种方法，备忘。
2. 方法一position: absolute;top: 0;left: 0;right: 0;bottom: 0;margin: auto;width: 82%;height: 40%;



3. 方法二position: absolute;left: 50%; top: 50%;margin-top: -20%;margin-left: -41%;width: 82%;height: 40%;

4. 方法三position: absolute;left: 50%; top: 50%;transform: translate(-50%, -50%);    /* 50%为自身尺寸的一半 */margin: auto;

5. 后记以上三种绝对定位居中布局方法，同样适用于固定定位，即position:fixed;，只不过是相对的容器变成了浏览器。
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>css</tag>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>input绑定回车事件</title>
    <url>/dev-enter-event/</url>
    <content><![CDATA[html部分：
&lt;input id=&quot;search-key&quot; type=&quot;text&quot; placeholder=&quot;请输入关键字&quot;&gt;



JavaScript部分：
$(&#x27;#search-key&#x27;).keypress(function(event) &#123;    var key = event.which;    console.log(key);    if(key == 13)&#123;        //do something    &#125;&#125;);
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>jquery</tag>
      </tags>
  </entry>
  <entry>
    <title>横向滚动效果</title>
    <url>/dev-horizontal-scrolling/</url>
    <content><![CDATA[html部分：
&lt;div class=&quot;tab-body&quot;&gt;    &lt;ul&gt;        &lt;li data-key=&quot;0&quot;&gt;全部&lt;/li&gt;        &lt;li data-key=&quot;1&quot;&gt;攻略&lt;/li&gt;        &lt;li data-key=&quot;2&quot;&gt;案例&lt;/li&gt;        &lt;li data-key=&quot;3&quot;&gt;故事会&lt;/li&gt;        &lt;li data-key=&quot;4&quot;&gt;特别策划&lt;/li&gt;        &lt;li data-key=&quot;5&quot;&gt;喜舍杯&lt;/li&gt;        &lt;li data-key=&quot;6&quot;&gt;家居轶事&lt;/li&gt;    &lt;/ul&gt;&lt;/div&gt;



sass部分：
.tab-body&#123;    background: #fff;    overflow-x: scroll;    ul&#123;        width: 1000%;        li&#123;            display: inline-block;            vertical-align: top;            height: 3.5rem;            line-height: 3.5rem;            font-size: 1.3rem;            color: #666;            padding: 0 .8rem 0 .8rem;            margin-right: 1.8rem;            &amp;.active&#123;                border-bottom: 4px solid #FF5500;                color: #FF5500;            &#125;        &#125;    &#125;&#125;

js部分：
// 初始type是全部$(&#x27;.tab-body li&#x27;).first().addClass(&#x27;active&#x27;);var type = 0;// 设置导航条宽度var totalWidth = 0;$(&#x27;.tab-body li&#x27;).each(function(index, el) &#123;    totalWidth += $(el).width()+parseInt($(el).css(&#x27;margin-right&#x27;));&#125;);$(&#x27;.tab-body ul&#x27;).css(&#123;    &#x27;width&#x27;: (totalWidth + 50)+ &#x27;px&#x27;&#125;);// 切换type$(&#x27;.tab-body li&#x27;).on(&#x27;click&#x27;,function()&#123;    $(&#x27;.tab-body li&#x27;).removeClass(&#x27;active&#x27;);    $(this).addClass(&#x27;active&#x27;);    type = $(this).attr(&#x27;data-key&#x27;);    var param = &#123;        articleType: type,        pageNo: 1,        pageSize: 4    &#125;    $.ajax(&#123;        url: &#x27;/inspiration/home/api&#x27;,        type: &#x27;POST&#x27;,        dataType: &#x27;json&#x27;,        data: param,        success: function(data)&#123;            //把新获得的数据插入到页面        &#125;,        error: function(xhr)&#123;            console.log(xhr)        &#125;    &#125;);         &#125;);

1. 效果图
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>javascript</tag>
      </tags>
  </entry>
  <entry>
    <title>通过浏览器打开应用程序</title>
    <url>/dev-start-app-from-browser/</url>
    <content><![CDATA[1. 原理通过浏览器打开应用程序，利用的是在浏览器地址栏中输入一个协议，如果有本地程序（应用）能够解析这个协议，那么这个应用将被调起。


2. 前端代码如果安装过app调起app，否则页面跳转到app下载页面，这个逻辑分支实现原理如下：
当在浏览器地址栏输入app定义的协议调起app时，如果已经安装过，系统中断浏览器进程，继而响应app进程，因而在切换到app的时候浏览器中js代码将不再执行，当再次切换回来时系统进程又切回浏览器，js代码从中断处继续执行。所做的逻辑分支就是利用这个关键点。可设置延时来做分支，当app可以被调起时，切换到app，js不再执行，这期中有个app调起时间（与系统有关），如果设置的延时时间过短，则js会继续执行。当切换回来，记录切换回来的时间点，与切换之前比较大于设定时间说明已经切换成功，将不再执行下载，否则下载。
PS：app的协议是与app开发同学约定，由app开发同学通过activity scheme设置的。
var appInfo = &#123;    iosUrl: &quot;&quot;,    //打开IOS客户端链接    androidUrl: &quot;&quot;,  //打开ANdroid客户端的链接    downloadUrl: &quot;&quot;  //下载客户端或者去App Store的链接&#125;;var t1 = new Date().getTime();iframe2open();jump2download();//利用iframe打开客户端function iframe2open()&#123;    var src = $.os.ios ? appInfo.iosUrl : appInfo.androidUrl,        iframe = document.createElement(&quot;iframe&quot;);    iframe.src = src;    iframe.style.display = &quot;none&quot;;    document.body.appendChild(iframe); &#125; //跳转下载页面function jump2download()&#123;    var _timer = null;    clearTimeout(_timer);    _timer = setTimeout(function()&#123;        var t2 = new Date().getTime();        if (t2 - t1 &lt;= 800) &#123;            window.location = appInfo.downloadUrl;        &#125;    &#125;,400);&#125;

3. 书签通过浏览器直接打开iOS/Android App 应用程序http://itindex.net/blog/1415353560000.html
Make a link in the Android browser start up my app?
Launch custom android application from android browser
非微信内置浏览器中的网页调起微信支付的方案研究http://blog.csdn.net/ahence/article/details/51317814
浏览器调起app应用方法http://blog.csdn.net/u012193330/article/details/52190143
android自定义协议和html加载时自动尝试调用本地APPhttp://www.oschina.net/code/snippet_256033_35330/
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>ReactNative、Ionic和NativeScript</title>
    <url>/dev-reactnative-ionic-nativescript/</url>
    <content><![CDATA[1. 前言最近几年，利用HTML5技术开发跨平台app，越来越流行。各种框架层出不穷，郝同学选择了其中三个，来对比研究一下究竟什么样的框架才更好。


2. ReactNativeReact Native 使你能够使用基于 JavaScript 和 React 一致的开发体验在本地平台上构建世界一流的应用程序体验。React Native 把重点放在所有开发人员关心的平台的开发效率上——开发者只需学习一种语言就能轻易为任何平台高效地编写代码。Facebook 在多个应用程序产品中使用了 React Native，并将继续为 React Native 投资。
优势：1、虽然不能做到一处编码到处运行，但是基本上即使是两套代码，也是相同的jsx语法，使用js进行开发。用户体验，高于html，开发效率较高。2、flexbox 布局，据说比native的自适应布局更加简单高效。3、可实现在线更新，AppStore审核政策调整：允许运行于JavascriptCore的动态加载代码。4、更贴近原生开发。
劣势：1、（引）对开发人员要求较高，不是懂点web技术就行的，当官方封装的控件、api无法满足需求时 就必然需要懂一些native的东西去扩展，扩展性仍然远远不如web，也远远不如直接写Native code。2、（引）官方说得很隐晦：learn once, write anywhere。人家可没说run anywhere。事实上，从官方的api来看SliderIOS，SwitchIOS..等等这些控件，之后势必会出现SliderAndroid，SwitchAndroid…，也就是很可能针对不同的平台会需要写多套代码。3、发展还不成熟，目前很多ui组件只有ios的实现，android的需要自己实现。4、（引）从Native到Web，要做很多概念转换，势必造成双方都要妥协。比如web要用一套CSS的阉割版，Native通过css-layout拿到最终样式再转换成native原生的表达方式（比如iOS的Constraint\origin\Center等属性），再比如动画。另外，若Android和iOS都要做相同的封装，概念转换就更复杂。5、文档还不够完整，学习起来困难。
3. IonicIONIC是目前最有潜力的一款 HTML5 手机应用开发框架。通过 SASS 构建应用程序，它提供了很多 UI 组件来帮助开发者开发强大的应用。 它使用 JavaScript MVVM 框架和 AngularJS 来增强应用。提供数据的双向绑定，使用它成为 Web 和移动开发者的共同选择。Ionic是一个专注于用WEB开发技术，基于HTML5创建类似于手机平台原生应用的一个开发框架。Ionic框架的目的是从web的角度开发手机应用，基于PhoneGap的编译平台，可以实现编译成各个平台的应用程序。
优势：1、ios 和 android 基本上可以共用代码，纯web思维，开发速度快，简单方便，一次编码，到处运行，如果熟悉web开发，则开发难度较低。2、文档很全，系统级支持封装较好，所有UI组件都是有html模拟，可以统一使用。3、可实现在线更新 允许加载动态加载web js。4、文档多，开发者多，视频教程多。容易学习，遇到问题容易解决，技术成熟。
劣势：1、占用内存高一些（不过手机内存都大了不影响），不适合做游戏类型app，web技术无法解决一切问题，对于比较耗性能的地方无法利用native的思维实现优势互补，如高体验的交互，动画等。
4. NativeScript和 ReactNative 相比，NativeScript 最大的特点是可以获得100%的原生 API 。也就是说，开发者可以通过 JavaScript 获取和原生开发语言同样多的原生接口。
优势：1、使用 JavaScript 直接访问所有原生 API。2、系统新功能0延时支持。3、第三方原生库全部支持。
劣势：1、NativeScript和React不同，无法与原生项目融合，即你只能纯写个NativeScript的应用，不可能把它抽离出来作为某原生应用的一部分来出现。虽然说它和React的出发点一致都是”用Web APP的开发速度打造Native App的体验”，但是实际上，它算是鸡肋吧，拿它来写个展示App或者简单的应用还是不错的。2、NativeScript中虽然已经支持了很多组件，比如说tabview、srcollview、button，但是提供的组件方法、属性过少，整个框架还不是很丰满。
5. 后记利用HTML5技术开发跨平台app，最重要的是两点：第一点，一套代码，所有平台都可以运行；第二点，利用HTML5技术实现，尽量少的用到其他技术。至于性能，肯定是不如原生应用的，只能期待框架以后的优化。ReactNative，很多时候都要写两套代码，不满足第一点；开发时要用到很多原生的android和ios技术，不满足第二点。Ionic，两点皆满足，但是更适合开发web类app，不适合开发游戏。NativeScript，两点皆满足。
综上，个人认为，Ionic和NativeScript更值得学习。如果非要二选一，我选择NativeScript。理由很简单，ReactNative的研发人数更少，参考文档、书籍和视频更少，要做，就做先行者。
6. 书签6.1. ReactNative整理了一份React-Native学习指南http://www.tuicool.com/articles/zaInUbA
RativeScript的工作原理：用JavaScript调用原生API实现跨平台 – OurJShttp://top.css88.com/archives/656
React Native中文社区http://bbs.reactnative.cn/
React 入门实例教程http://www.ruanyifeng.com/blog/2015/03/react.html
WebViewJavascriptBridge详细使用http://www.tuicool.com/articles/Q3AVnq2
WebViewJavascriptBridge 原理分析http://www.2cto.com/kf/201503/384998.html
教你怎么屏蔽掉在移动端的宽带运营商的流量劫持，屏蔽无耻的广告https://my.oschina.net/zxcholmes/blog/596192
ReactJS 傻瓜教程https://zhuanlan.zhihu.com/p/19896745?columnSlug=FrontendMagazine
React | A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACEShttps://facebook.github.io/react/index.html
6.2. IonicIonic: Advanced HTML5 Hybrid Mobile App Frameworkhttp://ionicframework.com/
Ionic中文文档教程http://www.ionic.wang/js_doc-index.html
ionic react-native native 优劣势对比http://www.ionic.wang/article-index-id-69.html
6.3. NativeScript使用NativeScript和Angular2构建跨平台APPhttps://zhuanlan.zhihu.com/p/21458458
Cross-Platform Native Development with Javascripthttps://www.nativescript.org/
NativeScript源码https://github.com/NativeScript
NativeScript中文手册 - GitBookhttps://www.gitbook.com/book/flowforever/nativescript-cn-book/details
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>使用accessToken记录登录状态</title>
    <url>/dev-login-state-accesstoken/</url>
    <content><![CDATA[1. js设置cookie1.1. 海哥的插件浏览器端用到了海哥写的Cookie插件：https://github.com/voidking/jquery-cookie ，值得好好学习一下。
cookie.prototype.setCookie(&#x27;accessToken&#x27;, accessToken,7);

1.2. jquery官方插件如果使用jquery官方给的cookie插件，用法如下：
$.cookie(&#x27;accessToken&#x27;, accessToken,&#123; expires: 7&#125;);


1.3. 原生js如果使用原生的JS来操作cookie，用法如下：
//获取当前时间var date=new Date();var expireDays=7;//将date设置为7天以后的时间date.setTime(date.getTime()+expireDays*24*3600*1000);//或者也可以用date.setDate(date.getDate+expireDays);//将accessToken这个cookie设置为7天后过期document.cookie = &#x27;accessToken=&#x27;+accessToken+&#x27;;expires=7&#x27;;

2. js删除cookie2.1. 海哥的插件cookie.prototype.delCookie(&#x27;accessToken&#x27;); 

2.2. jquery官方插件$.removeCookie(&#x27;accessToken&#x27;);

2.3. 原生JS//获取当前时间var date=new Date();//将date设置为过去的时间date.setTime(date.getTime()-10000);//将accessToken这个cookie删除document.cookie=&#x27;accessToken=v; expire=&#x27;+date.toGMTString();console.log(document.cookie);

3. js获取cookie3.1. 海哥的插件var accessToken = cookie.prototype.getCookie(&#x27;accessToken&#x27;);

3.2. jquery官方插件var accessToken = $.cookie(&#x27;accessToken&#x27;);

3.3. 原生jsvar accessToken = getCookie(&#x27;accessToken&#x27;);function getCookie(objName)&#123;    var arrStr = document.cookie.split(&quot;; &quot;);    for(var i = 0;i &lt; arrStr.length;i ++)&#123;            var temp = arrStr[i].split(&quot;=&quot;);            if(temp[0] == objName)                 return unescape(temp[1]);    &#125;&#125;


4. Node端获取cookievar Cookies = &#123;&#125;;req.headers.cookie &amp;&amp; req.headers.cookie.split(&#x27;;&#x27;).forEach(function( Cookie ) &#123;    var parts = Cookie.split(&#x27;=&#x27;);    Cookies[ parts[ 0 ].trim() ] = ( parts[ 1 ] || &#x27;&#x27; ).trim();&#125;);var accessToken = Cookies[&#x27;accessToken&#x27;] || &#x27;&#x27;;

5. 后记accessToken可以在前端获取后传给Node端，也可以在Node端获取。如果这个accessToken不为空，则表明该用户已经登录，否则表明该用户未登录。
6. 参考文档jQuery Cookiehttp://plugins.jquery.com/cookie/
jQuery Cookie项目地址https://github.com/carhartl/jquery-cookie
关于document.cookie的使用http://www.cnblogs.com/newsouls/archive/2766567.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>cookie</tag>
      </tags>
  </entry>
  <entry>
    <title>使用Socket.IO实现实时私信功能</title>
    <url>/dev-graduation-project-socketio/</url>
    <content><![CDATA[1. 功能描述在帖子详情页面，单击用户头像，可以发送私信给该用户。在消息页面，输入用户名，可以发送私信给该用户。如果该用户在线，则把私信实时发送给该用户，如果该用户不在线，则把私信存储到数据库，成为消息记录。


2. 原理WebSocket protocol 是HTML5一种新的协议。使用WebSocket API，浏览器和服务器只需要做一个握手的动作，就形成了一条快速通道。借助这个通道，浏览器和服务器之间就可以互相传送数据。
Socket.IO是一个完全由JavaScript实现、基于Node.js、支持WebSocket的协议用于实时通信、跨平台的开源框架，它包括了浏览器端的JavaScript和服务器端的Node.js。 Socket.IO除了支持WebSocket通讯协议外，还支持许多种轮询（Polling）机制以及其它实时通信方式，并封装成了通用的接口，并且在服务端实现了这些实时机制的相应代码。
页面初始化时，浏览器和服务端建立一个连接，用来发送事件和接收事件。浏览器和服务端建立连接后，服务端会触发一个“adduser”事件，把当前用户添加到在线用户列表中。当用户离开页面时，会触发一个“deleteuser”事件，把当前用户从在线用户列表中删除。用户A发私信给用户B时，服务端接收到“privatemsg”事件，会把该信息存储到数据库。同时，如果用户B在线，则触发用户B的浏览器的“sendback”事件，把信息发送给用户B的浏览器。用户B的浏览器接收到“sendback”事件，说明收到了新的消息，该消息实时插入到聊天对话框。
3. 代码3.1. js// 即时通讯// 建立连接var socket = io.connect();var from_user = $(&#x27;#from-user&#x27;).val();socket.emit(&quot;adduser&quot;,from_user,function(data)&#123;    console.log(data);&#125;);// 发送消息var from_user = $(&#x27;#from-user&#x27;).val();var to_user = friend_id;var content = $(&#x27;#chat .reply-content&#x27;).val();var data = &#123;    from_user: from_user,    to_user: to_user,    content: content&#125;;socket.emit(&quot;privatemsg&quot;,data,function(data)&#123;    console.log(data);&#125;);// 接受消息socket.on(&quot;sendback&quot;,function(data)&#123;    console.log(data);&#125;);// 断开连接window.onunload = function()&#123;    console.log(&#x27;删除用户&#x27;);    var from_user = $(&#x27;#from-user&#x27;).val();    socket.emit(&#x27;deleteuser&#x27;,from_user,function(data)&#123;        console.log(data);    &#125;);&#125;


3.2. Node端var User = require(&#x27;./models/user-model&#x27;);var Message = require(&#x27;./models/message-model&#x27;);var FriendCollect = require(&#x27;./models/friend-collect-model&#x27;);//var mongoose = require(&#x27;mongoose&#x27;);//mongoose.connect(&#x27;mongodb://localhost/forum&#x27;);var users = &#123;&#125;;var socket_event = function(socket) &#123;    socket.on(&#x27;disconnect&#x27;, function () &#123;        //console.log(&#x27;user disconnected&#x27;);    &#125;);    socket.on(&quot;adduser&quot;, function (from_user, callback) &#123;        if (from_user in users) &#123;            users[from_user] = socket;            callback(true);        &#125; else &#123;            socket.user_id = from_user;            users[socket.user_id] = socket;            callback(true);        &#125;    &#125;);    socket.on(&#x27;deleteuser&#x27;, function (from_user, callback) &#123;        if (from_user in users) &#123;            delete users[from_user];            callback(true);        &#125;    &#125;);    socket.on(&quot;privatemsg&quot;, function (data, callback) &#123;        var param = &#123;            from_user: data.from_user,            to_user: data.to_user,            content: data.content        &#125;;        var message = new Message(param);        message.save();        if (data.to_user in users) &#123;            User.findById(data.from_user,function(err,user)&#123;                users[data.to_user].emit(&#x27;sendback&#x27;, &#123;&#x27;content&#x27;: data.content,&#x27;friend&#x27;:user&#125;);                callback(true);            &#125;);        &#125; else &#123;            callback(&#x27;对方未登录，已留言&#x27;);        &#125;    &#125;);&#125;module.exports = socket_event;

4. 源码https://github.com/voidking/nodeforum/blob/master/views/message/message.htmlhttps://github.com/voidking/nodeforum/blob/master/public/js/message/message.jshttps://github.com/voidking/nodeforum/blob/master/app.jshttps://github.com/voidking/nodeforum/blob/master/socket-event.js
5. 参考文档Socket.IO官网http://socket.io/
Socke.IO项目地址https://github.com/socketio/socket.io
在线聊天Demohttp://chat.socket.io/http://socket.io/get-started/chat/
Socket.IO APIhttp://socket.io/docs/
Nodejs Socket.io 发送私信http://www.jenkihuang.com/job/2015/10/nodejs-socket-io-privatemsg.html
Socket.IO 和 Node.js 入门http://www.oschina.net/question/12_54009/?fromerr=pCDquuQy
Socket.io:有点意思http://www.cnblogs.com/edwardstudy/p/4358202.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>毕设</tag>
        <tag>socketio</tag>
      </tags>
  </entry>
  <entry>
    <title>使用百度地图实现发帖时的定位</title>
    <url>/dev-graduation-project-baidumap/</url>
    <content><![CDATA[1. 功能描述在发布帖子界面，用户可以选择是否在发布帖子时显示位置。位置图标默认灰色，不显示位置；用户选择位置后，位置图标变成绿色，同时把位置信息显示在发布帖子界面上。



2. 原理百度地图，提供了四种定位的方式，分别是根据浏览器定位、根据IP定位、根据城市名定位、根据经纬度定位。本功能模块中，采用根据浏览器定位的方式。
浏览器通过js请求百度地图API，获取到地址信息后，发送给我的服务器。
3. 代码3.1. html引入百度地图api，定义一个id，用来初始化BMap。
&lt;script type=&quot;text/javascript&quot; src=&quot;http://api.map.baidu.com/api?v=2.0&amp;ak=BC46e2b24290dba4d0267e2430f512fe&quot;&gt;&lt;/script&gt;&lt;div id=&quot;allmap&quot; style=&quot;display: none;&quot;&gt;&lt;/div&gt;

3.2. js新建BMap对象，给一个初始化坐标，然后浏览器通过百度地图API获取到位置信息，获取到位置信息后，和帖子的其他信息一起，发送给服务端。
// 百度地图定位var baidu_position = &#123;&#125;;var map = new BMap.Map(&quot;allmap&quot;);var point = new BMap.Point(118.895144,31.92596);map.centerAndZoom(point,12);var geolocation = new BMap.Geolocation();geolocation.getCurrentPosition(function(r)&#123;    if(this.getStatus() == BMAP_STATUS_SUCCESS)&#123;        var mk = new BMap.Marker(r.point);        map.addOverlay(mk);        map.panTo(r.point);        //alert(&#x27;您的位置：&#x27;+r.point.lng+&#x27;,&#x27;+r.point.lat);        console.log(r);        baidu_position = r;    &#125;    else &#123;        alert(&#x27;failed&#x27;+this.getStatus());    &#125;&#125;,&#123;enableHighAccuracy: true&#125;)

4. 源码https://github.com/voidking/nodeforum/blob/master/public/js/post/post-add.js
5. 书签百度地图APIhttp://developer.baidu.com/map/reference/
百度地图API示例http://developer.baidu.com/map/jsdemo.htm#d0_2
百度LSB API开发指南http://developer.baidu.com/map/wiki/index.php?title=uri/api/web
拾取坐标系统http://api.map.baidu.com/lbsapi/getpoint/index.html
根据标注点坐标范围计算显示缩放级别zoom自适应显示地图http://www.aichengxu.com/view/2456553
百度地图API详解和运用http://blog.csdn.net/binyao02123202/article/details/7955803
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>毕设</tag>
      </tags>
  </entry>
  <entry>
    <title>使用七牛管理图片</title>
    <url>/dev-graduation-project-qiniu/</url>
    <content><![CDATA[1. 功能描述在发布帖子界面，用户可以选择图片上传，图片上传成功后显示在发布帖子界面上，上传失败则提示“上传失败”。


2. 原理七牛是一个优秀的云存储平台，提供图片相关的缩略、剪裁、添加水印等功能。本系统的重心在于社区论坛，所以处理图片的工作就交给七牛了，避免重复造轮子。
在发布帖子页面，用户选择图片后，浏览器获取到图片数据imageData，发送给我的服务器，我的服务器转发图片数据给七牛服务器。上传成功后，七牛服务器返回数据给我的服务器，我的服务器返回数据给浏览器。
浏览器获得返回数据，如果获取到服务器返回的状态true和图片url，则把图片显示在页面上。否则，就把告知用户“上传失败”。
3. 代码3.1. html&lt;div class=&quot;form-group&quot;&gt;    &lt;label for=&quot;picture&quot; class=&quot;col-sm-2 control-label&quot;&gt;图片&lt;/label&gt;    &lt;div class=&quot;col-sm-9&quot;&gt;        &lt;!--&lt;input id=&quot;image&quot;  type=&quot;file&quot; multiple&gt;--&gt;        &lt;input type=&quot;file&quot; id=&quot;picture&quot; style=&quot;display: none;&quot; /&gt;        &lt;span class=&quot;icon-picture&quot;&gt;&lt;/span&gt;        &lt;span class=&quot;icon-loading&quot;&gt;&lt;/span&gt;        &lt;span class=&quot;addimage&quot;&gt;添加图片&lt;/span&gt;    &lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;form-group&quot;&gt;    &lt;div class=&quot;col-sm-offset-2 col-sm-9&quot;&gt;        &lt;div class=&quot;pictures&quot;&gt;            &lt;!--动态插入图片--&gt;        &lt;/div&gt;    &lt;/div&gt;&lt;/div&gt;

3.2. js$(&#x27;.icon-picture&#x27;).click(function()&#123;    $(&#x27;#picture&#x27;).click();    $(&#x27;#picture&#x27;).unbind().on(&#x27;change&#x27;,function()&#123;        var fileNumber = $(&#x27;#picture&#x27;).get(0).files.length;        if(fileNumber==0)&#123;            return;        &#125;        var file = $(&#x27;#picture&#x27;).get(0).files[0];        console.log(file);        var fileReader = new FileReader();        fileReader.readAsDataURL(file);        fileReader.onload = function(e) &#123;//            var fileType = file.name.substring(file.name.lastIndexOf(&#x27;.&#x27;), file.name.length);//            var now = new Date();//            var fileName = now.getTime() + &#x27;langting&#x27; + parseInt(Math.random() * 20) + fileType;            $(&quot;.addimage&quot;).text(&quot;上传中...&quot;);            $(&#x27;.icon-picture&#x27;).hide();            $(&#x27;.icon-loading&#x27;).css(&#123;                &#x27;display&#x27;: &#x27;inline-block&#x27;            &#125;);            $.ajax(&#123;                type: &#x27;POST&#x27;,                dataType: &#x27;json&#x27;,                url: &#x27;/qn-upload&#x27;,                data: &#123;                    imageData: e.target.result                &#125;,                success: function(data) &#123;                    if (data.state == true) &#123;                        setTimeout(function() &#123;                            $(&quot;.addimage&quot;).text(&quot;添加图片&quot;);                            $(&#x27;.icon-loading&#x27;).hide();                            $(&#x27;.icon-picture&#x27;).show();                            var html = template(&#x27;pic_template&#x27;,data);                            $(&#x27;.pictures&#x27;).append(html);                        &#125;, 1500);                    &#125; else &#123;                        alert(&quot;上传失败&quot;);                    &#125;                &#125;,                error: function()&#123;                &#125;            &#125;);        &#125;    &#125;);&#125;);// 删除图片$(&#x27;.pictures&#x27;).on(&#x27;click&#x27;,&#x27;.icon-delete&#x27;,function()&#123;    $(this).parents(&#x27;.pic&#x27;).remove();&#125;);

3.3. Node端exports.qn_upload = function(req,res)&#123;    var qn = require(&#x27;qn&#x27;);    var client = qn.create(&#123;        accessKey: &#x27;JEBuh6qG9FPI6atoycgdoypwOZJWuzYk1YXnC-6c&#x27;,        secretKey: &#x27;IBAa_7Mkj2_ROefIRcwVjcVEK9PVFDvzrtPiL9nO&#x27;,        bucket: &#x27;forum&#x27;,        domain: &#x27;http://7xstti.com2.z0.glb.clouddn.com&#x27;    &#125;);    var imageData = req.body.imageData;    var key = uuid.v1();    imageData = imageData.replace(/^data:image\/\w+;base64,/, &quot;&quot;);    var dataBuffer = new Buffer(imageData, &#x27;base64&#x27;);    client.upload(dataBuffer, &#123;        key: key    &#125;, function(err, result) &#123;        if (err) &#123;            res.json(&#123;                state: false,                imgname: imageName,                imgurl: &quot;&quot;,                imghash: &quot;&quot;            &#125;);        &#125; else &#123;            res.json(&#123;                state: true,                imgname: result.key,                imgurl: result.url,                imghash: result.hash            &#125;);        &#125;    &#125;);&#125;

4. 源码https://github.com/voidking/nodeforum/blob/master/views/post/post-add.htmlhttps://github.com/voidking/nodeforum/blob/master/public/js/post/post-add.jshttps://github.com/voidking/nodeforum/blob/master/controllers/post.js
5. 后记上传的交互过程，是前端把图片数据传给Node端，Node端转发图片数据到七牛服务器。七牛服务器返回结果数据给Node端，Node端转发结果数据给前端。
这个过程比较麻烦，更好的做法，是前端能够直接把图片数据传给七牛服务器，七牛服务器返回结果给前端。按照七牛给的文档，是可以实现的，感兴趣的小伙伴请研读七牛的《JavaScript SDK使用指南》。
6. 书签
七牛开发者中心
JavaScript SDK使用指南
图片基本处理 (imageView2)
七牛 Node.js SDK
File对象上传图片（nodejs版）
图片上传时input file change事件多次触发解决
用js获取上传前图片的宽高
HTML5 之文件操作(file)
Using files from web applications
保存裁剪后的图片的疑问
base64百度百科
好文推荐：移动端图片格式调研
Plupload

]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>毕设</tag>
      </tags>
  </entry>
  <entry>
    <title>响应式布局</title>
    <url>/dev-graduation-project-responsive-layout/</url>
    <content><![CDATA[1. 前言响应式布局，简单来说，就是利用CSS3的Media Query（媒介查询），来探测访问系统的终端的宽和高等属性，并以此来决定给用户展示什么样的页面。
毕设的社区网站系统设计了两套UI，以768px为两种UI的分割点。


2. 界面展示
3. sass/* 超小屏幕（手机，小于 768px） */@media (max-width: 767px) &#123;&#125;/* 小屏幕（平板，大于等于 768px） */@media (min-width: 768px) &#123;&#125;

源码：https://github.com/voidking/nodeforum/blob/master/public/scss/partial/header.scss
4. 书签Bootstrap v3中文文档http://v3.bootcss.com/getting-started/#examples
Bootstrap中文网开源项目免费 CDN 服务http://www.bootcdn.cn/
20分钟打造你的Bootstrap站点http://www.w3cplus.com/css/twitter-bootstrap-tutorial.html
程序员们最爱的12款Bootstrap模板 ！http://www.chinaz.com/free/2014/0924/368583.shtml
15个好看的Bootstrap HTML网站模板下载http://www.shejidaren.com/bootstrap-mo-ban.html
折腾响应式布局设计http://caibaojian.com/356.html
响应式设计的性能优化http://www.jianshu.com/p/193911ee72e2
Bootstrap File Input Demohttp://plugins.krajee.com/file-basic-usage-demo
彻底弄懂css中单位px和em,rem的区别http://www.cnblogs.com/leejersey/p/3662612.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>毕设</tag>
      </tags>
  </entry>
  <entry>
    <title>用div模拟input和textarea</title>
    <url>/dev-simulate-input-by-div/</url>
    <content><![CDATA[1. 前言先看需求：在输入评论的时候，当评论长度超过一行时，自动换行，同时评论框变高，效果如下。
然而，input并没有自动换行属性，也就无法实现这个效果；使用textarea，写js控制row？想想就不够优雅！
经评估，使用div模拟输入框，是最佳方案。


2. html&lt;div class=&quot;input-body&quot;&gt;    &lt;div class=&quot;left&quot;&gt;        &lt;div class=&quot;input&quot;&gt;            &lt;p id=&quot;input-comment&quot; contenteditable=&quot;true&quot;&gt;&lt;/p&gt;            &lt;p id=&quot;input-placeholder&quot;&gt;请输入评论内容&lt;/p&gt;        &lt;/div&gt;                  &lt;/div&gt;    &lt;div class=&quot;right&quot;&gt;        &lt;span id=&quot;send&quot;&gt;发送&lt;/span&gt;    &lt;/div&gt;  &lt;/div&gt;&lt;div class=&quot;input-body-back&quot;&gt;&lt;/div&gt;

3. scss.input-body&#123;    position: fixed;    border-top: 1px solid #e5e5e5;    border-bottom: 1px solid #e5e5e5;    width: 100%;    bottom: 0;    background: #f7f7f7;    font-size: 1.2rem;    .left&#123;        display: inline-block;        vertical-align: bottom;        width: 80%;        text-align: center;        padding: .5rem;        .input&#123;            position: relative;            #input-comment&#123;                display: inline-block;                width: 92%;                min-height: 2rem;                line-height: 2rem;                text-align: left;                padding: .2rem .5rem;                background: #fff;                border: 1px solid #e5e5e5;                border-radius: 3px;                outline: 0;            &#125;               #input-placeholder&#123;                position: absolute;                top: .5rem;                left: .8rem;                color: #8E8E93;            &#125;        &#125;           &#125;    .right&#123;        display: inline-block;        vertical-align: bottom;        width: 15%;        height: 2.5rem;        text-align: center;        font-weight: bold;        #send&#123;            margin-right: 1rem;            color: #8E8E93;            &amp;.active&#123;                color: #146AF3;            &#125;;        &#125;    &#125;&#125;.input-body-back&#123;    height: 4rem;&#125;

4. js$(&#x27;#input-comment&#x27;).on(&#x27;input propertychange&#x27;,function()&#123;    var str = $(this).html();    if(str == &#x27;&#x27;)&#123;        $(&#x27;#send&#x27;).removeClass(&#x27;active&#x27;);        $(&#x27;#input-placeholder&#x27;).show();    &#125;else&#123;        $(&#x27;#send&#x27;).addClass(&#x27;active&#x27;);        $(&#x27;#input-placeholder&#x27;).hide();    &#125;&#125;);$(&#x27;#send&#x27;).on(&#x27;click&#x27;,function()&#123;    if(!$(this).hasClass(&#x27;active&#x27;))&#123;        return;    &#125;    var comment = $(&#x27;#input-comment&#x27;).html();    if(comment.length &gt; 500)&#123;        alert(&#x27;字数过多，请删减&#x27;);        return;    &#125;    // ajax请求&#125;);


5. 书签div模拟textarea文本域轻松实现高度自适应http://www.zhangxinxu.com/wordpress/2010/12/div-textarea-height-auto/
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>css</tag>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>JS转换HTML转义符</title>
    <url>/dev-escape-to-html/</url>
    <content><![CDATA[1. 去掉html标签function removeHtmlTab(tab) &#123;    return tab.replace(/&lt;[^&lt;&gt;]+?&gt;/g,&#x27;&#x27;);//删除所有HTML标签&#125;



2. 普通字符转换成转意符function html2Escape(sHtml) &#123;    return sHtml.replace(/[&lt;&gt;&amp;&quot;]/g,function(c)&#123;return &#123;&#x27;&lt;&#x27;:&#x27;&amp;lt;&#x27;,&#x27;&gt;&#x27;:&#x27;&amp;gt;&#x27;,&#x27;&amp;&#x27;:&#x27;&amp;amp;&#x27;,&#x27;&quot;&#x27;:&#x27;&amp;quot;&#x27;&#125;[c];&#125;);&#125;

3. 转意符换成普通字符function escape2Html(str) &#123;    var arrEntities=&#123;&#x27;lt&#x27;:&#x27;&lt;&#x27;,&#x27;gt&#x27;:&#x27;&gt;&#x27;,&#x27;nbsp&#x27;:&#x27; &#x27;,&#x27;amp&#x27;:&#x27;&amp;&#x27;,&#x27;quot&#x27;:&#x27;&quot;&#x27;&#125;;    return str.replace(/&amp;(lt|gt|nbsp|amp|quot);/ig,function(all,t)&#123;return arrEntities[t];&#125;);&#125;

4. &nbsp;转成空格function nbsp2Space(str) &#123;    var arrEntities = &#123;&#x27;nbsp&#x27; : &#x27; &#x27;&#125;;    return str.replace(/&amp;(nbsp);/ig, function(all, t)&#123;return arrEntities[t]&#125;)&#125;

5. 回车转为br标签function return2Br(str) &#123;    return str.replace(/\r?\n/g,&quot;&lt;br /&gt;&quot;);&#125;

6. 去除开头结尾换行function trimBr(str) &#123;    str=str.replace(/((\s|&amp;nbsp;)*\r?\n)&#123;3,&#125;/g,&quot;\r\n\r\n&quot;);//限制最多2次换行    str=str.replace(/^((\s|&amp;nbsp;)*\r?\n)+/g,&#x27;&#x27;);//清除开头换行    str=str.replace(/((\s|&amp;nbsp;)*\r?\n)+$/g,&#x27;&#x27;);//清除结尾换行    return str;&#125;

7. 将多个连续空格合并成一个空格function mergeSpace(str) &#123;    str=str.replace(/(\s|&amp;nbsp;)+/g,&#x27; &#x27;);    return str;&#125;

转载自 http://www.cnblogs.com/leejersey/p/4568092.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>javascript</tag>
      </tags>
  </entry>
  <entry>
    <title>在Windows上安装mongodb</title>
    <url>/dev-install-mongodb-on-windows/</url>
    <content><![CDATA[1. 下载mongodbmongodb官网 https://www.mongodb.com/download-center#community ，如果下载msi版本，直接按照提示安装即可。
如果下载的是zip版本，则往下接着看。


2. 新建文件和文件夹1、解压安装包到喜欢的位置，郝同学解压到D:\Server目录下，重命名解压出来的文件夹为mongodb。
2、在mongodb文件夹下，新建文件夹data；在data下新建文件夹db和log；在log下新建文件mongodb.log。
3、在mongodb文件夹下，新建文件mongo.config，内容如下：
dbpath=D:\Server\mongodb\data\dblogpath=D:\Server\mongodb\data\log\mongodb.log  

3. 安装命令使用管理员打开cmd，输入命令：
sc create mongodb binPath=&quot;D:\Server\mongodb\bin\mongod.exe --service --config=D:\Server\mongodb\mongo.config&quot;

至此，安装结束。使用net start mongodb，检查能否正常启动。启动后，使用mongo，检查能否正常连接。
4. 开机自启动右击计算机，管理，服务，右击mongodb，属性，启动类型设置自动。
5. 后记顺便推荐个mongodb可视化管理工具——Robomongo，用起来很顺手。官方地址：https://robomongo.org/
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>mongodb</tag>
      </tags>
  </entry>
  <entry>
    <title>基于JQuery实现滚动到页面底端时自动加载更多信息</title>
    <url>/dev-auto-loading-when-scroll-to-the-bottom/</url>
    <content><![CDATA[这篇文章主要介绍了基于JQuery实现滚动到页面底端时自动加载更多信息，类似微博，新浪新闻的评论等，都采用了这方法,需要的朋友可以参考下关键代码：
var stop=true; $(window).scroll(function()&#123;     totalheight = parseFloat($(window).height()) + parseFloat($(window).scrollTop());     if($(document).height() &lt;= totalheight)&#123;         if(stop==true)&#123;             stop=false;             $.post(&quot;ajax.php&quot;, &#123;start:1, n:50&#125;,function(txt)&#123;                 $(&quot;#Loading&quot;).before(txt);                 stop=true;             &#125;,&quot;text&quot;);         &#125;     &#125; &#125;);



HTML代码如下:
&lt;div id=&quot;Loading&quot;&gt;Loading...&lt;/div&gt;

实现方法是比较页面总高度和下滚高度以判断是否到达底端，若到达底端则通过ajax读取更多的内容，用before插入到Loading之前。stop参数是考虑到ajax读取耗时，防止在一次ajax读取过程中多次触发事件，造成多次加载内容。
转载自http://www.3lian.com/edu/2014/02-10/127916.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>jquery</tag>
      </tags>
  </entry>
  <entry>
    <title>MongoDB入门篇</title>
    <url>/dev-mongodb-start/</url>
    <content><![CDATA[1. MongoDB简介
MongoDB 是一个文档数据库，旨在简化应用程序开发和扩展。

mongodb采用文档式存储结构，存储结构分为四个层次：键值对、文档、集合、数据库。文档数据库存储结构的基本单位是键值对，具体包含数据和类型。键值对的数据包含键和值，键的格式一般为字符串，值的格式可以包含字符串、数值、数组、文档等类型。文档是mongodb的核心概念，是数据的基本单元，文档的数据结构和json基本相同，实际存储时是bson（binary json）。文档存储在集合中，一个集合是由一些文档构成的对象。文档类似于关系型数据库的行，集合类似于表。同一个集合可以插入不同格式的文档，但是通常情况下一个集合中的文档应该具有相关性。数据库包含多个集合，通常一个应用的所有数据存储到同一个数据库。
mongodb的最大优点是非常适合层级式的数据存储。举个简单例子，比如A对象包含B对象，B对象包含C对象，关系型数据库应该会存三个表，查询时也需要三个表进行关联。而文档数据库可以把ABC三个对象按照层级关系作为一个文档存储到一个集合中，整存整取。此外，mongodb支持分布式存储，可以满足海量数据的存储和查询。
缺点：1）不支持事务；2）key字段占用额外空间
参考文档：

MongoDB官方文档
MongoDB的文档存储结构
mongodb与mysql相比的优缺点
k8s 部署 mongodb 三种模式
mongodb的认证(authentication)与授权(authorization)
MongoDB数据导出导入



2. 安装MongoDB参考文档：dockerhub mongo
mkdir -p /data/mongo/&#123;conf,data&#125;docker run -d --name mongodb-tmp mongo:5.0.31docker cp mongodb-tmp:/etc/mongod.conf.orig /data/mongo/confdocker rm -f mongodb-tmpdocker run -d --name mongodb \  --restart=always \  -v /data/mongo/conf/mongod.conf.orig:/etc/mongod.conf.orig \  -v /data/mongo/data:/data/db \  -p 27017:27017 \  -e MONGO_INITDB_ROOT_USERNAME=admin \  -e MONGO_INITDB_ROOT_PASSWORD=password \  mongo:5.0.31docker logs mongodb

3. 常用命令3.1. 进入MongoDB容器docker exec -it mongodb /bin/bash

3.2. 登录数据库#mongo --host localhost --port 27017mongo --host localhost --port 27017 -uroot -pmongo --host localhost --port 27017 -uroot -p --authenticationDatabase=admin

其中authenticationDatabase参数指定认证数据库，如果不指定，默认是admin。这是因为Mongodb允许存在多个同名的用户存在，但同名的用户认证db必须不能相同。
3.3. 查看帮助help

3.4. 操作数据库// 查看数据库show dbs// 切换数据库（不存在则创建）use local// 查看当前数据库db// 退出exit

3.5. 操作表// 查看有哪些表show tablesshow collections// 显示表中的全部内容db.startup_log.find()db.startup_log.find().pretty()

3.6. 操作用户和权限use admindb.createUser(&#123; user:&#x27;mongouser&#x27;,pwd:&#x27;thepasswordA1&#x27;,roles:[&#123; role:&#x27;readWrite&#x27;, db: &#x27;testdb&#x27;&#125;,&quot;readWriteAnyDatabase&quot;]&#125;);db.auth(&#x27;mongouser&#x27;, &#x27;thepasswordA1&#x27;)use testdb

3.7. 导出导入数据3.7.1. 整库导出导入# 导出数据库mongodump --host 10.66.187.127:27017 -u mongouser -p thepasswordA1 --authenticationDatabase=admin --db=testdb -o /data/dump_testdb# 导出数据库，根据提示输入密码mongodump --host 10.66.187.127:27017 -u mongouser --authenticationDatabase=admin --db=testdb -o /data/dump_testdb# 导入数据库mongorestore --host 10.66.187.127:27017 -u mongouser -p thepasswordA1 --authenticationDatabase=admin --dir=/data/dump_testdb

3.7.2. 单个集合导出导入# 导出数据集合mongoexport --host 10.66.187.127:27017 -u mongouser -p thepasswordA1 --authenticationDatabase=admin --db=testdb --collection=testcollection -o /data/export_testdb_testcollection.json# 导入数据集合mongoimport --host 10.66.187.127:27017 -u mongouser -p thepasswordA1 --authenticationDatabase=admin --db=testdb --collection=testcollection2 --file=/data/export_testdb_testcollection.json

4. 重置密码4.1. bin mongo重置密码1、修改配置文件，取消用户验证
vim /etc/mongod.conf

注释掉：
#security:#  authorization: enabled

2、重启mongo
systemctl restart mongod

3、重置密码
mongo&gt; show dbs&gt; use admin&gt; db.system.users.find()&gt; db.changeUserPassword(&quot;admin&quot;,&quot;xxx&quot;)

4、开启用户验证，重启mongo
4.2. docker mongo重置密码1、启动重启密码容器
docker run -it --name mongo-repair -d --volumes-from mongo mongo:4.2.11 --noauth

2、进入容器
docker exec -it mongo-repair /bin/bash

3、重置密码
mongo&gt; show dbs&gt; use admin&gt; db.system.users.find()&gt; db.changeUserPassword(&quot;admin&quot;,&quot;xxx&quot;)

4、启动原容器
docker stop mongo-repairdocker rm mongo-repairdocker start mongo-repair



]]></content>
      <categories>
        <category>engineering</category>
        <category>docker</category>
        <category>storage</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>docker</tag>
        <tag>存储</tag>
        <tag>数据库</tag>
        <tag>mongodb</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：在CentOS6上搭建Node.js环境</title>
    <url>/dev-install-nodejs-on-centos/</url>
    <content><![CDATA[1. 前言毕设进入到了最后阶段，基本功能都完成了，接下来就是一些功能的完善和bug的修改。以及，好长好长的论文要写。。。压力有点大哇！
为了方便在答辩的时候装逼一把，郝同学决定把毕设上线到阿里云服务器，让评审老师直接试用。
参考文档：

Centos 安装 NodeJS
Node Downloads
如何在CentOS 7安装Node.js
Linux平台安装MongoDB
CentOS6.5源码安装nodejs4.4
在CentOS 7上安装Node.js的4种方法
nvm项目
MongoDB在CentOS6下的安装以及服务启动
Linux 守护进程的启动方法



2. 安装Node.js1、安装nvm
wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.31.1/install.sh | bashsource ~/.bash_profile

2、安装node5.6.0
nvm list-remotenvm install v5.6.0

3、设置默认使用node5.6.0
nvm listnvm use v5.6.0nvm alias default v5.6.0

3. 安装MongoDB1、下载安装MongoDB
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.0.6.tgztar -zxvf mongodb-linux-x86_64-3.0.6.tgzmv  mongodb-linux-x86_64-3.0.6/ /usr/local/mongodb

2、测试启动MongoDB
cd /usr/local/mongodb/binmongod

3、再启动一个shell，测试连接MongoDB
cd /usr/local/mongodb/binmongo

如果连接成功，那么说明安装成功。
4、设置开机启动MongoDB
vim /etc/rc.d/rc.local

插入一行：
/usr/local/mongodb/bin/mongod

5、添加 mongo 命令到路径中
vim ~/.bash_profile

添加：
export PATH=/usr/local/mongodb/bin:$PATH

使立即生效：
source ~/.bash_profile


4. 准备项目1、安装git
yum install git

2、下载项目
git clone https://github.com/voidking/nodeforum.git

3、安装依赖
npm installnpm install bower -gbower install

4、运行项目
node app.js

5、测试访问浏览器访问 http://139.129.28.10:3000看到了熟悉的界面，上线成功！
5. 进阶配置5.1. 自定义配置MongoDB1、创建文件和目录
mkdir -p /data/dbmkdir -p /data/logtouch /data/log/mongodb.log

2、创建 mongod.conf 配置文件
vim /usr/local/mongodb/mongod.conf

mongod.conf 内容为：
dbpath = /data/db #数据文件存放目录logpath = /data/log/mongodb.log #日志文件存放目录port = 27017  #端口fork = true  #以守护程序的方式启用，即在后台运行

3、添加服务管理脚本，参考文档：《好好学Linux：Linux服务配置开机自启动》
vim /etc/rc.d/init.d/mongod

mongod 内容为：
#!/bin/sh## mongodb init file for starting up the MongoDB server## chkconfig: - 20 80# description: Starts and stops the MongDB daemon that handles all \# database requests.# Source function library.. /etc/rc.d/init.d/functionsexec=&quot;/usr/local/mongodb/bin/mongod&quot;prog=&quot;mongod&quot;logfile=&quot;/data/log/mongodb.log&quot;options=&quot; -f /usr/local/mongodb/mongod.conf&quot;[ -e /etc/sysconfig/$prog ] &amp;&amp; . /etc/sysconfig/$proglockfile=&quot;/var/lock/subsys/mongod&quot;start() &#123;    [ -x $exec ] || exit 5    echo -n $&quot;Starting $prog: &quot;    daemon --user root &quot;$exec --quiet $options run &gt;&gt; $logfile 2&gt;&amp;1 &amp;&quot;    retval=$?    echo    [ $retval -eq 0 ] &amp;&amp; touch $lockfile    return $retval&#125;stop() &#123;    echo -n $&quot;Stopping $prog: &quot;    killproc $prog    retval=$?    echo    [ $retval -eq 0 ] &amp;&amp; rm -f $lockfile    return $retval&#125;restart() &#123;    stop    start&#125;reload() &#123;    restart&#125;force_reload() &#123;    restart&#125;rh_status() &#123;    # run checks to determine if the service is running or use generic status    status $prog&#125;rh_status_q() &#123;    rh_status &gt;/dev/null 2&gt;&amp;1&#125;case &quot;$1&quot; in    start)    rh_status_q &amp;&amp; exit 0    $1    ;;    stop)    rh_status_q || exit 0    $1    ;;    restart)    $1    ;;    reload)    rh_status_q || exit 7    $1    ;;    force-reload)    force_reload    ;;    status)    rh_status    ;;    condrestart|try-restart)    rh_status_q || exit 0    restart    ;;    *)    echo $&quot;Usage: $0 &#123;start|stop|status|restart|condrestart|try-restart|reload|force-reload&#125;&quot;    exit 2esacexit $?

4、启动MongoDB
chmod a+x /etc/rc.d/init.d/mongodservice mongod start

5.2. Node项目后台运行参考文档：《好好学Linux：Linux后台运行脚本或程序》
1、安装nohup
yum provides */nohupyum install coreutils
2、启动服务，后台运行
nohup node app.js &amp;# nohup node app.js &gt; myout.txt 2&gt;&amp;1 &amp;

项目的标准输出和错误输出，默认会输出到nohup.out文件中
3、关闭服务
bgfg#Ctrl+C








]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>database</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>centos</tag>
        <tag>node</tag>
        <tag>毕设</tag>
        <tag>nvm</tag>
        <tag>mongodb</tag>
        <tag>好好学Node.js</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：迁移博客从gitcafe到coding</title>
    <url>/dev-hexo-gitcafe-to-coding/</url>
    <content><![CDATA[1. 前言
GitCafe 已加入 CODING 成为 CODING 的一员，共同打造最适合中国开发者使用的 Git 服务平台！GitCafe 将于 2016年5月31日 停止所有服务，届时您在 GitCafe 的账户资料及所有项目都将被永久删除，请尽快将您的资料和项目迁移至 Coding。

啊嘞，郝同学的博客就在gitcafe上，免不了又要折腾一下了，下面我们就研究一下hexo托管到coding的方法。
2. 项目迁移首先，注册一个coding账户；然后，按照提示，关联gitcafe账户，选择项目进行迁移。


3. Coding Pages 服务Coding Pages 服务，是一个支持 jekyll 静态站的服务，也就是我们搭建静态博客需要的服务。1、进入和用户名相同的项目下（郝同学用户名为voidking，那么就进入voidking项目），点击Pages。
2、开启服务，并且绑定需要的域名。
3、访问http://voidking.coding.me/voidking ，404错误，正常，因为我们还没有coding-pages分支。
4、点击分支，新建分支，输入名称为coding-pages，输入起点。
5、分支创建成功，访问http://voidking.coding.me/voidking ，依然404。
6、点击Pages，重新部署。等待十多秒，就可以正常访问了。
4. 域名解析在上一步中，我们已经在coding上绑定了域名。但是，要想通过域名访问，我们还需要在自己的域名服务器上完成解析。以万网为例，解析如下：然后，访问http://voidking.com ，http://www.voidking.com ，http://blog.voidking.com ，全部正常。
5. 发布博客可以正常访问了，接下来的问题是，它能不能和之前一样，使用hexo d就重新部署呢？试试看。
5.1. 设置_config.yml原配置如下：
deploy:  type: git  repository: git@gitcafe.com:voidking/voidking.git  branch: gitcafe-pages
修改如下：
deploy:  type: git  repository: https://git.coding.net/voidking/voidking.git  branch: coding-pages

5.2. 添加SSH key进入项目，设置，部署公钥，新建部署公钥。复制C:\Users\Administrator\.ssh\id_rsa.pub中的内容，粘贴进去即可。关于密钥的生成方法，参见《Hexo环境搭建》。
5.3. 发布测试hexo g，hexo d，根据提示输入用户名和密码，结果如下：
$ hexo dINFO  Deploying: gitINFO  Clearing .deploy folder...INFO  Copying files from public folder...[master a7d185c] Site updated: 2016-05-13 11:56:08 568 files changed, 8448 insertions(+), 4942 deletions(-) create mode 100644 dev-npm-install/index.html create mode 100644 dev-gitcafe-to-coding/index.html create mode 100644 archives/2016/05/index.html create mode 100644 &quot;categories/\350\256\276\350\256\241\345\274\200\345\217\221/page/2/index.html&quot; rewrite page/40/index.html (74%) create mode 100644 page/59/index.html create mode 100644 tags/bower/index.html create mode 100644 tags/coding/index.html create mode 100644 tags/node/index.html create mode 100644 tags/npm/index.html create mode 100644 &quot;tags/\345\215\232\345\256\242/index.html&quot;Username for &#x27;https://git.coding.net&#x27;: voidkingPassword for &#x27;https://voidking@git.coding.net&#x27;:Branch master set up to track remote branch coding-pages from https://git.coding.net/voidking/voidking.git.To https://git.coding.net/voidking/voidking.git   f14ee2d..a7d185c  master -&gt; coding-pagesINFO  Deploy done: git
访问http://www.voidking.com ，刷新下，再刷新下。。。nice，内容已经更新。可见，hexo d命令同样适用于coding。
6. 后记如果过了5月31号，还没有完成迁移，怎么办？参见参考文档的《Coding Pages 介绍》，正常创建项目就可以了。
7. 参考文档Coding Pages 介绍https://coding.net/help/doc/pages/index.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
        <tag>博客</tag>
      </tags>
  </entry>
  <entry>
    <title>node依赖包的管理</title>
    <url>/dev-npm-install/</url>
    <content><![CDATA[1. 前言在使用node进行开发时，我们会用到很多包（模块、中间件），而这些包，并不是需要我们维护的，所以，只要记住包名和版本号就够了。而这些包名和版本号等信息，就放在package.json文件里，该文件就类似于Maven中的pom.xml。
package.json文件，可以手动编写，也可以利用npm init命令生成。


2. npm常用命令1、npm install module-name -g，全局安装。2、npm install module-name --save，自动把模块和版本号添加到dependencies部分。3、spm install module-name --save-dev 自动把模块和版本号添加到devDependencies部分。那么，2和3有什么区别呢？根据官方的解释，就是说在dependencies部分用到的模块，是一些必须的模块；而devDependencies用到的模块，是我们在生产过程中做测试用到的模块。如果你把自己的代码提交到了git服务器，别人下载了你的代码，然后npm install，下载所有依赖的模块，这时，dependencies里的模块会下载，devDependencies里的模块不会下载。
3. 忘记使用–save补救npm init，根据提示可以创建一个package.json文件，这个过程会自动把已经安装的模块写入package.json的dependencies部分。
假设，在之后，我们又安装了很多模块，但是，在安装时，使用的命令是npm install，而不是npm install --save。这时，有没有什么办法，可以一次性把我们安装的所有模块名和版本号写入package.json呢？
郝同学摸索出来一个可行的办法：1、把原先的package.json（文件1）拷贝出来。2、npm init，不停回车至新的package.json（文件2）创建完成。3、把文件2的dependencies部分拷贝覆盖到文件1的dependencies部分。4、使用文件1覆盖文件2。5、npm ls，如果有报错，说明还有个别依赖模块没有写进package.json。6、根据5的提示，手动修改package.json；或者，npm install module-name --save，把没有写进package.json的模块重新安装一次。
4. git忽略上传在主目录下新建.gitignore文件，内容格式如下：1、最常用
/node_modules/*/bower_components/*

2、在已忽略文件夹中不忽略指定文件夹
/node_modules/*!/node_modules/layer/

3、在已忽略文件夹中不忽略指定文件
/node_modules/*!/node_modules/layer/layer.js
【注意项】注意写法 要忽略的文件夹一定要结尾 /* ，否则不忽略规则将无法生效
4、其他规则写法 (附)

以斜杠“/”开头表示目录；

以星号“*”通配多个字符；

以问号“?”通配单个字符

以方括号“[]”包含单个字符的匹配列表；

以叹号“!”表示不忽略(跟踪)匹配到的文件或目录；


5. 后记bower的用法和npm非常相似，两者可以对比着学习和记忆。
6. 参考文档npm Documentationhttps://docs.npmjs.com/install
package.jsonhttps://docs.npmjs.com/files/package.json
git添加 .ignore 忽略http://www.thinksaas.cn/topics/0/487/487787.html  
.gitignore 规则写法http://my.oschina.net/longyuan/blog/521098
用package.json来管理NPM里面的依赖包
淘宝npm镜像http://npm.taobao.org/
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>git</tag>
      </tags>
  </entry>
  <entry>
    <title>Apache与Tomcat区别和联系</title>
    <url>/dev-apache-and-tomcat/</url>
    <content><![CDATA[1. 概述Apache和Tomcat都是经常被用在网页服务器上的重要工具，它们虽然都是服务器，但有着不同的功能和特性，适应于不同的场景。Apache是一款自由开源的Web服务器软件，目前在全球使用范围最广泛。而Tomcat则是一个应用服务器，它是一种能够运行Servlet和JSP的中间件。


2. Apache和Tomcat的区别2.1. 运行环境Apache是一个HTTP服务器，用于静态的网页和图片，如HTML、CSS、JavaScript等；而Tomcat是一个应用服务器，它能够动态解释和运行Java代码，如Servlet和JSP。
2.2. 处理能力Apache更擅长请求分发，可以处理静态文件请求，而Tomcat处理动态页面较好。此外，Apache通过插件还可以支持PHP，而Tomcat则不具备这个能力。
2.3. 性能Apache的性能优于Tomcat，因为Apache只需要解析静态页面，而Tomcat需要解析和运行Java代码，这在一定程度上影响了Tomcat的性能。
3. Apache和Tomcat的联系3.1. 一个比喻Apache是一辆车，上面可以装一些东西（比如html），但是不能装水。要装水必须要有容器（桶），这个桶就是Tomcat。当然，这个桶可以不放在车上。
3.2. 互补性Apache和Tomcat可以互相补充，结合使用。在实际操作中，一般先通过Apache来处理用户的请求，对于静态页面的请求，Apache直接处理；对于需要处理Java代码的动态页面请求，Apache通过模块将请求转发给Tomcat处理。
3.3. 数据交互在Apache和Tomcat都运行的环境中，它们之间可以通过AJP（Apache JServ Protocol）协议进行通信和数据交互。这样，Apache可以将需要Tomcat处理的请求转发给Tomcat，然后将Tomcat处理后的结果返回给客户端。
3.4. 可以不要Apache吗？可以不要Apache，只使用Tomcat吗？可以的。但是，对于一个包含静态页面，也包含动态页面的网站来说，只使用Tomcat的话，程序性能会下降，因为Apache处理静态页面的性能更好。
4. JDK的作用Apache是一个web服务器环境程序，启用它可以作为web服务器使用。不过它只支持静态网页，不支持动态网页，如asp、jsp、php、cgi。
如果要在Apache环境下运行jsp就需要一个解释器来执行jsp网页，而这个jsp解释器就是Tomcat。
那为什么还要JDK呢？因为jsp需要连接数据库的话就要jdk来提供连接数据库的驱动，所以要运行jsp的web服务器平台就需要Apache+Tomcat+JDK。
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>apache</tag>
        <tag>java</tag>
        <tag>tomcat</tag>
      </tags>
  </entry>
  <entry>
    <title>artTemplate模板引擎</title>
    <url>/dev-arttemplate/</url>
    <content><![CDATA[1. 前言在开发过程中，常常要实现这样一种效果：获取数据，并且插入到当前页面。最基本的做法是把获取到的数据拼接到一个字符串中，然后使用html()或者append()函数插入到页面中。这种做法，拼接字符串时很麻烦。本文中，郝同学要介绍一下ArtTemplate，一个超快的前端模板引擎。


2. 快速上手2.1. 编写模板&lt;script id=&quot;test&quot; type=&quot;text/html&quot;&gt;&lt;h1&gt;&#123;&#123;title&#125;&#125;&lt;/h1&gt;&lt;ul&gt;    &#123;&#123;each list as value i&#125;&#125;        &lt;li&gt;索引 &#123;&#123;i + 1&#125;&#125; ：&#123;&#123;value&#125;&#125;&lt;/li&gt;    &#123;&#123;/each&#125;&#125;&lt;/ul&gt;&lt;/script&gt;

2.2. 渲染模板var data = &#123;    title: &#x27;标签&#x27;,    list: [&#x27;文艺&#x27;, &#x27;博客&#x27;, &#x27;摄影&#x27;, &#x27;电影&#x27;, &#x27;民谣&#x27;, &#x27;旅行&#x27;, &#x27;吉他&#x27;]&#125;;var html = template(&#x27;test&#x27;, data);document.getElementById(&#x27;content&#x27;).innerHTML = html;

3. 模板语法注意：简洁语法和原生语法引入的js文件不同。
3.1. 简洁语法&#123;&#123;if admin&#125;&#125;    &#123;&#123;include &#x27;admin_content&#x27;&#125;&#125;    &#123;&#123;each list&#125;&#125;        &lt;div&gt;&#123;&#123;$index&#125;&#125;. &#123;&#123;$value.user&#125;&#125;&lt;/div&gt;    &#123;&#123;/each&#125;&#125;&#123;&#123;/if&#125;&#125;

3.2. 原生语法&lt;%if (admin)&#123;%&gt;    &lt;%include(&#x27;admin_content&#x27;)%&gt;    &lt;%for (var i=0;i&lt;list.length;i++) &#123;%&gt;        &lt;div&gt;&lt;%=i%&gt;. &lt;%=list[i].user%&gt;&lt;/div&gt;    &lt;%&#125;%&gt;&lt;%&#125;%&gt;

4. 参考文档Arttemplate by auihttp://aui.github.io/artTemplate/
前端模版artTemplate的介绍及使用http://blog.csdn.net/playboyanta123/article/details/45536501
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>javascript</tag>
      </tags>
  </entry>
  <entry>
    <title>JavaScript模板引擎</title>
    <url>/dev-javascript-template-engine/</url>
    <content><![CDATA[1. 前言后端渲染和前端渲染，分类依据在于浏览器到底做了什么事情。
后端渲染HTML的情况下，浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串，这里的计算就是服务器经过解析存放在服务器端的模板文件来完成的，在这种情况下，浏览器只进行了HTML的解析，以及通过操作系统提供的操纵显示器显示内容的系统调用在显示器上把HTML所代表的图像显示给用户。
前端渲染就是指浏览器会从后端得到一些信息，这些信息或许是适用于题主所说的angularjs的模板文件，亦或是JSON等各种数据交换格式所包装的数据，甚至是直接的合法的HTML字符串。这些形式都不重要，重要的是，将这些信息组织排列形成最终可读的HTML字符串是由浏览器来完成的，在形成了HTML字符串之后，再进行显示。
根据前后端渲染的不同，模板引擎也分为两种，后端模板引擎和前端模板引擎。而前端模板引擎，就是本文要说的JavaScript模板引擎。


2. 构建自己的JavaScript模板小引擎2.1. html部分&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;   &lt;meta charset=utf-8&gt;   &lt;title&gt;Simple Templating&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;div class=&quot;result&quot;&gt;&lt;/div&gt;&lt;script type=&quot;template&quot; id=&quot;template&quot;&gt;    &lt;h2&gt;      &lt;a href=&quot;&#123;&#123;href&#125;&#125;&quot;&gt;        &#123;&#123;title&#125;&#125;      &lt;/a&gt;    &lt;/h2&gt;    &lt;img src=&quot;&#123;&#123;imgSrc&#125;&#125;&quot; alt=&quot;&#123;&#123;title&#125;&#125;&quot;&gt;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;

2.2. 仿造ajax获取的数据var data = [    &#123;      title: &quot;Knockout应用开发指南&quot;,      href: &quot;http://www.cnblogs.com/TomXu/archive/2257154.html&quot;,      imgSrc: &quot;http://images.cnblogs.com/cnblogs_com/TomXu/339203/o_knockout2.jpg&quot;    &#125;,    &#123;      title: &quot;微软ASP.NET站点部署指南&quot;,      href: &quot;http://www.cnblogs.com/TomXu/archive/2263050.html&quot;,      imgSrc: &quot;http://images.cnblogs.com/cnblogs_com/TomXu/339203/o_vs.jpg&quot;    &#125;,    &#123;      title: &quot;HTML5学习笔记简明版&quot;,      href: &quot;http://www.cnblogs.com/TomXu/archive/2277499.html&quot;,      imgSrc: &quot;http://images.cnblogs.com/cnblogs_com/TomXu/339203/o_LearningHtml5.png&quot;    &#125;];

2.3. 变量替换方法一template = document.querySelector(&#x27;#template&#x27;).innerHTML,result = document.querySelector(&#x27;.result&#x27;),i = 0, len = data.length,fragment = &#x27;&#x27;; for ( ; i &lt; len; i++ ) &#123;    fragment += template      .replace( /\&#123;\&#123;title\&#125;\&#125;/, data[i].title )      .replace( /\&#123;\&#123;href\&#125;\&#125;/, data[i].href )      .replace( /\&#123;\&#123;imgSrc\&#125;\&#125;/, data[i].imgSrc );&#125; result.innerHTML = fragment;

2.4. 变量替换方法二template = document.querySelector(&#x27;#template&#x27;).innerHTML,result = document.querySelector(&#x27;.result&#x27;),attachTemplateToData; // 将模板和数据作为参数，通过数据里所有的项将值替换到模板的标签上（注意不是遍历模板标签，因为标签可能不在数据里存在）。attachTemplateToData = function(template, data) &#123;    var i = 0,        len = data.length,        fragment = &#x27;&#x27;;     // 遍历数据集合里的每一个项，做相应的替换    function replace(obj) &#123;        var t, key, reg; 　　　　　　　　　　//遍历该数据项下所有的属性，将该属性作为key值来查找标签，然后替换        for (key in obj) &#123;            reg = new RegExp(&#x27;&#123;&#123;&#x27; + key + &#x27;&#125;&#125;&#x27;, &#x27;ig&#x27;);            t = (t || template).replace(reg, obj[key]);        &#125;         return t;    &#125;     for (; i &lt; len; i++) &#123;        fragment += replace(data[i]);    &#125;     return fragment;&#125;; result.innerHTML = attachTemplateToData(template, data);

3. 参考文档最简单的JavaScript模板引擎http://www.cnblogs.com/dolphinX/p/3489269.html
大叔手记（7）：构建自己的JavaScript模板小引擎http://www.cnblogs.com/TomXu/archive/2284752.html
JavaScript Micro-Templatinghttp://ejohn.org/blog/javascript-micro-templating/
JavaScript模板引擎原理，几行代码的事儿http://www.cnblogs.com/hustskyking/p/principle-of-javascript-template.html
全球最快的JS模板引擎https://cnodejs.org/topic/5490e8ca4823a0234c9e1767
Arttemplate by auihttp://aui.github.io/artTemplate/
laytpl官网http://laytpl.layui.com/
后端渲染html、前端模板渲染html，jquery的html，各有什么区别？http://www.zhihu.com/question/28725977/answer/42077482
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>javascript</tag>
      </tags>
  </entry>
  <entry>
    <title>CSS实现文本溢出显示省略号</title>
    <url>/dev-css-text-overflow-omit/</url>
    <content><![CDATA[1. 单行文本溢出显示省略号width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;

如果字符串长度超过300px，那么超出部分就变成...。
2. 多行文本溢出显示省略号display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 3;overflow: hidden;
因为使用了WebKit的CSS扩展属性，该方法适用于WebKit浏览器及移动端；
-webkit-line-clamp用来限制在一个块元素显示的文本的行数。为了实现该效果，它需要组合其他的WebKit属性。常见结合属性：display: -webkit-box; 必须结合的属性 ，将对象作为弹性伸缩盒子模型显示 。-webkit-box-orient 必须结合的属性 ，设置或检索伸缩盒对象的子元素的排列方式 。
3. 怎样在js中判断文本是否溢出问题描述：一段文字限定行数，使用css把多余文字显示为省略号，请问怎么通过js判断这段文字是否有文字显示为省略号？
3.1. 思路一王晨帅哥提供了一个思路：取消css的-webkit-line-clamp属性，看看元素高度是否发生了变化，变化了就是有文字显示为省略号。
好想法，最后郝同学改进后如下：这段文字在页面上放两份，一份限定行数，正常显示；另一份不限定行数，隐藏起来。然后，对比这两段文字的高度是否相同。具体实现：
// scss部分.info&#123;    font-size: 1.2rem;    margin-top: .6rem;    display: -webkit-box;    -webkit-box-orient: vertical;    -webkit-line-clamp: 4;    overflow: hidden;    p&#123;        &amp;:not(:first-child)&#123;            margin-top: .4rem;        &#125;;          line-height: 1.8rem;    &#125;&#125;.info-hidden&#123;    position: fixed;    z-index: -10;    visibility: hidden;    font-size: 1.2rem;    margin-top: .6rem;    p&#123;        &amp;:not(:first-child)&#123;            margin-top: .4rem;        &#125;;          line-height: 1.8rem;    &#125;&#125;

// js部分var $info = $(&#x27;.info&#x27;);var $info_hidden = $(&#x27;.info-hidden&#x27;);if($info.height() === $info_hidden.height())&#123;    $(&#x27;.more&#x27;).hide();&#125;

3.2. 思路二后来张伟林帅哥提供了一个更好的思路：既然已经知道了限定的行数，那么判断一个高度就可以了。结合line-height，高度用scrollheight，判断scrollheight &gt; line-height*你决定的行数。
郝同学马上搜索了一个scrollheight，发现，原来scrollHeight可以返回元素的完整高度。那么，比较一下scrollHeight和height不就可以了么？具体实现：
// js部分var $info = $(&#x27;.info&#x27;);console.log($info.height());console.log($info[0].scrollHeight);if($info.height() &gt;= $info[0].scrollHeight)&#123;    $(&#x27;.more&#x27;).hide();&#125;

4. 后记利用js也可以实现文本溢出显示省略号，可以参考书签中的dotdotdot，很形象的名字。。。
5. 书签CSS实现单行、多行文本溢出显示省略号（…）http://www.daqianduan.com/6179.html
jQuery.dotdotdothttp://www.bootcdn.cn/jQuery.dotdotdot/
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>css</tag>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>freemarker入门</title>
    <url>/dev-freemarker/</url>
    <content><![CDATA[1. 前言昨天，郝同学介绍了一款前端模板引擎artTemplate。今天，介绍一款后端模板引擎freemarker。

Apache FreeMarker is a template engine: a Java library to generate text output (HTML web pages, e-mails, configuration files, source code, etc.) based on templates and changing data. 



2. 基本语法2.1. 定义和输出&lt;#assign answer=42/&gt;$&#123;answer&#125;
结果为：42
2.2. 分支语句&lt;#assign age=23&gt;&lt;#if (age&gt;60)&gt;老年人&lt;#elseif (age&gt;40)&gt;中年人&lt;#elseif (age&gt;20)&gt;青年人&lt;#else&gt; 少年人&lt;/#if&gt;

2.3. 循环语句&lt;#list books as book&gt;    &lt;tr&gt;        &lt;td&gt;$&#123;book.name&#125;&lt;/td&gt;        &lt;td&gt;作者:$&#123;book.author&#125;&lt;/td&gt;    &lt;/tr&gt;&lt;/#list&gt;

2.4. 运算和函数&lt;#assign x=5&gt;$&#123; (x/2)?int &#125;$&#123; 1.1?int &#125;$&#123; 1.999?int &#125;$&#123; -1.1?int &#125;$&#123; -1.999?int &#125;
结果为:2 1 1 -1 -1
3. 源码分享https://github.com/voidking/freemarker.git
4. 参考文档FreeMarker Java Template Enginehttp://freemarker.org/
Download / Maven - Apache FreeMarkerhttp://freemarker.org/freemarkerdownload.html
Index (FreeMarker 2.3.24-incubating API)http://freemarker.org/docs/api/index-all.html
模板引擎freemarker的简单使用教程http://blog.csdn.net/stormwy/article/details/26172353
FreeMarker使用详解http://www.open-open.com/lib/view/open1394860597743.html
Freemarker由浅入深01-环境搭建、测试http://blog.csdn.net/it_wangxiangpan/article/details/17526869
ftl的使用http://blog.163.com/liuweiyoung@126/blog/static/1731310452012112755018198/
【FreeMarker】【模板文件FTL】模板自定义指令 macrohttp://blog.csdn.net/robinjwong/article/details/40427977
freemarker的使用心得http://blog.csdn.net/walkcode/article/details/40707997
Freemarker中Configuration的setClassForTemplateLoading方法参数问题http://www.cnblogs.com/fangjian0423/p/freemarker-templateloading-question.html
Servlet 3.0笔记之使用Freemarker替代JSP，更快更轻更高效http://www.blogjava.net/yongboy/archive/346224.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>freemarker</tag>
      </tags>
  </entry>
  <entry>
    <title>成为一个有思想的人</title>
    <url>/essay-to-be-a-man-with-thought/</url>
    <content><![CDATA[1. 前言
不把信息当作知识，不把收藏当作学习，不把阅读当作思考，不把储存当作掌握。不提炼总结、不应用学习、不深入思考、不实践反馈，就不会变成自己的能力。

什么是有思想的人？对一件事，有自己的观点，有自己的见解，有理有据，令人信服。
近朱者赤近墨者黑，和这样的人在一起，能够感受到TA的气场，能够感受到TA的能量，能够感受到收获和成长。
参考文档：

四种思维方式
牛人和普通人的区别在于思维方式！
如何成为有想法的人？如何培养独立思考的能力？
新闻联播该如何看？



2. 如何做那么，怎样成为一个有思想的人？
1、提问为什么？为什么不？有什么用？怎样去做？作者想表达什么样的观点？这个新闻想让我们相信什么？这个微博传达了朋友什么样的情绪？
2、批判知道自己的观点，理解文字（信息）中的观点。支持或反对，有理有据。相信或不信，找出原因。
3、想象想象和别人聊天，想象演讲，想象辩论。
3. 后记会思考的人不会无聊！保持好奇心，多问多想多读书。也许终其一生也无法成为有思想的人，但至少，要比昨天的自己更好一些。
]]></content>
      <categories>
        <category>essay</category>
      </categories>
      <tags>
        <tag>思考</tag>
      </tags>
  </entry>
  <entry>
    <title>前端开发常用插件</title>
    <url>/dev-front-end-plugin/</url>
    <content><![CDATA[1. 前言最近在阅读公司官网的前端代码，使用了很多插件。挨个百度，简单记录下，看看他们都是干什么的。
2. jQuery UIjQuery UI API中文文档http://www.css88.com/jquery-ui-api/


3. SwiperSwiper是纯javascript打造的滑动特效插件，面向手机、平板电脑等移动终端。
Swiper能实现触屏焦点图、触屏Tab切换、触屏多图切换等常用效果。Swiper开源、免费、稳定、使用简单、功能强大，是架构移动终端网站的重要选择！
Swiper中文网http://www.swiper.com.cn/
官网http://www.idangero.us/swiper/
4. BlockUIjQuery BlockUI Pluginhttp://malsup.com/jquery/block/
blockui githubhttps://github.com/malsup/blockui/
jQuery遮罩插件jQuery.blockUI.js简介http://bookshadow.com/weblog/jquery-blockui-js-introduction/
5. email-autocompleteemail-autocompletehttps://github.com/10w042/email-autocomplete
6. jQuery-AutocompletejQuery-Autocompletehttps://github.com/devbridge/jQuery-Autocomplete
7. chosenchosenhttps://github.com/harvesthq/chosen
Chosen (v1.4.2)http://harvesthq.github.io/chosen/
8. ajaxfileuploadjQuery插件之ajaxFileUploadhttp://www.cnblogs.com/kissdodog/archive/2819025.html
9. html2canvashtml2canvashttp://www.bootcdn.cn/html2canvas/
10. 百度地图百度地图APIhttp://lbsyun.baidu.com/
11. momentJavaScript 日期处理类库
Moment.js中文网http://momentjs.cn/
12. paginationjQuery Pagination pluginhttp://esimakin.github.io/twbs-pagination/
13. placeholderjquery-placeholderhttps://github.com/mathiasbynens/jquery-placeholder
14. romeromehttp://spmjs.io/package/rome
15. validationvalidation官方文档http://jqueryvalidation.org/documentation
16. zeptoZepto is a minimalist JavaScript library for modern browsers with a largely jQuery-compatible API.
zepto中文网http://www.zeptojs.cn/
17. tooltipsterA powerful, flexible jQuery plugin enabling you to easily create semantic, modern tooltips enhanced with the power of CSS.
官网http://iamceege.github.io/tooltipster/
18. lazyloadLazy Load Plugin for jQueryhttp://www.appelsiini.net/projects/lazyload
lazyload项目地址：https://github.com/tuupola/jquery_lazyload
19. MasonryMasonry官网http://masonry.desandro.com/
mosonry项目地址：https://github.com/desandro/masonry
20. imageLoadedimagesLoaded官网http://imagesloaded.desandro.com/
imagesloaded项目地址：https://github.com/desandro/imagesloaded
21. touch.jsTouch.js官网http://touch.code.baidu.com/
22. iscrolliScroll官网http://cubiq.org/iscroll-5
iScroll文档http://iscrolljs.com/
iScroll项目地址https://github.com/cubiq/iscroll
【iScroll源码学习00】模拟iScrollhttp://www.cnblogs.com/yexiaochai/p/3489676.html
23. 后记优点：开发成本低、时间短；稳定性、兼容性良好。缺点：不懂原理，遇到问题不好解决。
提供给开发者的 20 款最棒的 jQuery Bootstrap 插件http://www.oschina.net/translate/20-best-jquery-bootstrap-plugins-for-developers
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>插件</tag>
      </tags>
  </entry>
  <entry>
    <title>SASS使用说明</title>
    <url>/dev-sass-guide/</url>
    <content><![CDATA[1. 前言
Sass，Syntactically Awesome StyleSheets。Sass 是对 CSS 的扩展，让 CSS 语言更强大、优雅。 它允许你使用变量、嵌套规则、 mixins、导入等众多功能， 并且完全兼容 CSS 语法。 Sass 有助于保持大型样式表结构良好， 同时也让你能够快速开始小型项目， 特别是在搭配 Compass 样式库一同使用时。



2. 安装1、下载安装ruby2、安装好ruby后，再ruby命令行中输入gem install sass
3. 语法3.1. 变量sass中可以定义变量，方便统一修改和维护。
$fontStack:    Helvetica, sans-serif;$primaryColor: #333;body &#123;  font-family: $fontStack;  color: $primaryColor;&#125;

3.2. 嵌套sass可以进行选择器的嵌套，表示层级关系，看起来很优雅整齐。
nav &#123;  ul &#123;    margin: 0;    padding: 0;    list-style: none;  &#125;  li &#123; display: inline-block; &#125;  a &#123;    display: block;    padding: 6px 12px;    text-decoration: none`;  &#125;&#125;`。

3.3. 导入sass中如导入其他sass文件，最后编译为一个css文件，优于纯css的@import。
// reset.scsshtml,body,ul,ol &#123;  margin: 0;  padding: 0;&#125;

@import &#x27;reset&#x27;;body &#123;  font-size: 100% Helvetica, sans-serif;  background-color: #efefef;&#125;

3.4. mixinsass中可用mixin定义一些代码片段，且可传参数，方便日后根据需求调用。从此处理css3的前缀兼容轻松便捷。
@mixin box-sizing ($sizing) &#123;    -webkit-box-sizing:$sizing;            -moz-box-sizing:$sizing;            box-sizing:$sizing;&#125;.box-border&#123;    border:1px solid #ccc;    @include box-sizing(border-box);&#125;

3.5. 扩展/继承sass可通过@extend来实现代码组合声明，使代码更加优越简洁。
.message &#123;  border: 1px solid #ccc;  padding: 10px;  color: #333;&#125;.success &#123;  @extend .message;  border-color: green;&#125;.error &#123;  @extend .message;  border-color: red;&#125;.warning &#123;  @extend .message;  border-color: yellow;&#125;

3.6. 运算sass可进行简单的加减乘除运算等
.container &#123; width: 100%; &#125;article[role=&quot;main&quot;] &#123;  float: left;  width: 600px / 960px * 100%;&#125;aside[role=&quot;complimentary&quot;] &#123;  float: right;  width: 300px / 960px * 100%;&#125;

3.7. 颜色sass中集成了大量的颜色函数，让变换颜色更加简单。
$linkColor: #08c;a &#123;    text-decoration:none;    color:$linkColor;    &amp;:hover&#123;      color:darken($linkColor,10%);    &#125;&#125;

3.8. &amp;表示当前元素伪类和伪元素在CSS中是常用的一种方式，比如最常见的是链接的伪类或者说伪元素:after和:before的使用。大家常看到的就是清除浮动的clearfix:
.clearfix:before,.clearfix:after &#123;    content:&quot;&quot;;    display:table;&#125;.clearfix:after &#123;    clear:both;    overflow:hidden;&#125;.clearfix &#123;    *zoom: 1;&#125;

那么在Sass中，使用&amp;会变得更简单，更方便：
$lte-ie: true !default;.clearfix &#123;    @if $lte-ie &#123;        *zoom: 1;    &#125;    &amp;:before,    &amp;:after &#123;        content: &quot;&quot;;        display: table;    &#125;    &amp;:after &#123;        clear: both;        overflow: hidden;    &#125;&#125;

&amp;不止用于和伪类的结合，还可以用于多类选择器、后代选择器、相邻兄弟选择器、媒体查询中的嵌套等。
4. 2016.07.30补4.1. Sass编码错误ruby环境sass编译中文出现Syntax error: Invalid GBK character。解决办法如下：打开ruby中sass的安装目录，在其中找到engine.rb。在engine.rb中所有的require XXX之后，添加
Encoding.default_external = Encoding.find(&#x27;utf-8&#x27;)

PS:郝同学的engine.rb的路径为D:\Program Files\Ruby23-x64\lib\ruby\gems\2.3.0\gems\sass-3.4.22\lib\sass\engine.rb
4.2. scss自动编译为css如果希望某一个scss文件或者相应的文件夹下面文件修改后，自动进行编译，那么可以使用侦听命令。1、侦听文件
sass --watch --style compressed style.scss:style.css

2、侦听文件夹
sass --watch --style compressed scss:css

3、封装为脚本为了避免每次运行都敲命令，我们把上述命令分装为脚本scss.bat。
sass --watch --style compressed scss:css

海哥哥封装了不一样的scss-wap.bat，其中的参数至今没有看懂，可以实现同样的效果。
@echo offsass -C -t compressed --watch scss:cssrem  sass -C -t compact --watch scss:csspause

该命令的含义为：
5. 书签sass安装http://www.w3cplus.com/sassguide/install.html
SASS用法指南http://www.ruanyifeng.com/blog/2012/06/sass.html
sass入门http://www.w3cplus.com/sassguide/
SASS 初学者入门http://www.oschina.net/translate/the-absolute-beginners-guide-to-sass
Sass参考手册http://sass.bootcss.com/docs/sass-reference/
SassMeister在线调试http://www.sassmeister.com/
Sass中连体符（&amp;）的运用
ruby环境sass编译中文出现Syntax error: Invalid GBK character错误解决方法http://www.tuicool.com/articles/f2YVRvhttp://www.cnblogs.com/zhidong123/p/3902270.html
SASS的安装和转换为CSS的方法http://www.cnblogs.com/52css/archive/sass-how-to-install-and-use.html
前端之Sass/Scss实战笔记http://www.tuicool.com/articles/iERVJbBhttp://segmentfault.com/a/1190000003742313
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>css</tag>
        <tag>前端</tag>
        <tag>sass</tag>
      </tags>
  </entry>
  <entry>
    <title>如何学习编程</title>
    <url>/dev-how-to-learn-programming/</url>
    <content><![CDATA[1. 方法在编程这条路上，已经走了四年，而且，还要一直走下去。
个人关于学习编程的方法，三句话以蔽之：1、学而不思则罔，思而不学则殆。2、无论是什么工具，当你开始去使用它，一切都会变得简单！3、学一点，会一点，胜在不止。
求知，思考，实践，总结。知易行难，贵在坚持！


2. 书签十年学会编程http://daiyuwen.freeshell.org/gb/misc/21-days-cn.html
专访周家安：我的十年编程自学之路http://www.csdn.net/article/2013-02-26/2814263
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>前端</tag>
      </tags>
  </entry>
  <entry>
    <title>仿淘宝图片放大器</title>
    <url>/dev-jquery-image-zoom/</url>
    <content><![CDATA[1. 原理两个盒子，盒子1放小图片，盒子2放对应的大图片。盒子1里的图片正常显示，盒子2里的图片隐藏。在盒子1的图片标签中，加入大图的数据链接。当鼠标在盒子1上移动时，通过鼠标在盒子1的位置计算出盒子2中应该显示的大图的部分。


以下代码中，利用到了jquery.jqzoom.js插件。
2. 代码&lt;div id=&quot;preview&quot; class=&quot;spec-preview&quot;&gt;     &lt;span class=&quot;jqzoom&quot;&gt;        &lt;img jqimg=&quot;images/b1.jpg&quot; src=&quot;images/s1.jpg&quot; /&gt;    &lt;/span&gt; &lt;/div&gt;

$(function()&#123;    $(&quot;.jqzoom&quot;).jqueryzoom(&#123;xzoom:380,yzoom:380&#125;);&#125;);


3. 参考文档jquery.jqzoom.js图片放大镜http://www.cnblogs.com/sydeveloper/p/3796330.html
ImageZoom 图片放大效果http://www.cnblogs.com/cloudgamer/archive/ImageZoom.html
jQuery-实现图片的放大镜显示效果
jquery插件 放大镜http://www.jq-school.com/Article.aspx?kid=41
jQzoom简介http://www.oschina.net/p/jqzoom?fromerr=tUayHgqO
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>jquery</tag>
        <tag>插件</tag>
      </tags>
  </entry>
  <entry>
    <title>瀑布流特效</title>
    <url>/dev-jquery-waterfall/</url>
    <content><![CDATA[1. 原理假设页面上有三列图片，当我们下拉时，最短（或最长）的一列图片展示完的时候，就要请求加载新的图片，获取到的新的图片放到最短一列图片的下面。
尝试过用原生js或jquery实现瀑布流，源码放在本文最后。下面记录一种使用imageloaded和masonry插件实现的瀑布流，更简便。


2. imageloaded和masonryejs代码：
&lt;div id=&quot;meitulist&quot; class=&quot;meitulist&quot;&gt;    &lt;% data.obj.pictureList.forEach(function(picture)&#123; %&gt;        &lt;a href=&quot;/inspiration/imagedetail/&lt;%= picture.albumId%&gt;&quot; class=&quot;item&quot;&gt;            &lt;img src=&quot;&lt;%= picture.url%&gt;&quot; /&gt;        &lt;/a&gt;    &lt;% &#125;)%&gt;&lt;/div&gt;

每次加载图片并插入后，执行resetImage()函数。
function resetImage()&#123;    var width = document.documentElement.clientWidth;    var column = width&gt;320? 3 : 2;    var itemWidth = Math.ceil((width-(column+1)*10)/column);    $(&#x27;.meitulist .item&#x27;).css(&#x27;width&#x27;,itemWidth);    $(&#x27;.meitulist .item img&#x27;).css(&#123;        &#x27;width&#x27;:itemWidth        // &#x27;height&#x27;: itemWidth    &#125;)    window.imagesLoaded(&#x27;#meitulist&#x27;, function() &#123;        var msnry = new Masonry(&#x27;#meitulist&#x27;,&#123;            &#x27;columnWidth&#x27;: itemWidth,            &#x27;itemSelector&#x27;: &#x27;.item&#x27;,            &#x27;isAnimated&#x27;:true,            // &#x27;percentPosition&#x27;:true,            &#x27;gutter&#x27;: 10        &#125;);    &#125;);&#125;

3. js和jquery源码https://github.com/voidking/front-end-demo/tree/master/%E7%80%91%E5%B8%83%E6%B5%81
4. 参考文档瀑布流特效http://www.cnblogs.com/Leo_wl/p/4306295.html
Masonry官网http://masonry.desandro.com/
mosonry项目地址：https://github.com/desandro/masonry
imagesLoaded官网http://imagesloaded.desandro.com/
imagesloaded项目地址：https://github.com/desandro/imagesloaded
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>jquery</tag>
        <tag>插件</tag>
      </tags>
  </entry>
  <entry>
    <title>jQuery书签</title>
    <url>/dev-jquery-bookmark/</url>
    <content><![CDATA[从零开始学习jQuery (一) 开天辟地入门篇http://kb.cnblogs.com/page/46450/
jQuery APIhttp://jquery.cuishifeng.cn/
jQuery API资源专属下载页http://www.cuishifeng.cn/down.html


jQuery基础课程http://www.imooc.com/learn/11
CodePlayerhttp://www.365mini.com/
html中offsetTop、clientTop、scrollTop、offsetTop各属性介绍http://blog.csdn.net/fswan/article/details/17238933
值得 Web 开发人员学习的20个 jQuery 实例教程http://www.cnblogs.com/lhb25/p/20-must-have-jquery-tutorials.html
jQuery Mobilehttp://jquerymobile.com/
HTML5+JS手机web开发之jQuery Mobile初涉
10个优秀的jQuery Mobile主题http://caibaojian.com/10-best-free-jquery-mobile-theme.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>jquery</tag>
      </tags>
  </entry>
  <entry>
    <title>JavaScript书签</title>
    <url>/dev-javascript-bookmark/</url>
    <content><![CDATA[回调函数（callback）是什么？http://www.zhihu.com/question/19801131
读取JAVASCRIPT第一公民-函数http://www.cnblogs.com/YOUCAN/archive/2754968.html
《JAVASCRIPT语言精髓与编程实践》，周爱民http://www.chnxp.com.cn/soft/2013-12/22670.html


无废话JavaScripthttp://blog.csdn.net/aimingoo/article/details/3022379
深入理解javascript原型和闭包http://www.cnblogs.com/wangfupeng1988/p/3977987.html
JS的事件监听机制http://www.cnblogs.com/aji88/archive/2600492.html
js事件监听器用法实例详解http://www.jb51.net/article/67051.htm
HTML 最佳实践http://www.imooc.com/article/3666
Node.JS编码规范指南教程：教你优雅地写JavaScript代码
前端JavaScript规范http://www.imooc.com/article/1402
为JavaScript程序员准备的10本免费书籍http://www.imooc.com/article/1767
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>javascript</tag>
      </tags>
  </entry>
  <entry>
    <title>函数式编程</title>
    <url>/dev-functional-programming/</url>
    <content><![CDATA[1. 概念以下内容转载自用心阁的回答，详见知乎。
1、什么是函数式编程？函数式编程是一种编程范式，我们常见的编程范式有命令式编程（Imperative programming），函数式编程，逻辑式编程，常见的面向对象编程是也是一种命令式编程。
命令式编程是面向计算机硬件的抽象，有变量（对应着存储单元），赋值语句（获取，存储指令），表达式（内存引用和算术运算）和控制语句（跳转指令），一句话，命令式程序就是一个冯诺依曼机的指令序列。
而函数式编程是面向数学的抽象，将计算描述为一种表达式求值，一句话，函数式程序就是一个表达式。


函数式编程中的函数这个术语不是指计算机中的函数（实际上是Subroutine），而是指数学中的函数，即自变量的映射。也就是说一个函数的值仅决定于函数参数的值，不依赖其他状态。比如sqrt(x)函数计算x的平方根，只要x不变，不论什么时候调用，调用几次，值都是不变的。
在函数式语言中，函数作为一等公民，可以在任何地方定义，在函数内或函数外，可以作为函数的参数和返回值，可以对函数进行组合。
纯函数式编程语言中的变量也不是命令式编程语言中的变量，即存储状态的单元，而是代数中的变量，即一个值的名称。变量的值是不可变的（immutable），也就是说不允许像命令式编程语言中那样多次给一个变量赋值。比如说在命令式编程语言我们写“x = x + 1”，这依赖可变状态的事实，拿给程序员看说是对的，但拿给数学家看，却被认为这个等式为假。
函数式语言的如条件语句，循环语句也不是命令式编程语言中的控制语句，而是函数的语法糖，比如在Scala语言中，if else不是语句而是三元运算符，是有返回值的。
严格意义上的函数式编程意味着不使用可变的变量，赋值，循环和其他命令式控制结构进行编程。
从理论上说，函数式语言也不是通过冯诺伊曼体系结构的机器上运行的，而是通过λ演算来运行的，就是通过变量替换的方式进行，变量替换为其值或表达式，函数也替换为其表达式，并根据运算符进行计算。λ演算是图灵完全（Turing completeness）的，但是大多数情况，函数式程序还是被编译成（冯诺依曼机的）机器语言的指令执行的。
2、为什么需要函数式编程？由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数，函数式的最主要的好处主要是不可变性带来的。没有可变的状态，函数就是引用透明（Referential transparency）的和没有副作用（No Side Effect）。
一个好处是，函数即不依赖外部的状态也不修改外部的状态，函数调用的结果不依赖调用的时间和位置，这样写的代码容易进行推理，不容易出错。这使得单元测试和调试都更容易。
不变性带来的另一个好处是：由于（多个线程之间）不共享状态，不会造成资源争用(Race condition)，也就不需要用锁来保护可变状态，也就不会出现死锁，这样可以更好地并发起来，尤其是在对称多处理器（SMP）架构下能够更好地利用多个处理器（核）提供的并行处理能力。
由于函数是引用透明的，以及函数式编程不像命令式编程那样关注执行步骤，这个系统提供了优化函数式程序的空间，包括惰性求值和并性处理。
还有一个好处是，由于函数式语言是面向数学的抽象，更接近人的语言，而不是机器语言，代码会比较简洁，也更容易被理解。
2. 书签什么是函数式编程思维？https://www.zhihu.com/question/28292740
有趣的 Scala 语言: 使用递归的方式去思考http://www.ibm.com/developerworks/cn/java/j-lo-funinscala1/
函数式编程初探http://www.ruanyifeng.com/blog/2012/04/functional_programming.html
对函数式语言的误解http://blog.163.com/bowen_tong/blog/static/20681717420133142740252/
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>函数式编程</tag>
      </tags>
  </entry>
  <entry>
    <title>HTML5和CSS3学习资料</title>
    <url>/dev-html5-and-css3-resources/</url>
    <content><![CDATA[跟KingDZ学HTML5之四 继续探究Canvas之路径http://www.cnblogs.com/hihell/archive/2150413.html
SVG 多边形http://www.runoob.com/svg/svg-polygon.html
理解SVG的图形填充规则http://blog.csdn.net/cuixiping/article/details/7848369
地理定位（navigator.geolocation）http://blog.csdn.net/smok56888/article/details/20628161
HTML5 地理位置定位（HTML5 Geolocation）原理及应用http://www.cnblogs.com/lhb25/archive/html5-geolocation-api-demo.html
HTML5 Web Worker的使用http://www.cnblogs.com/feng_013/archive/2175007.html
HTML 参考手册http://www.w3school.com.cn/tags/index.asp
HTML5 测验http://www.w3school.com.cn/quiz/quiz.asp?quiz=html5
CSS3 border-image详解、应用及jQuery插件
利用css3-animation来制作逐帧动画https://www.qianduan.net/css3-animation/
css3 box-sizing属性http://www.cnblogs.com/zhaoran/archive/3097482.html
Selectors Level 3https://www.w3.org/TR/css3-selectors/
HTML5 Trickshttp://www.html5tricks.com/
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>艾佳生活</tag>
      </tags>
  </entry>
  <entry>
    <title>百度搜索结果页</title>
    <url>/dev-baidu-search-result/</url>
    <content><![CDATA[1. 前言1月18日，入职艾佳，办好了各种手续。从海哥那里，接到了第一个任务——周一到周三（18到20），高仿一个百度搜索结果页。郝同学随手挖了一个坑：“上海”，接着便开始填坑。


2. 界面布局浏览器，是一个长方体的不透明的可变体积的盒子，盒子上面开了一个长方形的窗口。从这个窗口，我们可以看到盒子里的内容。盒子有很多层，IE、FireFox、Safari有2147483647层，Opera有2147483584层。一般情况下，10层，就足够我们使用了。 
界面布局，也就是在浏览器盒子里面摆放东西的方式。一般分为三种：标准文档流、浮动和定位。
2.1. 标准文档流标准文档流，就是在盒子的第一层摆放东西，按照从左到右，从上到下的方式，符合我们平时的思维习惯。
2.2. 浮动浮动会脱离标准文档流，不再按照从左到右，从上到下的方式。但是，浮动的元素，仍然处于第一层。
对于浮动，最认同张鑫旭的看法。浮动出现的意义其实只是用来让文字环绕图片而已，仅此而已。浮动的本质为“包裹与破坏”！
1、包裹撇开浮动的“破坏性”，浮动就是个带有方位的display:inline-block属性。
2、破坏文字之所以会环绕含有float属性的图片，是因为浮动破坏了正常的line boxes。
2.3. 定位2.3.1. static元素出现在标准文档流中。
2.3.2. relative生成相对定位的元素，相对于其正常位置进行定位。
因此，”left:20” 会向元素的 LEFT 位置添加 20 像素。
2.3.3. absolute生成绝对定位的元素，相对于 static 定位以外的第一个父元素进行定位。
元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。
2.3.4. fixed生成绝对定位的元素，相对于浏览器窗口进行定位。
元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。
3. 盒子模型上面我们说，要在浏览器这个盒子里摆放东西。那么，摆放什么东西呢？盒子！没有顶的盒子。

margin：盒子间的距离。
border：盒子的板厚度。
padding：盒子里的减震泡沫厚度。
content：盒子里可以摆放物品的空间尺寸。

4. 全局设置排版的时候，郝同学发现，在内容和浏览器窗口之间，会存在缝隙。所以，一般会设置一个全局样式：
*&#123;    margin: 0;    padding: 0;&#125;

5. 下拉菜单鼠标移动到顶部的“设置”或者“用户名”下面，会出现下拉菜单。这个效果，就需要用到定位中的“absolute”了，
6. 块级元素行内元素1、行内元素与块级元素直观上的区别行内元素会在一条直线上排列，都是同一行的，水平方向排列块级元素各占据一行，垂直方向排列。块级元素从新行开始结束接着一个断行。
2、块级元素可以包含行内元素和块级元素，行内元素不能包含块级元素。
3、行内元素与块级元素属性的不同，主要是盒模型属性上行内元素设置width无效，height无效(可以设置line-height)，margin上下无效，padding上下无效
7. line-height和vertical-alignline-height适用于所有元素，设置元素中行的高度。vertical-align适用于行内元素和单元格（table-cell）元素，设置元素内容的垂直对齐方式。
8. 后记本次任务，遵循一个原则：能用HTML+CSS实现的，不用JavaScript；能用JavaScript实现的，不用jQuery。上面主要记录了这次任务用到的技术和原理，细节方面的东西，看代码吧，完成度80%：https://github.com/voidking/baidu-search-result.git
9. 参考文档如何利用 CSS 製作多級選單？https://zespia.tw/blog/css-multi-level-menu/
网页布局基础http://www.imooc.com/learn/95
css知多少（7）——盒子模型http://www.cnblogs.com/wangfupeng1988/p/4287292.html
CSS+DIV定位分析http://blog.163.com/love_heartbreaking/blog/static/124561901201211334714800/
DOM中关于脱离文档流的几种情况分析http://www.tuicool.com/articles/IBJvyy7http://www.cnblogs.com/chuaWeb/p/html_css_position_float.html
CSS float浮动的深入研究、详解及拓展(一)
CSS深入理解之float浮动http://www.imooc.com/view/121
行内元素与块级元素比较全面的区别和转换http://blog.csdn.net/sykent/article/details/7738408
line-height 和 vertical-align 行高与行对齐精解 （图文）http://www.jb51.net/css/29328.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>艾佳生活</tag>
      </tags>
  </entry>
  <entry>
    <title>微擎系统搭建</title>
    <url>/dev-we7-start/</url>
    <content><![CDATA[1. 前言时隔一年半，再次接触微信公众平台开发。相比于掌上大学、圈里、微站ABC、图灵机器人、小i机器人、FAQ免费智能问答机器人、V5KF、赛科智能机器人，个人更喜欢模块定制的微擎和捷微，源码在自己手里，想怎么搞怎么搞。本篇短文，就记录下微擎系统搭建的具体步骤。


2. 准备条件首先，你要有一个公网服务器，服务器上有PHP和MySQL的环境，官方推荐linux(centOS)+ nginx + php5.3，mysql5.6。其次，你要有远程操作服务器的工具，推荐使用xshell和xftp。最后，你需要从微擎官网下载微擎的源码。
2.1. 服务器2.1.1. 云擎先说国内的，BAE、CAE、JAE、SAE等，上次做微信开发时，它们还是免费的，现在有些开始收费了。
再说国外的，GAE、OpenShift、heroku、appfog、mongolab等，但是国内的访问速度一般，要么直接被墙。其中，OpenShift是我最喜欢的，以前使用WordPress在上面搭建了一个博客。
云擎的用法简单，基本都是建立某个类型的应用，然后把代码部署上去。因为云擎有各种限制，比如PHP版本限制、文件大小限制、访问流量限制等，所以不建议使用。但是，云擎的重点在于免费，或者免费一段时间。做做测试还是可以的，对于我等穷屌丝而言，不失为一种福利。
2.1.2. 主流服务器阿里云、腾讯云、亚马逊、西部数据、美团云等，按配置收费，可以根据实际需要和经济能力选择。这种服务器，就可以像本地主机一样随意安装配置了。本次的微擎环境，我们就使用阿里云。
2.2. PHP+MySQL在linux下配置PHP+MySQL的环境，具体步骤请自行百度。如果觉得麻烦，可以在阿里云购买一个配置好的镜像系统，10元左右。
2.3. 远程工具xshell，用来远程登录服务器系统（一般是Linux），进行一些配置。xftp，用来管理服务器上的文件。
2.4. 源码微擎官网：http://www.we7.cc/以前使用微擎，需要把整个微擎系统的源码下载下来，然后部署到服务器上。现在，只需要下载一个名叫“install.php”的文件就可以了。
3. 流程3.1. 连接服务器1、打开xshell，文件，新建，输入服务器的ip地址，确定，然后输入用户名和密码，便可以连接到服务器。哇咔咔，看到了黑黝黝的shell界面，congratulations！2、打开xftp，文件，新建，输入服务器的ip地址、用户名、密码，便可以连接到服务器。
3.2. 查看帮助通过xftp，下载帮助文件，就可以大致知道自己的服务器的配置。可以看到，郝同学的web主目录为/alidata/www，OK，我们进入到/alidata/www目录下，里面有一个default目录。没错，这就是默认的web网站了，虽然里面只有一个index.html。而我们在浏览器地址栏输入主机ip地址，看到的就是这个index.html。
3.3. 配置虚拟主机如果决定直接在default目录下搭建微擎，这个步骤可以忽略。很多情况下，我们希望在一个服务器上面搭建多个网站。以Apache为例，我们需要配置/etc/httpd/conf/httpd.conf，然后执行命令service httpd restart，具体步骤可以借鉴参考文档。最终结果是，我们配置了一个域名为http://test.voidking.com，对应服务器主机目录为/alidata/www/test。
3.4. 上传源码通过xftp，把从微擎官网下载的“install.php”上传到default目录下。（配置过虚拟主机的话，就上传到test目录下）在浏览器访问地址：ServerName/install.php，其中，ServerName为ip地址或者自己配置的域名。没有意外的话，可以看到微擎的安装引导页面。至此，成功了一半。
3.5. 环境检查微擎安装引导，会自动检测你的服务器环境是否符合系统安装的要求，很人性化。我们看到，目录权限有问题。打开xshell，进入到/alidata/www目录下，chmod -R 777 test，给test目录和test目录下所有文件增加读写执行权限。然后，再次检测，已经没有问题了。
3.6. 系统配置数据库选项，输入正确的用户名和密码即可，其他无需修改。管理选项，创建一个管理员账号，微擎安装完成后用来登录。
3.7. 下载文件系统配置完成后，单击“继续”，微擎系统就会下载需要的文件到test文件夹，并且创建一个名为“we7”的数据库。喝杯咖啡的时间，就可以完成下载。
3.8. 更新系统用刚才配置的管理员账号登录微擎系统，看上去，一切正常。现在就可以使用了吗？不，在线安装的系统是精简版，必须更新，注意，是必须！一般来说，登录后会有更新提示，点过去即可。
4. 测试4.1. 微信公众号微信公众号分两种，服务号和订阅号。什么差别呢？1、服务号只有企业或者团体才能申请，而订阅号申请要求较低；2、服务号显示在聊天列表页，而订阅号都在聊天列表页的订阅号里面；3、服务号初始就可以使用自定义菜单，而订阅号需要微博认证同时500人订阅才可以使用自定义菜单（2015年8月起，菜单也开放给订阅号了，但是不能在开发者模式使用，仍需认证）；4、服务号每月可以推送4条消息，而订阅号可以推送30条。
4.2. 交互原理被动处理用户的请求。图中的个人/企业服务器，指的就是微擎所在的服务器。
设置微信服务器，或者主动给用户发推送数据。
4.3. 双向绑定1、在微擎系统，添加公众号，输入自己的公众号和密码一键获取公众号信息，或者自己填入公众号信息。最终生成我们需要的URL、Token、EncodingAESKey。
2、在微信公众平台，登录自己的公众号。左边导航栏，开发，基本配置。其中，URL、Token、EncodingAESKey要和微擎中一致。
4.4. helloworld在微擎系统中，管理公众号，文字回复，添加基本文字回复。输入规则名称、触发规则、回复内容，保存，提交。手机关注自己的公众号，在聊天界面输入“helloworld”，看看返回了什么？“恭喜你进入了一个新的世界！”微擎系统，至此基本搭建完成，更多好玩的功能，等着你去发掘。
5. 后记在搭建微擎系统的过程中，会遇到各种各样意想不到的错误。卧槽，逗我吗？为什么写教程的家伙没有遇到这种错误！莫方，郝同学也遇到过各种不懂，各种错误。百度、官网、博客、论坛、QQ群、前辈，总能找到你想要的答案。
6. 参考文档微擎开发文档http://www.we7.cc/docs/#introduce
阿里云一键安装web攻略https://bbs.aliyun.com/read/153209.html
公钥和私钥http://blog.csdn.net/tanyujing/article/details/17348321
在一台服务器上搭建多个网站的方法（Apache版）https://help.aliyun.com/knowledge_detail/6701386.html
Apache 虚拟主机 VirtualHost 配置http://www.neoease.com/apache-virtual-host/
DocumentRoot does not exist解决方法http://blog.csdn.net/zhuoyr/article/details/8393854
微信公众号提交开发者提示token验证失败http://www.68ecshop.com/article-1656.html
Xshell 启动报缺少msvcp110.dll文件http://blog.csdn.net/z1154505909/article/details/50474104
]]></content>
      <categories>
        <category>engineering</category>
        <category>php</category>
      </categories>
      <tags>
        <tag>微信</tag>
      </tags>
  </entry>
  <entry>
    <title>Sublime Text3</title>
    <url>/dev-sublime/</url>
    <content><![CDATA[1. Sublime简介
Sublime Text is a sophisticated text editor for code, markup and prose.You’ll love the slick user interface, extraordinary features and amazing performance.

sublime是前端神器，支持markdown语法高亮，非常适合作为日常编辑器使用。
Sublime的安装很简单，访问Sublime官网，下载对应系统的安装包，双击安装即可。


2. 常用设置2.1. 变更字体Sublime默认字体是Consolas，Consolas英文显示很好，但是中文显示不太好看。有一款字体是Consolas和微软雅黑的混合字体，叫做YaHei Consolas Hybrid，小伙伴可以试试看。1、下载安装YaHei Consolas Hybrid2、Sublime菜单栏里点击Preferences，Setting。3、在弹出来的Preferences.sublime-settings——User文本里，添加如下一行:
&quot;font_face&quot;: &quot;YaHei Consolas Hybrid&quot;,

2.2. TAB转空格在用Sublime里编写Python代码时，需要把TAB键（制表键）转换成四个空格。具体做法如下：1、菜单栏里点击Preferences，Setting。2、在弹出来的Preferences.sublime-settings——User文本里，添加如下两行:
&#123;    &quot;tab_size&quot;: 4,    &quot;translate_tabs_to_spaces&quot;: true&#125;

以上配置，在使用sublime新建文件后，编辑时使用tab键，tab制表符会自动转成4个空格。但是，对于已经存在的使用tab制表符的文件，编辑时使用tab键依然还是tab制表符，不会自动转换成空格。这时，我们可以点击sublime右下角的 “Tab Size: N”，选择 “Convert Indentation to Spaces”。这样处理依然有点麻烦，能不能在保存时自动转换成空格呢？可以的。
3、菜单栏里点击 Preference，Browser Packages…，新建一个目录 ExpandTabsOnSave，目录里新建文件 ExpandTabsOnSave.py，内容为：
import sublime, sublime_plugin, osclass ExpandTabsOnSave(sublime_plugin.EventListener):    def on_pre_save(self, view):        if view.settings().get(&#x27;expand_tabs_on_save&#x27;) == 1:            view.window().run_command(&#x27;expand_tabs&#x27;)

4，Preferences.sublime-settings——User 里添加一行：
&#123;    &quot;expand_tabs_on_save&quot;: true&#125;
以上配置，就可以在保存时自动把tab制表符转换成空格。
2.3. 关闭更新提示点击菜单栏Preferences，Settings，在弹出的Settings-User部分中添加：
&quot;update_check&quot;: false,

PS：前提是sublime已经注册，注册方法参考Sublime Text 3注册码。
1、下载并安装Sublime Text 3.2.2 Build 3211
2、浏览器访问十六进制编辑器Hexed.it
3、打开文件sublime_text.exe，搜索97 94 0D，更改为00 00 00。搜索38 18 0F 94 C1，修改为C6 00 01 90 90。
4、下载修改后的sublime_text.exe，覆盖原有sublime_text.exe
2.4. 同一个窗口中打开文件双击某个文本使用sublime打开，sublime默认会新开一个窗口。但是大多数时候我们不想新开窗口，而是想和其他文件在同一个窗口中进行编辑。因此，我们需要修改默认配置，使用同一个窗口打开新文本。
点击菜单栏Preferences，Settings，在弹出的Settings-User部分中添加：
&quot;open_files_in_new_window&quot;: false

PS：Settings-Default部分无法编辑，open_files_in_new_window 参数值为 true，不用管它，因为Settings-User优先级更高，会覆盖Settings-Default的配置。
在 Sublime Text 4 (Build 4200) 版本中，配置为：
&quot;open_files_in_new_window&quot;: &quot;never&quot;

3. 常用快捷键
ctrl+D：选择单词，重复可增加选择下一个相同的单词。
ctrl+alt+↑↓。同列多行同时编辑。
ctrl+L：选择行，重复可依次增加选择下一行。
shift+ctrl+↑↓。可实现类似鼠标选中之后移动的效果。
ctrl+P：搜索项目中的文件，模糊匹配文件名。
ctrl+R：前往 method。
ctrl+F：查找字符串。
ctrl+shift+F：在整个项目中查找字符串。如果快捷键不可用，则Find-&gt;Find in Files…。
ctrl+H：查找并替换。
ctrl+shift+P：打开命令面板。输入set syntax:css，可以设置语法为css。
ctrl+shift+[：折叠代码段。
ctrl+shift+V：粘贴并格式化。
ctrl+enter：在当前行后插入一行。
ctrl+shift+enter：在当前行前插入一行。
ctrl+shift+D：快速复制光标所在的一整行，并复制到该行之前。
ctrl+shift+K：删除一行。
ctrl+shift+↑↓：可替换行。
ctrl+/：注释当前行。
ctrl+shift+/：当前位置插入注释。
ctrl+shift+A：选中标签内的内容不包括标签，继续按会继续往上一层选择。
f11：全屏。
shift+f11：全屏免打扰模式，只编辑当前文件。
esc：退出各种面板。

4. Package Control4.1. 安装1、访问Package Control官网，找到package control的安装代码（不定时更新）。
import urllib.request,os,hashlib; h = &#x27;6f4c264a24d933ce70df5dedcf1dcaee&#x27; + &#x27;ebe013ee18cced0ef93d5f746d80ef60&#x27;; pf = &#x27;Package Control.sublime-package&#x27;; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( &#x27;http://packagecontrol.io/&#x27; + pf.replace(&#x27; &#x27;, &#x27;%20&#x27;)).read(); dh = hashlib.sha256(by).hexdigest(); print(&#x27;Error validating download (got %s instead of %s), please try manual install&#x27; % (dh, h)) if dh != h else open(os.path.join( ipp, pf), &#x27;wb&#x27; ).write(by)

2、ctrl + ~ 调出 console（或者View - Show Console），将安装代码粘贴进去并 enter 执行。
3、安装完成后Preferences中出现Package Control。
4.2. 使用方法1、查看已安装插件ctrl+shift+P，输入package，选择list packages。
2、安装插件ctrl+shift+P，输入package，选择install packages。输入或选择你需要的插件，回车安装（注意左下角的小文字变化，会提示安装成功）。
3、卸载插件ctrl+shift+P，输入package，选择remove packages。
5. 常用插件5.1. Soda传说中完美的编码主题，官网：http://buymeasoda.github.io/soda-theme/
5.2. EmmetHTML/CSS代码快速编写神器，项目地址：https://github.com/sergeche/emmet-sublime#readme
5.3. Javascript Completions测试了多个js插件，这个是最好用的，项目地址：https://github.com/pichillilorenzo/JavaScript-Completions
5.4. sublime jQuery提供了额外的语法高亮和几乎所有jQuery方法的片段，项目地址：https://github.com/SublimeText/jQuery/
5.5. SideBar Enhancements改进了侧边栏，增加了许多功能。
5.6. sublimelinter语法检查插件，安装sublimelinter和sublimelinter-*，*为所用的语言，例如sublimelinter-php。
5.7. Jedi - Python autocompletionJedi - an awesome autocompletion/static analysis library for Python.项目地址：https://github.com/davidhalter/jedi
5.8. ConvertToUTF8sublime默认不支持GBK编码格式，因此打开GBK编码的文件会出现乱码，本插件可以使sublime支持GBK编码格式。
5.9. PackageResourceViewer更改侧边栏字体显示大小，参考Sublime text 3更改侧边栏【sidebar】的字体大小 和 修改Sublime Text3 的侧边栏字体大小。项目地址：https://packagecontrol.io/packages/PackageResourceViewer
6. 代码缩进格式化使用Jetbrains系列IDE进行代码格式化很方便，Windows上 Ctrl+Alt+L ，Mac上 command+option+L 。那么，使用sublime的时候，有没有这么方便的快捷键进行代码格式化呢？没有！
但是，sublime提供了一个调整缩进的方法：1、全选代码2、Edit，Line，Reindent
看起来也很方便，但是这种方法是有问题的。如果代码之前已经有了一些不规范的缩进，比如tab和space混用，比如三个空格缩进，我们会发现，最终格式化出来的代码仍然是有问题的。
正确的代码格式化方法为：1、全选代码2、shift+tab，重复直至所有行都不进行缩进3、Edit，Line，Reindent
当然，这种方法调整的只是缩进，格式化效果不如Jetbrains。
7. 打造便携sublime7.1. 制作1、在地址栏输入 %appdata% 然后删除该目录下的Sublime Text 3文件夹。2、在sublime的安装目录下（例如，我的电脑上的安装目录是：C:\Program Files\Sublime Text 3），新建 Data 文件夹(注意大小写)。3、打开Sublime Text，所有的配置文件都会生成在Data文件夹中。4、打包压缩Sublime Text 3，即可制作完成便携sublime。
7.2. 使用方法一直接解压自己制作的压缩包。
7.3. 使用方法二1、正常安装sublime，安装完成后不要启动。2、把自己制作的压缩包中的Data文件夹解压到sublime的安装目录。3、变更sublime的安装目录权限为完全控制。
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>编辑器</tag>
        <tag>前端</tag>
        <tag>markdown</tag>
        <tag>sublime</tag>
      </tags>
  </entry>
  <entry>
    <title>毕业专业环境的搭建</title>
    <url>/dev-graduation-project-dev-environment/</url>
    <content><![CDATA[1. Webstorm访问Jetbrains的Webstorm官网：http://www.jetbrains.com/webstorm/ ，下载Webstorm，双击安装。
2. Git访问Git官网：https://git-scm.com/download/ ，下载Git，双击安装。需要注意的是，在Select Components界面，点选Simple context menu，方便以后直接右键打开Git bash。
3. Node访问Node官网：https://nodejs.org/en/ ，下载稳定版的Node，双击安装。安装之后，win+R，cmd，打开命令提示符界面，输入node --version，如果可以看到版本号，就说明安装成功。
4. Express新建文件夹nodeforum，在文件夹下打开Git bash，npm install express。
5. 测试运行在nodeforum下新建文件app.js，内容如下：
var express = require(&#x27;express&#x27;);var app = express();app.get(&#x27;/&#x27;, function (req, res) &#123;   res.send(&#x27;Hello World&#x27;);&#125;);var server = app.listen(3000, function () &#123;  console.log(&#x27;应用启动在port:&#x27;+3000)&#125;);
在Git bash下输入：node app.js。然后访问http://localhost:3000 ，看到“Hello World”，就表明express安装成功！
6. MongoDB访问MongoDB官网：https://www.mongodb.com/download-center#community下载MongoDB，双击安装。之后，打开命令提示符界面，输入mongo，进入MongoDB shell，如果可以进入，说明安装成功。
7. 项目结构│  .gitignore│  app.js│  bower.json│  config.js│  package.json│  README.md│  socket-event.js│  web-router.js├─bower_components├─controllers├─models├─node_modules├─public│  ├─css│  ├─img│  ├─js│  └─scss├─schemas└─views

.gitignore内写的是不需要上传到Git服务器的文件。
app.js是程序的入口。
bower.json是Bower配置文件，声明了一系列与前端包有关的内容。
config.js声明了一些全局配置。
package.json是Npm的配置文件，声明了一系列与Node依赖包有关的内容。
README.md是关于整个项目的说明。
socket-event.js里定义了一个函数，用来处理socket.io的服务器端。
web-router.js里写的是路由控制。
bower_components文件夹里是开发过程中需要依赖的一些前端软件包。
controllers文件夹里是处理请求，以及控制页面跳转的一些文件。
models文件夹里是一些Mongoose建立的Model文件。
node_modules文件夹里是开发过程中需要依赖的一些Node软件包。
public文件夹里存放自己写的scss文件、js文件、需要用到图片文件。
schemas文件夹里是一些Mongoose建立的Schema文件。
views文件里存放的是前端模板文件。

]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>毕设</tag>
      </tags>
  </entry>
  <entry>
    <title>毕业设计开始</title>
    <url>/dev-graduation-project-start/</url>
    <content><![CDATA[1. 前言前前后后，投了有三十家公司了。然而，唯一的面试通知，居然听不到声音，回电也打不通。。。貌似，回家前很难找到工作了。没有工作，就专心做毕设，有个作品对找工作也有帮助。


2. 毕设选题想了很久，最终决定，做一个社区网站系统。主要有注册、登陆、发帖、回帖、关注、收藏、@、私信、分享、个人信息管理、系统管理等功能，参考 http://named.cn/.mine
3. 技术架构用的最熟练的是Java，但是，这一次，郝同学决定使用不够熟练的技术——Node.js。因为，这是我未来想要深入发展的方向——前端！
这个项目可能用到的技术和框架有：Node.js、Express、EJS、Bootstrap、MongoDB。
开发工具：Webstorm
4. 后记从今天开始，记录下做毕设的点点滴滴。同时，本项目开源，希望能够对其他小伙伴有所帮助。
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
        <tag>毕设</tag>
      </tags>
  </entry>
  <entry>
    <title>极客大赛回顾</title>
    <url>/essay-geek-review/</url>
    <content><![CDATA[大众点评极客大赛，一段难忘的经历。接下来的一段时间，有了吹牛逼的材料。差一点拿到一等奖，虽有遗憾，但是二等奖的ipad air，依然令人惊喜。
比赛之后，郝同学坚定了考研的信念。学历，原来可以如此重要。


本次大赛，实际上是大众点评的一次招聘会。通过一天一夜的编程马拉松，挑选出他们看中的人才。网上海选的参赛者共有近两千，然后选出60个人在上海大众点评总部参加决赛，报销来回路费，还包吃住。全季酒店，提供自助早餐，真心好吃。
到了酒店，我才知道，来参赛的几乎全部是985、211的研究生。一阵无语，就当长长见识吧，看看人家有多强！
第一天上午，公司简介，就是说说大众点评的牛逼之处，吸引大家加入。第一天下午，组队，由队长上前演讲，招揽队友。一共12队，每队5个人。差点成为队长，最终耍赖赖掉了，好险。。。我们组最终成员为：张琪学长、许榕学长、熊环宇学长、李乾科和我，张琪学长为队长。第一天晚上，我们队在酒店热烈讨论了需求以及设计方案，每个人都很有激情。
第二天早上到第三天早上，24小时，编程比赛！大致分工是许榕学长和李乾科负责后端，熊环宇学长和我负责前端，张琪学长负责支援。预期是美好的，但实际进度总要慢很多，好在大家都很乐观，嘻嘻哈哈就过来了。晚上，有点评的开发人员来帮我们解决问题，真正见识到了一些大牛！
第三天上午，休息。第三天下午，作品展示。我们抽中了最后一组，lucky，于是，趁着其他组展示的时间修改代码，最终作品上升了一个档次！印象深刻的是，张琪学长女朋友的助攻，很强悍！张琪学长的演讲水平，也是杠杠的！第一，近在眼前！第三天晚上，颁奖。关于第一名和第二名的争论，评委团几乎吵了起来。最终，我们组拿到了第二，一个令人遗憾和惊喜并存的成绩。组内没有队员收到offer，因为三位学长还没有入职意向，因为我和乾科是本科生（应该是仅有的两个本科生）。
期间的细节不作展开，留一份朦胧的美好记忆。
最后，附上一些照片。
]]></content>
      <categories>
        <category>essay</category>
      </categories>
      <tags>
        <tag>随笔</tag>
      </tags>
  </entry>
  <entry>
    <title>图书管理系统之ui</title>
    <url>/dev-bookmanage-ui/</url>
    <content><![CDATA[1. 前言这三篇博文，是一个整体，单独看也许艰涩难懂。但是，合起来看，很多疑问就可以迎刃而解。正如马哲所说，整体功能可以大于部分功能之和。
2. 新建工程1、打开Eclipse，File，New，Maven Project，勾选Create a simple project，Next。
2、填写Group Id和Artifact Id，Packaging选择war。


相当于命令：
mvn archetype:create -DgroupId=com.voidking.book -DartifactId=book-ui-DarchetypeArtifactId=maven-archetype-webapp

3. pom.xml&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;  &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;  &lt;parent&gt;		&lt;groupId&gt;com.voidking.book&lt;/groupId&gt;		&lt;artifactId&gt;book-parent&lt;/artifactId&gt;		&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;		&lt;relativePath&gt;../book-parent/pom.xml&lt;/relativePath&gt;	&lt;/parent&gt;  &lt;artifactId&gt;book-ui&lt;/artifactId&gt;    &lt;packaging&gt;war&lt;/packaging&gt;    &lt;dependencies&gt;		&lt;!-- struts2框架 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.apache.struts&lt;/groupId&gt;			&lt;artifactId&gt;struts2-core&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.apache.struts&lt;/groupId&gt;			&lt;artifactId&gt;struts2-spring-plugin&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.apache.struts&lt;/groupId&gt;			&lt;artifactId&gt;struts2-json-plugin&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.apache.struts&lt;/groupId&gt;			&lt;artifactId&gt;struts2-convention-plugin&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- Spring 核心库 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-context&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-core&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-beans&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-orm&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-aop&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-tx&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- Spring MVC 库 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.aspectj&lt;/groupId&gt;			&lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- servlet api --&gt;		&lt;dependency&gt;			&lt;groupId&gt;javax.servlet&lt;/groupId&gt;			&lt;artifactId&gt;servlet-api&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- 引用业务层的jar包 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;com.voidking.book&lt;/groupId&gt;			&lt;artifactId&gt;book-logic&lt;/artifactId&gt;			&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;			&lt;exclusions&gt;				&lt;exclusion&gt;					&lt;groupId&gt;org.hibernate&lt;/groupId&gt;					&lt;artifactId&gt;hibernate-entitymanager&lt;/artifactId&gt;				&lt;/exclusion&gt;			&lt;/exclusions&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;			&lt;artifactId&gt;hibernate-entitymanager&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-web&lt;/artifactId&gt;		&lt;/dependency&gt;	&lt;/dependencies&gt;	&lt;build&gt;		&lt;plugins&gt;			&lt;plugin&gt;				&lt;groupId&gt;org.mortbay.jetty&lt;/groupId&gt;				&lt;artifactId&gt;maven-jetty-plugin&lt;/artifactId&gt;				&lt;configuration&gt;					&lt;scanIntervalSeconds&gt;10&lt;/scanIntervalSeconds&gt;					&lt;connectors&gt;						&lt;connector implementation=&quot;org.mortbay.jetty.nio.SelectChannelConnector&quot;&gt;							&lt;port&gt;8099&lt;/port&gt;							&lt;maxIdleTime&gt;60000&lt;/maxIdleTime&gt;						&lt;/connector&gt;					&lt;/connectors&gt;				&lt;/configuration&gt;			&lt;/plugin&gt;		&lt;/plugins&gt;	&lt;/build&gt;&lt;/project&gt;
值得一提的是，maven的web工程中，习惯用jetty而不是tomcat。jetty发布的工程，在当前工程下的target/相应版本号下。比如本工程，发布后在target/book-ui-0.0.1-SNAPSHOT下。
4. 新建包新建包com.voidking.book.admin.action、com.voidking.book.bookbase.action、com.voidking.book.bookkind.action、com.voidking.book.readerbase.action、com.voidking.book.readerkind.action、com.voidking.book.borrowinfo.action、com.voidking.book.search.action、com.voidking.book.statistics.action。
5. admin.action包在admin.action包，新建LoginAction.java，内容如下。
package com.voidking.book.admin.action;import org.apache.struts2.convention.annotation.Action;import org.apache.struts2.convention.annotation.Namespace;import org.apache.struts2.convention.annotation.ParentPackage;import org.apache.struts2.convention.annotation.Result;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import com.opensymphony.xwork2.ActionContext;import com.opensymphony.xwork2.ActionSupport;import com.voidking.book.entity.Admin;import com.voidking.book.logic.AdminLogicService;@Controller@Namespace(&quot;/admin&quot;)@ParentPackage(&quot;custom-default&quot;)public class LoginAction extends ActionSupport &#123;	private static final long serialVersionUID = 2936224923889056993L;	@Autowired	private AdminLogicService adminLogicService;	private Admin admin;	private String info;	public Admin getAdmin() &#123;		return admin;	&#125;	public void setAdmin(Admin admin) &#123;		this.admin = admin;	&#125;	public String getInfo() &#123;		return info;	&#125;	public void setInfo(String info) &#123;		this.info = info;	&#125;	@Action(value = &quot;login&quot;, results = &#123; @Result(name = &quot;success&quot;, type = &quot;json&quot;) &#125;)	public String login() throws Exception &#123;		this.info = this.adminLogicService.login(admin);		if (admin.getId() != null)// 登录成功		&#123;			ActionContext.getContext().getSession().put(&quot;admin&quot;, admin);		&#125;		return SUCCESS;	&#125;&#125;
1、@Controller，Spring的注解。在SpringMVC中提供了一个非常简便的定义Controller的方法，你无需继承特定的类或实现特定的接口，只需使用@Controller标记一个类是Controller，然后使用@RequestMapping和@RequestParam等一些注解用以定义URL请求和Controller方法之间的映射，这样的Controller就能被外界访问到。此外Controller不会直接依赖于HttpServletRequest和HttpServletResponse等HttpServlet对象，它们可以通过Controller 的方法参数灵活的获取到。
2、我们知道通常情况下，Struts2是通过struts.xml配置的。但是随着系统规模的加大我们需要配置的文件会比较大，虽然我们可以根据不同的系统功能将不同模块的配置文件单独书写，然后通过include节点将不同的配置文件引入到最终的struts.xml文件中，但是毕竟还是要维护和管理这些文件，因此也会给维护工作带来很大的困扰。为了解决这个问题，可以考虑使用struts2的注解。实际上struts2中最主要的概念就是package、action以及Interceptor等等概念，所以只要明白这些注解就可以了。
3、@Namespace，Struts2的注解。命名空间，也就是xml文件中package的namespace属性。如果没有@Namespace(“/admin”)注解，按照Convention Plugin的约定，会将此包作为根包，对应Action URL的命名空间为“/”。
4、@ParentPackage，Struts2的注解。这个注解对应了xml文件中的package节点，它只有一个属性叫value，其实就是package的name属性；
5、@Action，Struts2的注解。这个注解对应action节点。这个注解可以应用于 action 类上，也可以应用于方法上。这个注解中有几个属性：

value，表示action的URL，也就是action节点中的name属性；
results，表示action的多个result，这个属性是一个数组属性，因此可以定义多个Result；
interceptorRefs，表示action的多个拦截器，这个属性也是一个数组属性，因此可以定义多个拦截器；
params，这是一个String类型的数组，它按照name/value的形式组织，是传给action的参数；
exceptionMappings，这是异常属性，它是一个ExceptionMapping的数组属性，表示action的异常，在使用时必须引用相应的拦截器；

6、@Result ，Struts2的注解。这个注解对应了result节点。这个注解只能应用于 action 类上。这个注解中也有几个属性：

name，表示action方法的返回值，也就是result节点的name属性，默认情况下是success；
location，表示view层文件的位置，可以是相对路径，也可以是绝对路径；
type，是action的类型，比如redirect；
params，是一个String数组。也是以name/value形式传送给result的参数；

6. struts.xml在src/main/resources文件下，新建struts.xml，内容如下。
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC &quot;-//Apache Software Foundation//DTD Struts Configuration 2.1//EN&quot; &quot;http://struts.apache.org/dtds/struts-2.1.dtd&quot;&gt;&lt;struts&gt;	&lt;constant name=&quot;struts.multipart.maxSize&quot; value=&quot;99999999999&quot; /&gt;	&lt;constant name=&quot;struts.allowed.action.names&quot; value=&quot;[a-zA-Z0-9._!/\-]*&quot;&gt;&lt;/constant&gt;	&lt;constant name=&quot;struts.multipart.saveDir&quot; value=&quot;/tmp&quot; /&gt;	&lt;constant name=&quot;struts.devMode&quot; value=&quot;true&quot; /&gt;	&lt;constant name=&quot;struts.i18n.encoding&quot; value=&quot;UTF-8&quot; /&gt;	&lt;package name=&quot;custom-default&quot; extends=&quot;json-default&quot;&gt;		&lt;interceptors&gt;			&lt;!--定义拦截器 name:拦截器名称 class:拦截器类路径--&gt;			&lt;!-- 定义拦截器栈 --&gt;			&lt;interceptor-stack name=&quot;myDefaultStack&quot;&gt;				&lt;interceptor-ref name=&quot;json&quot; /&gt;				&lt;interceptor-ref name=&quot;defaultStack&quot; /&gt;			&lt;/interceptor-stack&gt;		&lt;/interceptors&gt;		&lt;default-interceptor-ref name=&quot;myDefaultStack&quot; /&gt;	&lt;/package&gt;&lt;/struts&gt;    
1、struts.properties文件的内容均可在struts.xml中以&lt;constant name=&quot;&quot; value=&quot;&quot;&gt;&lt;/constant&gt;加载。
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.3//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.3.dtd&quot;&gt;&lt;struts&gt;    &lt;!-- 把它设置为开发模式，发布时要设置为false --&gt;    &lt;constant name=&quot;struts.devMode&quot; value=&quot;true&quot; /&gt;    &lt;!-- 设置在class被修改时是否热加载，发布时要设置为false --&gt;    &lt;constant name=&quot;struts.convention.classes.reload&quot; value=&quot;true&quot;/&gt;    &lt;!-- 自动动态方法的调用，使用这个设置后可以这样调用：action!method --&gt;    &lt;constant name=&quot;struts.enable.DynamicMethodInvocation&quot; value=&quot;true&quot; /&gt;    &lt;!-- 指定jsp文件所在的目录地址 --&gt;    &lt;constant name=&quot;struts.convention.result.path&quot; value=&quot;/WEB-INF/content/&quot; /&gt;    &lt;!-- 使用struts-default默认的转换器，如果是rest的使用：rest-default，rest需要rest的jar插件 --&gt;    &lt;constant name=&quot;struts.convention.default.parent.package&quot; value=&quot;struts-default&quot;/&gt;    &lt;!-- 用于配置包名后缀。默认为action、actions、struts--&gt;    &lt;constant name=&quot;struts.convention.package.locators&quot; value=&quot;actions&quot; /&gt;    &lt;!-- 用于配置类名后缀，默认为Action，设置后，Struts2只会去找这种后缀名的类做映射 --&gt;    &lt;constant name=&quot;struts.convention.action.suffix&quot; value=&quot;Action&quot;/&gt;    &lt;!-- 设置即使没有@Action注释，依然创建Action映射。默认值是false。因为Convention-Plugin是约定优于配置的风格，        可以不通过注解根据预先的定义就能访问相应Action中的方法 --&gt;    &lt;constant name=&quot;struts.convention.action.mapAllMatches&quot; value=&quot;true&quot;/&gt;    &lt;!-- 自定义jsp文件命名的分隔符 --&gt;    &lt;constant name=&quot;struts.convention.action.name.separator&quot; value=&quot;-&quot; /&gt;    &lt;!-- 国际化资源文件名称 --&gt;    &lt;constant name=&quot;struts.custom.i18n.resources&quot; value=&quot;i18n&quot; /&gt;    &lt;!-- 是否自动加载国际化资源文件  --&gt;    &lt;constant name=&quot;struts.i18n.reload&quot; value=&quot;true&quot; /&gt;    &lt;!-- 浏览器是否缓存静态内容 --&gt;    &lt;constant name=&quot;struts.serve.static.browserCache&quot; value=&quot;false&quot; /&gt;     &lt;!-- 上传文件大小限制设置 --&gt;    &lt;constant name=&quot;struts.multipart.maxSize&quot; value=&quot;-1&quot; /&gt;    &lt;!-- 主题，将值设置为simple，即不使用UI模板。这将不会生成额外的html标签 --&gt;    &lt;constant name=&quot;struts.ui.theme&quot; value=&quot;simple&quot; /&gt;    &lt;!-- 编码格式 --&gt;    &lt;constant name=&quot;struts.i18n.encoding&quot; value=&quot;UTF-8&quot; /&gt;&lt;/struts&gt;

2、package有以下几个常用属性：

name：该属性是必选的，指定包的名字，这个名字将作为引用该包的键。注意，包的名字必须是唯一的，在一个struts.xml文件中不能出现两个同名的包。
extends：该属性是可选的，允许一个包继承一个或多个先前定义的包。
abstract：该属性是可选的，将其设置为true，可以把一个包定义为抽象的。抽象包不能有action定义，它只能作为“父”包，被其他包所继承。注意，以为Struts2的配置文件是从上到下处理的，所以父包应该在子包前面定义。
namespace：该属性是可选的，将保存的action配置为不同的名称空间。如果接收到一个请求为/space/main.action，框架将首先查找/space名称空间，如果找到了，则执行main.action；如果没有找到，则到默认的名称空间（name=”default”）中继续查找。如果接收到一个请求为/main.action，框架将首先查找“/”名称空间，如果找到了，则执行main.action；如果没有找到，则到默认的名称空间（name=”default”）中继续查找。

7. 配置文件需要注意的是，在前两层中，我们的配置文件全部放到了src/test/resources中。但是，在这一层，我们的配置文件主要放在两个位置。一个是src/main/resources，另一个是src/main/webapp/WEB-INF。
1、在resources下，有struts.xml、database-conn.properties、log4j.properties。2、在WEB-INF下，有applicationContext.xml、spring-persist.xml、web.xml。
8. web.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;	xmlns=&quot;http://java.sun.com/xml/ns/javaee&quot;	xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd&quot;	version=&quot;2.5&quot;&gt;	&lt;display-name&gt;voidking&lt;/display-name&gt;	&lt;filter&gt;		&lt;filter-name&gt;entityManagerFilter&lt;/filter-name&gt;		&lt;filter-class&gt;org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter&lt;/filter-class&gt;	&lt;/filter&gt;	&lt;filter-mapping&gt;		&lt;filter-name&gt;entityManagerFilter&lt;/filter-name&gt;		&lt;url-pattern&gt;/*&lt;/url-pattern&gt;	&lt;/filter-mapping&gt;	&lt;filter&gt;		&lt;filter-name&gt;struts2&lt;/filter-name&gt;		&lt;filter-class&gt;org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter&lt;/filter-class&gt;	&lt;/filter&gt;	&lt;filter-mapping&gt;		&lt;filter-name&gt;struts2&lt;/filter-name&gt;		&lt;url-pattern&gt;/*&lt;/url-pattern&gt;	&lt;/filter-mapping&gt;	&lt;listener&gt;		&lt;listener-class&gt;org.springframework.web.context.ContextLoaderListener&lt;/listener-class&gt;	&lt;/listener&gt;&lt;/web-app&gt;
1、display-name，如果使用工具编辑部署描述符，display-name元素包含的就是XML编辑器显示的名称。
2、filter，用于指定Web容器中的过滤器。在请求和响应对象被servlet处理之前或之后，可以使用过滤器对这两个对象进行操作。配合filter-mapping，过滤器被映射到一个servlet或一个URL。这个过滤器的filter元素和filter-mapping元素必须具有相同的名称。
9. applicationContext.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;	xmlns=&quot;http://www.springframework.org/schema/beans&quot;	xsi:schemaLocation=&quot;    http://www.springframework.org/schema/beans     http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;    &lt;import resource=&quot;spring-persist.xml&quot;/&gt;    &lt;/beans&gt;

10. 工作流程1、客户端提交一个HttpServletRequest请求。
2、请求被提交到一系列Filter过滤器，如ActionCleanUp和FilterDispatcher等。
3、FilterDispatcher是Struts2控制器的核心，它通常是过滤器链中的最后一个过滤器。
4、请求被发送到FilterDispatcher后，FilterDispatcher询问ActionMapper时候需要调用某个action来处理这个Request。
5、如果ActionMapper决定需要调用某个action，FilterDispatcher则把请求交给ActionProxy进行处理。
6.ActionProxy通过Configuration Manager询问框架的配置文件struts.xml（本项目实现struts2零配置，所以询问action文件），找到调用的action类。
7.ActionProxy创建一个ActionInvocation实例，通过代理模式调用Action。
8.action执行完毕后，返回一个result字符串，此时再按相反的顺序通过Intercepter拦截器。
9.最后ActionInvocation实例，负责根据struts.xml中配置result元素，找到与之相对应的result，决定进一步输出。
11. spring-persist.xml加载问题在persist层和logic层测试的时候，我们是手动加载spring-persist.xml文件。那么，在ui层，spring-perisist.xml文件的加载流程是怎样的呢？在web.xml中，可以通过&lt;context-param&gt;指定Spring配置文件，如果没有这个标签，则默认加载WEB-INF下的applicationContext.xml，也可以用它指定其他配置文件。
12. 前端前端使用BootStrap+AngularJs，其中BootStrap负责界面显示，AngularJs则是沟通前后端的桥梁。
这个部分比较复杂，给个例子，讲的很好。http://www.runoob.com/angularjs/angularjs-application.html
13. 效果演示
14. 后记这个图书管理系统，使用的技术，很多郝同学也是一知半解。接下来，会有一篇文章，专门针对这个项目进行改进。全部代码自取https://github.com/voidking/bookmanage.git
15. 参考文档spring的applicationContext.xml如何自动加载http://blog.csdn.net/fuqingtian/article/details/5545860
Spring controllerhttp://my.oschina.net/hcliu/blog/396887
Struts2注解配置之@Namespace(四)http://blog.csdn.net/spyjava/article/details/13764757
Struts2注解使用说明http://blog.csdn.net/wk313753744/article/details/19920195
Struts2的注解功能详解http://my.oschina.net/victorHomePage/blog/56732http://www.cnblogs.com/wayne_wang/archive/1942927.html
Struts2 - 常用的constant总结http://www.cnblogs.com/HD/p/3653930.html
Struts2 常用的常量配置http://www.cnblogs.com/yokoboy/archive/2877145.html
struts2.0中struts.xml配置文件详解http://www.cnblogs.com/kay/archive/976120.html
Web.xml详解http://www.cnblogs.com/konbluesky/articles/1925295.html
web.xml详细介绍http://mianhuaman.iteye.com/blog/1105522
struts2工作流程http://huaxia524151.iteye.com/blog/1430148
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>maven</tag>
        <tag>struts2</tag>
      </tags>
  </entry>
  <entry>
    <title>图书管理系统之logic</title>
    <url>/dev-bookmanage-logic/</url>
    <content><![CDATA[1. 前言persist层基本搞清楚了，下面我们接着看logic层。
2. 新建工程1、打开Eclipse，File，New，Maven Project，勾选Create a simple project，Next。
2、填写Group Id和Artifact Id，Packaging选择jar。


3. pom.xml&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;  &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;  &lt;parent&gt;		&lt;groupId&gt;com.voidking.book&lt;/groupId&gt;		&lt;artifactId&gt;book-parent&lt;/artifactId&gt;		&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;		&lt;relativePath&gt;../book-parent/pom.xml&lt;/relativePath&gt;	&lt;/parent&gt;	&lt;artifactId&gt;book-logic&lt;/artifactId&gt;	&lt;dependencies&gt;		&lt;!-- Spring 核心库 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-context&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-core&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-beans&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-orm&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-aop&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-tx&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- Spring MVC 库 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt; org.aspectj&lt;/groupId&gt;			&lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- persist --&gt;		&lt;dependency&gt;			&lt;groupId&gt;com.voidking.book&lt;/groupId&gt;			&lt;artifactId&gt;book-persist&lt;/artifactId&gt;			&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;		&lt;/dependency&gt;		&lt;!-- 日志打印 --&gt;		&lt;!-- junit --&gt;		&lt;dependency&gt;			&lt;groupId&gt;junit&lt;/groupId&gt;			&lt;artifactId&gt;junit&lt;/artifactId&gt;			&lt;scope&gt;test&lt;/scope&gt;		&lt;/dependency&gt;		&lt;!-- hibernate --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;			&lt;artifactId&gt;hibernate-entitymanager&lt;/artifactId&gt;			&lt;scope&gt;provided&lt;/scope&gt;		&lt;/dependency&gt;	&lt;/dependencies&gt;  &lt;/project&gt;

4. logic包新建包com.voidking.book.logic，在该包下，新建AdminLogicService.java，内容如下。
package com.voidking.book.logic;import com.voidking.book.entity.Admin;public interface AdminLogicService &#123;		String login(Admin admin);	&#125;
新建BookBaseLogicService.java、BookKindLogicService.java、ReaderBaseLogicService.java、ReaderKindLogicService.java、BorrowInfoLogicService.java、SearchLogicService.java、StatisticsLogicService.java，详细代码移步https://github.com/voidking/bookmanage.git
5. logic.impl包新建包com.voidking.book.logic.impl，在该包下，新建AdminLogicServiceImpl.java，内容如下。
package com.voidking.book.logic.imp;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import com.voidking.book.entity.Admin;import com.voidking.book.logic.AdminLogicService;import com.voidking.book.repository.AdminRepository;import com.voidking.book.service.AdminService;@Servicepublic class AdminLogicServiceImpl implements AdminLogicService &#123;	@Autowired	private AdminService adminService;		@Autowired	private AdminRepository adminRepository;	public String login(Admin admin) &#123;		if (admin == null) &#123;			return &quot;表现层错误&quot;;		&#125;		if (admin.getName() == null || admin.getName().equals(&quot;&quot;)) &#123;			return &quot;用户名错误&quot;;		&#125;		if (admin.getPwd() == null || admin.getPwd().equals(&quot;&quot;)) &#123;			return &quot;密码错误&quot;;		&#125;//		AdminBase tmp = this.adminBaseService.findByNameAndPwd(//				adminBase.getName(), adminBase.getPwd());		Admin tmp = this.adminRepository.findByNameAndPwd(admin.getName(), admin.getPwd());				if (tmp == null) &#123;			return &quot;用户名或者密码错误&quot;;		&#125;		admin.setId(tmp.getId());		//this.adminBaseService.save(adminBase);		return &quot;登录成功&quot;;	&#125;&#125;
新建BookBaseLogicServiceImpl.java、BookKindLogicServiceImpl.java、ReaderBaseLogicServiceImpl.java、ReaderKindLogicServiceImpl.java、BorrowInfoLogicServiceImpl.java、SearchLogicServiceImpl.java、StatisticsLogicServiceImpl.java，详细代码移步https://github.com/voidking/bookmanage.git
6. 配置文件spring-persist.xml、database-conn.properties、log4j.properties，和persist层相同。
7. context:component-scan范围book-persist工程下含有com.voidking.book.entity、com.voidking.book.repository、com.voidking.book.service、com.voidking.book.service.impl四个包。book-persist工程中Spring的配置文件，有这么一句：
&lt;context:component-scan base-package=&quot;com.voidking.book&quot; /&gt;
这个配置，会自动扫描com.voidking.book包及其子包下的注解，@Repository、@Service、@Controller 和 @Component，并把它们注册为Spring Bean。在book-persist工程中，似乎没有什么问题。
下面注意，问题来了！！！
在book-logic工程中，含有com.voidking.book.logic、com.voidking.book.logic.impl两个包。同时，引入了book-persist打包成的jar文件，jar文件中并没有Spring的配置文件，更没有加载配置文件这一过程。而book-logic工程中Spring的配置文件，也有这么一句：
&lt;context:component-scan base-package=&quot;com.voidking.book&quot; /&gt;
这时，请问，这里的com.voidking.book是指在当前工程下，还是jar文件里面，又或是两者都包括？郝同学猜测两者都包括，那么怎么证明？1、由book-persist工程，可以看出，当前工程下的包，是肯定会被扫描的。2、由book-logic工程，可以看出，它使用了jar文件中的类，而且没有“new”，而是“@Autowired”，说明jar包中的包也是会被扫描的。
这时新的问题出现了，book-persist和book-logic这两个工程，是否需要什么必然的要求？比如必须在同一个groupId下面？
答案是没有特殊要求！证明过程如下：1、新建任意Maven工程（以book-jpa为例）。2、在pom.xml中，像book-logic一样，引入book-persist的jar文件。3、拷贝book-logic的Spring配置文件到book-jpa的对应位置。4、在src/test/java下新建任意包，包中新建任意java文件。5、在Test方法中，加载配置文件，使用book-persist中的Bean。最后运行结果和预期一致，答案得证！
总结：context:component-scan扫描的范围包括当前工程和引入的jar文件，而且不要求当前工程和jar文件同处于一个groupId。
8. 单元测试在src/test/java文件夹下，新建包com.voidking.book.logic，新建AdminLogicServiceTest.java文件，内容如下。
package com.voidking.book.logic;import org.junit.Before;import org.junit.Test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import com.voidking.book.entity.Admin;public class AdminLogicServiceTest &#123;	private AdminLogicService adminLogicService;	@Before	public void prepare()&#123;		ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;spring-persist.xml&quot;);		adminLogicService = ctx.getBean(AdminLogicService.class);	&#125;		@Test	public void testLogin() &#123;		Admin admin = new Admin(&quot;haojin&quot;,&quot;haojin&quot;);		System.err.println(this.adminLogicService.login(admin));	&#125;&#125;
其他服务类的测试用例，自行编写。
注：打包jar文件时，src/test/java下的配置文件和src/test/resources下的类，不会被打包。
9. 后记没有人能随随便便成功，踏踏实实，一点一滴。。。
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>maven</tag>
      </tags>
  </entry>
  <entry>
    <title>图书管理系统之Spring JPA</title>
    <url>/dev-bookmanage-persist/</url>
    <content><![CDATA[1. 前言这一个小系列的项目实战，郝同学会尽可能解释清楚每一个细节。有些内容在以前的文章中已经写过，这次就当复习了。
2. Maven父工程2.1. 新建工程1、打开Eclipse，File，New，Maven Project，勾选Create a simple project，Next。


2、填写Group Id和Artifact Id，Packaging选择pom。如下图：解释：Maven是一个四维的仓库，里面存在着无数的jar包。四维向量分别是GroupId、ArtifactID、Version、Packaging，创建Maven工程的时候，需要输入这四个参数，其中Version默认为0.0.1-SNAPSHOT，Packaging默认为jar。上图相当于命令：
mvn archetype:create -DgroupId=com.voidking.book -DartifactId=book-persist 
然后把pom.xml中的packaging改成pom。其中需要注意的是，在根目录下无法使用mvn命令，比如“E:\”。
2.2. pom.xml详解pom.xml文件，是Maven的核心。
&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;	xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;	&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;	&lt;groupId&gt;com.voidking.book&lt;/groupId&gt;	&lt;artifactId&gt;book-parent&lt;/artifactId&gt;	&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;	&lt;packaging&gt;pom&lt;/packaging&gt;		&lt;!--聚合模块，一条命令就能构建三个模块--&gt;	&lt;modules&gt;		&lt;!--module的值是以当前POM为主目录的相对路径--&gt;		&lt;module&gt;../book-persist&lt;/module&gt;		&lt;module&gt;../book-logic&lt;/module&gt;		&lt;module&gt;../book-ui&lt;/module&gt;		&lt;!--使用继承的话，parent也要加入这里--&gt;		&lt;!--（但是个人实验证明，不加也可以）--&gt;		&lt;module&gt;book-parent&lt;/module&gt;	&lt;/modules&gt;		&lt;properties&gt;		&lt;!--自定义属性值，一般用于统一版本--&gt;		&lt;struts.version&gt;2.3.1&lt;/struts.version&gt;		&lt;springframework.version&gt;3.2.4.RELEASE&lt;/springframework.version&gt;	&lt;/properties&gt;	&lt;!--dependencyManagement中配置的元素既不会给parent引入依赖，--&gt;	&lt;!--也不会给它的子模块引入依赖，仅仅是它的配置是可继承的。--&gt;	&lt;!--pluginManagement类似，它是用来进行插件管理的。--&gt;	&lt;dependencyManagement&gt;		&lt;!--在父POM中声明依赖--&gt;		&lt;dependencies&gt;			&lt;!-- struts2框架 --&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.apache.struts&lt;/groupId&gt;				&lt;artifactId&gt;struts2-core&lt;/artifactId&gt;				&lt;version&gt;$&#123;struts.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.apache.struts&lt;/groupId&gt;				&lt;artifactId&gt;struts2-spring-plugin&lt;/artifactId&gt;				&lt;version&gt;$&#123;struts.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.apache.struts&lt;/groupId&gt;				&lt;artifactId&gt;struts2-json-plugin&lt;/artifactId&gt;				&lt;version&gt;$&#123;struts.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.apache.struts&lt;/groupId&gt;				&lt;artifactId&gt;struts2-convention-plugin&lt;/artifactId&gt;				&lt;version&gt;$&#123;struts.version&#125;&lt;/version&gt;			&lt;/dependency&gt;						&lt;!-- sql server 驱动 --&gt;			&lt;dependency&gt;				&lt;groupId&gt;com.microsoft.sqlserver&lt;/groupId&gt;				&lt;artifactId&gt;sqljdbc4&lt;/artifactId&gt;				&lt;version&gt;1.1.1&lt;/version&gt;			&lt;/dependency&gt;						&lt;!-- mysql驱动 --&gt;			&lt;dependency&gt;				&lt;groupId&gt;mysql&lt;/groupId&gt;				&lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;				&lt;version&gt;5.1.30&lt;/version&gt;			&lt;/dependency&gt;						&lt;!-- Oracle驱动 --&gt;			&lt;dependency&gt;				&lt;groupId&gt;com.oracle&lt;/groupId&gt;				&lt;artifactId&gt;ojdbc14&lt;/artifactId&gt;				&lt;version&gt;10.2.0.3.0&lt;/version&gt;			&lt;/dependency&gt;						&lt;!-- Hibernat 4 --&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.hibernate&lt;/groupId&gt;				&lt;artifactId&gt;hibernate-core&lt;/artifactId&gt;				&lt;version&gt;4.2.7.Final&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.hibernate&lt;/groupId&gt;				&lt;artifactId&gt;hibernate-validator&lt;/artifactId&gt;				&lt;version&gt;5.0.1.Final&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.hibernate&lt;/groupId&gt;				&lt;artifactId&gt;hibernate-entitymanager&lt;/artifactId&gt;				&lt;version&gt;4.2.6.Final&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;junit&lt;/groupId&gt;				&lt;artifactId&gt;junit&lt;/artifactId&gt;				&lt;version&gt;4.11&lt;/version&gt;				&lt;scope&gt;test&lt;/scope&gt;			&lt;/dependency&gt;			&lt;!-- Spring 核心库 --&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework&lt;/groupId&gt;				&lt;artifactId&gt;spring-context&lt;/artifactId&gt;				&lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework&lt;/groupId&gt;				&lt;artifactId&gt;spring-core&lt;/artifactId&gt;				&lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework&lt;/groupId&gt;				&lt;artifactId&gt;spring-beans&lt;/artifactId&gt;				&lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework&lt;/groupId&gt;				&lt;artifactId&gt;spring-orm&lt;/artifactId&gt;				&lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework&lt;/groupId&gt;				&lt;artifactId&gt;spring-aop&lt;/artifactId&gt;				&lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework&lt;/groupId&gt;				&lt;artifactId&gt;spring-tx&lt;/artifactId&gt;				&lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;			&lt;/dependency&gt;						&lt;!-- Spring MVC 库 --&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework&lt;/groupId&gt;				&lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;				&lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework&lt;/groupId&gt;				&lt;artifactId&gt;spring-web&lt;/artifactId&gt;				&lt;version&gt;$&#123;springframework.version&#125;&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt; org.aspectj&lt;/groupId&gt;				&lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;				&lt;version&gt; 1.6.11&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.springframework.data&lt;/groupId&gt;				&lt;artifactId&gt;spring-data-jpa&lt;/artifactId&gt;				&lt;version&gt;1.4.1.RELEASE&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;org.slf4j&lt;/groupId&gt;				&lt;artifactId&gt;slf4j-log4j12&lt;/artifactId&gt;				&lt;version&gt;1.7.5&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;log4j&lt;/groupId&gt;				&lt;artifactId&gt;log4j&lt;/artifactId&gt;				&lt;version&gt;1.2.17&lt;/version&gt;			&lt;/dependency&gt;			&lt;!-- 数据库连接池 --&gt;			&lt;dependency&gt;				&lt;groupId&gt;commons-dbcp&lt;/groupId&gt;				&lt;artifactId&gt;commons-dbcp&lt;/artifactId&gt;				&lt;version&gt;1.4&lt;/version&gt;			&lt;/dependency&gt;			&lt;dependency&gt;				&lt;groupId&gt;javax.servlet&lt;/groupId&gt;				&lt;artifactId&gt;servlet-api&lt;/artifactId&gt;				&lt;version&gt;2.5&lt;/version&gt;			&lt;/dependency&gt;		&lt;/dependencies&gt;	&lt;/dependencyManagement&gt;&lt;/project&gt;

3. Persist层工程3.1. 新建工程1、File，New，Maven Project，勾选Create a simple project，Next。
2、填写Group Id和Artifact Id，Packaging选择jar。
3.2. pom.xml详解&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;	xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;	&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;	&lt;!--groupId和version省略了，因为使用了继承。--&gt;	&lt;!--还有一些继承的元素，详见之前总结的《Maven之pom.xml》--&gt;	&lt;artifactId&gt;book-persist&lt;/artifactId&gt;		&lt;!--写出parent的坐标和相对路径--&gt;	&lt;parent&gt;		&lt;groupId&gt;com.voidking.book&lt;/groupId&gt;		&lt;artifactId&gt;book-parent&lt;/artifactId&gt;		&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;		&lt;relativePath&gt;../book-parent/pom.xml&lt;/relativePath&gt;	&lt;/parent&gt;	&lt;!--真实依赖--&gt;	&lt;dependencies&gt;		&lt;!--只有子模块配置了继承的元素，才会真正的有效，否则不加载--&gt;		&lt;!--主要配置groupId和artifactId--&gt;		&lt;dependency&gt;				&lt;groupId&gt;com.microsoft.sqlserver&lt;/groupId&gt;			&lt;artifactId&gt;sqljdbc4&lt;/artifactId&gt;			&lt;!--这里并没有指定version，因为继承了parent的version。--&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;mysql&lt;/groupId&gt;			&lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- Oracle驱动 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;com.oracle&lt;/groupId&gt;			&lt;artifactId&gt;ojdbc14&lt;/artifactId&gt;			&lt;!--当然，也可以覆盖父类的version。--&gt;			&lt;version&gt;10.2.0.3.0&lt;/version&gt;		&lt;/dependency&gt;		&lt;!-- Hibernat 4 框架 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;			&lt;artifactId&gt;hibernate-core&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;			&lt;artifactId&gt;hibernate-validator&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;			&lt;artifactId&gt;hibernate-entitymanager&lt;/artifactId&gt;			&lt;scope&gt;provided&lt;/scope&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;junit&lt;/groupId&gt;			&lt;artifactId&gt;junit&lt;/artifactId&gt;			&lt;scope&gt;test&lt;/scope&gt;		&lt;/dependency&gt;		&lt;!-- Spring 核心库 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-context&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-core&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-beans&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-orm&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-aop&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-tx&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- Spring MVC 库 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework&lt;/groupId&gt;			&lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.aspectj&lt;/groupId&gt;			&lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework.data&lt;/groupId&gt;			&lt;artifactId&gt;spring-data-jpa&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- 日志打印 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.slf4j&lt;/groupId&gt;			&lt;artifactId&gt;slf4j-log4j12&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;log4j&lt;/groupId&gt;			&lt;artifactId&gt;log4j&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;!-- 数据库连接池 --&gt;		&lt;dependency&gt;			&lt;groupId&gt;commons-dbcp&lt;/groupId&gt;			&lt;artifactId&gt;commons-dbcp&lt;/artifactId&gt;		&lt;/dependency&gt;	&lt;/dependencies&gt;&lt;/project&gt;
3.3. 聚合和继承的关系从上面两个pom.xml文件中，我们看到，模块聚合是写在父工程中的，而模块的继承则是写在子工程中的。
区别 ：1、对于聚合模块来说，它知道有哪些被聚合的模块，但那些被聚合的模块不知道这个聚合模块的存在。2、对于继承关系的父POM来说，它不知道有哪些子模块继承于它，但那些子模块都必须知道自己的父POM是什么。
共同点 ：1、聚合POM与继承关系中的父POM的packaging都是pom。2、聚合模块与继承关系中的父模块除了POM之外都没有实际的内容。注：在现有的实际项目中，一个POM既是聚合POM，又是父POM，这么做主要是为了方便。
3.4. 新建包新建包com.voidking.book.entity、com.voidking.book.repository、com.voidking.book.service、com.voidking.book.service.impl。这样命名，比较规范。
其中，entity是数据映射层（有人习惯命名为domain，郝同学还是喜欢entity）；repository是一个独立的层，介于领域层与数据映射层（数据访问层）之间。它的存在让领域层感觉不到数据访问层的存在，它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository是仓库管理员，领域层需要什么东西只需告诉仓库管理员，由仓库管理员把东西拿给它，并不需要知道东西实际放在哪；service对业务逻辑层提供的访问接口；service.impl是对访问接口的实现。
3.5. entity包在entity包下，新建Admin.java文件，内容如下。
package com.voidking.book.entity;import javax.persistence.Column;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.Table;@Entity@Table(name=&quot;admin&quot;)public class Admin &#123;		@Id	@GeneratedValue	private Long id;		@Column(length=20,nullable=false)	private String name;		@Column(length=20,nullable=false)	private String pwd;	public Long getId() &#123;		return id;	&#125;	public void setId(Long id) &#123;		this.id = id;	&#125;	public String getName() &#123;		return name;	&#125;	public void setName(String name) &#123;		this.name = name;	&#125;	public String getPwd() &#123;		return pwd;	&#125;	public void setPwd(String pwd) &#123;		this.pwd = pwd;	&#125;		public Admin() &#123;		super();	&#125;	public Admin(String name, String pwd) &#123;		super();		this.name = name;		this.pwd = pwd;	&#125;	@Override	public int hashCode() &#123;		final int prime = 31;		int result = 1;		result = prime * result + ((id == null) ? 0 : id.hashCode());		return result;	&#125;	@Override	public boolean equals(Object obj) &#123;		if (this == obj)			return true;		if (obj == null)			return false;		if (getClass() != obj.getClass())			return false;		Admin other = (Admin) obj;		if (id == null) &#123;			if (other.id != null)				return false;		&#125; else if (!id.equals(other.id))			return false;		return true;	&#125;		&#125;
1、@Entity注解标识实体Bean。2、@Table(name=数据库表名)注解，标识该实体Bean映射到关系型数据库中的表名3、如果不指定则JPA实现会自动生成默认的表名称。4、@Id注解标识主键。5、@GeneratedValue注解标识主键生成策略，@GeneratedValue(strategy=GenerationType.AUTO)是默认策略，相当于@GeneratedValue或者省略。

TABLE：使用一个特定的数据库表格来保存主键。 
SEQUENCE：根据底层数据库的序列来生成主键，条件是数据库支持序列。（适用于oracle） 
IDENTITY：主键由数据库自动生成（主要是自动增长型，适用于sqlserver，mysql数据库中）
AUTO：主键由程序控制。 

6、@Column注解有name、unique、nullable、length参数。若不写@Column注解，则一切使用@Column注解的默认值。
在entity下，新建BookBase.java、BookKind.java、ReaderBase.java、ReaderKind.java、BorrowInfo.java，内容分别如下。
package com.voidking.book.entity;/** * BookBase *  */@Entity@Table(name=&quot;bookbase&quot;)public class BookBase &#123;	@Id	@GeneratedValue(strategy=GenerationType.AUTO)	private Long id;		@Column(length=100,nullable=false)	private String name;		@Column(length=100,nullable=false)	private String author;		@Column(length=100,nullable=false)	private String press;		@Column(length=50,nullable=true)	private String publishdate;		private Float price;		private Integer page;	@Column(length=100,nullable=true)	private String keyword;		@Column(length=50,nullable=true)	private String registerdate;		@Column(length=2,nullable=false)	private String borrowed;		@Lob	private String notice;		@ManyToOne(cascade=CascadeType.REFRESH)	@JoinColumn(name=&quot;bookkind&quot;)	private BookKind bookkind;		//setter和getter方法省略	&#125;
详细代码请移步https://github.com/voidking/bookmanage.git
1、JPA定义了one-to-one、one-to-many、many-to-one、many-to-many 4种关系。
对于数据库来说，通常在一个表中记录对另一个表的外键关联；对应到实体对象，持有关联数据的一方称为owning-side，另一方称为inverse-side。
为了编程的方便，我们经常会希望在inverse-side也能引用到owning-side的对象，此时就构建了双向关联关系。 在双向关联中，需要在inverse-side定义mappedBy属性，以指明在owning-side是哪一个属性持有的关联数据。
对关联关系映射的要点如下：



关系类型
Owning-Side
Inverse-Side



one-to-one
@OneToOne
@OneToOne(mappedBy=”othersideName”)


one-to-many/many-to-one
@ManyToOne
@OneToMany(mappedBy=”othersideName”)


many-to-many
@ManyToMany
@ManyToMany(mappedBy=”othersideName”)


关联关系还可以定制延迟加载和级联操作的行为（owning-side和inverse-side可以分别设置）：通过设置fetch=FetchType.LAZY 或 fetch=FetchType.EAGER来决定关联对象是延迟加载或立即加载。
通过设置cascade={options}可以设置级联操作的行为，其中options可以是以下组合：

CascadeType.MERGE 级联更新
CascadeType.PERSIST 级联保存
CascadeType.REFRESH 级联刷新
CascadeType.REMOVE 级联删除
CascadeType.ALL 级联上述4种操作

2、@JoinColumn(name=”bookkind”)注释本表中指向另一个表的外键。如果不指定name，它会自动帮你生成你指向这个类的类名加上下划线再加上id的列,也就是默认列名是:bookkind_id。
3.6. repository包在repository包下，新建AdminRepository.java，内容如下。
package com.voidking.book.repository;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;import com.voidking.book.entity.Admin;@Repositorypublic interface AdminRepository extends JpaRepository&lt;Admin,Long&gt;&#123;	Admin findByNameAndPwd(String name,String pwd);&#125;
Repository（资源库）：通过用来访问领域对象的一个类似集合的接口，在领域与数据映射层之间进行协调。这个叫法就类似于我们通常所说的DAO，在这里，我们就按照这一习惯把数据访问层叫RepositorySpring Data给我们提供几个Repository，基础的Repository提供了最基本的数据访问功能，其几个子接口则扩展了一些功能。它们的继承关系如下：

Repository：仅仅是一个标识，表明任何继承它的均为仓库接口类，方便Spring自动扫描识别 
CrudRepository：继承Repository，实现了一组CRUD相关的方法 
PagingAndSortingRepository：继承CrudRepository，实现了一组分页排序相关的方法 
JpaRepository：继承PagingAndSortingRepository，实现一组JPA规范相关的方法 
JpaSpecificationExecutor：比较特殊，不属于Repository体系，实现一组JPA Criteria查询相关的方法 
我们自己定义的XxxxRepository需要继承JpaRepository，这样我们的XxxxRepository接口就具备了通用的数据访问控制层的能力。 

1、CrudRepository&lt;T, ID extends Serializable&gt;：这个接口提供了最基本的对实体类的添删改查操作 
T save(T entity);//保存单个实体 Iterable&lt;T&gt; save(Iterable&lt;? extends T&gt; entities);//保存集合 T findOne(ID id);//根据id查找实体 boolean exists(ID id);//根据id判断实体是否存在 Iterable&lt;T&gt; findAll();//查询所有实体,不用或慎用! long count();//查询实体数量 void delete(ID id);//根据Id删除实体 void delete(T entity);//删除一个实体 void delete(Iterable&lt;? extends T&gt; entities);//删除一个实体的集合 void deleteAll();//删除所有实体,不用或慎用! 

2、PagingAndSortingRepository&lt;T, ID extends Serializable&gt;：这个接口提供了分页与排序功能 
Iterable&lt;T&gt; findAll(Sort sort);//排序 Page&lt;T&gt; findAll(Pageable pageable);//分页查询（含排序功能） 

3、JpaRepository&lt;T, ID extends Serializable&gt;：这个接口提供了JPA的相关功能
List&lt;T&gt; findAll();//查找所有实体 List&lt;T&gt; findAll(Sort sort);//排序 查找所有实体 List&lt;T&gt; save(Iterable&lt;? extends T&gt; entities);//保存集合 void flush();//执行缓存与数据库同步 T saveAndFlush(T entity);//强制执行持久化 void deleteInBatch(Iterable&lt;T&gt; entities);//删除一个实体集合 


1、简单条件查询:查询某一个实体类或者集合按照Spring data 定义的规则，查询方法以find|read|get开头 
2、涉及条件查询时，条件的属性用条件关键字连接，要注意的是：条件属性以首字母大写其余字母小写为规定。例如：定义一个Entity实体类class User｛    private String firstname;    private String lastname;｝使用And条件连接时，应这样写：findByLastnameAndFirstname(String lastname,String firstname);条件的属性名称与个数要与参数的位置与个数一一对应。
新建BookBaseRepository.java、BookKindRepository.java、ReaderBaseRepository.java、ReaderKindRepository.java、BorrowInfoRepository.java，详细代码请移步https://github.com/voidking/bookmanage.git
3.7. service包在service包中，新建BaseService.java，内容如下。
package com.voidking.book.service.impl;import java.io.Serializable;import java.util.List;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import org.springframework.data.jpa.repository.JpaRepository;public interface BaseService&lt;T,ID extends Serializable&gt; &#123;	public boolean isExists(ID id);		public T save(T t);		public void delete(ID id);		public void delete(T t);		public List&lt;T&gt; findAll();		public Page&lt;T&gt; findAll(Pageable pageable);		public Page&lt;T&gt; findAll(int page,int size);		public long count();		public T findOne(ID id);		void setJpaRepository(JpaRepository&lt;T, ID&gt; repository);&#125;
新建AdminService.java，内容如下。
package com.voidking.book.service.impl;import com.voidking.book.entity.Admin;public interface AdminService extends BaseService&lt;Admin,Long&gt; &#123;		Admin findByNameAndPwd(String name,String pwd);&#125;
问：我们只要entity包、repository包，就可以提供对logic层的服务了，为什么加一个service包呢？答：信息隐藏，封装实现细节！有了service负责给logic提供服务，那么，我们就不知道底层是用repository还是其他方法实现。
新建BookBaseService.java、BookKindService.java、ReaderBaseService.java、ReaderKindService.java、BorrowInfoService.java，详细代码请移步https://github.com/voidking/bookmanage.git
3.8. service.impl包在service.impl包下，新建BaseServiceImpl.java，内容如下。
package com.voidking.book.service.impl;import java.io.Serializable;import java.util.List;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Pageable;import org.springframework.data.jpa.repository.JpaRepository;import com.voidking.book.service.BaseService;public class BaseServiceImpl&lt;T,ID extends Serializable&gt; implements BaseService&lt;T, ID&gt;&#123;	protected JpaRepository&lt;T, ID&gt; repository;		public boolean isExists(ID id) &#123;				return repository.exists(id);	&#125;	public T save(T t) &#123;				return repository.save(t);	&#125;	public void setJpaRepository(JpaRepository&lt;T, ID&gt; repository) &#123;				this.repository = repository;	&#125;	public void delete(ID id) &#123;				this.repository.delete(id);	&#125;	public void delete(T t) &#123;				this.repository.delete(t);			&#125;	public List&lt;T&gt; findAll() &#123;				return this.repository.findAll();	&#125;	public Page&lt;T&gt; findAll(Pageable pageable) &#123;				return this.repository.findAll(pageable);	&#125;	public long count() &#123;				return this.repository.count();	&#125;	public T findOne(ID id) &#123;				return this.repository.findOne(id);	&#125;	public Page&lt;T&gt; findAll(int page, int size) &#123;				return this.repository.findAll(new PageRequest(page, size));	&#125;&#125;
新建AdminServiceImpl.java，内容如下。
package com.voidking.book.service.impl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Service;import com.voidking.book.entity.Admin;import com.voidking.book.repository.AdminRepository;import com.voidking.book.service.AdminService;@Servicepublic class AdminServiceImpl extends BaseServiceImpl&lt;Admin, Long&gt; implements AdminService&#123;	private AdminRepository adminRepository;	@Autowired	@Override	public void setJpaRepository(JpaRepository&lt;Admin, Long&gt; adminRepository) &#123;		super.repository = adminRepository;		this.adminRepository=(AdminRepository) adminRepository;	&#125;	public Admin findByNameAndPwd(String name, String pwd) &#123;		return this.adminRepository.findByNameAndPwd(name, pwd);	&#125;	&#125;
1、Spring2.5引入了@Autowired注释，它可以对类成员变量、方法及构造函数进行标注，完成自动装配的工作。不过，现在更推荐使用@Resource。
2、Spring不但支持自己定义的@Autowired注解，还支持几个由JSR-250规范定义的注解，它们分别是@Resource、@PostConstruct以及@PreDestroy。@Resource的作用相当于@Autowired，只不过@Autowired按byType自动注入，而@Resource默认按byName自动注入罢了。
新建BookBaseServiceImpl.java、BookKindServiceImpl.java、ReaderBaseServiceImpl.java、ReaderKindServiceImpl.java、BorrowInfoServiceImpl.java，详细代码请移步https://github.com/voidking/bookmanage.git
3.9. spring-persist.xml在src/test/resources文件夹下，新建spring-persist.xml文件，内容如下。
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;	xmlns:cache=&quot;http://www.springframework.org/schema/cache&quot;	xmlns:context=&quot;http://www.springframework.org/schema/context&quot; xmlns:p=&quot;http://www.springframework.org/schema/p&quot;	xmlns:jpa=&quot;http://www.springframework.org/schema/data/jpa&quot; xmlns:task=&quot;http://www.springframework.org/schema/task&quot;	xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;	xsi:schemaLocation=&quot;http://www.springframework.org/schema/aop 	http://www.springframework.org/schema/aop/spring-aop-3.2.xsd    http://www.springframework.org/schema/task     http://www.springframework.org/schema/task/spring-task-3.2.xsd    http://www.springframework.org/schema/beans     http://www.springframework.org/schema/beans/spring-beans.xsd    http://www.springframework.org/schema/cache     http://www.springframework.org/schema/cache/spring-cache-3.2.xsd    http://www.springframework.org/schema/tx     http://www.springframework.org/schema/tx/spring-tx-3.2.xsd    http://www.springframework.org/schema/context     http://www.springframework.org/schema/context/spring-context-3.2.xsd 	http://www.springframework.org/schema/data/jpa	http://www.springframework.org/schema/data/jpa/spring-jpa.xsd&quot;&gt;	&lt;context:component-scan base-package=&quot;com.voidking.book&quot; /&gt;	&lt;!-- 数据库属性文件 --&gt;	&lt;bean id=&quot;propertyConfigure&quot;		class=&quot;org.springframework.beans.factory.config.PropertyPlaceholderConfigurer&quot;&gt;		&lt;property name=&quot;location&quot; value=&quot;classpath:database-conn.properties&quot; /&gt;	&lt;/bean&gt;	&lt;!-- 配置MySQL数据源 --&gt;	&lt;bean id=&quot;dataSource&quot; class=&quot;org.apache.commons.dbcp.BasicDataSource&quot;		destroy-method=&quot;close&quot;&gt;		&lt;property name=&quot;driverClassName&quot; value=&quot;$&#123;database.driver&#125;&quot; /&gt;		&lt;property name=&quot;url&quot; value=&quot;$&#123;database.uri&#125;&quot; /&gt;		&lt;property name=&quot;username&quot; value=&quot;$&#123;database.username&#125;&quot; /&gt;		&lt;property name=&quot;password&quot; value=&quot;$&#123;database.password&#125;&quot; /&gt;	&lt;/bean&gt;	&lt;!-- JPA 配置 --&gt;	&lt;jpa:repositories base-package=&quot;com.voidking.book.repository&quot;&gt;&lt;/jpa:repositories&gt;	&lt;bean id=&quot;entityManagerFactory&quot;		class=&quot;org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean&quot;&gt;		&lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot; /&gt;		&lt;property name=&quot;packagesToScan&quot; value=&quot;com.voidking.book.entity&quot; /&gt;		&lt;property name=&quot;jpaVendorAdapter&quot;&gt;			&lt;bean class=&quot;org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter&quot;&gt;				&lt;property name=&quot;showSql&quot; value=&quot;$&#123;hibernate.show_sql&#125;&quot; /&gt;				&lt;property name=&quot;databasePlatform&quot; value=&quot;$&#123;hibernate.dialect&#125;&quot; /&gt;				&lt;!--  &lt;property name=&quot;database&quot; value=&quot;$&#123;database.name&#125;&quot; /&gt;--&gt;			&lt;/bean&gt;		&lt;/property&gt;		&lt;property name=&quot;jpaProperties&quot;&gt;			&lt;props&gt;				&lt;prop key=&quot;hibernate.hbm2ddl.auto&quot;&gt;update&lt;/prop&gt;				&lt;prop key=&quot;hibernate.connection.useUnicode&quot;&gt;true&lt;/prop&gt;				&lt;prop key=&quot;hibernate.connection.characterEncoding&quot;&gt;UTF-8&lt;/prop&gt;				&lt;prop key=&quot;hibernate.connection.charSet&quot;&gt;UTF-8&lt;/prop&gt;			&lt;/props&gt;		&lt;/property&gt;	&lt;/bean&gt;	&lt;!-- 事务处理 --&gt;	&lt;bean id=&quot;transactionManager&quot; class=&quot;org.springframework.orm.jpa.JpaTransactionManager&quot;&gt;		&lt;property name=&quot;entityManagerFactory&quot; ref=&quot;entityManagerFactory&quot; /&gt;		&lt;property name=&quot;jpaDialect&quot;&gt;			&lt;bean class=&quot;org.springframework.orm.jpa.vendor.HibernateJpaDialect&quot;&gt;&lt;/bean&gt;		&lt;/property&gt;	&lt;/bean&gt;	&lt;tx:advice id=&quot;txAdvice&quot; transaction-manager=&quot;transactionManager&quot;&gt;		&lt;tx:attributes&gt;			&lt;!-- hibernate4必须配置为开启事务 否则 getCurrentSession()获取不到 --&gt;			&lt;tx:method name=&quot;find*&quot; propagation=&quot;REQUIRED&quot; read-only=&quot;true&quot; /&gt;			&lt;tx:method name=&quot;update*&quot; propagation=&quot;REQUIRED&quot; /&gt;			&lt;tx:method name=&quot;delete*&quot; propagation=&quot;REQUIRED&quot; /&gt;			&lt;tx:method name=&quot;save*&quot; propagation=&quot;REQUIRED&quot; /&gt;			&lt;tx:method name=&quot;*&quot; propagation=&quot;SUPPORTS&quot; /&gt;		&lt;/tx:attributes&gt;	&lt;/tx:advice&gt;	&lt;tx:annotation-driven transaction-manager=&quot;transactionManager&quot; /&gt;	&lt;!-- 事务处理配置完毕 --&gt;	&lt;aop:aspectj-autoproxy expose-proxy=&quot;true&quot; /&gt;	&lt;aop:config expose-proxy=&quot;true&quot;&gt;		&lt;aop:pointcut id=&quot;baseBizMethods&quot;			expression=&quot;execution(public * com.voidking.book.service.impl.*.*(..))&quot; /&gt;		&lt;aop:advisor pointcut-ref=&quot;baseBizMethods&quot; advice-ref=&quot;txAdvice&quot; /&gt;	&lt;/aop:config&gt;&lt;/beans&gt;
1、如果配置了context:component-scan那么context:annotation-config标签就可以不用在xml中配置了，因为前者包含了后者。在xml配置了前者后，spring可以自动去扫描base-pack下面或者子包下面的java文件，如果扫描到有@Component、@Controller、@Service等这些注解的类，则把这些类注册为bean。
2、PropertyPlaceholderConfigurer用于Spring从外部属性文件中载入属性，并使用这些属性值替换Spring 配置文件中的占位符变量（${varible}）。 
3、Spring配置文件中关于事务配置总是由三个组成部分，分别是DataSource、TransactionManager和代理机制这三部分，无论哪种配置方式，一般变化的只是代理机制这部分。
3.10. database-conn.properties在src/test/resources文件夹下，新建database-conn.properties文件，内容如下。
database.name=MYSQLdatabase.driver=com.mysql.jdbc.Driverdatabase.uri=jdbc:mysql://localhost:3306/bookdatabase.username=rootdatabase.password=voidking #hibernatehibernate.dialect=org.hibernate.dialect.MySQL5Dialecthibernate.show_sql=true
在mysql数据库中，新建book数据库。
3.11. log4j.properties在src/test/resources文件夹下，新建log4j.properties文件，内容如下。
log4j.rootCategory=INFO, stdout###. \u5b9a\u4e49\u540d\u4e3a stdout \u7684\u8f93\u51fa\u7aef\u7684\u7c7b\u578b log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=[Project] %p [%t] %C.%M(%L) | %m%n log4j.appender.stdout.encoding=UTF-8### .spring \u914d\u7f6e log4j.logger.org.springframework=ERROR ### . hibernate \u914d\u7f6e log4j.logger.org.hibernate=ERROR
log4j启动时，默认会寻找source folder下的log4j.xml配置文件，若没有，会寻找log4j.properties文件。然后加载配置。配置文件放置位置正确，不用在程序中手动加载log4j配置文件。如果将配置文件放到了config文件夹下，在build Path中设置下就好了。
3.12. 单元测试在src/test/java文件夹下，新建包com.voidking.book.service，新建AdminServiceTest.java文件，内容如下。
package com.voidking.book.service;import org.junit.Before;import org.junit.Test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class AdminServiceTest &#123;	private AdminService adminService;	@Before	public void prepare()&#123;		ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;spring-persist.xml&quot;);		this.adminService = ctx.getBean(AdminService.class);			&#125;	@Test	public void testAdminService()&#123;			&#125;&#125;

上面的测试，主要用来加载配置文件，检验配置文件是否正确，以及数据库的连接是否正常。
4. 后记@Repository、@Service、@Controller和@Component都可以不用在xml文件里进行显式配置，他们都会被默认注册为Spring Bean。
5. 参考文档JPA入门例子(采用JPA的hibernate实现版本)http://blog.csdn.net/hmk2011/article/details/6289151
Spring声明式事务配置管理方法http://www.cnblogs.com/rushoooooo/archive/2155960.html
@Repository、@Service、@Controller 和 @Componenthttp://blog.csdn.net/ye1992/article/details/19971467
Spring组件扫描&lt;context:component-scan/&gt;使用详解http://blog.csdn.net/a9529lty/article/details/8251003
 &lt;context:component-scan&gt;使用说明http://blog.csdn.net/chunqiuwei/article/details/16115135
使用注解来构造IoC容器http://www.cnblogs.com/xdp-gacl/p/3495887.html
使用Spring Data JPA 简化 JPA 开发http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-jpa/
实用的代码规范–SpringSide Coding Standardshttp://blog.chinaunix.net/uid-9354-id-2425025.html
Repository（资源库）模式http://blog.csdn.net/happinessmoon/article/details/7708785http://www.cnblogs.com/dudu/archive/repository_pattern.html
@GeneratedValuehttp://blog.csdn.net/fancylovejava/article/details/7438660
JPA常用注解http://1194867672-qq-com.iteye.com/blog/1730513
@JoinColumn详解http://blog.sina.com.cn/s/blog_64e8d29b0100z7hx.html
hibernate的注解属性mappedBy详解http://shenyuc629.iteye.com/blog/1681225
JPA概要http://www.cnblogs.com/holbrook/archive/2839842.html
主键中mappedBy的具体使用及其含义http://blog.sina.com.cn/s/blog_697b968901016s7f.html
简单的Spring JPA实现例子http://blog.csdn.net/kongxx/article/details/5653370
spring mvc的jpa JpaRepository数据层访问方式汇总http://jishiweili.iteye.com/blog/2088265
Spring @Autowired详解http://my.oschina.net/u/138995/blog/181626?fromerr=x1lK5xJq
Spring注解http://www.360doc.cn/article/495229_37264450.html
Spring注解讲解http://www.douban.com/note/71602488/
Spring及springmvc注解(annotation)使用详解http://www.360sdn.com/springmvc/2013/0627/407.html
context:component-scan使用说明http://blog.csdn.net/chunqiuwei/article/details/16115135
Spring配置之PropertyPlaceholderConfigurerhttp://bjyzxxds.iteye.com/blog/427437
Spring事务Transaction配置的五种注入方式详解http://blog.csdn.net/yaerfeng/article/details/28390773
log4j，如何“自动加载”？http://www.cnblogs.com/alipayhutu/archive/3028249.html
log4j.properties配置与加载应用http://blog.csdn.net/javaloveiphone/article/details/7994313
log4j.properties路径问题http://blog.csdn.net/caomiao2006/article/details/22062001
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>maven</tag>
        <tag>jpa</tag>
      </tags>
  </entry>
  <entry>
    <title>Spring JPA</title>
    <url>/dev-spring-jpa/</url>
    <content><![CDATA[1. JPA例子（h2database版）这个例子高仿照搬Spring官网给出的例子，只是包不同而已。
1.1. 新建工程Eclipse，File，New，Maven Project，Create a simple project打上勾，Next。
Group Id输入反向域名加上工程名，Artifact Id输入子工程名，Packaging处选择jar（意思是普通工程；如果是web工程，选择war；如果用来定义父工程，选择pom）。


1.2. pom.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;	xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd&quot;	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&gt;	&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;	&lt;groupId&gt;com.voidking.book&lt;/groupId&gt;	&lt;artifactId&gt;book-jpa&lt;/artifactId&gt;	&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;	&lt;parent&gt;		&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;		&lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;		&lt;version&gt;1.2.5.RELEASE&lt;/version&gt;	&lt;/parent&gt;	&lt;properties&gt;		&lt;java.version&gt;1.8&lt;/java.version&gt;	&lt;/properties&gt;	&lt;dependencies&gt;		&lt;dependency&gt;			&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;			&lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;		&lt;/dependency&gt;		&lt;dependency&gt;			&lt;groupId&gt;com.h2database&lt;/groupId&gt;			&lt;artifactId&gt;h2&lt;/artifactId&gt;		&lt;/dependency&gt;	&lt;/dependencies&gt;	&lt;build&gt;		&lt;plugins&gt;			&lt;plugin&gt;				&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;				&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;			&lt;/plugin&gt;		&lt;/plugins&gt;	&lt;/build&gt;	&lt;repositories&gt;		&lt;repository&gt;			&lt;id&gt;spring-releases&lt;/id&gt;			&lt;name&gt;Spring Releases&lt;/name&gt;			&lt;url&gt;https://repo.spring.io/libs-release&lt;/url&gt;		&lt;/repository&gt;		&lt;repository&gt;			&lt;id&gt;org.jboss.repository.releases&lt;/id&gt;			&lt;name&gt;JBoss Maven Release Repository&lt;/name&gt;			&lt;url&gt;https://repository.jboss.org/nexus/content/repositories/releases&lt;/url&gt;		&lt;/repository&gt;	&lt;/repositories&gt;	&lt;pluginRepositories&gt;		&lt;pluginRepository&gt;			&lt;id&gt;spring-releases&lt;/id&gt;			&lt;name&gt;Spring Releases&lt;/name&gt;			&lt;url&gt;https://repo.spring.io/libs-release&lt;/url&gt;		&lt;/pluginRepository&gt;	&lt;/pluginRepositories&gt;&lt;/project&gt;

1.3. Customer.javasrc/main/java/com/voidking/book/entity/Customer.java
package com.voidking.book.entity;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;@Entitypublic class Customer &#123;    @Id    @GeneratedValue(strategy=GenerationType.AUTO)    private long id;    private String firstName;    private String lastName;    protected Customer() &#123;&#125;    public Customer(String firstName, String lastName) &#123;        this.firstName = firstName;        this.lastName = lastName;    &#125;    @Override    public String toString() &#123;        return String.format(                &quot;Customer[id=%d, firstName=&#x27;%s&#x27;, lastName=&#x27;%s&#x27;]&quot;,                id, firstName, lastName);    &#125;&#125;

1.4. CustomerRepository.javasrc/main/java/com/voidking/book/repository/Customer.java
package com.voidking.book.repository;import java.util.List;import org.springframework.data.repository.CrudRepository;import com.voidking.book.entity.Customer;public interface CustomerRepository extends CrudRepository&lt;Customer, Long&gt; &#123;    List&lt;Customer&gt; findByLastName(String lastName);&#125;
Repository（资源库）：通过用来访问领域对象的一个类似集合的接口，在领域与数据映射层之间进行协调。这个叫法就类似于我们通常所说的DAO，在这里，我们就按照这一习惯把数据访问层叫Repository Spring Data，基础的Repository提供了最基本的数据访问功能，其几个子接口则扩展了一些功能。它们的继承关系如下：Repository：仅仅是一个标识，表明任何继承它的均为仓库接口类，方便Spring自动扫描识别CrudRepository： 继承Repository，实现了一组CRUD相关的方法PagingAndSortingRepository： 继承CrudRepository，实现了一组分页排序相关的方法JpaRepository： 继承PagingAndSortingRepository，实现一组JPA规范相关的方法JpaSpecificationExecutor： 比较特殊，不属于Repository体系，实现一组JPA Criteria查询相关的方法我们自己定义的XxxxRepository需要继承JpaRepository，这样我们的XxxxRepository接口就具备了通用的数据访问控制层的能力。 
1.5. Application.javasrc/main/java/com/voidking/book/app/Application.java
package com.voidking.book.app;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.CommandLineRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import com.voidking.book.entity.Customer;import com.voidking.book.repository.CustomerRepository;@SpringBootApplicationpublic class Application implements CommandLineRunner &#123;    @Autowired    CustomerRepository repository;    public static void main(String[] args) &#123;        SpringApplication.run(Application.class);    &#125;        @Override    public void run(String... strings) throws Exception &#123;        // save a couple of customers        repository.save(new Customer(&quot;Jack&quot;, &quot;Bauer&quot;));        repository.save(new Customer(&quot;Chloe&quot;, &quot;O&#x27;Brian&quot;));        repository.save(new Customer(&quot;Kim&quot;, &quot;Bauer&quot;));        repository.save(new Customer(&quot;David&quot;, &quot;Palmer&quot;));        repository.save(new Customer(&quot;Michelle&quot;, &quot;Dessler&quot;));        // fetch all customers        System.out.println(&quot;Customers found with findAll():&quot;);        System.out.println(&quot;-------------------------------&quot;);        for (Customer customer : repository.findAll()) &#123;            System.out.println(customer);        &#125;        System.out.println();        // fetch an individual customer by ID        Customer customer = repository.findOne(1L);        System.out.println(&quot;Customer found with findOne(1L):&quot;);        System.out.println(&quot;--------------------------------&quot;);        System.out.println(customer);        System.out.println();        // fetch customers by last name        System.out.println(&quot;Customer found with findByLastName(&#x27;Bauer&#x27;):&quot;);        System.out.println(&quot;--------------------------------------------&quot;);        for (Customer bauer : repository.findByLastName(&quot;Bauer&quot;)) &#123;            System.out.println(bauer);        &#125;    &#125;&#125;


1.6. mvn test异常执行命令mvn test，抛出异常：
Exception in thread &quot;main&quot; java.lang.UnsupportedClassVersionError:com/voidking/book/app/Application : Unsupported major.minor version 52.0
这个异常，是因为本机没有安装jdk8，我就把Properties-&gt;java build path-&gt;Libraries中的JVM8换成JVM7。
解决方法：右击项目，Properties，Java Compiler，Compiler compliance level从1.8改成1.7，之后就可以运行了。
1.7. 运行异常mvn spring-boot:run，异常：
org.springframework.beans.factory.BeanCreationException: Error creating bean with name &#x27;application&#x27;: Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: com.voidking.book.repository.CustomerRepository com.voidking.book.app.Application.repository; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.voidking.book.repository.CustomerRepository] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: &#123;@org.springframework.beans.factory.annotation.Autowired(required=true)&#125;
可以看到，异常层层连锁，看起来那么长，实际上重点在后面。原来是缺少CustomerRepository类型的bean，那么，为什么缺少呢？是否是因为需要一个类似spring.xml的配置文件？
1.8. Spring官方代码看看Spring官方给出的例子，确实没有配置文件。mvn spring-boot:run，结果如下：运行成功，可见，这个例子是没有任何问题的。那么，问题出在哪里？因为分了不同的包？
试一试，把所有.java移动到一个包中，运行。。。成功！
1.9. 醉了从中午12点忙到晚上12点，此问题没有解决，我也是醉了。。。
1.10. spring-boot第二天，继续查找资料，终于明白了，这一切都是spring-boot搞的鬼。
spring-boot会自动配置通过jpa进行数据访问需要的bean。
spring-boot会根据classpath包含的内容自动推测用户的需求并自动配置。例如如果在classpath包含了hsqldb，并且用户未配置数据库连接，spring-boot将会配置一个hsqldb内存数据库和数据源。
spring-boot无代码生成，所有的配置可通过代码完成（spring 的javaconfig），不需要使用xml（虽然可以使用）。 
2. JPA例子（hsql版）h2database和hsql非常类似，适合作为嵌入式数据库使用，其它的数据库大部分都需要安装独立的客户端和服务器端。顺便比较一下各个数据库，如下图：
name属性用于定义持久化单元的名字(name必选,空值也合法);transaction-type指定事务类型(可选)  
3. JPA例子（Mysql版）4. 源代码分享Spring官方例子git clone https://github.com/spring-guides/gs-accessing-data-jpa.git
5. 后记使用Spring，和单纯的JPA有什么区别？

多了一些标记为Bean的注解，比如@Repository、@Service、@Controller和@Component。

JAP有持久化配置文件persistence.xml（src/META-INF），默认加载。在这个文件中，可以配置数据库连接信息，实体类等；使用了Spring，除了persistence.xml，还多出了一个或多个Spring的配置文件，配置文件间可以通过&lt;import&gt;标签引用；persistence.xml中的内容，可以改写到spring的配置文件中，如果内容全部改写，此时persistence.xml文件可以省略。


6. 参考文档Accessing Data with JPAhttp://spring.io/guides/gs/accessing-data-jpa/
Spring Data JPA - Reference Documentationhttp://docs.spring.io/spring-data/data-jpa/docs/current/reference/html/
使用 Spring Data JPA 简化 JPA 开发http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-jpa/
简单的Spring JPA实现例子http://blog.csdn.net/kongxx/article/details/5653370
spring mvc 的jpa JpaRepository数据层 访问方式汇总http://jishiweili.iteye.com/blog/2088265
使用spring-boot快速开发spring应用http://itindex.net/detail/49108-spring-boot-%E5%BC%80%E5%8F%91
JPA 不在 persistence.xml 文件中配置每个Entity实体类的2种解决办法http://www.cnblogs.com/taven/archive/3351841.html
Jpa规范中persistence.xml 配置文件解析http://www.cnblogs.com/edwardlauxh/archive/2632292.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>spring</tag>
        <tag>jpa</tag>
      </tags>
  </entry>
  <entry>
    <title>JPA基础</title>
    <url>/dev-jpa-base/</url>
    <content><![CDATA[1. 简介Java持久化规范，是从EJB2.x以前的实体Bean(Entity bean)分离出来的，EJB3以后不再有实体bean，而是将实体bean放到JPA中实现。JPA是sun提出的一个对象持久化规范，各JavaEE应用服务器自主选择具体实现，JPA的设计者是Hibernate框架的作者，因此Hibernate作为Jboss服务器中JPA的默认实现，Oracle的Weblogic使用EclipseLink(以前叫TopLink)作为默认的JPA实现，IBM的Websphere和Sun的Glassfish默认使用OpenJPA(Apache的一个开源项目)作为其默认的JPA实现。
JPA的底层实现是一些流行的开源ORM(对象关系映射)框架，因此JPA其实也就是java实体对象和关系型数据库建立起映射关系，通过面向对象编程的思想操作关系型数据库的规范。


2. JPA的持久化策略文件根据JPA规范要求，在实体bean应用中，需要在应用类路径(classpath)的META-INF目录下加入持久化配置文件——persistence.xml，该文件就是持久化策略文件。其内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;persistence version=&quot;1.0&quot; xmlns=&quot;http://java.sun.com/xml/ns/persistence&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd&quot;&gt;	&lt;persistence-unit name=&quot;持久化单元名&quot; transaction-type=&quot;JTA&quot;&gt;		&lt;jta-data-source&gt;使用JCA部署在javaEE服务器上的数据源JNDI&lt;/jta-data-source&gt;        &lt;!--指定JPA实现提供者，不指定就采用JavaEE服务器默认的JPA提供者，这里以OpenJPA为例--&gt;	&lt;provider&gt;org.apache.openjpa.persistence.PersistenceProviderImpl&lt;/provider&gt;		&lt;class&gt;实体bean全路径名&lt;/class&gt;		……		&lt;properties&gt;            &lt;!--配置JPA实现提供者的一些信息--&gt;		&lt;/properties&gt;	&lt;/persistence-unit&gt;&lt;/persistence&gt;
注意：可以在持久化策略文件中配置多个持久化单元persistence-unit，在使用分布式数据库时经常配置多个持久化单元。
3. 实体Bean的开发：JPA规范中定义的实体bean开发的基本规范：(1)在实体bean上添加”@Entity”注解，标识该bean为实体bean。(2)实体bean必须序列化。(3)使用”@Table(name=数据库表名)”注解，标识该实体bean映射到关系型数据库中的表名，如果不指定则JPA实现会自动生成默认的表名称。(4)必须有一个使用”@Id”注解标识的主键，如指定自增主键的写法为：
@Id@Column(name=”列名”)@GeneratedValue(Strategy=GenerationType.Auto)private int id;
注意：@Id和@GeneratedValue两个注解必须同时使用，标识了注解之后，要主键的生成策略。(5)Bean的其他属性使用”@Column(name=列名)”注解，指定该属性映射到数据库表中的列名，如果不指定在JPA实现会自动生成默认的列名。(6)实体Bean必须严格遵循JavaBean的规范，提供无参的默认构造方法，属性提供set和get方法。(7)最好重写hashcode()和equals()方法，实体bean的唯一标识是主键，因此，使用实体bean的主键来比较。
4. 实体bean的主键生成策略实体bean的注解生成策略使用”@GeneratedValue”注解来指定主键的生成策略，默认使用Auto自增的策略，JPA不支持Hibernate的UUID主键生成策略，若要使用Hibernate的UUID做主键方式，方法如下：(1)将Hibernate.annotation jar包加入到类路径下。(2)在使用”@Id”的字段上同时加入如下的注解：
@GeneratedValue(generator=”UUID主键生成策略”)@GenericGenerator(name=”UUID主键生成策略”, strategy=”uuid”)

5. JPA开发的注意事项JPA支持xml方式和注解方式，目前随着注解方式越来越成熟和流行，开发JPA时候，基本都只用注解方式，因此必须在JavaEE5以上版本才行，因为以前的版本没有对注解的支持。和xml文件相比，java注解的优缺点：优势：(1)描述符减少，大大提高开发效率。(2)编译期校验，错误的注解在编译期间就会报错。(3)注解在java代码中，从而避免了额外的文件维护工作。(4)注解被编译成java字节码，消耗的内存小，读取速度快，往往比xml配置文件解析快几个数量级，利用测试和维护。缺点：(1)配置信息分散，不利于集中维护管理。(2)改动时涉及到了程序源代码，需要找到类的源代码才可以，而且必须通过编译这一步。相比较之下xml文件可能不需要找到类源码，同时也不需要重新编译。
6. JPA注解的使用注意事项JPA的注解支持字段注解，也支持属性注解。字段注解：直接在变量上加注解。属性注解：在set/get方法上加注解。注意：不管使用哪种注解方式，在一个实体类中，不能混用两种注解方式。
7. JPA其他的常用注解除了@Entity、@Id、@Table、@Column、@GeneratedValue等注解以外，JPA还有以下常用的注解：(1)@Temporal：主要用于标注时间类型，在javax.persistence.TemporalType枚举中定义了3种时间类型：a.DATE：等同于java.sql.Date；b.TIME：等同于java.sql.Time；c.TIMESTAMP：等同于java.sql.Timestamp；(2)@Transient：实体bean中默认所有的字段都会被映射到数据库中，如果某个属性不想被映射到数据库中，则需要对其加该注解。(3)@Lob：将属性持久化为Blob或者Clob类型，为大数据类型。(4)@Basic：一般可以用来控制是否进行延迟加载，用法示例：延迟加载：@Basic(fetch=FetchType.Lazy)非延迟加载：@Basic(fetch=FetchType.EAGER)(5)@NamedQueryies和@NamedQuery：在实体Bean上定义命名查询。名称查询类似于jdbc中的PrepareStatement，是在数据库中预编译的查询，可以大大提高查询效率，用法如下：
@NamedQueries(&#123;       @NamedQuery(name=”命名查询名字”,query=JPQL查询语句),       ……&#125;)
(6)@OneToOne：一对一映射注解，双向的一对一关系需要在关系维护端(owner side)的@OneToOne注解中添加mappedBy属性，建表时在关系被维护端(inverse side)建立外键指向关系维护端的主键列。用法：@OneToOne(optional=true,casecade=CasecadeType.ALL,mappedBy=”被维护端外键”)(7)@OneToMany：一对多映射注解，双向一对多关系中，一端是关系维护端(owner side)，只能在一端添加mapped属性。多端是关系被维护端(inverse side)。建表时在关系被维护端(多端)建立外键指向关系维护端(一端)的主键列。用法：@OneToMany(mappedBy = “维护端(一端)主键”, cascade=CascadeType.ALL)注意：在Hibernate中有个术语叫做维护关系反转，即由对方维护关联关系，使用inverse=false来表示关系维护放，在JPA的注解中，mappedBy就相当于inverse=false，即由mappedBy来维护关系。(8)@ManyToOne：多对一映射注解，在双向的一对多关系中，一端一方使用@OneToMany注解，多端的一方使用@ManyToOne注解。多对一注解用法很简单，它不用维护关系。用法：@ManyToOne(optional = false, fetch = FetchType.EAGER)(9)@ManyToMany：多对多映射，采取中间表连接的映射策略，建立的中间关系表分别引入两边的主键作为外键，形成两个多对一关系。双向的多对多关系中，在关系维护端(owner side)的@ManyToMany注解中添加mappedBy属性，另一方是关系的被维护端(inverse side)，关系的被维护端不能加mappedBy属性，建表时，根据两个多端的主键生成一个中间表，中间表的外键是两个多端的主键。用法：关系维护端——&gt;@ManyToMany(mappedBy=”另一方的关系引用属性”)关系被维护端——&gt;@ManyToMany(cascade=CascadeType.ALL ,fetch = FetchType.Lazy)
8. JPA的一对一关联映射在JPA中两个实体之间是一一对应的关系称为一对一关联关系映射，如人和身份证号关系。(1)一对一单向关联映射：只能从映射端查找到随关联的一方，而不能反向查找。在关联映射方关联属性上添加@OneToOne注解。(2)一对一双向关联映射：可以双向查找关联关系的实体。关系维护端：在关联属性或字段上添加@OneToOne注解，同时制定@OneToOne注解的mappedBy属性。关系被维护端：在关联属性或字段上添加@OneToOne注解。
9. 一对一关联映射两种策略(1)一对一主键关联：一对一关联映射中，主键关联策略不会在两个关联实体对应的数据库表中添加外键字段，两个实体的表公用同一个主键(主键相同)，其中一个实体的主键既是主键又是外键。主键关联映射：在实体关联属性或方法上添加@OneToOne注解的同时添加@PrimaryKeyJoinColumn注解(在一对一注解关联映射的任意一端实体添加即可)。(2)一对一唯一外键关联：一对一关联关系映射中，唯一外键关联策略会在其中一个实体对应数据库表中添加外键字段指向另一个实体表的主键，也是一对一映射关系中最常用的映射策略。唯一外键关联：在关联属性或字段上添加@OneToOne注解的同时添加@JoinColumn(name=”数据表列名”，unique=true)注解。
10. 一对多关联映射在JPA中两个实体之间是一对多关系的称为一对多关联关系映射，如班级和学生关系。(1)一对多单向关联映射：在一对多单向关联映射中，JPA会在数据库中自动生成公有的中间表记录关联关系的情况。在一端关联集合属性或字段上添加@OneToMany注解即可。(2)一对多双向关联映射：在一对多双向关联映射中，JPA不会在数据库中生成公有中间表。在一端关联集合属性或字段上添加@OneToMany注解，同时指定其mappedBy属性。在多端关联属性或字段上添加@ManyToOne注解。注意：一对多关系映射中，mappedBy只能添加在OneToMany注解中，即在多端生成外键。
11. 多对多关联映射在JPA中两个实体之间是多对多关系的称为多对多关联关系映射，如学生和教师关系。(1)多对多单向映射：在其中任意实体一方关联属性或字段上添加@ManyToMany注解。(2)多对多双向映射：关系维护端关联属性或字段上添加@ManyToMany注解，同时指定该注解的mappedBy属性。关系被维护端关联属性或字段上添加@ManyToMany注解。
12. JPA中实体继承映射策略在JPA中，实体继承关系的映射策略共有三种：单表继承策略、Joined策略和Table_PER_Class策略。(1)单表继承策略：单表继承策略，父类实体和子类实体共用一张数据库表，在表中通过一列辨别字段来区别不同类别的实体。具体做法如下：a.在父类实体的@Entity注解下添加如下的注解：
@Inheritance(Strategy=InheritanceType.SINGLE_TABLE)@DiscriminatorColumn(name=”辨别字段列名”)@DiscriminatorValue(父类实体辨别字段列值)
b.在子类实体的@Entity注解下添加如下的注解：
@DiscriminatorValue(子类实体辨别字段列值)
(2)Joined策略：Joined策略，父类实体和子类实体分别对应数据库中不同的表，子类实体的表中只存在其扩展的特殊属性，父类的公共属性保存在父类实体映射表中。具体做法：只需在父类实体的@Entity注解下添加如下注解：
@Inheritance(Strategy=InheritanceType.JOINED)
子类实体不需要特殊说明。(3)Table_PER_Class策略：Table_PER_Class策略，父类实体和子类实体每个类分别对应一张数据库中的表，子类表中保存所有属性，包括从父类实体中继承的属性。具体做法：只需在父类实体的@Entity注解下添加如下注解：
@Inheritance(Strategy=InheritanceType.TABLE_PER_CLASS)
子类实体不需要特殊说明。
13. 参考文档JPA学习笔记1——JPA基础http://blog.csdn.net/chjttony/article/details/6086298
JPA学习笔记2——JPA高级http://blog.csdn.net/chjttony/article/details/6086305
JPA学习笔记http://www.blogjava.net/luyongfa/archive/390572.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>jpa</tag>
      </tags>
  </entry>
  <entry>
    <title>大众点评极客大赛</title>
    <url>/essay-dianping-geek/</url>
    <content><![CDATA[1. 前言前段时间，参加了大众点评的极客大赛初赛。线上编程排位赛，最初排名第六，第二次排名掉到第十，第三次排名明天出结果。没有关系了，今天收到大众点评的决赛通知，看来排名应该进了前六十。


2. 准备8月8日到8月10日，上海大众点评总部。
决赛第一名iPhone6 plus，第二名iPad Air，还有大众点评的录用意向书，很有吸引力，好好准备！
24小时的编程比赛，组队做一个Demo。以前的项目经验，估计可以派上用场，但是基本上都忘记了。这几天，Android暂时放下，复习一下以前的项目，搞一搞！
主要有三个项目值得重做：

图书管理系统
团队博客
微信公众平台iNJIT

3. 后记估计，可以遇到很多牛逼的小伙伴，真是令人期待！
4. 点评链接大众点评校园招聘http://campus.dianping.com/#!/index
点评极客挑战赛http://evt.dianping.com/event/campus15/pc.html
]]></content>
      <categories>
        <category>essay</category>
      </categories>
      <tags>
        <tag>随笔</tag>
      </tags>
  </entry>
  <entry>
    <title>开源镜像站</title>
    <url>/dev-open-source-mirror-site/</url>
    <content><![CDATA[1. 前言开源镜像站是一个放置开源系统镜像文件的站点，免费提供镜像文件下载。


2. Linux镜像kernel 镜像站： http://mirrors.kernel.org/CentOS 官方镜像站：http://mirror-status.centos.org/Fedora 官方镜像站：http://mirrors.fedoraproject.org/publiclistDebian 全球镜像站：http://www.debian.org/mirror/listUbuntu 官方镜像站：http://releases.ubuntu.com/releases/http://cdimage.ubuntu.com/suse官方镜像站：http://download.opensuse.org/RedHat镜像站：http://ftp.corbina.net/pub/Linux/redhat/Linux运维派开源镜像站：http://mirrors.skyshe.cn/


3. 企业机构开源镜像站阿里云开源镜像站：http://mirrors.aliyun.com/网易开源镜像站：http://mirrors.163.com/搜狐开源镜像站：http://mirrors.sohu.com/天翼云：http://mirrors.ctyun.cn/首都在线科技股份有限公司：http://mirrors.yun-idc.com/开源中国：http://mirrors.oschina.net/阿里云开源镜像：http://mirrors.aliyun.com/常州贝特康姆软件技术有限公司(原cn99）：http://centos.bitcomm.cn/开源世界：http://mirror.lupaworld.com/
4. 国内大学开源镜像站香港中文大学 ：http://ftp.cuhk.edu.hk/pub/Linux/台湾淡江大学：http://ftp.tku.edu.tw/Linux/电子科技大学: http://ubuntu.uestc.edu.cn/厦门大学：http://mirrors.xmu.edu.cn/浙江大学：http://mirrors.zju.edu.cn/东软信息学院：http://mirrors.neusoft.edu.cn/中山大学镜像：http://mirror.sysu.edu.cn/华中科技大学： http://mirrors.hustunique.com/大连理工大学：http://mirror.dlut.edu.cn/兰州大学：http://mirror.lzu.edu.cn/重庆大学：http://mirrors.cqu.edu.cn/北京理工大学：http://mirror.bit.edu.cn (IPv4 only)http://mirror.bit6.edu.cn (IPv6 only)北京交通大学：http://mirror.bjtu.edu.cn (IPv4 only)http://mirror6.bjtu.edu.cn (IPv6 only)http://debian.bjtu.edu.cn (IPv4+IPv6)上海交通大学：http://ftp.sjtu.edu.cn/ (IPv4 only)http://ftp6.sjtu.edu.cn (IPv6 only)清华大学：http://mirrors.tuna.tsinghua.edu.cn/ (IPv4+IPv6)http://mirrors.6.tuna.tsinghua.edu.cn/ (IPv6 only)http://mirrors.4.tuna.tsinghua.edu.cn/ (IPv4 only)中国科学技术大学：http://mirrors.ustc.edu.cn/ (IPv4+IPv6)http://mirrors4.ustc.edu.cn/http://mirrors6.ustc.edu.cn/东北大学：http://mirror.neu.edu.cn/ (IPv4 only)http://mirror.neu6.edu.cn/ (IPv6 only)华中科技大学：http://mirrors.hust.edu.cn/http://mirrors.hustunique.com/
5. 国外大学开源镜像站北陆先端科学技术大学院大学JAIST: http://ftp.jaist.ac.jp/pub/卡内基梅隆大学CMU: http://www.club.cc.cmu.edu/pub麻省理工学院MIT: http://mirrors.mit.edu/哥伦比亚大学: http://mirror.cc.columbia.edu/俄勒冈州立大学: http://ftp.osuosl.org/pub伊利诺伊大学厄巴纳-香槟分校: http://cosmos.cites.illinois.edu/杜克大学: http://archive.linux.duke.edu/约翰·霍普金斯大学: http://mirrors.acm.jhu.edu/俄罗斯镜像服务器：http://ftp.kddilabs.jp/http://ftp.jaist.ac.jp/pub/http://ftp.kaist.ac.kr/http://mirror.karneval.cz/pub/http://ftp.gwdg.de/pub/http://ftp.estpak.ee/pub/
6. 分类镜像服务器6.1. PyPi豆瓣：http://pypi.douban.com/中山大学：http://mirror.sysu.edu.cn/pypi/
6.2. RubyGems淘宝：http://ruby.taobao.org/中山大学：http://mirror.sysu.edu.cn/rubygems/
6.3. npmcnpmjs：http://cnpmjs.org/
7. 书签国内外开源镜像服务器站点汇总http://www.douban.com/note/375227086/
国内开源镜像站点汇总http://segmentfault.com/a/1190000000375848
中国Linux开源镜像站大全http://www.centoscn.com/yunwei/news/2012/1227/131.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
      </tags>
  </entry>
  <entry>
    <title>Android开发——-Android Source&amp;SDK的国内镜像</title>
    <url>/dev-android-source-and-sdk/</url>
    <content><![CDATA[1. 前言谷歌被墙，在下载安卓源码和SDK时，各种没有进度。这时，就要感谢国内镜像站了。
2. Android Source2.1. 清华大学TUNA镜像源https://aosp.tuna.tsinghua.edu.cn/https://wiki.tuna.tsinghua.edu.cn/MirrorUsage/android
2.2. 为什么要下载Android Source研究Android尤其是Android系统核心或者是驱动的开发，首先需要做的就是克隆建立本地Android Source版本库。


2.3. AOSP、AOKP、CMAOSP是“Android Open-Source Project”的缩写，中文名称为Android开放源代码项目。大家都知道Android是开源操作系统，所以Google每发布一个Android版本，都会给开源社区发放对应版本的源代码，也就是我们所说的AOSP ROM，这可以称得上是最为纯净的Android系统。可以用类比来让大家更清楚一些，例如国内多数盗版的Windows系统，几乎都是基于微软的MSDN制作，AOSP ROM即等相当于微软MSDN母盘的角色。
AOKP是“Android Open-Source Kang Project”，比AOSP多了一个“Kang”。在Android社区中，Kang表示这是一个被别人定制过的ROM，是一个来自民间的ROM。
CM是CyanogenMod的简称，Cyanogen团队是全球最大的第三方ROM编译团队，覆盖机型范围特别广，国内知名的ROM例如MIUI、锤子ROM都是基于CM实现的。严格意义上来说，CM ROM属于AOKP的范畴。CM ROM虽然一直遵从原生Android，但只有Google官方的才算真正的AOSP。
2.4. 为什么使用RepoAndroid使用Git作为代码管理工具，开发了Gerrit进行代码审核以便更好的对代码进行集中式管理，还开发了Repo命令行工具，对Git部分命令封装，将百多个Git库有效的进行组织。
AOSP是由许许多多有Git管理的项目组成的，例如，在Android4.2中，就包含了329个项目，每一个项目都是一个独立的Git仓库。这意味着，如果我们要创建一个AOSP分支来做新的feature开发，那么就需要到每一个子项目去创建对应的分支。这显然不能手动地到每一个子项目里面去创建分支，必须要采用一种自动化的方式来处理。这些自动化处理工作就是由Repo工具来完成的。当然，Repo工具所负责的自动化工作不只是创建分支那么简单，查看分支状态、提交代码、更新代码等基础的Git操作都可以用Repo来替代完成。
2.5. 下载安装配置Repo假设在VirtualBox Ubuntu环境下，打开终端：mkdir ~/binPATH=~/bin:$PATHgit clone git://aosp.tuna.tsinghua.edu.cn/android/git-repo.gitcp git-repo/repo ~/bin/gedit ~/bin/repo在该文件中修改
REPO_URL = &#x27;git://aosp.tuna.tsinghua.edu.cn/android/git-repo&#x27;

2.6. 初始化Repomkdir anroidcd androidgit config --global user.email &quot;voidking@qq.com&quot;git config --global user.name &quot;voidking&quot;repo init -u git://aosp.tuna.tsinghua.edu.cn/android/platform/manifest -b android-5.0.2_r1这一步大概有一分多钟就完成了。如果需要下载其他分支将android-5.0.2_r1改成其他分支名称就可以了。
Android官网详细地介绍了当前Android的各个版本名称、Version、对应的API Level、Branch TAG、以及Supported devices，该链接地址如下：http://source.android.com/source/build-numbers.html
如果谷歌北墙，请查看https://github.com/Jhuster/AOSP/tree/master/documents 的build-numbers.html，雷锋哥哥在这里备份了一份。
2.7. 开始下载repo sync然后就是漫长的等待了，大约32G的数据，有点大！
2.8. 使用1、查看源码

在Ubuntu下，查看没有问题。
如果想要在Windows下查看，那么不妨建立一个Ubuntu和Windows的共享文件夹，具体方法见《VirtualBox Ubuntu共享文件夹》。

2、开发按照曾经修改Linux内核的经验，郝同学推测，Android的内核开发最好在Linux下进行；Mac是程序猿的最爱，估计也可以；各种编译工具和依赖包，Windows折腾不起哇！
3. Android SDK3.1. 北京化工大学镜像站http://ubuntu.buct.edu.cn/http://ubuntu.buct.edu.cn/android/repository/
3.2. We - 开源镜像站http://mirrors.neusoft.edu.cn/http://mirrors.neusoft.edu.cn/android/repository/
3.3. 配置1、启动Android SDK Manager，Tools，Options。2、在弹出的Android SDK Manager-Settings中

HTTP Proxy Server填入mirrors.neusoft.edu.cn
HTTP Proxy Port填入80
选中Force https://... sources to be fetched using http://...复选框。

3、单击Close返回主界面，Packages，Reload。
4. 后记Android Source，前期用不上，郝同学还没有深入底层的打算。Android的SDK，这是个好东西！最喜欢里面的官方文档和例子，实在是学习Android的最好资料啊！
5. 参考文档同步、更新、下载Android Source &amp; SDK from 国内镜像站http://www.cnblogs.com/bluestorm/p/4419051.html
Android A to Z: What is the AOSP?http://www.androidcentral.com/android-z-what-aosp
Android源代码仓库及其管理工具Repo分析http://blog.csdn.net/luoshengyang/article/details/18195205
Android源码仓库和Repo工具使用http://www.2cto.com/kf/201409/336722.html
windows系统中国国内镜像网站上用repo下载Android5.0源码http://jileniao.net/post-156.html
repo用法详解http://blog.csdn.net/sunweizhong1024/article/details/8055372
关于Android Repohttp://www.cnblogs.com/hongzg1982/articles/2101980.html
Android内核开发：源码的版本与分支详解http://ticktick.blog.51cto.com/823160/1654759http://www.tuicool.com/articles/RjeEZb
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
      </tags>
  </entry>
  <entry>
    <title>Android开发——常见开源项目</title>
    <url>/dev-android-open-sourse/</url>
    <content><![CDATA[1. 前言见多识广，方便模仿。
看的多了，写的多了，懂的多了。也就怕的少了，思路广了，境界高了。。。
今天我们来聊聊拓宽世界的途径之一，学习那些牛逼的开源项目！
2. 雨辰推荐
ActionBarSherlock
HoloEverywhere
ViewPagerIndicator




Android-Universal-Image-Loader
Sliding Menu
Android-PullToRefresh
ViewBader
Progress Wheel
Cards UI
SmoothProgressBar
SataliteMenu
Radial Menu Widget
Indexable ListView
Sticky ListHeaders
Slide ExpandableListView
Jazzy ListView
PinterestListView
SwipeListView

3. 后记初入Android世界，便看到高山景行，突然感觉热血被点燃！也许，我会在这条路上走很远。。。
4. 参考资源7天学会Androidhttp://www.duobei.com/room/trial/9118721751
直接拿来用！最火的Android开源项目（一）http://www.csdn.net/article/2013-05-03/2815127-Android-open-source-projects
直接拿来用！最火的Android开源项目（二）http://www.csdn.net/article/2013-05-06/2815145-Android-open-source-projects-two
直接拿来用！最火的Android开源项目（完结篇）http://www.csdn.net/article/1970-01-01/2815370
直接拿来用！最火的Android开源项目整理http://blog.csdn.net/djun100/article/details/13776005
直接拿来用！十大Material Design开源项目http://www.csdn.net/article/2014-11-21/2822753-material-design-libs
Android开源项目分类汇总【老外汇总版本】http://blog.csdn.net/forlong401/article/details/26848951
安卓巴士总结了近百个Android优秀开源项目http://www.apkbus.com/android-17627-1-1.html
GitHub 优秀的 Android 开源项目http://blog.csdn.net/shulianghan/article/details/18046021http://www.cnblogs.com/hawkon/p/3593709.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git分支</title>
    <url>/dev-git-branch/</url>
    <content><![CDATA[1. 前言Branches in Git are incredibly lightweight as well. They are simply pointers to a specific commit – nothing more. This is why many Git enthusiasts chant the mantra:

branch early, and branch often.

Because there is no storage / memory overhead with making many branches, it’s easier to logically divide up your work than have big beefy branches.
When we start mixing branches and commits, we will see how these two features combine. For now though, just remember that a branch essentially says “I want to include the work of this commit and all parent commits.”
参考文档：

Learn Git Branching
git merge简介
Git Book - rebase



2. git分支规范参考文档：

A successful Git branching model
图解git flow开发流程

3. 分支基本操作3.1. 查看分支git branch -a

3.2. 本地仓库中创建分支本地仓库中创建与当前分支相同的bugfix分支，并切换到bugfix分支
git checkout -b bugfix# orgit branch bugfix &amp;&amp; git checkout bugfix

创建与本地仓库main分支相同的分支
git checkout main -b bugfix

创建与远程仓库main分支相同的分支，追踪远程分支
git checkout origin/main -b bugfix

3.3. 切换分支下载仓库后，切换到bugfix分支，bugfix分支追踪远程 origin/bugfix 分支
git pullgit checkout origin/bugfix

3.4. 远程仓库中创建分支创建远程仓库bugfix分支和bugfix2分支
git push origin HEAD:bugfixgit push origin HEAD:bugfix2

3.5. 上传分支在bugfix分支下进行了修改，然后提交修改
git add .git commit -m &quot;something&quot;git push origin HEAD:bugfix

3.6. 删除分支1、删除本地分支
git branch -D bugfix

2、删除远程分支
git push --delete origin bugfix

4. 恢复已删除分支1、查看全局日志
git reflog

或者
git log -g

2、新建分支
git branch bugfix 3eac14d

3、上传分支
git checkout bugfixgit push origin HEAD:bugfix

5. 合并分支5.1. 合并分支概述合并分支，可以使用merge，也可以使用rebase。它们俩有什么区别呢？主要是适用场景不同：
场景一：基于 master 拉出来一个开发分支 dev，在 dev 分支开发完成了之后，要合并到 master 上。操作：切换到 master 分支，使用 git merge devPS：对于git服务器端的merge合并，往往要发起MR，也就是Merge Request为什么不使用git rebase dev？因为master是主干，rebase的话，主干就长歪了。为了保证master提交记录是笔直的主干，是一条直线，所以使用merge。
场景二：基于 master 拉出来一个开发分支 dev，在 dev 上开发了一段时间后，master分支也有了新的变更。为了保证我们在最新的版本基础上进行开发，这时需要把 master 分支提交的新内容更新到 dev 分支。操作：切换到 dev 分支，使用 git rebase master为什么不使用git merge master？因为dev最终是要merge到master的，如果master先merge到dev，开发一段时间后dev最后又merge到master，整个提交树会很混乱。为了保证提交记录的清晰明了，所以使用rebase。
查看提交树命令：git log --graph --pretty=oneline
5.2. 模拟分支变更1、初始化仓库和分支
mkdir testcd testgit inittouch test.txtgit add .git commit -m &quot;first commit&quot;git checkout -b bugfix

2、bugfix分支上，新建test.txt文件，提交
echo &quot;hello bugfix&quot; &gt; test.txtgit add .git commit -m &quot;update file in bugfix branch&quot;

3、切到master分支，新建test.txt文件，提交
git checkout masterecho &quot;hello master&quot; &gt; test.txtgit add .git commit -m &quot;update file in master branch&quot;

5.3. merge1、通过merge命令合并bugfix分支到master
git merge bugfix

提示冲突：
Auto-merging test.txtCONFLICT (content): Merge conflict in test.txtAutomatic merge failed; fix conflicts and then commit the result.

2、查看冲突git status，提示test.txt文件冲突。打开test.txt，可以看到如下冲突：
&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEADhello master=======hello bugfix&gt;&gt;&gt;&gt;&gt;&gt;&gt; update file in bugfix branch
其中 ======= 隔开的上半部分，是 HEAD（即 master 分支，在运行 merge 命令时检出的分支）中的内容，下半部分是在bugfix分支中的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。
3、解决冲突修改test.txt如下：
hello master

4、提交
git add test.txtgit commit -m &quot;merge bugfix into master&quot;

5、查看提交记录
git log

5.4. rebase5.4.1. rebase合并内容需求：text.txt 打算合并master分支和bugfix分支中的内容。text.txt最终内容为：
hello master bugfix

1、通过rebase命令合并master分支到bugfix
git checkout bugfixgit rebase master

提示冲突：
First, rewinding head to replay your work on top of it...Applying: update file in bugfix branchUsing index info to reconstruct a base tree...M   test.txtFalling back to patching base and 3-way merge...Auto-merging test.txtCONFLICT (content): Merge conflict in test.txterror: Failed to merge in the changes.Patch failed at 0001 update file in bugfix branchhint: Use &#x27;git am --show-current-patch&#x27; to see the failed patchResolve all conflicts manually, mark them as resolved with&quot;git add/rm &lt;conflicted_files&gt;&quot;, then run &quot;git rebase --continue&quot;.You can instead skip this commit: run &quot;git rebase --skip&quot;.To abort and get back to the state before &quot;git rebase&quot;, run &quot;git rebase --abort&quot;
此时我们进入了一个临时分支。
2、查看冲突git status，提示test.txt文件冲突。打开test.txt，可以看到如下冲突：
&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEADhello master=======hello bugfix&gt;&gt;&gt;&gt;&gt;&gt;&gt; update file in bugfix branch

3、解决冲突修改test.txt如下：
hello master bugfix

4、提交
git add test.txtgit rebase --continue

5、查看提交记录
git log

可以看到三条记录：
update file in bugfix branchupdate file in master branchfirst commit

5.4.2. rebase保留master需求：text.txt 打算保留master分支中的内容，舍弃bugfix分支中的内容。text.txt最终内容为：
hello master

1、通过rebase命令合并master分支到bugfix
git checkout bugfixgit rebase master

提示冲突：
First, rewinding head to replay your work on top of it...Applying: update file in bugfix branchUsing index info to reconstruct a base tree...M   test.txtFalling back to patching base and 3-way merge...Auto-merging test.txtCONFLICT (content): Merge conflict in test.txterror: Failed to merge in the changes.Patch failed at 0001 update file in bugfix branchhint: Use &#x27;git am --show-current-patch&#x27; to see the failed patchResolve all conflicts manually, mark them as resolved with&quot;git add/rm &lt;conflicted_files&gt;&quot;, then run &quot;git rebase --continue&quot;.You can instead skip this commit: run &quot;git rebase --skip&quot;.To abort and get back to the state before &quot;git rebase&quot;, run &quot;git rebase --abort&quot;
此时我们进入了一个临时分支。
2、查看冲突git status，提示test.txt文件冲突。打开test.txt，可以看到如下冲突：
&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEADhello master=======hello bugfix&gt;&gt;&gt;&gt;&gt;&gt;&gt; update file in bugfix branch

3、解决冲突修改test.txt如下：
hello master

或者直接使用命令行解决冲突：
git checkout --ours test.txt

4、提交
git add test.txtgit rebase --skip

5、查看提交记录
git log

可以看到两条记录：
update file in master branchfirst commit

5.4.3. 放弃rebaserebase过程中，可以随时放弃rebase
git rebase --abort

5.5. 解决冲突小结合并时难免代码冲突，git会将冲突的代码用 &lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt; xxx 标识出来，其中=======之前表示的是ours分支，之后表示theirs分支。
解决冲突办法：首先确定保留哪一部分代码，然后手动删除标志 &lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt; xxx，最后commit。
如果想要保留两个分支中的某一个，可以使用 git chekout --ours &lt;fileName&gt; 或者 git checkout --theirs &lt;fileName&gt;。
对于merge和rebase来说，这两个选项对应的分支正好是相反的。在使用 merge 时，ours指的是当前分支（master），theirs指的是要被合并的分支（dev）。而在 rebase 时，theirs指的是当前分支（dev），ours指向（master）。
6. HEAD分支告警6.1. 问题描述执行 git status，出现了warning
warning: refname &#x27;HEAD&#x27; is ambiguous.

6.2. 解决办法解决办法：删除 HEAD 分支
git branch -D HEAD

原因分析：这个问题是因为我们有一个叫做 HEAD 或者 head 的分支造成的，这个名字是一个保留字，它用于标示哪个分支是当前分支 ，而不应该直接拿来做分支的名字。但是 remote 的 HEAD 分支是 OK 的，比如 origin/HEAD，经常有人把这个 checkout 下来，结果就变成了本地的 HEAD 分支，这就是此问题产生的源头。如果用 HEAD 创建了一个 tag，这也会引起相同的问题。
参考文档：git warning: refname ‘HEAD’ is ambiguous是什么问题
]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：gitignore用法</title>
    <url>/dev-gitignore/</url>
    <content><![CDATA[1. 前言有些时候，我们必须把某些文件放到Git工作目录中，但又不能提交它们，比如保存了数据库密码的配置文件、项目运行时生成的临时文件等等，每次git status都会显示Untracked files ...，让人不爽。
好在Git考虑到了大家的感受，在Git工作区的根目录下创建一个特殊的.gitignore文件，然后把要忽略的文件名填进去，Git就会自动忽略这些文件。


2. gitignore规则2.1. 基本语法
以斜杠/开头表示目录
以星号*通配多个字符；
以问号?通配单个字符
以方括号[]包含单个字符的匹配列表
以叹号!表示不忽略(跟踪)匹配到的文件或目录

2.2. 示例# 这是注释行，将被忽略.DS_Store # 忽略所有的.DS_Store*.a       # 忽略所有以.a为扩展名的文件    !lib.a    # 但是名为lib.a的文件或目录不要忽略，即使前面设置了对*.a的忽略/TODO     # 只忽略根目录下的TODO文件，子目录中的TODO文件不忽略/target/ # 只忽略根目录下的target目录中的全部文件target/  # 忽略所有target目录中的文件doc/*.txt # 忽略文件如doc/notes.txt，但是文件如doc/server/arch.txt不忽略

3. push之后添加gitignoregitignore只能作用于 Untracked Files，如果某些文件（add和commit过的文件）已经被纳入了版本管理中，则修改gitignore是无效的。解决方法就是先把本地缓存删除（改变成Untracked状态），然后再提交。
git pullgit rm -r --cached .git add .git commit -m &#x27;update .gitignore&#x27;git push

如果是单个文件需要Untrack，git rm -r --cached .建议改成git rm -r --cached path/filename。
4. 添加gitignore后pull错误push之后添加了gitignore，也就是一部分的Tracked Files，现在不再Track。这时，其他用户在pull代码的时候，会出现错误：“The following untracked working tree files would be overwritten by merge”
解决办法：
git reset --hard HEAD    git clean -f -d    git pull

5. 自动生成gitignore1、访问网址：gitignore.io
2、输入操作系统、IDE、编程语言等，就能自动生成通用的gitignore文件。
3、根据自己的实际需要，增删一些配置。
6. 一个Demo### macOS #### General.DS_Store.AppleDouble.LSOverride# Thumbnails._*# Files that might appear in the root of a volume.DocumentRevisions-V100.fseventsd.Spotlight-V100.TemporaryItems.Trashes.VolumeIcon.icns.com.apple.timemachine.donotpresent# Directories potentially created on remote AFP share.AppleDB.AppleDesktopNetwork Trash FolderTemporary Items.apdisk### End of macOS ###### Intellij #### Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839# General.idea/# mpeltonen/sbt-idea plugin.idea_modules/# File-based project format*.iws# IntelliJout/# CMakecmake-build-*/# Crashlytics plugin (for Android Studio and IntelliJ)com_crashlytics_export_strings.xmlcrashlytics.propertiescrashlytics-build.propertiesfabric.properties### End of Intellij ###

7. 书签A collection of useful .gitignore templates
Github使用.gitignore文件忽略不必要上传的文件
Git忽略规则
忽略特殊文件
解决Git在添加ignore文件之前就提交了项目无法再过滤问题
]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
        <tag>gitignore</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git常用命令</title>
    <url>/dev-git-command/</url>
    <content><![CDATA[1. 前言本文记录一下常用的Git命令，方便以后查阅。
参考文档：

Git - Documentation
阮一峰 Git 教程
常用 Git 命令清单
Git远程操作详解



2. Git安装与配置参考文档：《好好学Git：Git安装与配置》
3. pull代码3.1. pull相关配置.git/config文件里，配置了remote和branch，以下为例。
[core]    repositoryformatversion = 0    filemode = false    bare = false    logallrefupdates = true    symlinks = false    ignorecase = true[remote &quot;origin&quot;]    url = https://github.com/voidking/voidking.git    fetch = +refs/heads/*:refs/remotes/origin/*[branch &quot;master&quot;]    remote = origin    merge = refs/heads/master
拉取项目的时候，会根据这个配置来拉取。
3.2. pull相关命令# 查看本地分支追踪的远程分支# 一个本地分支可以追踪多个远程分支git branch -vv# 格式说明git pull &lt;远程仓库名&gt; &lt;远程分支名&gt;:&lt;本地分支名&gt;# 拉取当前分支追踪的唯一远程分支git pull# 拉取当前分支追踪的origin/master分支git pull origin mastergit pull origin# 拉取remote origin的next分支，与本地master合并git pull origin next:master# 拉取origin主机的next分支，与当前分支合并git pull origin next

3.3. 解决冲突问题描述：git pull 之后，出现冲突。冲突文件中，包含：
&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEADxxx=======yyy&gt;&gt;&gt;&gt;&gt;&gt;&gt; f406c5e398760c7d9116c8af31ea5748bd344abe
其中，&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD 和 ======= 中间的是自己的代码；======= 和 &gt;&gt;&gt;&gt;&gt;&gt;&gt; f406c5e398760c7d9116c8af31ea5748bd344abe 中间的是其他人修改的代码。
解决冲突办法：确定保留哪一部分代码，然后删除标志 &lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD ，=======  ，&gt;&gt;&gt;&gt;&gt;&gt;&gt; f406c5e398760c7d9116c8af31ea5748bd344abe
详情参考文档《git branch》中的代码合并部分，原理是一样的。
4. fetch代码拉取github的某个pr：
git fetch origin pull/29/head:pr29git branch -agit chackout pr29

5. 分支操作详情参考文档《git branch》
6. commit代码6.1. 单行commitgit add .git commit -m &quot;something&quot;

6.2. 多行commitgit add .git commit -m &quot;dquote&gt; &lt;类型&gt;[可选的作用域]: &lt;描述&gt;dquote&gt;dquote&gt; [可选的正文]dquote&gt;dquote&gt; [可选的脚注]dquote&gt; &quot;

6.3. 查看commit记录1、最常用查看commit记录
git log
只可以查看到当前分支的commit日志，而且不能查看已经删除了的commit操作。
2、查看单个文件commit记录
git log filename

3、查看全局日志
git refloggit log -g
可以查看所有分支的所有操作记录，包括reset操作、checkout操作、已经删除了的commit操作等等。
4、查看所有分支的commit
git log --graph --pretty=oneline --abbrev-commit

7. 查看变更7.1. commit之前执行commit之前，查看修改了哪些文件。
git status

7.2. commit之后执行commit之后，获取最近一次修改的文件列表
git diff --name-only HEAD~ HEAD

查看两次commit之间修改的文件
git diff --name-only &lt;commit-1&gt; &lt;commit-2&gt;

8. push代码8.1. push分支push命令格式：
git push &lt;远程仓库名&gt; &lt;本地分支名&gt;:&lt;远程分支名&gt;

上传代码到唯一远程追踪分支
git push

上传到指定远程追踪分支
git push origin HEAD:branch_name

8.2. 同时push多个git仓库1、编辑 .git/config，添加
[remote &quot;all&quot;]    url = https://github.com/voidking/hexo-backup    url = https://gitee.com/voidking/hexo-backup

2、push代码
git push all

8.3. 同时push到多个git仓库的不同分支1、添加多个远程仓库
git remote add repo1 &lt;远程仓库URL&gt;git remote add repo2 &lt;远程仓库URL&gt;

2、push到多个远程仓库
git push repo1 HEAD:maingit push repo2 HEAD:master

此时仍然需要两条命令。
3、配置别名
git config alias.pushall &#x27;!git push repo1 HEAD:main &amp;&amp; git push repo2 HEAD:master&#x27;

配置别名错误的话，可以在配置文件中修改 .git/config
4、push到多个远程仓库
git pushall

8.4. push到其他git仓库需求：已有仓库voidking，包含多个分支。现在想要把该仓库根据不同的分支拆分成多个项目，于是创建项目voidkingA和voidkingB。接下来，该怎么把voidking的不同分支，push到voidkingA和voidkingB呢？
cd voidkinggit checkout -b voidkingA origin/branchAgit push --force --quiet &quot;https://voidking:xxxxxx@github.com/voidking/voidkingA&quot; voidkingA:maingit checkout -b voidkingB origin/branchBgit push --force --quiet &quot;https://voidking:xxxxxx@github.com/voidking/voidkingB&quot; voidkingB:main

8.5. git push 403公司电脑（win10系统）以前有一个用户，github账号是haojin。现在电脑分配给我使用，但是，我的github账号是voidking，在我git push的时候报错403。
$ git pushremote: Permission to voidking/hexo-back-up.git denied to haojin.fatal: unable to access &#x27;https://github.com/voidking/hexo-back-up.git/&#x27;: The requested URL returned error: 403
哪怕重置了全局设置的user.name和user.email，依然会报错。原因：win10系统自动保存用户凭据，gitpush的时候默认使用以前的用户凭据。解决办法：Win+X，搜索，控制面板，用户账户，凭据管理器，管理Windows凭据，普通凭据，删除github的凭据即可。
9. 版本回退版本回退常用git reset和git revert命令，这两个命令都是作用于当前分支（HEAD所在的分支）。git reset 是把HEAD向后移动了一下，而git revert是HEAD继续前进，只是新的commit的内容和要revert的内容正好相反，能够抵消要被revert的内容。
9.1. reset1、每个文件单独版本回退
git statusgit loggit reset  1fe37e1bcbb894a1b594cf405ae31880cbaa6cd7 filepath/filenamegit checkout filepath/filename

2、全部文件版本回退
git reset --hard 1fe37e1bcbb894a1b594cf405ae31880cbaa6cd7

3、远程仓库回退
git reset --hard 1fe37e1bcbb894a1b594cf405ae31880cbaa6cd7git push -f

4、回退后回退reset后，回退版本之后的commit操作记录，都被删除掉了。如果回退过之后又后悔了，那么，需要查看全局日志，然后再次执行reset操作。
git refloggit reset a246dcd --hard

需要注意的是，版本回退，我们没有使用git checkout。假设当前在bugFix分支，使用git checkout，理论上输入如下命令：
git refloggit checkout a246dcd

这时，输入git log，发现版本完美回退了。但是，这只是假象，输入git branch -a，发现HEAD从bugFix分支切换到了a246dcd那个commit。而bugFix分支，没有进行任何改变。所以，我们不使用git checkout进行版本回退，而是使用它进行历史版本的查看。
9.2. revert1、全部文件版本回退
git revert cf000

2、远程仓库回退
git push origin HEAD:branch_name

回退过之后又后悔了，那么，执行reset操作即可。
git loggit reset a246dcd --hard

10. git stash本地有修改，但是想拉取远端仓库上的最新代码到本地。
# 保存stashgit stashgit stash save &quot;something&quot;# 拉取最新代码git pull# 查看stashgit stash list# 弹出之前的修改git stash pop# 弹出之前的修改（只能恢复一次）git stash pop stash@&#123;num&#125;# 弹出之前的修改（可恢复多次）git stash apply stash@&#123;num&#125;# 删除stashgit stash drop stash@&#123;num&#125;# 删除所有stashgit stash clear

11. 统计# 统计个人代码量git log --author=&quot;voidking&quot; --pretty=tformat: --numstat | awk &#x27;&#123; add += $1; subs += $2; loc += $1 - $2 &#125; END &#123; printf &quot;added lines: %s, removed lines: %s, total lines: %s\n&quot;, add, subs, loc &#125;&#x27; -# 统计每个人代码量git log --format=&#x27;%aN&#x27; | sort -u | while read name; do echo -en &quot;$name\t&quot;; git log --author=&quot;$name&quot; --pretty=tformat: --numstat | awk &#x27;&#123; add += $1; subs += $2; loc += $1 - $2 &#125; END &#123; printf &quot;added lines: %s, removed lines: %s, total lines: %s\n&quot;, add, subs, loc &#125;&#x27; -; done# 统计提交者前五git log --pretty=&#x27;%aN&#x27; | sort | uniq -c | sort -k1 -n -r | head -n 5# 统计提交者数量git log --pretty=&#x27;%aN&#x27; | sort -u | wc -l# 统计提交数git log --oneline | wc -l# 添加或修改的代码行数git log --stat|perl -ne &#x27;END &#123; print $c &#125; $c += $1 if /(\d+) insertions/&#x27;


]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git打标签</title>
    <url>/dev-git-tag/</url>
    <content><![CDATA[1. 打标签所谓打标签，就是对某一时间点上的版本打上标签。Git使用的标签有两种类型：轻量级的（lightweight）和含附注的（annotated）。轻量级标签就像是个不会变化的分支，实际上它就是个指向特定提交对象的引用。而含附注标签，实际上是存储在仓库中的一个独立对象，它有自身的校验和信息，包含着标签的名字，电子邮件地址和日期，以及标签说明，标签本身也允许使用 GNU Privacy Guard (GPG) 来签署或验证。一般我们都建议使用含附注型的标签，以便保留相关信息；当然，如果只是临时性加注标签，或者不需要旁注额外信息，用轻量级标签也没问题。



轻量标签。创建轻量标签不需要传递参数，直接指定标签名称即可。git tag v1.0.0_light

附注标签。参数a即annotated的缩写，指定标签名。参数m指定标签说明，说明信息会保存在标签对象中。git tag -a v1.0.0 -m &quot;v1.0.0&quot;


2. 查看本地标签git taggit tag -l &#39;v1.0.*&#39;git show v1.0.0
3. 删除本地标签git tag -d &lt;tagname&gt;
4. 切换到标签git checkout &lt;tagname&gt;
5. 给指定的commit打标签git log，我们看到的commit是一长串十六进制数，比如759f084c094a2e3930224b2dad389349e36ab083，我们要用到至少前4位。
git tag -a v0.0.0 759f -m &quot;v0.0.0&quot;，而且，每个commit可以打多个标签。
6. 添加服务器端标签默认情况下，git push 并不会把标签传送到远端服务器上，只有通过显式命令才能分享标签到远端仓库。git push origin &lt;tagname&gt;git push origin --tag
7. 删除服务器端标签方法一：git push --delete origin tag &lt;tagname&gt;
方法二：git tag -d &lt;tagname&gt;git push origin :refs/tags/&lt;tagname&gt;
8. 完整例子打标签的操作发生在我们commit修改到本地仓库之后。git add .git commit -m &quot;fixed some bugs&quot;git tag -a 0.1.3 -m &quot;Release version 0.1.3&quot;
分享提交标签到远程服务器上git pushgit push --tags–tags参数表示提交所有tag至服务器端，普通的git push操作不会推送标签到服务器端。
删除本地标签git tag -d 0.1.3
删除远端服务器标签git push origin :refs/tags/0.1.3
9. 参考文档Git 基础 - 打标签http://git-scm.com/book/zh/v1/Git-%E5%9F%BA%E7%A1%80-%E6%89%93%E6%A0%87%E7%AD%BE
git命令之git tag 给当前分支打标签http://blog.csdn.net/wangjia55/article/details/8793577
git tag简介http://blog.csdn.net/hudashi/article/details/7664468
tag打标签http://blog.csdn.net/wh_19910525/article/details/7470850
git tag操作教程http://blog.csdn.net/waterforest_pang/article/details/9762863
]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
      </tags>
  </entry>
  <entry>
    <title>github下载加速</title>
    <url>/dev-github-download-speedup/</url>
    <content><![CDATA[1. 替换加速网址1、github.com 替换为 hub.fastgit.org2、raw.githubusercontent.com 替换为 raw.staticdn.net
以macos安装brew为例：
curl -L https://raw.staticdn.net/Homebrew/install/HEAD/install.sh -o brew.shsed -i &#x27;s#https://github.com/#https://hub.fastgit.org/#g&#x27; brew.shsh brew.sh



2. 使用国内git平台加速使用gitee导入github上的项目，然后从gitee下载。
以下载kubernetes项目为例：登录gitee-&gt; 新建仓库-&gt; 点击导入-&gt; 输入github上kubernetes Git仓库URL，https://github.com/kubernetes/kubernetes-&gt; 输入仓库名称，kubernetes-&gt; 导入-&gt; 从gitee下载kubernetes，git clone https://gitee.com/voidking/kubernetes
类似的git平台，还有gitcode
3. 科学上网终极解决办法。
]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Git：Git安装与配置</title>
    <url>/dev-git-install-config/</url>
    <content><![CDATA[1. Git简介
Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.

Git是一个免费的开源分布式版本控制系统，旨在快速高效地管理项目的所有文件。Git中的项目被称为仓库(repository)，或者是代码库。
本文主要学习Git的安装和配置，是我们使用Git的第一步。参考文档：

Git - Documentation
阮一峰 Git 教程
常用 Git 命令清单
Git远程操作详解



2. 安装Git2.1. Windows在Windows操作系统中，推荐使用Git官方提供的Git for Windows。访问Git官方下载页面，点击Windows图标下载exe文件，然后按照提示进行安装即可。
2.2. macOS在macOS操作系统中，推荐使用Homebrew进行安装。在终端中输入以下命令：
brew install git

Homebrew的安装方法，可以参考《MacOS安装配置Homebrew》
2.3. Linux在Linux操作系统中，可以使用包管理器进行安装。
Ubuntu/Debian：
apt-get install -y git

CentOS7：
yum install -y git

CentOS7默认安装的git版本比较老旧（1.8.3.1），如果想要安装新版的git，比较简单的方法是先安装包含新版git的第三方软件仓库（常用epel-release，但是其中不包含最新版git）。
下面两个第三方软件仓库，任选其一安装，之后就可以安装到最新版的git了。
rpm -ivh http://opensource.wandisco.com/centos/7/git/x86_64/wandisco-git-release-7-1.noarch.rpmyum install https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm

PS：安装第三方软件仓库，并不安装实际的软件，而是会在 /etc/yum.repos.d/ 目录下添加一个或多个 .repo 文件。
3. 配置Git用户信息3.1. 全局配置git config --global --listgit config --global user.name &quot;voidking&quot;git config --global user.email &quot;voidking@qq.com&quot;

3.2. 针对单独项目配置git config --listgit config user.name &quot;haojin&quot;git config user.email &quot;voidking@vip.qq.com&quot;
提交的时候，如果项目自身没有配置信息就会使用全局配置，有配置就会使用单独的配置信息。
4. 配置中文不转义git status看到的中文文件名，不要转义
git config --global core.quotepath false

5. 配置免密上传下载5.1. 配置免密上传下载概述配置免密上传下载，有两种方式：一种是使用ssh密钥对，一种是使用access token。使用ssh密钥对时，需要通过ssh协议上传下载代码；使用access token时，需要通过https协议上传下载代码。
5.2. 使用ssh密钥对1、生成ssh密钥对参考《shell命令之ssh》，生成ssh密钥对。
2、在git remote server上添加ssh公钥打开公钥（id_rsa.pub），复制全文。访问git remote server的ssh配置页面，把公钥粘贴进去。例如，如果git remote server 是 github，那么访问 settings-ssh 页面，New SSH key，把公钥粘贴进去即可。
3、测试
ssh git@github.com

可能出现下面的提示，表明配置成功了：
The authenticity of host &#x27;github.com (192.30.252.128)&#x27; can&#x27;t be established.RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.Are you sure you want to continue connecting (yes/no)? yesWarning: Permanently added &#x27;github.com,192.30.252.128&#x27; (RSA) to the list of known hosts.Hi voidking! You&#x27;ve successfully authenticated, but GitHub does not provide shell access.Connection to github.com closed.

5.3. 指定ssh私钥5.3.1. 方法一：使用ssh配置修改 ~/.ssh/config 文件，添加：
host github.com  HostName github.com  IdentityFile ~/.ssh/id_rsa_github  User git

5.3.2. 方法二：使用环境变量GIT_SSH_COMMAND=&quot;ssh -i ~/.ssh/id_rsa_github -F /dev/null&quot; git clone https://github.com/voidking/wecms.git

5.4. 使用 access token1、生成access token访问github的 Personal access tokens 页面，Generate new token。Note输入 voidking-pc ，Select scope选择repo，然后点击Generate token。生成了一个token，保存它。
2、开启密码记录
git config --global credential.helper store

用户家目录下的 .gitconfig 文件末尾会添加：
[credential]    helper = store

3、记住密码再次使用git pull或者git push，系统会提示输入git用户名和密码，这里的密码输入access token即可，不要使用真实密码。系统会记住用户的access token，存储在用户家目录下的 .git-credentials 文件中。之后就不再需要输入git密码了。
5.5. 修改 access token如果access token过期需要替换，那么需要执行以下步骤：1、清除当前token
git credential rejectprotocol=httpshost=github.com

2、使用新token再次使用git pull或者git push，系统会提示输入git用户名和密码，这里的密码输入access token即可。
或者，使用命令设置新token（不建议，macos上经尝试无效）。
git credential fillprotocol=httpshost=github.com
这时 Git 会提示输入新的密码。输入后保存即可。
6. 多个用户配置免密上传下载如果我们有多个 github 账户或者多个 gitlab 账户，都想要免密上传和下载代码，该怎样配置？
如果是使用ssh密钥对的方式，那么比较简单，只要给这些不同账户配置相同的公钥即可。
如果是使用access token的方式，那么就麻烦一些了。已知 ~/.git-credentials 文件中记录了 github.com 的账密，所有和 github.com 的交互都会使用这个账密。而我们有多个用户名，不同的仓库需要使用不同的用户名进行操作，那么 .git-credentials 文件中会记录多个 github.com 的账密，例如：
https://voidking:ghp_xxx@github.comhttps://quizthink:ghp_xxx@github.com

问题来了，在实际上传下载代码时，项目怎么知道该使用哪一条账密记录呢？解决办法：在项目的配置中指定用户名
vim .git/config

添加配置：
[credential]  username = quizthink

如果是首次使用一个新的用户，那么在执行git pull或者git push时，系统会提示输入这个新用户的密码，然后自动保存到 .git-credentials 文件中。
如果想要指定用户git clone一个项目，那么需要在url中指定用户，例如：
git clone https://voidking@github.com/voidking/k8s-client-go

注意：如果在项目的 .git/config 中不配置 username ，那么默认会使用 .git-credentials 文件中的最后一个 github.com 账密。
7. 配置使用代理如果克隆项目特别慢，自己又有科学上网的ssr，那么可以开启代理。
git config --global http.proxy &#x27;socks5://127.0.0.1:1080&#x27; git config --global https.proxy &#x27;socks5://127.0.0.1:1080&#x27;

关闭代理：
git config --global --unset http.proxygit config --global --unset https.proxy


]]></content>
      <categories>
        <category>engineering</category>
        <category>git</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>好好学Git</tag>
      </tags>
  </entry>
  <entry>
    <title>软件版本号命名规则</title>
    <url>/dev-software-version-name/</url>
    <content><![CDATA[1. 软件版本号有没有什么标准？项目在发行时，肯定需要一个版本号，那么这个版本号应该遵循什么样的标准呢？语义化版本（Semantic Versioning semver）版本号，是一个在开源社区中广泛接受的标准，详情参考Semantic Versioning Specification。


2. SemVer 的版本格式版本格式：主版本号.次版本号.修订号（MAJOR.MINOR.PATCH），版本号递增规则如下：
主版本号：当你做了不兼容的 API 修改次版本号：当你做了向下兼容的功能性新增修订号：当你做了向下兼容的问题修正
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面，作为延伸。
3. 补充此外，百度经验中有一篇软件版本命名规范的文章，写的也很不错，摘录如下。
软件版本号有四部分组成，第一部分为主版本号，第二部分为次版本号，第三部分为修订版本号，第四部分为日期版本号加希腊字母版本号，希腊字母版本号共有五种，分别为base、alpha、beta 、RC 、 release。
版本号修改规则：

主版本号：当功能模块有较大的变动，比如增加模块或是整体架构发生变化。此版本号由项目决定是否修改。

次版本号：相对于主版本号而言，次版本号的升级对应的只是局部的变动，但该局部的变动造成程序和以前版本不能兼容，或者对该程序以前的协作关系产生了破坏，或者是功能上有大的改进或增强。此版本号由项目决定是否修改。

修订版本号：一般是Bug的修复或是一些小的变动或是一些功能的扩充，要经常发布修订版，修复一个严重 Bug 即可发布一个修订版。此版本号由项目经理决定是否修改。

日期版本号：用于记录修改项目的当前日期，每天对项目的修改都需要更改日期版本号。此版本号由开发人员决定是否修改。

希腊字母版本号：此版本号用于标注当前版本的软件处于哪个开发阶段，当软件进入到另一个阶段时需要修改此版本号。此版本号由项目决定是否修改。



上一级有变动时，下级要归零。

软件版本阶段说明：

Base：此版本表示该软件仅仅是一个假页面链接，通常包括所有的功能和页面布局，但是页面中的功能都没有做完整的实现，只是做为整体网站的一个基础架构。

Alpha：软件的初级版本，表示该软件在此阶段以实现软件功能为主，通常只在软件开发者内部交流，一般而言，该版本软件的Bug较多，需要继续修改，是测试版本。测试人员提交Bug经开发人员修改确认之后，发布到测试网址让测试人员测试，此时可将软件版本标注为alpha版。

Beta：该版本相对于Alpha版已经有了很大的进步，消除了严重错误，但还需要经过多次测试来进一步消除，此版本主要的修改对象是软件的UI。修改的Bug经测试人员测试确认后可发布到外网上，此时可将软件版本标注为 beta版。

RC：该版本已经相当成熟了，基本上不存在导致错误的Bug，与即将发行的正式版本相差无几。

Release：该版本意味“最终版本”，在前面版本的一系列测试版之后，终归会有一个正式的版本，是最终交付用户使用的一个版本。该版本有时也称标准版。


]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>android</tag>
        <tag>java</tag>
      </tags>
  </entry>
  <entry>
    <title>Java API概览</title>
    <url>/dev-java-api/</url>
    <content><![CDATA[1. 前言今天，在开发Android的时候，发现自己对Android缺乏全局掌控的感觉。为什么这么说呢？比如，要实现的功能是帧动画，我根本不知道Android提供了一个AnimationDrawable类。
是因为Java没学好？不是，但是Java确实还有很大的进步空间。借此机会，捋一捋Java各个包和类的作用！


2. javajava类库是java发布之初就确定了的基础库，是核心包。
2.1. applet提供创建 applet 所必需的类和 applet 用来与其 applet 上下文通信的类。 
applet 框架包括两种实体：applet 和 applet 上下文。applet 是一种可嵌入的窗体（参见 Panel 类），它带有几个 applet 上下文用来初始化、启动和终止 applet 的额外方法。 
applet 上下文是负责加载和运行 applet 的应用程序。例如，applet 上下文可能是 Web 浏览器或 applet 开发环境。 
2.2. awt包含用于创建用户界面和绘制图形图像的所有类。在 AWT 术语中，诸如按钮或滚动条之类的用户界面对象称为组件。Component 类是所有 AWT 组件的根。有关所有 AWT 组件的公共属性的详细描述，请参见 Component。 
当用户与组件交互时，一些组件会激发事件。AWTEvent 类及其子类用于表示 AWT 组件能够激发的事件。有关 AWT 事件模型的描述，请参见 AWTEvent。 
容器是一个可以包含组件和其他容器的组件。容器还可以具有布局管理器，用来控制容器中组件的可视化布局。AWT 包带有几个布局管理器类和一个接口，此接口可用于构建自己的布局管理器。有关更多信息，请参见 Container 和 LayoutManager。
2.2.1. color2.2.2. dnd2.2.3. datatransfer2.2.4. event2.2.5. font2.2.6. geom2.2.7. im2.2.8. image2.2.9. print2.2.10. doc-files2.3. beans包含与开发 beans 有关的类，即基于 JavaBeans™ 架构的组件。少数类可由 bean 使用，也能以应用程序的形式运行。例如，event 类由激发属性和禁止更改事件的 bean 使用（参见 PropertyChangeEvent）。不过，此包中的大多数类由 bean 编辑器（即自定义 bean 并将其汇集起来以创建应用程序的开发环境）使用。特别要指出的是，这些类帮助 bean 编辑器创建用户可以用来自定义 bean 的用户界面。例如，bean 可能包含 bean 编辑器也许不知道如何处理的特殊类型的属性。通过使用 PropertyEditor 接口，bean 开发人员可以为此特殊类型提供一个编辑器。 
为了最大限度地减少 bean 使用的资源，只在要编辑 bean 时加载 bean 编辑器使用的类。当 bean 以应用程序的形式运行时，不需要这些类，所以不用加载它们。此信息在称为 bean-info 的类中（参见 BeanInfo）。 
除非显式声明，否则 null 值或空 String 对于此包中的方法是无效参数。如果使用这些参数，可能将引发异常。
2.3.1. beancontext2.4. io通过数据流、序列化和文件系统提供系统输入和输出。 除非另有说明，否则向此包的任何类或接口中的构造方法或方法传递 null 参数时，都将抛出 NullPointerException。 
2.5. lang提供利用 Java 编程语言进行程序设计的基础类。最重要的类是 Object（它是类层次结构的根）和 Class（它的实例表示正在运行的应用程序中的类）。 
把基本类型的值当成一个对象来表示通常很有必要。包装器类 Boolean、Character、Integer、Long、Float 和 Double 就是用于这个目的。例如，一个 Double 类型的对象包含了一个类型为 double 的字段，这表示如果引用某个值，则可以将该值存储在引用类型的变量中。这些类还提供了大量用于转换基值的方法，并支持一些标准方法，比如 equals 和 hashCode。Void 类是一个非实例化的类，它保持一个对表示基本类型 void 的 Class 对象的引用。 
类 Math 提供了常用的数学函数，比如正弦、余弦和平方根。类似地，类 String 和 StringBuffer 提供了常用的字符串操作。 
类 ClassLoader、Process、Runtime、SecurityManager 和 System 提供了管理类的动态加载、外部进程创建、主机环境查询（比如时间）和安全策略实施等“系统操作”。 
类 Throwable 包含了可能由 throw 语句抛出的对象(§14.16)。Throwable 的子类表示错误和异常。
2.5.1. annotation2.5.2. instrument2.5.3. management2.5.4. ref2.5.5. reflect2.6. math提供用于执行任意精度整数算法 (BigInteger) 和任意精度小数算法 (BigDecimal) 的类。BigInteger 除提供任意精度之外，它类似于 Java 的基本整数类型，因此在 BigInteger 上执行的操作不产生溢出，也不会丢失精度。除标准算法操作外，BigInteger 还提供模 (modular) 算法、GCD 计算、基本 (primality) 测试、素数生成、位处理以及一些其他操作。 BigDecimal 提供适用于货币计算和类似计算的任意精度的有符号十进制数字。BigDecimal 允许用户对舍入行为进行完全控制，并允许用户选择所有八个舍入模式。 
2.7. net为实现网络应用程序提供类。 
java.net 包可以大致分为两个部分：
1、低级 API，用于处理以下抽象：

地址，也就是网络标识符，如 IP 地址。

套接字，也就是基本双向数据通信机制。

接口，用于描述网络接口。 


2、高级 API，用于处理以下抽象：

URI，表示统一资源标识符。

URL，表示统一资源定位符。

连接，表示到 URL 所指向资源的连接。


2.8. nio定义作为数据容器的缓冲区，并提供其他 NIO 包的概述。 
NIO API 的集中抽象为： 

缓冲区，它们是数据容器； 

字符集及其相关解码器和编码器，它们在字节和 Unicode 字符之间进行转换；

各种类型的通道，它们表示到能够执行IO操作的实体的连接；以及选择器和选择键，它们与可选择信道 一起定义了多路的、无阻塞的 I/O 设施。 


2.8.1. channel2.8.2. charset2.9. rmi提供 RMI 包。RMI 指的是远程方法调用 (Remote Method Invocation)。它是一种机制，能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。可以用此方法调用的任何对象必须实现该远程接口。调用这样一个对象时，其参数为 “marshalled” 并将其从本地虚拟机发送到远程虚拟机（该远程虚拟机的参数为 “unmarshalled”）上。该方法终止时，将编组来自远程机的结果并将结果发送到调用方的虚拟机。如果方法调用导致抛出异常，则该异常将指示给调用方。 
2.9.1. activation2.9.2. dgc2.9.3. registry2.9.4. server2.10. security为安全框架提供类和接口。包括那些实现了可方便配置的、细粒度的访问控制安全架构的类。此包也支持密码公钥对的生成和存储，以及包括信息摘要和签名生成在内的可输出密码操作。最后，此包提供支持 signed/guarded 对象和安全随机数生成的对象。此包中提供的许多类（特别是密码和安全随机数生成器类）是基于提供商的。该类本身定义了应用程序可以写入的编程接口。实现本身可由独立的第三方厂商来写，可以根据需要进行无缝的插入。因此，应用程序开发人员可以利用任何数量的基于提供商的实现而不必添加或重写代码。 
2.10.1. acl2.10.2. cert2.10.3. interfaces2.10.4. spec2.11. sql提供使用Java™编程语言访问并处理存储在数据源（通常是一个关系数据库）中的数据的 API。此 API 包括一个框架，凭借此框架可以动态地安装不同驱动程序来访问不同数据源。尽管 JDBC™ API 主要用于将 SQL 语句传递给数据库，但它还可以用于以表格方式从任何数据源中读写数据。通过接口的 javax.sql.RowSet 组可以使用的 reader/writer 实用程序，可以被定制以使用和更新来自电子表格、纯文本文件或其他任何表格式数据源的数据。
2.12. text提供以与自然语言无关的方式来处理文本、日期、数字和消息的类和接口。这意味着所编写的主程序或 applet 是与语言无关的，并且它可以依靠独立的、动态链接的本地化资源。这实现了随时为新本地化添加本地化的灵活性。 
这些类能够格式化日期、数字和消息、解析、搜索和排序字符串，以及迭代字符、单词、语句和换行符。此包包含类和接口的三大主要组： 

用于迭代文本的类 
用于格式化和分析的类 
用于整理字符串的类 2.12.1. spi

2.13. util包含 collection 框架、遗留的 collection 类、事件模型、日期和时间设施、国际化和各种实用工具类（字符串标记生成器、随机数生成器和位数组）。 
2.13.1. concurrent2.13.2. jar2.13.3. logging2.13.4. prefs2.13.5. regex2.13.6. spi2.13.7. zip3. javaxjavax的x是extension的意思，也就是扩展包。javax类库是在java类库上面增加的一层东西，就是为了保持版本兼容要保存原来的，但有些东西有了更好的解决方案，所以，就加上些，典型的就是awt(Abstract Windowing ToolKit) 和swing。
3.1. accessibility定义了用户界面组件与提供对这些组件进行访问的辅助技术之间的协定。如果 Java 应用程序完全支持 Java Accessibility API，则它应该与屏幕读取器、屏幕放大器这样的辅助技术保持兼容和友好。使用完全支持 Java Accessibility API 的 Java 应用程序，将不再需要离屏模型的屏幕读取器 ，因为该 API 提供了离屏模型中通常所包含的所有信息。 
3.2. activation3.2.1. processing3.3. activity包含解组期间通过 ORB 机制抛出的与 Activity 服务有关的异常。 
3.4. annotation3.5. crypto为加密操作提供类和接口。在此包中定义的加密操作包括加密、密钥生成和密钥协商，以及消息验证码（Message Authentication Code，MAC）生成。 
加密支持包括对称密码、不对称密码、块密码和流密码。此包还支持安全流和密封的对象。 
此包中提供的许多类都是基于提供者的。该类本身定义可以写入应用程序的编程接口。然后可由独立的第三方供应商编写实现本身，并根据需要无缝嵌入。因此，应用程序开发人员可以利用任意数量的基于提供者的实现，而无需添加或重写代码。 
3.5.1. interfaces3.5.2. spec3.6. imageioJava Image I/O API 的主要包。 
使用 ImageIO 类的静态方法可以执行许多常见的图像 I/O 操作。 
此包包含一些基本类和接口，有的用来描述图像文件内容（包括元数据和缩略图）(IIOImage)；有的用来控制图像读取过程（ImageReader、ImageReadParam 和 ImageTypeSpecifier）和图像写入过程（ImageWriter 和 ImageWriteParam）；还有的用来执行格式之间的代码转换 (ImageTranscoder) 和报告错误 (IIOException)。 
3.6.1. envent3.6.2. metadata3.6.3. plugins3.6.4. spi3.6.5. stream3.7. jws3.7.1. soap3.8. lang3.8.1. model3.9. management提供 Java Management Extensions 的核心类。
Java Management Extensions (JMXTM) API 是一个用于管理和监视的标准 API。典型用途包括：

查询并更改应用程序配置 
累积有关应用程序行为的统计数据并使其可用 
通知状态更改及错误状况。

JMX API 还可以作为解决方案的一部分来管理系统、网络等。
API 包括远程访问，因此，远程管理程序可以基于这些目的与正在运行的应用程序进行交互。
3.9.1. loading3.9.2. monitor3.9.3. timer3.9.4. relation3.9.5. openmbean3.9.6. modelmbean3.9.7. remote3.10. naming为访问命名服务提供类和接口。 
此包定义 Java Naming and Directory InterfaceTM (JNDI) 的命名操作。  JNDI 向使用 Java 编程语言编写的应用程序提供命名和目录功能。它被设计成与任何特定的命名或目录服务实现无关。因此可以使用共同的方式对多种服务（新的、新出现的及已经部署的服务）进行访问。
3.10.1. directory3.10.2. event3.10.3. ldap3.10.4. spi3.11. net提供用于网络应用程序的类。这些类包括用于创建套接字的工厂。使用套接字工厂可以封装套接字的创建和配置行为。 
3.11.1. ssl3.12. print为 Java™ Print Service API 提供了主要类和接口。Java Print Service API 允许客户端和服务器应用程序具备如下功能： 

根据其性能发现并选择 PrintService。 
指定打印数据的格式。 
向支持所打印文档类型的服务提交 PrintJob。 

3.12.1. attribute3.12.2. event3.13. rmi包含 RMI-IIOP 的用户 API。这些 API 供 RMI-IIOP 应用程序使用，并在 IIOP 或 JRMP 上运行时提等效的语法。另请参阅 javax.rmi.CORBA 包。 
3.13.1. CORBA3.13.2. ssl3.14. script脚本 API 由定义 Java™ Scripting Engines 的接口和类组成，并为它们在 Java 应用程序中的使用提供框架。此 API 供那些希望在其 Java 应用程序中执行用脚本语言编写的程序的应用程序编程人员使用。脚本语言程序通常由应用程序的终端用户提供。 
3.15. security3.15.1. auth3.15.2. cert3.15.3. sasl3.16. sound3.16.1. sampled3.16.2. midi3.17. sql为通过 Java™ 编程语言进行服务器端数据源访问和处理提供 API。此包补充了 java.sql 包，它从 1.4 版本开始包含在 Java 平台、标准版 (Java SETM) 中。它保留了 Java 平台、企业版 (Java EETM) 中的精华部分。 
java.sql 包中提供以下内容： 

DataSource 接口，用于建立到数据源的连接，是 DriverManager 的替代项。 
连接池和语句池 
分布式事务 
Rowset 

应用程序直接使用 DataSource 和 RowSet API，但连接池和分布式事务 API 只能由中间层基础设施在内部使用。
3.17.1. rowset3.18. swing提供一组“轻量级”（全部是 Java 语言）组件，尽量让这些组件在所有平台上的工作方式都相同。有关使用这些组件的程序员指南，请参阅 Creating a GUI with JFC/Swing，该内容在 The Java Tutorial 的结尾处。有关其他参考资料，请参阅相关文档。
3.18.1. border3.18.2. colorchooser3.18.3. filechooser3.18.4. event3.18.5. table3.18.6. text3.18.7. tree3.18.8. undo3.18.9. plaf3.19. tools为能够从程序（例如，编译器）中调用的工具提供接口。 
要求这些接口和类作为 Java™ Platform, Standard Edition (Java SE) 的一部分，但是不要求提供任何实现它们的工具。 
除非明确允许，否则只要给定 null 参数或给定包含 null 元素的列表或集合，此包中的所有方法都将抛出 NullPointerException。类似地，除非明确允许，否则所有方法都不可以返回 null。 
此包是 Java 编程语言编译器框架的主要部分。此框架允许框架的客户端查找并运行程序中的编译器。该框架还为结构化访问诊断（DiagnosticListener）提供服务提供者接口（SPI），为重写文件访问提供文件抽象（JavaFileManager 和 JavaFileObject）。有关使用 SPI 的详细信息，请参阅 JavaCompiler。 
3.20. transaction包含解组期间通过 ORB 机制抛出的三个异常。 
3.20.1. xa3.21. xml根据 XML 规范定义核心 XML 常量和功能。
3.21.1. parsers3.21.2. bind3.21.3. soap3.21.4. ws3.21.5. transform3.21.6. crypto3.21.7. datatype3.21.8. validation3.21.9. namespace3.21.10. xpath3.21.11. stream4. orgJava的org包是由企业或者组织提供的java类库。集成到jdk中但大部分不是sun公司的，可以直接使用。其中比较常用的是w3c提供的对XML、网页、服务器的类和接口。
4.1. ietf4.1.1. igss4.2. omg4.2.1. CORBA4.2.2. stub4.2.3. CORBA_2_34.2.4. CosNaming4.2.5. SendingContext4.2.6. PortableServer4.2.7. POrtableInterceptor4.2.8. Messaging4.2.9. IOP4.2.10. Dynamic4.2.11. DynamicAny4.3. w3c4.3.1. dom4.4. xml4.4.1. sax5. 后记只展开到了第二级，今后如果有需要，再往下展开。
附上Android API概览：http://www.android-doc.com/reference/packages.html
6. 参考文档《JDK API 1.6.0 中文版》
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
      </tags>
  </entry>
  <entry>
    <title>Android开发——版本控制和代码分享</title>
    <url>/dev-android-version-control/</url>
    <content><![CDATA[1. 前言很自然的，觉得自己该使用一些版本控制工具了。虽然以前也使用过，但一直没有系统学习，现在，搞一搞吧！
以《Android开发——帧动画》中的Demo为例。
2. git作为一名程序员，不使用git，都不好意思和别人讲话。git的安装过程自行百度，或者参考郝同学的《Hexo环境搭建》。


2.1. 在github新建repository新建repository，命名为demo。可以看到，repository的地址https://github.com/voidking/demo.git
2.2. 配置git环境AS中，Settings，Version Control，Git。配置Path to Git executable，然后Test。
2.3. git initAS中，VCS，Import into Version Cotrol，Create Git Repository。OK之后，发现Project里的文件变成了红色。

git init，其实就是新建了一个本地仓库.git，相当于hexo里的.deploy_git。

2.4. git add单击Project名Demo，VCS，Git，Add。发现Project里的文件变成了绿色。

git add，其实就是把目标文件快照放入暂存区域，同时未曾跟踪过的文件标记为需要跟踪。

2.5. git commitVCS，Git，Commit Directory。单击Commit。

git commit，提交时记录的是放在暂存区域的快照，任何还未暂存的仍然保持已修改状态，可以在下次提交时纳入版本管理。每一次运行提交操作，都是对你项目作一次快照，以后可以回到这个状态，或者进行比较。

2.6. 指定连接目标指定本地库与github的repository相连。
方法一（推荐）：在Demo目录下，右键，Git Bash Heregit remote add origin https://github.com/voidking/demo.git
方法二：VCS，Git，Push。如果没有执行方法一，这时可以看到Define Remote。填入https://github.com/voidking/demo.git，会报错，不知道为什么。
Remote URL is invalid: unable to access &#x27;https://github.com/voidking/demo.git&#x27;

2.7. git pushVCS，Git，Push。
填入用户名和密码。之后，可以看到窗口最下面有个push进度条在跑。
如果弹出提示：我们勾选Encrypt with OS user credentials，意思是使用本机的SSH key。不明白的地方请见郝同学另一篇文章，《Hexo环境搭建》。
过一会儿，便可以看到成功的提示！
2.8. github我们查看一下github上的repository。至此，大功告成！

AS集成了GitHub插件，配置起来比上面的git配置要简单的多，钟爱GitHub的小伙伴不妨百度学习一下。

3. svn在实习的时候，使用过一段时间的svn。个人感觉，svn在团队开发中很好用。但是，当时在局域网使用，有些局限性。如果svn服务器搭在公网服务器上（比如阿里云），应该是极好的。值得一提的是，利用svn向sae的项目中上传文件，非常方便。
3.1. 免费SVN服务器比起自己搭建服务器，抠门的郝同学更愿意使用免费svn服务器。但是，这种免费svn服务器极少，找了很久，只找到一个TaoCode。
http://code.taobao.org/
3.2. 安装svnsvn，分为服务器和客户端，因为服务器使用TaoCode，所以我们只安装客户端就可以了。
1、服务器下载地址https://subversion.apache.org/download/
2、客户端下载地址http://tortoisesvn.net/downloads.html
3、服务器安装略。（突然感觉一个略字大气磅礴，瞬间省了好多字）
4、客户端安装需要注意的是，安装时选择安装command line（默认不安装），为了在AS中使用。
3.3. 在TaoCode新建项目新建项目，命名为vkdemo。可以看到，svn repo地址为http://code.taobao.org/svn/vkdemo/ 
3.4. Checkout from Subversion1、打开AS，VCS，Checkout from Version Control，Subversion。2、点击加号（Add Repository Location）。3、添加Repository URL。此时Repository里啥也没有，咱们就不Checkout了。
3.5. Import into Subversion1、打开AS，VCS，Import from Version Control，Import into Subversion。2、选中URL，Import。3、选中需要Import的目录，OK。
点击OK，啊嘞，报错了！
哥哥是见过大世面的人，这点小错算什么！Settings，Version Control，Subversion，去掉General选项卡中所有勾。再次import，成功！不过，由于是整个项目上传，包括编译文件等，上传过程会很慢。
3.6. 浏览svn文件夹打开AS，VCS，Browse VCS Repository，Browse Subversion Repository，选择URL。
再看Project目录：

文件红色：表示文件没有添加到服务器

绿色：表示没有更新新的修改到服务器

普通黑色：表示和服务器同步


3.7. 文件夹符号说明
黄色感叹号(有冲突)：这是有冲突了，冲突就是说你对某个文件进行了修改，别人也对这个文件进行了修改，别人抢在你提交之前先提交了，这时你再提交就会被提示发生冲突，而不允许你提交，防止你的提交覆盖了别人的修改。要解决冲突，如果你确认你的修改是无效的，则用TSVN还原你的修改就行了；如果认为你的修改是正确的，别人的提交是无效的，那么用TSVN先标记为“解决冲突”，然后就可以提交了；如果你认为你的修改和别人的修改都有一部分是有效的，那么你就把别人的修改手动合并到你的修改中，然后使用TSVN标注为“解决冲突”，然后就可以提交了。
米字号(有本地修改代码)：这是说明你有未提交的本地代码。 
问号(新加入的资源)：这说明该文件是项目中新增文件资源，新增资源可以是文件、图片、代码等。
红色感叹号(本地代码与库没有保持一致)：这说明本地代码跟库上没有保持一致，如果用户想修复，可以将带红色感叹号图标文件删除，直接update即可。 
灰色向右箭头(本地修改过)：本地代码没有及时上库。
蓝色向左箭头(SVN上修改过)：记得更新代码后修改，提交前跟svn对比习惯。
灰色向右且中间有个加号的箭头(本地比SVN上多出的文件)：修改完记得跟svn保持一致。
蓝色向左且中间有个加号的箭头(SVN上比本地多出的文件)：删除该文件后，再次更新，将svn上文件全部更新下来。 
灰色向右且中间有个减号的箭头(本地删除了,而SVN上未删除的文件)：也就是说你删除确认后，一定要记得上库，跟svn保持一致 
蓝色向左且中间有个减号的箭头(SVN上删除了,而本地未删除的文件)：比对svn库上代码，确定需要删除后，更新svn(删除无用代码)。
红色双向箭头(SVN上修改过,本地也修改过的文件)：这个表示本地和svn上都修改过，最好就是把本地修改合并到svn，修改代码前最后先更新。
3.8. 单个文件签入签出右键subversionAdd：添加到服务器
Commit：提交
Update：更新，获取新版本
Integrate：合并
4. 后记对于git和svn的区别，哥只想说——啥，啥，这都是啥！。。。还是稍微总结一下吧！
1、git是分布式的，svn不是。

svn貌似进化了，它提供的命令svnsync就可以checkout所有版本到本地，这是分布式的特点哇！

2、git直接记录快照，而非差异比较。
3、git几乎所有操作都是本地执行（因为区别1）。
知道了这三条，平时吹吹牛逼应该够了，不满足的小伙伴请移步参考资料。
5. 参考资料《Pro Git》http://git-scm.com/book/zh/v1
Learn Git Branchinghttp://learngitbranching.js.org/
git-使用简易指南http://www.bootcss.com/p/git-guide/
Android Studio之版本管理工具Githttp://www.wfuyu.com/technology/22499.html
Android studio -VSN 使用笔记http://www.cnblogs.com/shaocm/p/4182380.html
GIT和SVN之间的五个基本区别http://www.oschina.net/news/12542/git-and-svn
Git与svn的区别http://blog.csdn.net/huacuilaifa/article/details/19124635
为什么说 Git 比 SVN 更好http://www.oschina.net/news/29214/why-git-is-better-than-svn
分布式和集中式版本控制工具-svn,git,mercurial比较分析http://blog.csdn.net/augusdi/article/details/29846253
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
        <tag>git</tag>
      </tags>
  </entry>
  <entry>
    <title>Android开发——帧动画</title>
    <url>/dev-android-frameanimation/</url>
    <content><![CDATA[1. 前言获得成就感是学习的灵魂！对于编程，尤其如此。模仿着做一些简单的应用先，之后再补充知识点。This is the plan !
2. 应用简介单击手机屏幕，播放动画。
3. 设计思路1、拷贝关键帧（图片）到资源文件夹。2、在xml文件中定义关键帧、帧顺序、切换时间。PS：一般切换时间为1/24秒，我们这里用0.1秒。3、图片显示在ImageView。4、响应单击事件。5、使用AnimationDrawable绘制动画。


4. 实现流程4.1. 新建Project新建Project，命名为Demo。
4.2. 重命名appapp重命名为frameanimation，或者删除app，新建Module，命名为frameanimation。
4.3. 准备素材1、百度“帧图片素材”，找到一套自己喜欢的帧图片。2、图片重命名为以字母开头，这里郝同学准备的图片名称为fight_01.png到fight_16.png。3、将图片拷贝到src/main/res/drawable文件夹。PS：郝同学尝试在res新建image文件夹，结果不可以使用，不知道为什么。
4.4. 新建xml文件右击drawable文件夹，New，Drawable resource file，File name随意（郝同学起名为frameanimation.xml），Root element选择animation-list。frameanimation.xml内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;animation-list xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;    android:oneshot=&quot;true&quot;&gt;    &lt;item android:drawable=&quot;@drawable/fight_01&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_02&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_03&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_04&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_05&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_06&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_07&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_08&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_09&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_10&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_11&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_12&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_13&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_14&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_15&quot; android:duration=&quot;100&quot; /&gt;    &lt;item android:drawable=&quot;@drawable/fight_16&quot; android:duration=&quot;100&quot; /&gt;&lt;/animation-list&gt;

一起来分析一下这个文件：

android:oneshot=”true”，默认循环播放，加上这个属性，只播放一遍。

android:drawable=”@drawable/fight_01”，原来，.png是需要省略掉的。

android:duration=”100”，切换间隔时间100ms。


4.5. 添加ImageView控件1、打开activity_main.xml，Design，在Widgets中选中ImageView放到布局中。2、双击，在src中选择Project&gt;Drawable&gt;frameanimation，OK。3、指定一个id，添加的第一个ImageView默认为imageView，第二个默认为imageView2，依次类推。4、拖拉一下，修改一下长宽和大小。
4.6. 修改activity_main.xml&lt;RelativeLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;    xmlns:tools=&quot;http://schemas.android.com/tools&quot; android:layout_width=&quot;match_parent&quot;    android:layout_height=&quot;match_parent&quot; android:paddingLeft=&quot;0dp&quot;    android:paddingRight=&quot;0dp&quot;    android:paddingTop=&quot;0dp&quot;    android:paddingBottom=&quot;0dp&quot; tools:context=&quot;.MainActivity&quot;&gt;    &lt;ImageView        android:layout_width=&quot;match_parent&quot;        android:layout_height=&quot;match_parent&quot;        android:scaleType=&quot;fitXY&quot;        android:id=&quot;@+id/imageView&quot;        android:src=&quot;@drawable/frameanimation&quot;        android:layout_alignParentLeft=&quot;true&quot;        android:layout_alignParentStart=&quot;true&quot;        android:layout_alignParentRight=&quot;true&quot;        android:layout_alignParentEnd=&quot;true&quot;        android:layout_alignParentBottom=&quot;true&quot;        android:layout_alignParentTop=&quot;true&quot; /&gt;&lt;/RelativeLayout&gt;
这一步主要修改了一下ImageView的布局，使其填充整个界面，看起来效果比较好。非必要步骤，可不做。
4.7. 修改MainActivity.javapackage com.voidking.android.demo;import android.graphics.drawable.AnimationDrawable;import android.support.v7.app.ActionBarActivity;import android.os.Bundle;import android.view.Menu;import android.view.MenuItem;import android.view.View;import android.widget.ImageView;public class MainActivity extends ActionBarActivity &#123;    @Override    protected void onCreate(Bundle savedInstanceState) &#123;        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        ImageView imageView = (ImageView) findViewById(R.id.imageView);        final AnimationDrawable animationDrawable = (AnimationDrawable)imageView.getDrawable();        imageView.setOnClickListener(new View.OnClickListener() &#123;            @Override            public void onClick(View v) &#123;                animationDrawable.stop();                animationDrawable.start();            &#125;        &#125;);    &#125;    @Override    public boolean onCreateOptionsMenu(Menu menu) &#123;        // Inflate the menu; this adds items to the action bar if it is present.        getMenuInflater().inflate(R.menu.menu_main, menu);        return true;    &#125;    @Override    public boolean onOptionsItemSelected(MenuItem item) &#123;        // Handle action bar item clicks here. The action bar will        // automatically handle clicks on the Home/Up button, so long        // as you specify a parent activity in AndroidManifest.xml.        int id = item.getItemId();        //noinspection SimplifiableIfStatement        if (id == R.id.action_settings) &#123;            return true;        &#125;        return super.onOptionsItemSelected(item);    &#125;&#125;
我们修改的，其实只有OnCreate函数，在里面添加了对ImageView的单击事件的监听和动作。之所以先stop再start，是因为animationDrawable.start()之后，显示到了最后一个帧，停留在最后一个帧，但是，这时候animationDrawable的运行并没有结束，必须stop之后，才能开始新一轮动画播放。
4.8. 运行单击工具栏上的绿色三角形，选择一个AVD，运行即可。
5. 效果展示
6. 代码分享https://github.com/voidking/android-frameanimation ，需要的同学移步自取。
7. 后记一步一步，不怕慢，胜在不止！
8. 参考资料7天学会Androidhttp://www.duobei.com/room/trial/9152038430
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
      </tags>
  </entry>
  <entry>
    <title>Android开发——Android Studio</title>
    <url>/dev-android-as/</url>
    <content><![CDATA[1. 简介Android Studio 是一个Android开发环境，基于IntelliJ IDEA. 类似 Eclipse ADT，Android Studio 提供了集成的 Android 开发工具用于开发和调试。
在IDEA的基础上，Android Studio 提供：

基于Gradle的构建支持
Android 专属的重构和快速修复
提示工具以捕获性能、可用性、版本兼容性等问题
支持ProGuard 和应用签名
基于模板的向导来生成常用的 Android 应用设计和组件
功能强大的布局编辑器，可以让你拖拉 UI 控件并进行效果预览



2. 界面1、Project 面板用于浏览项目文件。 Project面板会显示当前的所有的module 。
2、Build Variants 面板用于设置当前项目的Build Variants。
3、Android 面板功能类似于Eclipse中的Logcat，但是比其多了一些常用功能，例如：截图，查看系统信息等。
4、编辑区用于编辑文件。
5、Gradle 面板Gradle任务列表，双击可执行Gradle任务。常用任务： build 、 clean 、 assemble 、 assembleRelease 、 assembleDebug 、 lint 。
3. 项目结构3.1. Android模式新建一个项目，默认项目结构如下：可以看到和Eclipse的目录结构有很大区别，AS一个窗口只能有一个项目，而Eclipse则可以同时存在很多项目。
3.2. Project模式
目录结构中将java代码和资源文件（图片、布局文件等）全部归结为src，在src/main下，有java和res两个文件夹，java文件夹则相当于Eclipse下的src文件夹，res目录结构则一样。
下面详解一下Project模式下的目录结构：
3.2.1. .gradle官网www.gradle.org上介绍Gradle是升级版(evolved)的自动化构建工具。它可以自动构建，测试，发布，部署，同时使更多的软件包或其他类型诸如生成静态网站，文档等项目自动化。
说白了，gradle是maven的同类工具，而且比maven更好用。
打开.gradle文件夹，我们看到，里面除了有配置文件，还有一些二进制文件。由此，郝同学推测，这个目录中存放的就是gracle这款工具。
3.2.2. .ideaProject的配置文件目录，类似Eclipse中的project.properties。
3.2.3. app一个Module，基本开发单位。

build构建目录，相当于Eclipse中默认Java工程的bin目录。 

libs依赖包。

src/main/javaJava代码。

src/main/res/layoutApp布局及界面元素配置。

src/main/res/menuApp菜单配置。

src/main/res/valuesdimens.xml 定义css的配置文件；strings.xml 定义字符串的配置文件；styles.xml 定义style的配置文件。

src/main/res/mipmap-*App图标。

src/main/AndroidMainfest.xmlApp基本信息（名称、版本、SDK、权限等等）。

src/.gitignore顾名思义，郝同学猜测这个文件是用来配置上传到github时忽略的文件夹。

src/app.iml.iml文件是AS识别项目的配置文件，跟Eclipse里面的.project文件作用类似

src/build.gradlegradle构建脚本。

src/proguard-rules.pro混淆规则的声明。混淆代码，可以保护自己的作品不被别人破解。


3.2.4. build
3.2.5. gradle
3.2.6. .gitignore等.gitignore、build.gradle、First.iml三个文件，在Module中也存在，只不过作用范围不一样。
3.2.7. gradle.propertiesProject-wide Gradle settings。
3.2.8. gradlew和gradlew.batgradlew是一个shell脚本，适用于Linux；gradlew.bat是一个批处理文件，适用于windows。
gradlew，是对gradle的一个封装，在使用这个命令时会自动去下载gradle，如果已经安装了gradle则在编译过程中使用的gradlew命令应该都可以替换成gradle。自动下载gradle是根据gradle\wrapper目录中的gradle-wrapper.properties中配置的，distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
3.2.9. local.propertiesThis file is automatically generated by Android Studio。
3.2.10. setting.gradle配置Project中包含的Module。在完全删除某个Module之前,如果还想要把它添加到当前project中,直接setting.gradle中添加该Module的名称即可。

AS中有Project和Module的概念：project in Android Studio is like a workspace in Eclipse。module in Android Studio is like a project in Eclipse。

4. 增删项目4.1. 新建Project新建Project的时候，会同时新建一个默认Module，name为app，可以重命名。新建Project和打开Project，都会打开一个新的AS窗口，可以在菜单栏Window中切换Project。
4.2. 删除Project在工作路径下，删除Project文件夹。
4.3. 新建Module新建Module，存放在当前的Project下，共享当前Project的一些资源，比如External Libraries和部分配置文件。
4.4. 删除Module1、File，Project Structure；或者右击项目，Open Module Settings。2、选中Module，单击右上角的“-”号。3、右击Module（此时Module上的手机符号已经没有了），delete。
5. 个性设置5.1. 编辑器字体1、File，Settings；或者直接点击工具栏的Settings。2、Editor，Color&amp;Fonts，Font。3、把Darcula主题sava as为自己的主题，然后进行设置。
5.2. 编码格式默认编码格式：File，Other Settings，Default Settings，Editor，File Encodings。
当前Project编码格式：File，Settings，Editor，File Encodings。
5.3. 代码提示Settings，Editor，General，Code Completion，Case sensitive completion改为None。
5.4. 悬停提示Settings，Editor，General，Show quick doc on mouse move前打勾。
6. Lint工具对代码进行测试是一回事，但同样重要的是、我们还需要在编写代码的同时引入各种最佳实践。这不仅能够显著改进性能表现，也能增加应用程序的整体稳定性。另外，经过合理结构调整的项目在维护方面也更为轻松。
Android Studio中提供的Android Lint是一款静态分析工具，它负责对项目源代码加以分析。它能够检测出应用程序中的潜在漏洞以及其它可能被编译器所忽略的其它代码问题。
就以下面这幅截图为例，大家可以看到该布局中的LinearLayout并未得到实际使用。Android Lint的优势在于，它能帮助我们重视警告或报错信息的出现原因，从而更轻松地修复或者解决这些问题。
请大家养成重复使用Android Studio Lint工具的好习惯，这能帮助我们准确检测到项目当中存在的潜在问题。Lint工具甚至能告诉我们应用程序中是否存在重复的图片或者编译内容。
要运行Lint工具，大家首先需要在Android Studio的“Analyze”菜单中选择“Inspect Code…”。当Android Studio完成了对项目的检测之后，它会在窗口底部显示出分析结果。请注意，除了Android Lint之外，Android Studio还提供一系列其它检查功能。只需双击某个已经发现的问题，系统就会帮助大家定位到对应文件中存在问题的位置。
7. 富布局编辑器AS提供一套富布局编辑器，大家可以在其中随意拖拽各类用户界面组件。大家还可以在多屏幕配置中同时查看多种布局的显示效果，这一点我们在前文中已经提到过。
这款富布局编辑器在使用方面非常直观简单。我们首先需要一套要处理的布局方案。浏览到项目中res文件夹下的layout文件夹，右键点击layout文件夹，然后在弹出的菜单中选择New&gt;Layout resource file。
下面为新布局设定一个名称与root元素，而后点击“OK”。AS会自动在窗口右侧的编辑器当中打开该布局。
在编辑器的底部，大家会看到两个标签，分别是Design与Text。点击Text标签后编辑器将被激活，这样我们就能对当前选定的布局方案作出变更。
点击Design标签则会激活另一套编辑器内容，其中显示出布局的预览效果。要向布局当中添加其它功能性组件，我们只需将其从布局左侧的组件列表中拖出并放入布局内即可。是的，就这么简单。

8. 快捷键Settings，Keymap，选择一个Keymaps。如果习惯用Eclipse，那么不妨选择Eclipse。谷歌建议使用默认快捷键，因为比Eclipse要丰富。如果使用默认快捷键，下面的几个快捷键最好记住。
8.1. ctrl+shift+enter代码自动补全。
8.2. alt+enter代码提示。
8.3. ctrl+alt+o引入包管理。
8.4. ctrl+q查看定义。
8.5. ctrl+alt+t弹出包围结构。
8.6. ctrl+j模板提示。
9. 后记AS很强大，不可能面面俱到，先就这些吧。无论使用什么工具，当你开始使用它，一切都会变得简单！
10. 参考文档与Android Studio的第一次亲密接触http://www.imooc.com/learn/206
Android Studio 常用功能介绍http://ask.android-studio.org/?/article/23
Android Studio系列教程二–基本设置与运行http://stormzhang.com/devtools/android-studio-tutorial2/
Android学习系列(41)–Android Studio简单使用http://www.cnblogs.com/qianxudetianxia/p/3848272.html
最全面的Android Studio使用教程http://www.open-open.com/lib/view/open1416883124753.html
Gradle基础http://segmentfault.com/a/1190000002439306
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
      </tags>
  </entry>
  <entry>
    <title>Android开发——helloworld</title>
    <url>/dev-android-helloworld/</url>
    <content><![CDATA[1. 前言Android的学习，拖拉了太久太久，这个夏天，搞个作品出来吧！从0开始，用博客记录进度！
2. 工具之前使用adt，断断续续学习过安卓开发。今年谷歌宣布Android Studio将取代Eclipse，正式成为官方集成开发软件，中止对后者支持。郝同学也紧跟潮流，换用Android Studio（以后简称AS）。


3. 步骤3.1. 下载安装1、Android Studio中文社区http://www.android-studio.org/
2、百度“Android Studio”，从百度软件中心下载即可。
3.2. 新建工程打开AS，选择工作路径，输入工程名（首字母习惯大写），选择sdk版本，选择一个模板，finish即可。（和Eclipse非常类似）
3.3. 新建模拟器点击AVD Manager，Create Virtual Device，选择一款模拟器，next，输入模拟器名称，finish即可。
3.4. 运行点击Run ‘app’，Choose Device，OK。正常情况下，就可以看到经典的“helloworld”了。郝同学在启动AVD的时候出现了端口冲突，下面是解决办法。
4. 端口冲突4.1. 详细描述启动AVD报错：
4.2. 解决办法4.2.1. 查看端口netstat -ano|findstr &quot;5037&quot;看到PID为20968。
4.2.2. kill进程ctrl+shift+esc，打开任务管理器，详细信息，找到PID为20968的进程，结束进程。
5. 运行结果解决完上述错误，再次Run ‘app’，就可以看到：
6. 后记安卓开发，就当做娱乐来搞吧。白天复习备考，晚上搞搞开发，也是一件很有意思的事情。贵在坚持，不出作品不止！
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
      </tags>
  </entry>
  <entry>
    <title>Markdown文档导出为pdf</title>
    <url>/hobby-markdown-to-pdf/</url>
    <content><![CDATA[一直非常眼热Mac下的Mou，从Markdown导出pdf非常方便，而且格式良好。
没有Mac，只能找一些Windows下可以使用的工具。之前一直使用MarkdownPad2，这货导出格式特别难看就算了，而且，收费！
今天找到了另一种方式，虽然麻烦了一点点，但是格式很不错。现记录如下：1、使用在线编辑器StackEdit：https://stackedit.io/editor
2、预览
3、打印打印时保存为pdf。
4、添加书签使用福昕PDF阅读器添加书签（包括子目录书签）。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>markdown</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux复习整理</title>
    <url>/dev-linux-review/</url>
    <content><![CDATA[1. 题型1、填空（10*1’)2、选择（10*1’）3、简单操作题（写命令）(5*2’)4、简答题（6*5’）5、综合题（2*20’）（程序或者流程图）


2. 考纲2.1. 命令题2.1.1. 定时任务如何设计小明是个网管，老板要求他每天早晨3点起来看看磁盘空间满不满，请问你如何用学过的知识为小明解决这一痛苦的问题。
2.1.1.1. 编写脚本文件假设在/root目录下：vim diskfree.sh，内容如下：
#!/bin/bash#取得每个分区的使用百分比（不要百分号）percent=`df -h | grep -v Filesystem| awk &#x27;&#123;print int($5)&#125;&#x27;`#循环判断分区使用率是否超过90%for each_one in $percentdo		#判断使用率是否超过90%        if [ $each_one -ge 90 ];then				#如果超过90 则把使用情况发给mail_address                df | mail -s &quot;Disk Critical&quot; mail_address        fidone
稍微解释一下代码：df，检查文件系统的磁盘空间占用情况；-h，以方便阅读方式展示，这个参数可不加。|，管道命令，左侧命令的处理结果传递给右侧。grep -v，忽略含有Filesystem的这一行。awk ‘{print int($5)}’，逐行读入文件流，以空格或TAB为默认分隔符将每行切片，切开的部分再进行各种分析处理。这里实现的效果是，把第5个域（Use%）的数据转换为int类型。-ge，大于或等于，可以换成-gt，表示大于。mail -s，之后跟的是标题和收件人邮箱，管道之前的是内容。
2.1.1.2. 添加执行权限chmod +x diskfree.sh
2.1.1.3. 添加自动执行vim /etc/crontab，追加如下一句：
0 3 * * * root /root/diskfree.sh &gt; /dev/null 2&gt;&amp;1
上面的代码依次对应：
m（分）：1～59 每分钟用*或者 */1表示h（时）：1～23（0表示0点）dom（日）：1～31mon（月）：1～12dow（周）：0～6（0表示星期天）user（用户）：用户command（命令）：命令
下面是一些时间例子：1、每天早上6点10分2、每两个小时3、晚上11点到早上8点之间每两个小时，早上8点4、每个月的4号和每个礼拜的礼拜一到礼拜三的早上11点5、1月份日早上4点 
10 6 * * * 0 */2 * * * 0 23-7/2，8 * * * 0 11 4 * mon-wed 0 4 1 jan * 


&gt; /dev/null 2&gt;&amp;1，这一句可以省略。如果不加这一句，当程序在你所指定的时间执行后，系统会发一份邮件到你的mail里面(/usr/spool/mail/用户名)，显示该程序执行的内容。

2.1.2. 命令使用2.1.2.1. mv某领导要求小刚把bin目录下后缀名为“.tx.htm”的文件重命名为“.html”假设当前目录为bin，则：1、单个文件重命名mv test.tx.htm test.html2、批量重命名rename &#39;s/\.tx\.htm$/\.html/&#39; *.tx.htmPS：移动文件mv test.tx.htm ..，把test.tx.htm移动到上一层目录。
2.1.2.2. mount把/dev/sdb1挂载到/mnt/sdb1。mkdir /mnt/sdb1，新建文件夹sdb1。mount /dev/sdb1 /mnt/sdb1，挂载。umount /mnt/sdb1，卸载。
2.1.2.3. cp登录到电信机房，发现木有显示屏，请问如何把can目录底下所有后缀名为“txt”的文件拷贝到U盘中，假设U盘的目录是/udsk，当前所在目录为can。cp *.txt /udsk
2.1.2.4. passwd给新员工小明分配一个账户并设置默认密码“12345678”。useradd xiaomingpasswd xiaoming，之后两次输入密码12345678
2.1.2.5. ifconfig小明发现网络不通，他想看看第二块网卡是否分正常分配了IP地址，他如何为第二块网卡分配192.168.1.1。ifconfig eth1ifconfig eth1 192.168.1.1 netmask 255.255.255.0
2.1.2.6. netstat某web服务器部署在linux上，当远程登录时发现该应用不通。请问如何检查该应用是否起动，假设该应用的服务端口号是“8181”。netstat -anp | grep 8181

a ：all，表示列出所有的连接，服务监听，Socket资料
t ：tcp，列出tcp协议的服务
u ：udp，列出udp协议的服务
n ：port number， 用端口号来显示
l ：listening，列出当前监听服务
p ：program，列出服务程序的PID

2.1.2.7. pwd如何知道工作目录？pwd
-P：如果当前的工作路径是链接的话，显示链接的原始路径，也就是实际路径。
2.1.2.8. who当前终端登录的用户是谁？who am i
2.1.2.9. ls查看当前文件夹下所有文件，包含隐藏文件和属性ls -la
2.2. 简答题2.2.1. Linux有哪些运行级别？Linux系统有7个运行级别(runlevel)。运行级别0：系统停机状态，系统默认运行级别不能设为0，否则不能正常启动运行级别1：单用户工作状态，root权限，用于系统维护，禁止远程登陆运行级别2：多用户状态(没有NFS)运行级别3：完全的多用户状态(有NFS)，登陆后进入控制台命令行模式运行级别4：系统未使用，保留运行级别5：X11控制台，登陆后进入图形GUI模式运行级别6：系统正常关闭并重启，默认运行级别不能设为6，否则不能正常启动
PS：1、查看当前运行级别runlevel2、切换运行级别init N，N的值为0到6，其中，0为关机，6为重启。
2.2.2. 如何创建一个用户？1、su，切换到root用户。2、useradd testuser，创建一个名为testuser的用户。

c： comment，指定一段注释性描述。
d：目录，指定用户主目录，如果此目录不存在，则同时使用-m选项，可以创建主目录。
g：用户组，指定用户所属的用户组。

3、passwd testuser，之后回车，为testuser设置密码。
2.2.3. 文件系统linux有哪些常见的文件系统？linux管理Windows下的文件系统？
linux常见的文件系统有ext、ext2、ext3、ext4、JFS、XFS、ReiserFS等。假设linux下有一个盘hda1的文件系统为fat32，那么：mkdir /mnt/win1mount -t vfat /dev/hda1 /mnt/win1假设linux下有一个盘hda2的文件系统为ntfs，如果内核支持ntfs，那么：mkdir /mnt/win2mount -t ntfs /dev/hda2 /mnt/win2假设linux下有一个盘hda2的文件系统为ntfs，如果内核不支持ntfs，那么：apt-get intall ntfs-3g，此命令适合Ubuntu系统mkdir /mnt/win2ntfs-3g /dev/hda2 /mnt/win2
2.2.4. gcc/g++gcc编译的步骤有哪些？会生成哪些文件？
gcc/g++在执行编译工作的时候，总共需要4步。1、预处理，生成.i的文件。（预处理器cpp）2、将预处理后的文件转换成汇编代码，生成文件.s。（编译器egcs）3、将汇编代码变为目标代码(机器代码)生成.o的文件。（汇编器as）4、连接目标代码，生成可执行程序。（链接器ld）
假设hello.c为最初的源代码：gcc -E hello.c -o hello.i，生成经过预处理的代码；gcc -S hello.i -o hello.s，生成汇编处理后的汇编代码；gcc -c hello.s -o hello.o，生成编译后的目标文件，含有最终编译出的机器码，但它里面所引用的其他文件中函数的内存位置尚未定义；gcc hello.o -o hello，可执行程序。上面的四个命令，可以合成一个gcc hello.c -o hello。
2.2.5. vi的使用vi有哪些使用方法有哪些模式？模式下有怎样的编辑？如何为vi当中增加行号？
三种模式：

命令行模式（command mode）：控制屏幕光标的移动，字符、字或行的删除，移动复制某区段等。按下“i”，进入插入模式；按下“:”，进入底行模式。

插入模式（Insert mode）：只有在Insert mode下，才可以做文字输入，按「ESC」键可回到命令行模式。

底行模式（last line mode）：将文件保存或退出vi，也可以设置编辑环境，如寻找字符串、列出行号……等。


增加行号：在底行模式中，输入set number
基本操作：1、进入vivi filename进入vi之后，是处于命令行模式。输入“i”，切换到插入模式。 
2、在插入模式编辑文件编辑文件，编辑完成点击「ESC」，回到命令行模式。
3、退出vi及保存文件在命令行模式下，按一下“:”进入底行模式： 

wq (存盘并退出vi) 
q! (不存盘强制退出vi) 

2.3. 综合题2.3.1. shell编程接收用户从键盘输入的十个整数，然后求出其总和、最大值、最小值，从小到大排序。
#!/bin/shfor i in `seq 10`do  read  var  echo $var &gt;&gt; tempfile.tmpdone echo &quot;min number is :&quot;`sort -n  tempfile.tmp |head -n1 `echo &quot;max number is :&quot;`sort -rn  tempfile.tmp |head -n1 `echo &quot;sum of all number:&quot;`awk &#x27;&#123; a+=$0&#125;END&#123; print a&#125;&#x27; tempfile.tmp `echo &quot;the order is:&quot;`sort -n tempfile.tmp`rm tempfile.tmp

2.3.2. 操作系统最后五个实验Liunx当中的ls，mao，pwd，硬盘引导，内核编译。重点关注内核引导，内核编译，pwd。
3. 考纲升级版3.1. 基本概念3.1.1. 登陆提示符超级用户和普通用户的登录提示符是什么？超级用户root的提示符是#，普通用户的提示符是$。
3.1.2. 桌面环境Linux系统下经常使用的两种桌面环境是什么？（1）GNOME，最常见（2）KDE
3.1.3. Linux系统如何标识硬盘Linux下硬盘分区的标识在Linux下用hda、hdb等来标识不同的硬盘；用hda1、hda2、hda5、hda6 来标识不同的分区。
前两个字母：hdx（x为a-d）代表IDE硬盘，sdx（x为a-z）代表SCSI、SATA、USB硬盘。第三个字母：a\b\c……代表的是1、2、3……的意思。hda代表第一块IDE硬盘，sdb代表第二块SCSI硬盘。
第四位数字：可以理解为Windows盘下的C\D\E盘符。hda1代表第一个IDE硬盘的第一个分区，sdb2代表第二个SCSI硬盘的第二个分区。
3.1.4. Linux在I386体系结构中的分页支持几级？i386采用二级分页，其线性地址的结构如下：Dir有10位，表示页表目录项的下标，指向一个页表；Page有10位，表示一个具体页表中的目录项的下标，指向一个物理页面；Offset有12位，表示在物理页面中的偏移量（单位为字节）。
3.1.5. 什么时候需要编译内核？1、尝鲜，linux内核发行了新版本，想在第一时间使用新功能。
2、使用一些工具，需要使用包含debuginfo的内核，而一般的发行版本不包含debuginfo。
3、修改了内核代码，比如添加了系统调用。
4、做arm的嵌入式开发，将其烧制到主板上。
3.2. 命令操作3.2.1. 执行定时任务1、执行一次语法：
at [参数] [时间]at&gt; 执行的指令
退出at命令 ctrl+d。atq，查询当前的等待任务，被执行之后就不会显示。atrm 任务的工作号，删除系统中由at建立的正在等待被执行的任务。
at命令的参数：-m ：当指定的任务被完成之后，将给用户发送邮件，即使没有标准输出-I ：atq的别名-d ：atrm的别名-v ：显示任务将被执行的时间-c ：打印任务的内容到标准输出-V ：显示版本信息-q ：后面加&lt;列队&gt; 使用指定的列队-f ：后面加&lt;文件&gt; 从指定文件读入任务而不是从标准输入读入-t ：后面&lt;时间参数&gt; 以时间参数的形式提交要运行的任务 
例子：明天17:20，输出时间到指定文件内
at 17:20 tomorrowat&gt; date &gt; /home/voidking/date.txtat&gt; &lt;EOT&gt;


2、定期执行详情请见：小明是个网管，老板要求他每天早晨3点起来看看磁盘空间满不满。。。
3.2.2. 重命名文件1、单个文件重命名mv test.tx.htm test.html2、批量重命名rename &#39;s/\.tx\.htm$/\.html/&#39; *.tx.htm
3.2.3. 加载光驱ls -l /dev | grep cdrom，假设看到光驱全名为cdrom1。mount /dev/cdrom1 /mnt/，把光盘挂载到/mnt目录下。
3.2.4. 如何修改密码1、修改自己的密码passwd，输入当前密码，输入新密码。2、root用户修改其他用户的密码passwd username，输入新密码。
3.2.5. 如何看本机网址1、查看本机IP地址ifconfig2、设置IP地址ifconfig eth1 192.168.1.1 netmask 255.255.255.0
PS：查看主机名hostname
3.2.6. 如何合并文件cat file1.txt file2.txt &gt; file.txt，file1.txt和file2.txt合成file.txt。cat file1.txt &gt;&gt; file.txt，file1.txt中的内容添加到file.txt的最后。
3.2.7. 如何创建用户1、su，切换到root用户。2、useradd testuser，创建一个名为testuser的用户。3、passwd testuser，之后回车，为testuser设置密码。
3.2.8. 如何删除文件rm filename-f, –force    忽略不存在的文件，从不给出提示。-i, –interactive 进行交互式删除-r, -R, –recursive   指示rm将参数中列出的全部目录和子目录均递归地删除。-v, –verbose    详细显示进行的步骤
3.2.9. 如何查看当前路径pwd-P：如果当前的工作路径是链接的话，显示链接的原始路径，也就是实际路径。
3.3. 综合3.3.1. linux的运行级别Linux系统有7个运行级别(runlevel)。运行级别0：系统停机状态，系统默认运行级别不能设为0，否则不能正常启动运行级别1：单用户工作状态，root权限，用于系统维护，禁止远程登陆运行级别2：多用户状态(没有NFS)运行级别3：完全的多用户状态(有NFS)，登陆后进入控制台命令行模式运行级别4：系统未使用，保留运行级别5：X11控制台，登陆后进入图形GUI模式运行级别6：系统正常关闭并重启，默认运行级别不能设为6，否则不能正常启动
PS：1、查看当前运行级别runlevel2、切换运行级别init N，N的值为0到6，其中，0为关机，6为重启。
3.3.2. linux的文件权限如图，-rwxrwxr-x，这10位表示文件的权限，第2～10个字符中每3个为一组。
左边三个字符（rwx）表示所有者权限，中间3个字符（rwx）表示与所有者同一组的用户的权限，右边3个字符（r-x）是其他用户的权限。
r(Read，读取)：对文件而言，具有读取文件内容的权限；对目录来说，具有浏览目录的权w(Write,写入)：对文件而言，具有新增、修改文件内容的权限；对目录来说，具有删除、移动目录内文件的权限。x(eXecute，执行)：对文件而言，具有执行文件的权限；对目录了来说该用户具有进入目录的权限。
改变文件权限：chmod [options] [who][opcode]mode files
options：

-R，–recursive可递归遍历子目录，把修改应到目录下所有文件和子目录

who：

a，默认值，所有用户
u，拥有者
g，同组用户
o，其他用户。

opcode:

 +，增加权限
 -，删除权限
=，重新分配权限

mode：

r=4，读
w=2，写
x=1，执行

还可设置第四位，它位于三位权限序列的前面：

4，执行时设置用户ID，用于授权给基于文件属主的进程，而不是给创建此进程的用户。
2，执行时设置用户组ID，用于授权给基于文件所在组的进程，而不是基于创建此进程的用户。
1，设置粘贴位。

实例：
chmod u+x file	给属主增加执行权限chmod 751 file	给属主所有权限，给组分配读和执行权限，给其他用户执行权限chmod u=rwx,g=rx,o=x file	上例的另一种形式chmod =r file	为所有用户分配读权限chmod 444 file	同上例chmod a-wx,a+r file	同上例chmod -R u+r directory	directory目录下所有文件和子目录分配读的权限chmod 4755	设置用ID，给属主分配读、写和执行权限，给组和其他用户分配读、执行的权限。

3.3.3. 如何创建文件系统假设新添加了一块硬盘sdb，想要把这块硬盘的所有空间分给第一个分区，而且该分区文件系统为ext4。1、cd /dev2、fdisk sdb3、命令p：查看当前新盘状态。4、命令n：创建一个新的分区。5、两个选项e（扩展分区）和p（主分区），选择p。6、连续两次回车，使用默认的起始和结束sector。7、创建了一个sdb1，大小为整块虚拟硬盘。8、命令w：保存退出。9、mkfs -t ext4 sdb1，格式化为ext4文件系统。
3.3.4. 如何用GCC编译文件？gcc编译的步骤有哪些？会生成哪些文件？
gcc/g++在执行编译工作的时候，总共需要4步。1、预处理，生成.i的文件。（预处理器cpp）2、将预处理后的文件转换成汇编代码，生成文件.s。（编译器egcs）3、将汇编代码变为目标代码(机器代码)生成.o的文件。（汇编器as）4、连接目标代码，生成可执行程序。（链接器ld）
假设hello.c为最初的源代码：gcc -E hello.c -o hello.i，生成经过预处理的代码；gcc -S hello.i -o hello.s，生成汇编处理后的汇编代码；gcc -c hello.s -o hello.o，生成编译后的目标文件，含有最终编译出的机器码，但它里面所引用的其他文件中函数的内存位置尚未定义；gcc hello.o -o hello，可执行程序。上面的四个命令，可以合成一个gcc hello.c -o hello。
3.3.5. vi的3种模式三种模式：

命令行模式（command mode）：控制屏幕光标的移动，字符、字或行的删除，移动复制某区段等。按下“i”，进入插入模式；按下“:”，进入底行模式。

插入模式（Insert mode）：只有在Insert mode下，才可以做文字输入，按「ESC」键可回到命令行模式。

底行模式（last line mode）：将文件保存或退出vi，也可以设置编辑环境，如寻找字符串、列出行号……等。


增加行号：在底行模式中，输入set number
基本操作：1、进入vivi filename进入vi之后，是处于命令行模式。输入“i”，切换到插入模式。 
2、在插入模式编辑文件编辑文件，编辑完成点击「ESC」，回到命令行模式。
3、退出vi及保存文件在命令行模式下，按一下“:”进入底行模式： 

wq (存盘并退出vi) 
q! (不存盘强制退出vi) 

3.3.6. 使用shell完成高斯求和所谓高斯求和，就是等差数列求和，这里分别输入开始值、结束值、等差：
#!/bin/shread -p &quot;Input value of start: &quot; startread -p &quot;Input value of end: &quot; endread -p &quot;Input value of diff: &quot; diffcount=$(($(($(($end-$start))/$diff))+1))sum=$(($(($end+$start))*$count/2))echo &quot;SUM is $sum&quot;


3.3.7. 用汇编语言实现操作系统引导3.3.7.1. 第一种思路CODES	SEGMENT		ORG 07C00H		; 将此段加载到内存0x0000:7C00处start:		MOV AX, CODES		MOV ES, AX		MOV AX, OFFSET msg		; 将字符串拷贝到ax		MOV BP, AX		; es:bp = 串地址		MOV CX, OFFSET strend              		MOV DX, OFFSET msg		SUB CX, DX		; cl= 串长度 		MOV len, CXnext:	INC color		AND color, 0FH		MOV AX, 1301H		; ah = 13 TELTYPE 显示字符串, al = 00h 		MOV BH, 00H		; 页号为0（bh = 0） 		MOV BL, color		;黑底红字（bl = 0ch，高亮）		MOV CX, len		MOV DX, 0815H	;第0h行15h列（dh = 0 dl = 15h）		INT 10H		; 10h号中断		JMP nextover:	RET				color	DB 00H		len		DW 0000H 		msg		DB &quot;Hello World!&quot;		strend	DB &#x27;$&#x27;		ORG 07C00H+200H-2H	;把结尾标志加载到(07c00h+200h-2h)处     		DW 0AA55H (07c00h+512d-2d)CODES	ENDS    		END	start
参考文档：简单OS开发前奏(三)http://blog.csdn.net/otishiono/article/details/5906119用汇编语言编写一个Boot Sectorhttp://blog.csdn.net/misskissc/article/details/8702337
感觉有点难，考试时还是画流程图吧！╮(╯▽╰)╭
3.3.7.2. 第二种思路第一种思路有问题，虽然实现了题目要求，但是和Linux这门课没啥关系，提供第二种思路如下。
首先说一下操作系统启动过程：POST-&gt;BIOS-&gt;Bootloader-&gt;Linux kernel-&gt;init-&gt;system ready。
我们要完成的，就是Bootloader中的启动代码。Bootloader的工作有：

初始化RAM 
初始化串口  
检测处理器类型 
设置Linux启动参数 
调用Linux内核映像

没有找到合适的参考代码，小伙伴们找到了记得在群里分享一下。
参考文档：linux bootloaderhttp://blog.chinaunix.net/uid-28440799-id-3484616.htmlBootloader分析http://blog.csdn.net/xiaomt_rush/article/details/6582337Boot Linuxhttp://www.almesberger.net/cv/papers/ols2k-9.pdfLinux 引导过程内幕http://www.ibm.com/developerworks/cn/linux/l-linuxboot/引导加载程序之争：了解 LILO 和 GRUBhttp://www.ibm.com/developerworks/cn/linux/l-bootload.htmlLinux系统引导过程（BIOS和Bootloader部分）http://blog.csdn.net/keminlau/article/details/4523973
4. 老师赠言大题目都给大家了，小题目看上课的听讲情况了，尽量不要空着试卷，考试不会难为大家。 
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>Ubuntu Kylin编译内核和添加系统调用</title>
    <url>/dev-ubuntu-kylin-compile-kernel-and-add-systemcall/</url>
    <content><![CDATA[1. 下载内核1.1. 查看当前内核版本uname -a可以看到，当前内核版本为3.16.0。


1.2. 获取当前版本内核源码sudo apt-get install linux-source内核源码默认下载到/usr/src文件夹下，这里，我们就使用这套源码。
1.3. 官方下载地址https://www.kernel.org/这个网站，你可以找到最新的内核版本，如果要想要给内核升级，可以来这里下载。
2. 解压内核cd /usr/src/tar -jvf linux-source-3.16.0.tar.bz2解压失败：
tar: You must specify one of the &#x27;-Acdtrux&#x27;, &#x27;--delete&#x27; or &#x27;--test-label&#x27; optionsTry &#x27;tar --help&#x27; or &#x27;tar --usage&#x27; for more information.

添加一个参数x：tar -xjvf linux-source-3.16.0.tar.bz2再次失败，各种Permission denied。
sutar -xjvf linux-source-3.16.0.tar.bz2虽然速度很慢（3分钟左右），但是好歹解压成功了。
3. 添加系统调用3.1. 修改sys.ccd linux-source-3.16.0/kernel/gedit sys.c添加如下代码：
/* *new systemcall added by voidking */asmlinkage int sys_callvoidking(void)&#123;	printk(&quot;You are handsome,VoidKing!&quot;);	return 0;&#125;

3.2. 修改syscall_32.tblcd /usr/src/linux-source-3.16.0/arch/x86/syscallsgedit syscall_32.tbl在最后一行插入
355	i386	callvoidking		sys_callvoidking

3.3. 修改syscalls.hcd /usr/src/linux-source-3.16.0/include/linux/gedit syscalls.h在#endif前添加：
asmlinkage int sys_callvoidking(void);

4. 编译准备4.1. libncurses5-dev安装nucurses库，为了make menuconfig调用。apt-get install libncurses5-dev
4.2. 配置文件把原先系统的配置文件拷贝到当前源码目录。cp /boot/config-\uname -r` .config`
4.3. 加载配置文件make menuconfig，打开编译配置界面，load，ok，save，exit。
5. 编译命令5.1. 编译内核make -j8或者make bzImage，前者更快一点。
有些同学要两个小时才能编译完成，郝同学使用VirtualBox，只用了20分钟，电脑配置之牛逼可见一斑！哇哈哈哈！
5.2. 编译模块make modules，编译模块时，郝同学自己添加的系统调用出现了警告，不过不影响接下来的编译。一个小时之后。。。噗！要吐血了，之前分给虚拟机8G存储空间，没想到编译过程中，空间不足了，没法继续编译。。。
5.3. 扩展Ubuntu空间1、首先关闭虚拟机，在设置中给Ubuntu再添加一块虚拟硬盘。2、打开虚拟机，cd /dev3、fdisk sdb4、命令p：查看当前新盘状态。5、命令n：创建一个新的分区。6、两个选项e（扩展分区）和p（主分区），选择p。7、连续两次回车，使用默认的起始和结束sector。8、创建了一个sdb1，大小为整块虚拟硬盘。9、命令w：保存退出。10、mkfs -t ext4 sdb1，格式化。11、cd /mnt，mkdir sdb1，mount /dev/sdb1 sdb112、cd sdb1，cp -rv /usr/src/linux-source-3.16.0 .13、umount sdb1，mount /dev/sdb1 /usr/src
5.4. 重新编译模块cd /usr/src/linux-source-3.16.0，make modules等。。。再等。。。继续等。。。四个小时之后，提示空间不足。。。我靠，你丫到底需要多大空间？8G的空间不够你编译程序用的！！！哭了。。。
5.5. 加速编译寻找加速编译内核的办法，发现，似乎make -j8等于make bzImage加make modules。去掉编译模块这个步骤试试：make mrproper，make clean，make -j8，然后make modules_install。报错：
  INSTALL arch/x86/crypto/aes-x86_64.kocp: cannot stat ‘arch/x86/crypto/aes-x86_64.ko’: No such file or directoryCan&#x27;t read private keyscripts/Makefile.modinst:30: recipe for target &#x27;arch/x86/crypto/aes-x86_64.ko&#x27; failedmake[1]: *** [arch/x86/crypto/aes-x86_64.ko] Error 2Makefile:1089: recipe for target &#x27;_modinst_&#x27; failedmake: *** [_modinst_] Error 2
猜测是配置文件.config文件有问题，换成/usr/src/linux-headers-3.16.0-23-generic目录下的.config文件。重新编译，安装，还是报同样错误。那就再加上编译模块这个步骤吧！
5.6. 第三次编译模块make -j8 modules，大概四个小时之后，终于编译完成。安装模块报错，和make -j8编译后直接安装模块报错相同。给跪了，能不能愉快地玩耍了！
5.7. 找到错误后来，仔细检查了前面的步骤。发现修改syscalls.h的时候，声明类型写错了，本该是int，却写成了long类型。修改成int，一路顺利。实践证明，make -j8等于make bzImage加make modules。
5.8. 安装模块make modules_install
5.9. 建立映像文件建立要载入ramdisk的映像文件mkinitramfs -o /boot/initrd-mylinux.imgcd /boot，ll
5.10. 安装内核make install
6. 更新grub引导update-grub
7. 测试7.1. 选择新内核开机时，选择新内核引导系统。
7.2. voidking.c新建文件voidking.c，内容如下：
#include &lt;unistd.h&gt;#include &lt;stdio.h&gt;#define callvoidking 355int main()&#123;	syscall(callvoidking);	return 0;&#125;
7.3. 编译执行gcc -o voidking voidking.c./voidking最终什么都没有显示出来，不知道错在哪里。。。也许是sys.c中的函数没有写好，不重做了，整个流程耗时太久，领会精神就够了。
8. 小结为Linux添加新的系统调用，主要包括有4个步骤：编写系统调用服务例程；添加系统调用号；修改系统调用表；重新编译内核并测试新添加的系统调用。
虽然这次实验最终失败了，但是，我掌握了添加系统调用和编译内核的流程，培养了耐心以及分析解决问题的能力。也算是收获颇丰，吼吼！
9. 参考文档Ubuntu 14.04 TLS 内核升级和添加系统调用http://yunpan.cn/cQtsTfkbeTPvG  访问密码 adce

如何实现一个新的系统调用http://book.51cto.com/art/201007/213651.htm

ubuntu下重新编译内核http://m.blog.csdn.net/blog/chen_jianjian/42168449

Ubuntu Linux内核编译步骤http://www.linuxidc.com/Linux/2012-03/57303.htm

Add a system call to the linux kernel in Ubuntuhttp://www.franksthinktank.com/howto/addsyscall/

Ubuntu添加系统调用http://gnodsy.blog.163.com/blog/static/17754713320112135352447/

Ubuntu内核的重新编译安装http://www.linuxidc.com/Linux/2009-12/23721.htm
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>VirtualBox Ubuntu共享文件夹</title>
    <url>/dev-virtualbox-ubuntu-share/</url>
    <content><![CDATA[1. 挂载共享文件夹1、设置，共享文件夹，添加，选择共享文件夹路径（假设为E:\VirtualBoxShare，对应共享文件夹名称为VirtualBoxShare），注意此时不要勾选自动挂载。
2、启动Ubuntu，打开Terminal。
3、挂载共享文件夹
cd /mnt/sudo mkdir sharedsudo mount -t vboxsf VirtualBoxShare /mnt/shared/cd sharedll



1.1. 共享文件夹如果想卸载，可运行命令：sudo umount -f /mnt/shared
1.2. 参考文档virtualbox+ubuntu设置共享文件夹
virtualbox中ubuntu和windows共享文件夹设置
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>ubuntu</tag>
        <tag>virtualbox</tag>
      </tags>
  </entry>
  <entry>
    <title>新浪博客手机版转网页版</title>
    <url>/hobby-sina-blog-mobile-to-page/</url>
    <content><![CDATA[用手机访问新浪博客的时候，访问到的是手机版。但是，如果我想要查看PC网页版，应该怎么办？
以下面的手机版url为例：http://blog.sina.cn/dpool/blog/s/blog_675cc1ad0100zegv.html
直接访问该url，在PC端，看到的也是手机版的内容。
经过对比网页版的url，郝同学找到了方法，修改url如下：http://blog.sina.com.cn/s/blog_675cc1ad0100zegv.html
也就是说，在sina后面加上.com，同时删掉/dpool/blog就可以了！
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>笔记</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：文件系统基础知识</title>
    <url>/dev-linux-file-system-basic/</url>
    <content><![CDATA[1. 前言教授Linux的丁宋涛老师，安排大家轮流讲课，6月4号就到我了，赶紧备课！我们组要讲的内容有：Linux第9题：

利用dumpe2fs把linux一个ext2(ext3也可)分区的信息dump出来，解释dump出来的信息。
结合课程的知识和ext2的目录结构，解析文件打开的过程。

课本第11章：

进程间通信。

本来想把这两部分内容都做一下，后来发现，内容实在太多，搞不定，还是大家均摊吧！题目我来，课本部分交给小伙伴们！PPT是我已经从百度文库Down好了，同志们，剩下的看你们的了！


2. Linux第9题2.1. 第一小问利用dumpe2fs把linux一个ext2(ext3也可)分区的信息dump出来，解释dump出来的信息。
2.1.1. 命令mount或者df -lhT，查看有哪些文件系统。dumpe2fs /dev/sda1 &gt; dump.txt，把分区信息写入dump.txt。由于郝同学在使用虚拟机安装Ubuntu的时候，选择了让系统自动分区，所以，没有文件系统使用ext2或者ext3。最终导出的，是一个ext4系统的分区信息。不再更改分区了，分区命令也忘得差不多了。。。
2.1.2. dump.txtFilesystem volume name:   &lt;none&gt;Last mounted on:          /Filesystem UUID:          56f9c6d7-9968-4c57-b2e5-c63d82738033Filesystem magic number:  0xEF53Filesystem revision #:    1 (dynamic)Filesystem features:      has_journal ext_attr resize_inode dir_index filetype needs_recovery extent flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isizeFilesystem flags:         signed_directory_hash Default mount options:    user_xattr aclFilesystem state:         cleanErrors behavior:          ContinueFilesystem OS type:       LinuxInode count:              458752Block count:              1834752Reserved block count:     91737Free blocks:              738375Free inodes:              287057First block:              0Block size:               4096Fragment size:            4096Reserved GDT blocks:      447Blocks per group:         32768Fragments per group:      32768Inodes per group:         8192Inode blocks per group:   512Flex block group size:    16Filesystem created:       Tue Jun  2 13:05:17 2015Last mount time:          Tue Jun  2 13:47:14 2015Last write time:          Tue Jun  2 13:47:14 2015Mount count:              3Maximum mount count:      -1Last checked:             Tue Jun  2 13:05:17 2015Check interval:           0 (&lt;none&gt;)Lifetime writes:          5791 MBReserved blocks uid:      0 (user root)Reserved blocks gid:      0 (group root)First inode:              11Inode size:	          256Required extra isize:     28Desired extra isize:      28Journal inode:            8First orphan inode:       7969Default directory hash:   half_md4Directory Hash Seed:      04a70c20-f9ac-430b-9b6b-4179feaa85c6Journal backup:           inode blocksJournal features:         journal_incompat_revokeJournal size:             128MJournal length:           32768Journal sequence:         0x00000a4fJournal start:            1Group 0: (Blocks 0-32767) [ITABLE_ZEROED]  Checksum 0xdf8b, unused inodes 0  Primary superblock at 0, Group descriptors at 1-1  Reserved GDT blocks at 2-448  Block bitmap at 449 (+449), Inode bitmap at 465 (+465)  Inode table at 481-992 (+481)  19210 free blocks, 56 free inodes, 1061 directories  Free blocks: 9640-9649, 9652-9661, 10382, 10388, 10499, 10669, 10755, 10786, 10892-10893, 10899, 13400, 13572-13573, 13579, 13591-32767  Free inodes: 7970-7975, 7977-8025, 8032Group 1: (Blocks 32768-65535) [ITABLE_ZEROED]  Checksum 0xd97d, unused inodes 0  Backup superblock at 32768, Group descriptors at 32769-32769  Reserved GDT blocks at 32770-33216  Block bitmap at 450 (bg #0 + 450), Inode bitmap at 466 (bg #0 + 466)  Inode table at 993-1504 (bg #0 + 993)  0 free blocks, 15 free inodes, 961 directories  Free blocks:   Free inodes: 9270, 9272, 9317, 11214, 11246, 15869, 15878, 15883, 15931, 15937, 16048, 16218, 16304, 16349-16350Group 2: (Blocks 65536-98303) [ITABLE_ZEROED]  Checksum 0x2380, unused inodes 0  Block bitmap at 451 (bg #0 + 451), Inode bitmap at 467 (bg #0 + 467)  Inode table at 1505-2016 (bg #0 + 1505)  0 free blocks, 33 free inodes, 58 directories  Free blocks:   Free inodes: 16416-16418, 16436-16441, 16627-16630, 16647-16651, 17142-17146, 17871-17872, 18189-18190, 18887, 18906, 18908, 19092-19094..................Group 55: (Blocks 1802240-1834751) [INODE_UNINIT, ITABLE_ZEROED]  Checksum 0x182b, unused inodes 8192  Block bitmap at 1572871 (bg #48 + 7), Inode bitmap at 1572887 (bg #48 + 23)  Inode table at 1576480-1576991 (bg #48 + 3616)  32512 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes  Free blocks: 1802240-1834751  Free inodes: 450561-458752

2.1.3. ext2文件系统分区信息Filesystem volume name:   /                                 文件系统标签Last mounted on:          &lt;not available&gt;Filesystem UUID:          a5d248ed-850b-4392-929d-b86e2b4d65b8Filesystem magic number:  0xEF53Filesystem revision #:    1 (dynamic)Filesystem features:      has_journal ext_attr resize_inode dir_index filetype needs_recovery sparse_super large_file        ext2文件系统可以包含的几种功能Default mount options:    user_xattr aclFilesystem state:         cleanErrors behavior.:          ContinueFilesystem OS type:       LinuxInode count:              2097152                            文件系统中inode节点的数目Block count:              2096474                            文件系统中块的数目Reserved block count:     104823Free blocks:              1910125Free inodes:              2091098First block:              0Block size:               4096                                       块大小Fragment size:            4096Reserved GDT blocks:      511Blocks per group:         32768Fragments per group:      32768Inodes per group:         32768Inode blocks per group:   1024Filesystem created:       Sat Nov 20 00:27:31 2010Last mount time:          Sat Nov 20 23:53:31 2010Last write time:          Sat Nov 20 23:53:31 2010Mount count:              10Maximum mount count:      -1Last checked:             Sat Nov 20 00:27:31 2010Check interval:           0 (&lt;none&gt;)Reserved blocks uid:      0 (user root)Reserved blocks gid:      0 (group root)First inode:              11Inode size:               128Journal inode:            8Default directory hash:   teaDirectory Hash Seed:      3302e23c-fc75-4a98-b9d1-9660c335988cJournal backup:           inode blocksJournal size:             128MGroup 0: (Blocks 0-32767)  Primary superblock at 0, Group descriptors at 1-1  Reserved GDT blocks at 2-512  Block bitmap at 513 (+513), Inode bitmap at 514 (+514)  Inode table at 515-1538 (+515)  0 free blocks, 32757 free inodes, 2 directories  Free blocks:   Free inodes: 12-32768Group 1: (Blocks 32768-65535)  Backup superblock at 32768, Group descriptors at 32769-32769  Reserved GDT blocks at 32770-33280  Block bitmap at 33281 (+513), Inode bitmap at 33282 (+514)  Inode table at 33283-34306 (+515)  26899 free blocks, 32592 free inodes, 44 directories  Free blocks: 35887-55295, 58045, 58047-65535  Free inodes: 32944, 32946-65536Group 2: (Blocks 65536-98303)  Block bitmap at 65536 (+0), Inode bitmap at 65537 (+1)  Inode table at 65538-66561 (+2)  31742 free blocks, 32768 free inodes, 0 directories  Free blocks: 66562-98303  Free inodes: 65537-98304..................Group 63: (Blocks 2064384-2096473)  Block bitmap at 2064384 (+0), Inode bitmap at 2064385 (+1)  Inode table at 2064386-2065409 (+2)  22114 free blocks, 32187 free inodes, 113 directories  Free blocks: 2065411-2086911, 2095861-2096473  Free inodes: 2064966-2097152

2.1.4. dump.txt详解Filesystem volume name:   &lt;none&gt;	文件系统 volume 名称Last mounted on:          /			上一次挂载文件系统的挂载点路径Filesystem UUID:          56f9c6d7-9968-4c57-b2e5-c63d82738033	由乱数产生的识别码，可以用来识别文件系统。Filesystem magic number:  0xEF53	用来识别此文档系统为 Ext2/Ext3/Ext4 的签名，位置在文档系统的 0x0438 - 0x0439 (Superblock 的 0x38-0x39)，必定是 0xEF53。Filesystem revision #:    1 (dynamic)	文件系统版本编号Filesystem features:      has_journal ext_attr resize_inode dir_index filetype needs_recovery extent flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize开启了的文件系统的功能has_journal - 有日志 (journal)，亦代表此文件系统必为 Ext3 或 Ext4ext_attr - 支持 extended attributeresize_inode - resize2fs 可以改变文件系统大小dir_index - 支持目录索引，可以加快在大目录中搜索文件。filetype - 目录项目为否记录文件类型needs_recovery - e2fsck 检查 Ext3/Ext4 文件系统时用来决定是否需要完成日志记录中未完成的工作，快速自动修复文件系统extent - 支持 Ext4 extent 功能，可以加快文件系统效能和减少 external fragmentationflex_bgsparse_super - 只有少数 superblock 备份，而不是每个区块组都有 superblock 备份，节省空间。large_file - 支持大于 2G 的档案huge_fileuninit_bgdir_nlinkextra_isizeFilesystem flags:         signed_directory_hash 	文件系统旗号Default mount options:    user_xattr acl	默认挂载选项Filesystem state:         clean		文件系统状态，可以为 clean(文件系统已成功地被卸载)、not-clean(表示文件系统挂载成读写模式后，仍未被卸载)或 erroneous (文件系统被发现有问题)Errors behavior:          Continue		文件系统发生问题时的处理方案，可以为 continue (继续正常运作) 、remount-ro (重新挂载成只读模式) 或 panic (即时当掉系统)。可以使用 tune2fs -e 改变。Filesystem OS type:       Linux		建立文件系统的作业系统，可以为 Linux/Hurd/MASIX/FreeBSD/LitesInode count:              458752	文件系统的总inode 数目，亦是整个文件系统所可能拥有文件数目的上限Block count:              1834752	文件系统的总区块数目Reserved block count:     91737		保留给系统管理员工作之用的区块数目Free blocks:              738375	未使用区块数目Free inodes:              287057	未使用 inode 数目First block:              0			Superblock 或第一个区块组开始的区块编数。此值在 1 KiB 区块大小的文件系统为 1，大于1 KiB 区块大小的文件系统为 0。(Superblock/第一个区块组一般都在文件系统 0x0400 (1024) 开始)Block size:               4096		区块大小，可以为 1024, 2048 或 4096 字节 (Compaq Alpha 系统可以使用 8192 字节的区块)Fragment size:            4096		Fragment大小，实际上Ext2/Ext3/Ext4并不支持Fragment，所以此值一般和区块大小一样Reserved GDT blocks:      447	保留GDT区块数目，保留作在线改变文件系统大小的区块数目。若此值为0，只可以先卸载才可脱机改变文件系统大小Blocks per group:         32768		每个区块组的区块数目Fragments per group:      32768		每个区块组的片段数目，亦用来计算每个区块组中 block bitmap 的大小Inodes per group:         8192		每个区块组的inode数目Inode blocks per group:   512		每个区块组的inode区块数目Flex block group size:    16		Filesystem created:       Tue Jun  2 13:05:17 2015	文件系统建立时间Last mount time:          Tue Jun  2 13:47:14 2015	上一次挂载此文件系统的时间Last write time:          Tue Jun  2 13:47:14 2015	上一次改变此文件系统内容的时间Mount count:              3		挂载次数。距上一次作完整文件系统检查后文件系统被挂载的次数，让fsck决定是否应进行另一次完整文件系统检查Maximum mount count:      -1	最大挂载次数。文件系统进行另一次完整检查可以被挂载的次数，若挂载次数(Mount count)大于此值，fsck 会进行另一次完整文件系统检查Last checked:             Tue Jun  2 13:05:17 2015	上一次文件系统作完整检查的时间Check interval:           0 (&lt;none&gt;)	文件系统应该进行另一次完整检查的最大时间距Lifetime writes:          5791 MBReserved blocks uid:      0 (user root)		保留区块使用者识别码 Reserved blocks gid:      0 (group root)	保留区块群组识别码First inode:              11		第一个可以用作存放正常文件属性的inode编号，在原格式此值一定为11，V2格式可以改变此值Inode size:	          256	Inode 大小，传统为 128 字节，新系统会使用 256 字节的 inode 令扩充功能更方便Required extra isize:     28	必须的额外isizeDesired extra isize:      28	请求的额外isizeJournal inode:            8		日志文件的 inode 编号First orphan inode:       7969Default directory hash:   half_md4		缺省目录 hash 算法Directory Hash Seed:      04a70c20-f9ac-430b-9b6b-4179feaa85c6		目录 hash 种子Journal backup:           inode blocks		日志备份Journal features:         journal_incompat_revoke	日志特点Journal size:             128M		日志文件的大小Journal length:           32768		日志长度Journal sequence:         0x00000a4f	日志序列Journal start:            1		日志开始位置Group 0: (Blocks 0-32767) [ITABLE_ZEROED]  Checksum 0xdf8b, unused inodes 0  Primary superblock at 0, Group descriptors at 1-1  Reserved GDT blocks at 2-448  Block bitmap at 449 (+449), Inode bitmap at 465 (+465)  Inode table at 481-992 (+481)  19210 free blocks, 56 free inodes, 1061 directories  Free blocks: 9640-9649, 9652-9661, 10382, 10388, 10499, 10669, 10755, 10786, 10892-10893, 10899, 13400, 13572-13573, 13579, 13591-32767  Free inodes: 7970-7975, 7977-8025, 8032

2.2. 第二小问结合课程的知识和ext2的目录结构，解析文件打开的过程。
2.2.1. ext2文件系统
文件系统中存储的最小单位是块（Block），一个块究竟多大是在格式化时确定的，例如mke2fs的-b选项可以设定块大小为1024、2048或4096字节。而上图中启动块（Boot Block）的大小是确定的，就是1KB，启动块是由PC标准规定的，用来存储磁盘分区信息和启动信息，任何文件系统都不能使用启动块。启动块之后才是ext2文件系统的开始，ext2文件系统将整个分区划成若干个同样大小的块组（Block Group），每个块组都由以下部分组成。
超级块（Super Block）描述整个分区的文件系统信息，例如块大小、文件系统版本号、上次mount的时间等等。超级块在每个块组的开头都有一份拷贝。
块组描述符表（GDT，Group Descriptor Table）由很多块组描述符组成，整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符（Group Descriptor）存储一个块组的描述信息，例如在这个块组中从哪里开始是inode表，从哪里开始是数据块，空闲的inode和数据块还有多少个等等。和超级块类似，块组描述符表在每个块组的开头也都有一份拷贝，这些信息是非常重要的，一旦超级块意外损坏就会丢失整个分区的数据，一旦块组描述符意外损坏就会丢失整个块组的数据，因此它们都有多份拷贝。通常内核只用到第0个块组中的拷贝，当执行e2fsck检查文件系统一致性时，第0个块组中的超级块和块组描述符表就会拷贝到其它块组，这样当第0个块组的开头意外损坏时就可以用其它拷贝来恢复，从而减少损失。
块位图（Block Bitmap）一个块组中的块是这样利用的：数据块（Data Block）存储所有文件的数据，比如某个分区的块大小是1024字节，某个文件是2049字节，那么就需要三个数据块来存，即使第三个块只存了一个字节也需要占用一个整块；超级块、块组描述符表、块位图、inode位图、inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其它描述信息，哪些块仍然空闲可用呢？块位图就是用来描述整个块组中哪些块已用哪些块空闲的，它本身占一个块，其中的每个bit代表本块组中的一个块，这个bit为1表示该块已用，这个bit为0表示该块空闲可用。
为什么用df命令统计整个磁盘的已用空间非常快呢？因为只需要查看每个块组的块位图即可，而不需要搜遍整个分区。相反，用du命令查看一个较大目录的已用空间就非常慢，因为不可避免地要搜遍整个目录的所有文件。
与此相联系的另一个问题是：在格式化一个分区时究竟会划出多少个块组呢？主要的限制在于块位图本身必须只占一个块。用mke2fs格式化时默认块大小是1024字节，可以用-b参数指定块大小，现在设块大小指定为b字节，那么一个块可以有8b个bit，这样大小的一个块位图就可以表示8b个块的占用情况，因此一个块组最多可以有8b个块，如果整个分区有s个块，那么就可以有s/(8b)个块组。格式化时可以用-g参数指定一个块组有多少个块，但是通常不需要手动指定，mke2fs工具会计算出最优的数值。
inode位图（inode Bitmap）和块位图类似，本身占一个块，其中每个bit表示一个inode是否空闲可用。
inode表（inode Table）我们知道，一个文件除了数据需要存储之外，一些描述信息也需要存储，例如文件类型（常规、目录、符号链接等），权限，文件大小，创建/修改/访问时间等，也就是ls -l命令看到的那些信息，这些信息存在inode中而不是数据块中。每个文件都有一个inode，一个块组中的所有inode组成了inode表。
inode表占多少个块在格式化时就要决定并写入块组描述符中，mke2fs格式化工具的默认策略是一个块组有多少个8KB就分配多少个inode。由于数据块占了整个块组的绝大部分，也可以近似认为数据块有多少个8KB就分配多少个inode，换句话说，如果平均每个文件的大小是8KB，当分区存满的时候inode表会得到比较充分的利用，数据块也不浪费。如果这个分区存的都是很大的文件（比如电影），则数据块用完的时候inode会有一些浪费，如果这个分区存的都是很小的文件（比如源代码），则有可能数据块还没用完inode就已经用完了，数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测，也可以用mke2fs的-i参数手动指定每多少个字节分配一个inode。
数据块根据不同的文件类型有以下几种情况
对于常规文件，文件的数据存储在数据块中。

对于目录，该目录下的所有文件名和目录名存储在数据块中，注意文件名保存在它所在目录的数据块中，除文件名之外，ls -l命令看到的其它信息都保存在该文件的inode中。注意这个概念：目录也是一种文件，是一种特殊类型的文件。

对于符号链接，如果目标路径名较短则直接保存在inode中以便更快地查找，如果目标路径名较长则分配一个数据块来保存。

设备文件、FIFO和socket等特殊文件没有数据块，设备文件的主设备号和次设备号保存在inode中。


2.2.2. inode tableinode记录的文件数据至少有：1、该文件的访问模式；（rwx）2、该文件的所有者与组（ower/group）；3、该文件的大小；4、该文件创建或状态改变的时间（ctime）；5、最近一次读的时间（atime）；6、最近修改的时间（mtime）；7、该文件的特性的标志（flag）；8、该文件真正内容的指向（pointer）；
而有这么强大功能的inode的大小均固定为每个128B。inode除了文件权限属性记录区域外，还有12个直接，1个间接，一个双间接与一个三间接记录区。12个直接指向号码的对照，这12个记录就能够直接取得block号码，至于所谓的间接就是再拿一个block来当作block号码的记录区，如果文件太大，就会使用间接的block来记录编号。同理，如果文件持续长大，那么就复用所谓的双间接，第一个仅再指出下一个记录编号的block在哪里，实际记录在第二个block当中。依此类推，三间接就是复用第三层block来记录编号。如下图所示：

2.2.3. 打开/opt/file打开/opt/file，查找的顺序是：
1、读出inode表中第2项，也就是根目录的inode，从中找出根目录数据块的位置
2、从根目录的数据块中找出文件名为opt的记录，从记录中读出它的inode号
3、读出opt目录的inode，从中找出它的数据块的位置
4、从opt目录的数据块中找出文件名为file的记录，从记录中读出它的inode号
5、读出file文件的inode，找到它的数据块的位置
6、读出file文件的内容
2.2.4. ls、cp、mv等命令实现不会。
3. 课件分享进程间通信PPT：http://yunpan.cn/cQTqJ2t3fPeCF  访问密码 a5c9
4. 参考文档
《UNIX/Linux程序设计教程》，赵克佳 沈志宇 编著，机械工业出版社
linux中使用dumpe2fs查看EXT2(或EXT3)文件系统信息
初窥Linux 之 ext2/ext3文件系统
Linux进程间通信
Linux下的进程间通信-详解
ext4 笔记二（dumpe2fs）
ext2文件系统

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo主题优化</title>
    <url>/dev-hexo-theme-optimize/</url>
    <content><![CDATA[1. 前言大概一年前开始使用hexo，主题一直使用jacman。后来Hexo更新到3.0，主题也跟着更新，但是，懒惰的郝同学并没有参与这一更新活动。如今，安装了hexo3.0，问题来了——主题不匹配。当年千辛万苦优化过的主题，又要重新搞起！
2. 选择主题啊嘞，几个月不见，hexo已经如此牛逼！不仅官网更加高端大气上档次，而且主题数量翻了几翻！https://github.com/hexojs/hexo/wiki/Themes ，可以看到，群众的力量是巨大的！换个主题，换个心情，这次我选择了yilia，简洁大气！
3. 添加“关于”1、创建 about 页面hexo new page &quot;about&quot;
2、按需编辑 about 页面页面路径为 hexo/source/about/index.md
3、配置文件中引入 about编辑 hexo/themes/yilia/_config.yml，添加：
menu:  关于: /about



4. 添加RSSnpm install hexo-generator-feed --save
注意，后面的参数--save绝对不能省，否则该插件信息不会写入package.json。hexo clean，hexo g，查看public文件夹，可以看到atom.xml文件。
5. 添加sitemapnpm install hexo-generator-sitemap --save。hexo clean，hexo g，查看public文件夹，可以看到sitemap.xml文件。sitemap的初衷是给搜索引擎看的，为了提高搜索引擎对自己站点的收录效果，我们最好手动到google和百度等搜索引擎提交sitemap.xml。
6. 添加多说http://duoshuo.com/ ，注册一个账号。登录，添加新站点voidking。在工具，获取代码界面，可以看到通用代码：
&lt;!-- 多说评论框 start --&gt;	&lt;div class=&quot;ds-thread&quot; data-thread-key=&quot;请将此处替换成文章在你的站点中的ID&quot; data-title=&quot;请替换成文章的标题&quot; data-url=&quot;请替换成文章的网址&quot;&gt;&lt;/div&gt;&lt;!-- 多说评论框 end --&gt;&lt;!-- 多说公共JS代码 start (一个网页只需插入一次) --&gt;&lt;script type=&quot;text/javascript&quot;&gt;var duoshuoQuery = &#123;short_name:&quot;voidking&quot;&#125;;	(function() &#123;		var ds = document.createElement(&#x27;script&#x27;);		ds.type = &#x27;text/javascript&#x27;;ds.async = true;		ds.src = (document.location.protocol == &#x27;https:&#x27; ? &#x27;https:&#x27; : &#x27;http:&#x27;) + &#x27;//static.duoshuo.com/embed.js&#x27;;		ds.charset = &#x27;UTF-8&#x27;;		(document.getElementsByTagName(&#x27;head&#x27;)[0] 		 || document.getElementsByTagName(&#x27;body&#x27;)[0]).appendChild(ds);	&#125;)();	&lt;/script&gt;&lt;!-- 多说公共JS代码 end --&gt;

var duoshuoQuery = &#123;short_name:&quot;voidking&quot;&#125;;中的voidking，就是我们要用到的duoshuo-key，也有人称之为duoshuo_shortname。打开hexo/themes/yilia/layout/_partial/post/duoshuo.ejs，可以看到：
&lt;div class=&quot;duoshuo&quot;&gt;	&lt;!-- 多说评论框 start --&gt;	&lt;div class=&quot;ds-thread&quot; data-thread-key=&quot;&lt;%=key%&gt;&quot; data-title=&quot;&lt;%=title%&gt;&quot; data-url=&quot;&lt;%=url%&gt;&quot;&gt;&lt;/div&gt;	&lt;!-- 多说评论框 end --&gt;	&lt;!-- 多说公共JS代码 start (一个网页只需插入一次) --&gt;	&lt;script type=&quot;text/javascript&quot;&gt;	var duoshuoQuery = &#123;short_name:&quot;&lt;%=theme.duoshuo%&gt;&quot;&#125;;	(function() &#123;		var ds = document.createElement(&#x27;script&#x27;);		ds.type = &#x27;text/javascript&#x27;;ds.async = true;		ds.src = (document.location.protocol == &#x27;https:&#x27; ? &#x27;https:&#x27; : &#x27;http:&#x27;) + &#x27;//static.duoshuo.com/embed.js&#x27;;		ds.charset = &#x27;UTF-8&#x27;;		(document.getElementsByTagName(&#x27;head&#x27;)[0] 		 || document.getElementsByTagName(&#x27;body&#x27;)[0]).appendChild(ds);	&#125;)();	&lt;/script&gt;	&lt;!-- 多说公共JS代码 end --&gt;&lt;/div&gt;
我们需要填入的，是&lt;%=theme.duoshuo%&gt;的值，那么，在哪里填入呢？在hexo/themes/yilia/_config.yml文件中。是这样的，我们可以认为，_config.yml就是theme，而在_config.yml中写入duoshuo: voidking，就是给duoshuo赋值为voidking。
当然，你也可以直接在duoshuo.ejs中把&lt;%=theme.duoshuo%&gt;替换为voidking。但是不建议这么做，因为这样破坏了作者可复用性设计。
7. 添加头像在hexo/themes/yilia/source/img中添加一个图片headportrait.jpg，然后编辑hexo/themes/yilia/_config.yml：
#你的头像urlavatar: &quot;img/headportrait.jpg&quot;

8. 添加favicon使用Axialis IconWorkshop，制作一个16X16的favicon.png。放置到hexo/themes/yilia/source中，然后编辑hexo/themes/yilia/_config.yml：
favicon: /favicon.png

添加好之后，本地测试，只有主页会显示favicon，其他页面不显示，不知道是什么原因。
找了很久没找到解决办法，狠了狠心，直接上传。惊奇地发现，上传到gitcafe之后，所有页面都可以显示favicon。
9. archieves数量要修改hexo主题，多少要懂得一点hexo的结构和原理。全局配置文件hexo/_config.yml中有这么一段：
per_page: 5pagination_dir: page

这里的per_page，同时设置index、archive、tag、categoriy。如果想要单独设置，可以这么写：
index_generator:  per_page: 5archive_generator:  per_page: 200  yearly: true    monthly: true tag_generator:  per_page: 10 category_generator:   per_page: 10 

10. CategoriesLitten大哥，你这不要Categories是什么节奏？修改起来，非常不容易哇！需要修改的文件有hexo/themes/yilia/source/js/main.js，hexo/themes/yilia/source/css/_partial/main.styl，hexo/themes/yilia/layout/_partial/left-col.ejs。也许还有别的文件，修改了一下午，以失败告终。。。总之，还是能力不够，看来得找时间系统学习一下hexo主题的制作了。。。先放着吧！
11. 添加文章目录TOC (Table of contents) ，中文翻译做目录。之前用jacman，感觉目录很方便，可以很快定位文章内容，尤其对于长文章来说。但是，yilia貌似没有集成，那就自己添加吧！查找资料，居然找到了好友twiceYuan的写的教程，世界真小哇！详情请访问参考文档，摘录twiceYuan的教程如下：
11.1. 修改article.ejs打开hexo/themes/yilia/layout/_partial/article.ejs，在&lt;%- post.content %&gt;前面插入：
&lt;% if(post.toc !== false)&#123; %&gt;	&lt;div id=&quot;toc&quot; class=&quot;toc-article&quot;&gt;	  &lt;strong class=&quot;toc-title&quot;&gt;文章目录&lt;/strong&gt;	  &lt;%- toc(post.content) %&gt;	&lt;/div&gt;&lt;%&#125;%&gt;

这里加了一个post.toc的判断，也就是说，只有在启用toc的post会出现目录，而不是每篇文章都出现。
11.2. 修改article.yml打开hexo/themes/yilia/layout/source/css/_partial/article.yml，在最后插入：
/*toc*/.toc-article  background #eeeeee  margin 2em 0 0 0.2em  padding 1em  border-radius 5px  .toc-title    font-size 120%  strong    padding 0.3em 1ol.toc  width 100%  margin 1em 2em 0 0#toc  line-height 1em  font-size 0.8em  float right  .toc    padding 0     li      list-style-type none  .toc-child     padding-left 0em#toc.toc-aside  display none  width 13%  position fixed  right 2%  top 320px  overflow hidden  line-height 1.5em  font-size 1em  color color-heading  opacity .6  transition opacity 1s ease-out  strong    padding 0.3em 0    color color-font  &amp;:hover    transition opacity .3s ease-out    opacity 1  a    transition color 1s ease-out    &amp;:hover      color color-theme      transition color .3s ease-out

11.3. 使用编辑.md文档，如果要使用文章目录，就要文章开头添加toc属性并且设置为true。
title: hexo主题优化date: 2015-05-31 16:22:12updated: 2015-05-31 16:22:12tags:categories: 专业toc: true

11.4. 新的问题小伙伴很靠谱，这个方法可用！但是，新的问题来了，文章目录特别紧凑，不够人性化，应该是因为主题差别。于是，参考格式良好的css文件，改写了article.styl的/*toc*/部分：
/*toc*/.toc-article&#123;  background #eee  margin 0 0 0 0.2em  padding 1em  border-radius 5px  -webkit-border-radius 5px  strong&#123;	padding 0.3em 0  &#125;&#125;  #toc&#123;  line-height 1.5em  font-size 1em  float right  ol&#123;	margin-left: 0  &#125;	  .toc&#123;	padding 0	li&#123;	  list-style-type none	&#125;    &#125;      .toc-child&#123;	padding-left 1.5em  &#125;&#125; 
问题完美解决，看来，不同的theme，添加TOC的时候，确实需要个性定制。
11.5. 奇怪现象添加目录后，出现了一个奇怪的现象：如果一篇文章在主页展示全部内容，也就是没有添加&lt;!--more--&gt;，那么，该文章就会自动生成目录，无论文章中有没有添加toc属性。且不去管它，等到技术更加牛逼了再来处理。……等一下，貌似有了灵感，修改article.ejs如下：
&lt;% if(post.toc &amp;&amp; post.toc !== false)&#123; %&gt;	&lt;div id=&quot;toc&quot; class=&quot;toc-article&quot;&gt;	  &lt;strong class=&quot;toc-title&quot;&gt;文章目录&lt;/strong&gt;	  &lt;%- toc(post.content) %&gt;	&lt;/div&gt;&lt;%&#125;%&gt;


修改完成，执行命令：
hexo clean，hexo g，hexo s。
查看效果，主页目录没有了，Perfect！
12. 书写代码块一直以来，写博客的时候，如果用到代码块，都是用三个反引号包围。现在发现，似乎出现了问题。如果连续两个代码块，那么，经常出现一种情况：判断代码块不准确，两个代码块被当成了一个代码块。而如果代码块使用缩进一个TAB的方法，那么，代码块前面就会没有行号。。。。麻烦的问题，且不去管它。。。
13. 文章模板打开hexo/scaffolds/post.md，可以看到：
title: &#123;&#123; title &#125;&#125;date: &#123;&#123; date &#125;&#125;tags:---
如果我们想要在每次生成.md文件的时候，自动生成categories和toc，可以修改如下：
title: &#123;&#123; title &#125;&#125;date: &#123;&#123; date &#125;&#125;tags:categories: toc: true---
14. 参考文档
hexo博客的优化技巧续
如何在谷歌站长工具提交网站地图sitemap.xml
Hexo 3.0 静态博客使用指南
hexo你的博客
twiceYuan：为Hexo添加文章目录

]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo托管到gitcafe</title>
    <url>/dev-hexo-deposit-to-gitcafe/</url>
    <content><![CDATA[1. 前言gitcafe作为国内的代码托管网站，访问速度远远快于github，国内的同学建议选择gitcafe！在《Hexo环境搭建》中，我们已经讲过了SSH key的生成和使用方法。下面还要用到SSH key，没学会小伙伴的自行百度。


2. 注册官网：http://www.gitcafe.com
3. 创建Projecthttps://gitcafe.com/projects/new ，创建和用户名相同的project，比如我的用户名为voidking，那么project名称就为voidking。
4. 添加SSH keyhttps://gitcafe.com/account/public_keys ，Add a new public key，复制id_rsa.pub中的内容，粘贴进去即可。
5. 测试连接ssh git@gitcafe.com
Hi voidking! You&#x27;ve successfully authenticated, but GitCafe does not provide shell access.Connection to gitcafe.com closed.

6. 小结通过连接github和gitcafe，我们可以得出结论：在本地生成密钥的时候，根本不用考虑今后的服务器。github还是gitcafe，或者任何其他使用git的服务器，都没有关系。在需要使用某个服务器时，只需要把密钥添加到该服务器上面，就完成了配置工作。
7. 修改_config.yml编辑E:\hexo下的_config.yml，修改Deployment部分：
deploy:  type: git  repository: git@gitcafe.com:voidking/voidking.git  branch: gitcafe-pages

8. 部署deploy d，不需要输入密码，直接就可以上传到gitcafe。
9. 访问测试访问：http://voidking.gitcafe.io或者：http://voidking.com或者：http://www.voidking.com
10. 参考文档托管博客到gitcafehttp://zipperary.com/hexo-to-gitcafe/
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo环境搭建</title>
    <url>/dev-hexo-build-environment/</url>
    <content><![CDATA[1. 前言重装了系统，Hexo没法使用了。第一次安装的时候，没有做任何记录，这次刚好记录一下。
2. hexo官网http://hexo.io/


3. 下载安装gitgit官方下载地址：http://git-scm.com/download/分享一个14年版本：http://yunpan.cn/cwqWapRIyFWrA  访问密码 8f1b下载好之后，双击安装，一路next即可。唯一需要注意的是，在Select Components界面，点选Simple context menu。
4. 下载安装Node.jsNode.js官方下载地址：https://nodejs.org/ ，Current version: v0.12.4分享一个v0.10.31：http://yunpan.cn/cwqtqY8FCpdZc  访问密码 951e下载好之后，双击安装，一路next即可。
5. 安装Hexo右击任意位置，选择Git Bash Here。执行命令：npm install -g hexo，报错如下：
npm ERR! Error: shasum check failed for C:\Users\ADMINI~1\AppData\Local\Temp\npm-30024-KDJHJzgP\registry.npmjs.org\hexo-cli\-\hexo-cli-0.1.6.tgznpm ERR! Expected: 7dc3ab939d0889c4bed6a961605ff3e2d67f67a2npm ERR! Actual:   41de7d67a9b764352eb07c49c32fc38dd7f479b9npm ERR! From:     https://registry.npmjs.org/hexo-cli/-/hexo-cli-0.1.6.tgznpm ERR!     at d:\Program Files\nodejs\node_modules\npm\node_modules\sha\index.js:38:8npm ERR!     at ReadStream.&lt;anonymous&gt; (d:\Program Files\nodejs\node_modules\npm\node_modules\sha\index.js:85:7)npm ERR!     at ReadStream.emit (events.js:117:20)npm ERR!     at _stream_readable.js:943:16npm ERR!     at process._tickCallback (node.js:419:13)npm ERR! If you need help, you may report this *entire* log,npm ERR! including the npm and node versions, at:npm ERR!     &lt;http://github.com/npm/npm/issues&gt;npm ERR! System Windows_NT 6.2.9200npm ERR! command &quot;d:\\Program Files\\nodejs\\node.exe&quot; &quot;d:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js&quot; &quot;install&quot; &quot;-g&quot; &quot;hexo&quot;npm ERR! cwd C:\Users\Administrator\Desktopnpm ERR! node -v v0.10.31npm ERR! npm -v 1.4.23npm ERR! registry error parsing jsonnpm ERR!npm ERR! Additional logging details can be found in:npm ERR!     C:\Users\Administrator\Desktop\npm-debug.lognpm ERR! not ok code 0
莫非是因为被墙了？换国内镜像源试试。npm config set registry=&quot;http://registry.cnpmjs.org&quot;，然后再次执行npm install -g hexo，成功！
6. 创建hexo文件夹在任意文件夹下（如E:\hexo），打开Git Bash，执行命令：hexo init，Hexo 即会在目标文件夹建立网站所需要的所有文件。
7. 安装依赖包npm install
8. 命令缩写hexo generate = hexo ghexo server = hexo shexo deploy = hexo dhexo new = hexo n
9. 本地查看执行以下命令：hexo g，hexo s，然后到浏览器输入http://localhost:4000查看效果。至此，本地博客已经搭建起来了，别人看不到的。
10. 部署到GitHub10.1. 注册GitHub官网：http://www.github.com
10.2. 创建repositorycreat new repository，Repository name和自己的用户名相同。比如我的用户名为voidking，那么Repository name就填voidking.github.io。
10.3. _config.yml编辑E:\hexo下的_config.yml，修改Deployment部分：
# Deployment## Docs: http://hexo.io/docs/deployment.htmldeploy:  type: git  repository: https://github.com/voidking/voidking.github.io.git  branch: master

10.4. 部署hexo d，执行该命令，报错：
ERROR Deployer not found: git
执行命令：npm install hexo-deployer-git --save，再次执行hexo d,报错：
INFO  Deploying: gitINFO  Clearing .deploy folder...INFO  Copying files from public folder...warning: LF will be replaced by CRLF in hello-world/index.html.The file will have its original line endings in your working directory.......*** Please tell me who you are.Run  git config --global user.email &quot;you@example.com&quot;  git config --global user.name &quot;Your Name&quot;to set your account&#x27;s default identity.Omit --global to set the identity only in this repository.fatal: unable to auto-detect email address (got &#x27;Administrator@PC-201505290750.(none)&#x27;)Username for &#x27;https://github.com&#x27;: voidkingPassword for &#x27;https://voidking@github.com&#x27;:error: src refspec master does not match any.error: failed to push some refs to &#x27;https://github.com/voidking/voidking.github.io.git&#x27;FATAL Something&#x27;s wrong. Maybe you can find the solution here: http://hexo.io/docs/troubleshooting.htmlError: error: src refspec master does not match any.error: failed to push some refs to &#x27;https://github.com/voidking/voidking.github.io.git&#x27;    at ChildProcess.&lt;anonymous&gt; (E:\hexo\node_modules\hexo-deployer-git\node_modules\hexo-util\lib\spawn.js:42:17)    at ChildProcess.emit (events.js:98:17)    at maybeClose (child_process.js:756:16)    at Process.ChildProcess._handle.onexit (child_process.js:823:5)
估计必须要先配置SSH，然后才可以上传文件。
10.5. GitHub之SSH key1、设置Git的user.name和user.email在第一次使用Git时，你需要告诉你的协同开发者，你是谁以及你的邮箱，在你提交的时候，Git需要这两个信息。具体通过以下命令设置：git config --global user.name &quot;voidking&quot;git config --global user.email &quot;voidking@qq.com&quot;需要注意的是，这里的name随意，邮箱是你的联系邮箱，与github完全无关。查看配置命令：git config --list
2、生成SSH密钥ssh-keygen -t rsa -C &quot;voidking@qq.com&quot;，按3个回车，密码为空。
Generating public/private rsa key pair.Enter file in which to save the key (/c/Users/Administrator/.ssh/id_rsa):Created directory &#x27;/c/Users/Administrator/.ssh&#x27;.Enter passphrase (empty for no passphrase):Enter same passphrase again:Your identification has been saved in /c/Users/Administrator/.ssh/id_rsa.Your public key has been saved in /c/Users/Administrator/.ssh/id_rsa.pub.The key fingerprint is:e6:9e:e7:ae:ad:ee:9f:f0:e5:6b:60:63:85:e8:cd:ae voidking@qq.com

在C:\Users\Administrator\.ssh下，得到两个文件id_rsa和id_rsa.pub。需要注意的是，命令中的-C参数，后面跟的内容是注释。也就是说，内容随意，与github完全无关。
3、在GitHub上添加SSH密钥打开id_rsa.pub，复制全文。https://github.com/settings/ssh ，Add SSH key，粘贴进去。
4、测试ssh git@github.com，提示：
The authenticity of host &#x27;github.com (192.30.252.128)&#x27; can&#x27;t be established.RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.Are you sure you want to continue connecting (yes/no)? yesWarning: Permanently added &#x27;github.com,192.30.252.128&#x27; (RSA) to the list of known hosts.Hi voidking! You&#x27;ve successfully authenticated, but GitHub does not provide shell access.Connection to github.com closed.

11. 再次部署hexo d，根据提示输入用户名和密码，等待一会儿便成功了！
INFO  Deploying: gitINFO  Clearing .deploy folder...INFO  Copying files from public folder...warning: LF will be replaced by CRLF in hello-world/index.html.The file will have its original line endings in your working directory.......[master (root-commit) 7124e90] Site updated: 2015-05-30 23:04:14 28 files changed, 5746 insertions(+) create mode 100644 hello-world/index.html ......warning: LF will be replaced by CRLF in hello-world/index.html.The file will have its original line endings in your working directory.......Username for &#x27;https://github.com&#x27;: voidkingPassword for &#x27;https://voidking@github.com&#x27;:Branch master set up to track remote branch master from https://github.com/voidking/voidking.github.io.git.To https://github.com/voidking/voidking.github.io.git * [new branch]      master -&gt; masterINFO  Deploy done: git

12. 访问测试访问：http://voidking.github.io/ ，可以看到，Hexo已经搭建成功！
13. 参考文档官方文档：http://hexo.io/docs/
GitHub Pageshttps://pages.github.com/
hexo系列教程：（二）搭建hexo博客http://zipperary.com/hexo-guide-2/
Git SSH Key 生成步骤http://blog.csdn.net/hustpzb/article/details/8230454/
Getting Started - First-Time Git Setuphttp://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup
git-scmhttp://git-scm.com/
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>git</tag>
        <tag>ssh</tag>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>Win8+UbuntuKylin注意事项</title>
    <url>/dev-win8-ubuntukylin-notice/</url>
    <content><![CDATA[1. 前言接近一年半没有换过操作系统（Win7），各种大型软件，各种系统后台服务，怎一个牛逼了得！近期安装的Oracle，成功地再次拖慢了系统速度，终于达到了郝同学的忍耐极限！做完了所有实验，果断换个系统！
忙碌了一天，安装了Win8+Ubuntu Kylin，并且对它们进行了基本配置和简单优化。在此简单记录下过程中遇到的问题，以及解决办法，分享给大家。


2. 安装方法详细方法请见《安装系统》系列，这里简单叙述一下。1、准备两个U盘，一个使用U大师做成启动盘（U盘1），另一个利用Universal-USB-Installer-*.exe做成Ubuntu安装盘（U盘2）。2、使用U盘1启动，进入WinPe。3、分区，分成四个，然后删除最后一个分区（等会儿安装Ubuntu）。4、安装Win8。5、使用U盘2启动，进入Ubuntu的安装界面。6、在空白分区上分区，swap分区4096MB（Logical），/分区占用剩下的全部空间（Primary）。7、安装Ubuntu。
3. Win8不见了安装完成，郝同学发现，开机不会出现启动选项，直接进入Ubuntu。应该是因为安装Ubuntu时重写了引导扇区，没关系，我们只要在Ubuntu系统下执行sudo update-grub。该命令会自动检测启动项，重新生成/boot/grub/grub.cfg文件。重新启动，已经多出了Win8的启动选项。
很多小伙伴认为，必须要GPT分区，必须要EFI分区。其实没必要，郝同学就是传统的MBR分区，毫无问题。
4. Win8初始软件卸载完自带的软件，接下来就要个性化定制了。先美化一下桌面，设置一下头像密码等，需要的软件有：

搜狗输入法
Notepad++
JDK
Eclipse
WPS
FastStone Capture
Photoshop
360安全卫士
360极速浏览器（IE不好用，Firefox笨重且历史记录太丑，Chrome插件受限，UC连主页都可以设置）
360云盘
百度云盘
QQ
QQ飞车
ADSafe
其他。。。

5. Ubuntu初始软件
搜狗输入法
JDK
Eclipse
WPS
vim。vim输入法需要更新，否则上下左右键不可用，更新命令：sudo apt-get remove vim-common，sudo apt-get install vim。

6. Ubuntu中文文件夹转英文想到以前使用Ubuntu的痛苦经历，郝同学决定直接安装英文版。安装好之后，在Language设置中选择中文，重启。启动后，系统会询问是否把Home文件夹中的文件夹转换为中文，选择否。如此，便可以保持中文界面，同时在使用命令的时候，不用中英文输入法来回切换。
7. Win8比Ubuntu慢8小时设置好Ubuntu，重启切换到Win8，发现Win8时间要比Ubuntu的时间慢8个小时！
原因：Windows把系统硬件时间当作本地时间（local time），即操作系统中显示的时间跟BIOS中显示的时间是一样的。Linux/Unix/Mac把硬件时间当作UTC，操作系统中显示的时间是硬件时间经过换算得来的，比如说北京时间是GMT+8，则系统中显示时间是硬件时间+8。
解决办法：修改ubuntu时间如下：sudo gedit /etc/default/rcS找到这一行： UTC=yes把 yes 改为 no 。
8. 设置Win8为默认启动项1、在Ubuntu下，进入/etc/grub.d2、sudo mv 30_os-prober 06_os_prober3、sudo update-grub
9. 参考文档win8和ubuntu13.10 双系统时间错误http://blog.csdn.net/flywindmouse/article/details/13025017

将Ubuntu主文件夹里的中文文件夹名称改成英文http://blog.csdn.net/l0605020112/article/details/20285239

Windows与Ubuntu双系统启动菜单管理和配置（补充win8）http://blog.chinaunix.net/uid-23586172-id-3075207.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>ubuntu</tag>
        <tag>操作系统</tag>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>SQL Server迁移到Oracle问题</title>
    <url>/dev-sqlserver-to-oracle/</url>
    <content><![CDATA[1. 前言Oracle最后一次实验，郝同学打算移植SQL Server上的一个项目到Oracle。因为使用了Maven+JPA+Spring+Struts2+AngularJs，所以，虽然复杂了一点，但是可移植性非常好。修改一个database-conn.properties配置文件，在pom.xml中添加Oracle驱动即可。
database-conn.properties原文件：
database.driver=com.microsoft.sqlserver.jdbc.SQLServerDriverdatabase.uri=jdbc:sqlserver://localhost:1433;databaseName=design;integratedSecurity=falsedatabase.username=sadatabase.password=123#hibernatehibernate.dialect=org.hibernate.dialect.SQLServer2008Dialecthibernate.show_sql=true



database-conn.properties修改后文件：
database.name=Oracledatabase.driver=oracle.jdbc.OracleDriverdatabase.uri=jdbc:oracle:thin:@localhost:1521:orcldatabase.username=scottdatabase.password=tiger #hibernatehibernate.dialect=org.hibernate.dialect.OracleDialecthibernate.show_sql=true

使用Maven把ojdbc14.jar安装到本地仓库，pom.xml添加：
&lt;!-- Oracle驱动 --&gt;&lt;dependency&gt;	&lt;groupId&gt;com.oracle&lt;/groupId&gt;	&lt;artifactId&gt;ojdbc14&lt;/artifactId&gt;	&lt;version&gt;10.2.0.3.0&lt;/version&gt;&lt;/dependency&gt;
大功告成，clean install，jetty:run。
2. 问题描述及解答在Oracle的adminbase中插入一条数据：
insert into adminbase values(1,&#x27;voidking&#x27;,&#x27;voidking&#x27;);select * from adminbase;
两条命令都正常。然后在网页上使用账号密码登录，结果提示，用户名或密码错误。啊勒，怎么回事？莫非是因为Oracle的编码方式不对？修改完字符编码方式，问题没有解决。但是，我仍然倔强的认为，应该是编码方式的问题，因为以前在这方面吃的亏太多了。
上面的命令都是在sqldeveloper中执行的，换到sql plus试试。
select * from adminbase;
我靠，问题来了！结果显示未选定行！这是肿么回事？我好像知道为什么无法登录了，因为根本找不到数据哇！
经过查找资料，终于明白，在pl/sql developer插入数据后，需要commit（提交）。在SQL Plus中才能看到这种改变，因为二者处于两个不同的会话中。
在sqldeveloper中执行commit，再次登录，果然成功！
PS：字符编码查看命令：
select userenv(&#x27;language&#x27;) from dual;select * from nls_database_parameters;

3. 新的问题不报错，但是不可以插入数据。比如添加书籍种类，输入信息后，点击“添加”，之后便没有反应了。理想的效果是，界面出现添加成功的信息。控制台并没有报错信息，只是提示：
Hibernate: select hibernate_sequence.nextval from dualHibernate: insert into readerkind (enddate, name, notice, quantity, validity, id) values (?, ?, ?, ?, ?, ?)
不知道哪里出现了问题，也许，从SQLServer移植到Oracle，没有想象的那么简单。。。问题待解决。。。
4. 参考文档Oracle 字符集的查看和修改http://www.cnblogs.com/rootq/articles/2049324.htmlhttp://blog.chinaunix.net/uid-25492475-id-3140218.html

使用pl/sqldeveloper和使用sql*plus得到的结果不同http://www.educity.cn/wenda/410219.html

]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
        <tag>sqlserver</tag>
      </tags>
  </entry>
  <entry>
    <title>感冒后为什么咳嗽不止</title>
    <url>/essay-cold-cough/</url>
    <content><![CDATA[一般感冒只持续3～5天，长的也就7～10天。有些患者感冒的其他症状都消失了，只有咳嗽还持续不断，甚至1个多月才好，这叫做感冒后慢性咳嗽。这种咳嗽的特点是一声声或一阵阵，为刺激性干咳。夜间重，遇到寒冷、灰尘环境、刺激性气体、运动、烟雾或再次感冒，都可诱发加重。随着时间推移，不治也能痊愈。为什么多数人得了感冒很快就好了，而有些人感冒之后咳嗽不断呢?


从病原体来说，感冒多由病毒引起，如鼻病毒、冠状病毒、流感病毒、副流感病毒、合胞病毒等，也有少数是由支原体、衣原体及百日咳杆菌引起。这些病原体侵犯了呼吸道上皮，引起损伤，上皮坏死裸露，常达基底膜。有一小部分人因为过敏体质，上述损害通过迷走神经反射引起气道收缩，加重咳嗽，上呼吸道感染引起的支气管高反应（过敏反应），可持续5～7周。从本质来说，病毒感染引起的呼吸道感染，可诱导免疫细胞分泌一些介质，如激肽、白三烯、组胺、白介素1、6、8、巨噬细胞、T细胞等，这些免疫细胞促使局部气道过敏。从病理学上符合炎症表现（病理学上凡具有渗出、变性坏死及增生形成的就叫炎症），对气道进一步形成损害。这种气道炎症对激素治疗最敏感。
因为感冒后慢性咳嗽是一种自限性疾病，不治自己也能好，所以医生治疗起来多半不太积极，不乱用药。具体治疗方法：1．先给予止咳药右美沙芬，其止咳作用与可待因相当，但无成瘾性，对呼吸中枢也无抑制，口服每次15～30毫克，每天3～4次；或用喷妥维林，作用相当于可待因的1／3，口服每次25毫克，每日3次。2．若上法无效可吸入丙酸倍氯美松（必可酮），经鼻吸入，每次2喷，每天3次，共用3～7天，也可口服泼尼松（强的松），每次10～20毫克，每日1次，共用3～7天。3．不愿意用激素者也可吸入溴化异丙托品，每天吸人320毫克，共用3～7天，本药对吸烟者疗效差。4．通常不用抗生素，如疑为支原体、衣原体或百日咳杆菌引起者，可应用大环内酯类抗生素，如罗红霉素，每次100毫克，口服，每天2次，共用3～7天。（邵　荣）
本文转载自：http://blog.sina.com.cn/s/blog_4d25c4f40100gyg3.html
]]></content>
      <categories>
        <category>essay</category>
      </categories>
      <tags>
        <tag>笔记</tag>
      </tags>
  </entry>
  <entry>
    <title>加快Android离线文档的访问速度</title>
    <url>/dev-quickly-open-android-offline-document/</url>
    <content><![CDATA[1. 前言学习编程三年，已经确信，学习一门技术最好的资料就是官方文档。如今，拖沓了很久的Android学习计划，终于启动。学习资料：Android官方文档；学习方法：写demo写博客（总结）。
2. 阅读途径2.1. 官网http://developer.android.com/index.html不幸的是，这个网站的国内访问速度，慢到令人发指。很多时候，根本打不开。当然，如果你喜欢翻墙玩，就另当别论。
2.2. 国内镜像站不得不说，咱们国家有很多雷锋。国外的网站不好访问，那就在国内搭建一个镜像站，方便大家访问。这里郝同学推荐几个国内镜像站供大家使用：1、踏得网http://wear.techbrood.com/training/index.html
2、Android中文APIhttp://www.android-doc.comhttp://www.android-doc.com/reference/packages.htmlhttp://www.android-doc.com/guide/components/activities.html
3、Android官方培训课程中文版(v0.9.2)http://hukai.me/android-training-course-in-chinese/index.html


2.3. 离线文档打开adt中的SDK Manager，安装Documentation for Android SDK。安装好之后，在adt/sdk/docs目录下，就是官方文档了。当然，我们也可以直接下载别人下载打包好的官方文档，速度会快很多。
这时候问题来了，docs里的很多文件，会加载谷歌的字体和js。这样，严重拖慢了打开速度。
3. 解决办法3.1. 修改index.html打开index.html，然后注释掉stylesheet和js两个地方：
&lt;!--&lt;link rel=&quot;stylesheet&quot; href=&quot;http://fonts.googleapis.com/css?family=Roboto:regular,medium,thin,italic,mediumitalic,bold&quot; title=&quot;roboto&quot;&gt;--&gt;&lt;!--&lt;script src=&quot;http://www.google.com/jsapi&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;--&gt;
3.2. 修改本机hosts文件C:\Windows\System32\drivers\etc\hosts增加如下部分：
127.0.0.1 fonts.googleapis.com127.0.0.1 www.google.com
3.3. Java实现批量注释/* * 去掉Android文档中需要联网的javascript代码 */import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.File;import java.io.FileNotFoundException;import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;public class FormatDoc &#123;    public static int j=1;    /**     * @param args     */    public static void main(String[] args) &#123;                File file = new File(&quot;D:/android/android-sdk-windows/docs/&quot;);        searchDirectory(file, 0);        System.out.println(&quot;OVER&quot;);    &#125;    public static void searchDirectory(File f, int depth) &#123;        if (!f.isDirectory()) &#123;            String fileName = f.getName();            if (fileName.matches(&quot;.*.&#123;1&#125;html&quot;)) &#123;                String src= &quot;&lt;(link rel)[=]\&quot;(stylesheet)\&quot;\n(href)[=]\&quot;(http)://(fonts.googleapis.com/css)[?](family)[=](Roboto)[:](regular,medium,thin,italic,mediumitalic,bold)\&quot;( title)[=]\&quot;roboto\&quot;&gt;&quot;;                String src1 = &quot;&lt;script src=\&quot;http://www.google.com/jsapi\&quot; type=\&quot;text/javascript\&quot;&gt;&lt;/script&gt;&quot;;                String dst = &quot;&quot;;                //如果是html文件则注释掉其中的特定javascript代码                annotation(f, src, dst);                annotation(f, src1, dst);            &#125;        &#125; else &#123;            File[] fs = f.listFiles();            depth++;            for (int i = 0; i &lt; fs.length; ++i) &#123;                File file = fs[i];                searchDirectory(file, depth);            &#125;        &#125;    &#125;    /*     * f 将要修改其中特定内容的文件      * src 将被替换的内容      * dst 将被替换层的内容     */    public static void annotation(File f, String src, String dst) &#123;        String content = FormatDoc.read(f);        content = content.replaceFirst(src, dst);        int ll=content.lastIndexOf(src);        System.out.println(ll);        FormatDoc.write(content, f);        System.out.println(j++);        return;    &#125;    public static String read(File src) &#123;        StringBuffer res = new StringBuffer();        String line = null;        try &#123;            BufferedReader reader = new BufferedReader(new FileReader(src));            int i=0;            while ((line = reader.readLine()) != null) &#123;                if (i!=0) &#123;                    res.append(&#x27;\n&#x27;);                &#125;                res.append(line);                i++;            &#125;            reader.close();        &#125; catch (FileNotFoundException e) &#123;            e.printStackTrace();        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;        return res.toString();    &#125;    public static boolean write(String cont, File dist) &#123;        try &#123;            BufferedWriter writer = new BufferedWriter(new FileWriter(dist));            writer.write(cont);            writer.flush();            writer.close();            return true;        &#125; catch (IOException e) &#123;            e.printStackTrace();            return false;        &#125;    &#125;&#125;

3.4. C++实现批量注释详见参考文档：优化Android离线文档的访问速度。velable兄很牛逼，感谢他分享给我们的代码和文档！郝同学存入了网盘一份，方便大家下载。源代码：http://yunpan.cn/cj7VgRg9tUS6e  访问密码 edb4文档：http://yunpan.cn/cj7Vsaavmey62  访问密码 4e38
4. 参考文档Android帮助文档本地打开慢的解决方案http://blog.csdn.net/wc0077/article/details/39669885

优化Android离线文档的访问速度http://git.oschina.net/velable/OptAndroidDocs

Android离线文档，急速访问+搜索功能正常。http://git.oschina.net/velable/Android_Offline_Docs

]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——存储过程和函数</title>
    <url>/dev-oracle-experiment-store-procedure-and-function/</url>
    <content><![CDATA[1. 创建、编译并运行PL/SQL存储过程在SQL Developer中，创建、编译并运行PL/SQL存储过程1、右击过程，创建过程2、输入过程名“emp_list”3、添加参数



Name
Type
Mode
Default Value



v_empno
VARCHAR2
IN



v_ename
VARCHAR2
OUT



4、显示指定参数的过程的框架
CREATE OR REPLACEPROCEDURE EMP_LIST( v_empno IN VARCHAR2, v_ename OUT VARCHAR2) ASBEGIN  NULL;END EMP_LIST;
5、替换NULL为：
select ename into v_ename from emp where empno = v_empno;
单击工具栏的save按钮，编译PL/SQL子程序。
6、运行PL/SQL过程单击编译图标，过程成功编译。右击emp_list，选择运行。该操作将调用“运行PL/SQL”对话框。运行PL/SQL对话框允许选择要运行的目标过程或函数（对程序包有用），并显示所选目标的参数列表。PL/SQL块文本区域中包含的SQL Developer用来调用所选程序的生成代码。使用该区域填充要传送到程序单元的参数及处理复杂的返回类型。将 “V_EMPNO:=NULL;”更改为“V_EMPNO:=7369;”，然后单击确定。可以看到运行日志：
连接到数据库 scott。V_ENAME = SMITH进程已退出。从数据库 scott 断开连接。

2. 无参数的存储过程创建一个存储过程，使用游标实现，每输出dept表的一条记录（deptno、dname、loc）后，诉后输出该部门的员工记录（empno、ename、sal）。
create or replace procedure query_dept_emp is  type sp_emp_cursor is ref cursor;  test_cursor1 sp_emp_cursor;  test_cursor2 sp_emp_cursor;  v_deptno dept.Deptno%type;  v_dname dept.dname%type;  v_loc dept.loc%type;  v_empno emp.empno%type;  v_ename emp.ename%type;  v_sal emp.sal%type;begin  open test_cursor1 for select deptno,dname,loc from dept;  loop    fetch test_cursor1 into v_deptno, v_dname, v_loc;    exit when test_cursor1%notfound;    dbms_output.put_line(&#x27;部门编号：&#x27;||v_deptno||&#x27;部门名称：&#x27;||v_dname||&#x27;部门位置：&#x27;|| v_loc);    dbms_output.put_line(&#x27;-----------------------------------------------&#x27;);    open test_cursor2 for select empno,ename,sal from emp where deptno = v_deptno;    loop      fetch test_cursor2 into v_empno, v_ename, v_sal;      exit when test_cursor2%notfound;      dbms_output.put_line(v_empno||&#x27;          &#x27;|| v_ename||&#x27;         &#x27;|| v_sal);    end loop;  end loop;  close test_cursor1;  close test_cursor2;end;exec query_dept_emp;

3. 带in参数的存储过程创建一个PL/SQL块，根据输入的部门编号，用游标实现逐条输出emp表中该部门每位员工的编号（empno）、姓名（ename）和工资（sal）信息。
create or replace procedure query_by_deptno(v_deptno in emp.Deptno%type) is  type sp_emp_cursor is ref cursor;  test_cursor sp_emp_cursor;  v_empno emp.empno%type;  v_ename emp.ename%type;  v_sal emp.sal%type;begin  open test_cursor for select empno,ename,sal from emp where deptno = v_deptno;  dbms_output.put_line(&#x27;员工编号    姓名    工资&#x27;);  loop    fetch test_cursor into v_empno, v_ename, v_sal;    exit when test_cursor%notfound;    dbms_output.put_line(v_empno||&#x27;    &#x27;|| v_ename||&#x27;    &#x27;|| v_sal);  end loop;  close test_cursor;end;exec query_by_deptno(10);
4. 带输入in、输出out参数的存储过程查询emp中给定职工号的姓名、工资和佣金。
create or replaceprocedure query_emp  (v_emp_no in emp.empno%type,   v_emp_name out emp.ename%type,  v_emp_sal out emp.sal%type,  v_emp_comm out emp.comm%type  )is  begin    select ename,sal,comm    into v_emp_name, v_emp_sal, v_emp_comm    from emp     where empno = v_emp_no;end query_emp;variable emp_name varchar2(15);variable emp_sal number;variable emp_comm number;execute query_emp(7369,:emp_name,:emp_sal,:emp_comm);print emp_name;
5. 使用隐式游标SQL%NOTFOUND的存储过程解雇给定职工号的职工。如果职工号7654的职工不存在则出错。为了避免出错可使用隐式游标SQL%NOTFOUND语句。
create or replace procedure fire_emp(v_emp_no in emp.empno%type)isbegin  delete from emp where empno = v_emp_no;  if sql%notfound then    dbms_output.put_line(&#x27;雇员号为：&#x27;||v_emp_no||&#x27;的员工不存在！&#x27;);  else     dbms_output.put_line(&#x27;已删除雇员号为：&#x27;|| v_emp_no || &#x27;的员工。&#x27;);  end if;end fire_emp;execute fire_emp(7654);

6. 自定义函数用Function查询出EMP中给定职工号的工资。
create or replace function get_sal  (v_emp_no in emp.empno%type)  return number is   v_emp_sal emp.sal%type:=0;begin  select sal into v_emp_sal  from emp where empno = v_emp_no;  return (v_emp_sal);end get_sal;variable emp_sal number;execute:emp_sal:=get_sal(&#x27;7900&#x27;);print emp_sal;
7. 小结这个实验肯定是和PL/SQL程序设计一伙的！感觉在专业中基本用不到。本篇是Oracle实验记录的的最后一篇，书上还有一些内容，什么分区表的创建和使用、使用RMAN工具、闪回技术等等，需要时再去学习。
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——PL/SQL程序设计</title>
    <url>/dev-oracle-experiment-plsql-design/</url>
    <content><![CDATA[1. PL/SQL块创建一个PL/SQL块，利用替换变量输入职工号，查询该职工的工资，如果工资小于300元，那么把工资更改为加200元。如果没有该员工，则显示“没有该员工！”
select empno from emp;
记住一些empno，方便接下来的输入。
undefine 员工号;declare  v_sal emp.sal%type;begin  select sal into v_sal from emp where empno = &amp;&amp;员工号;  if v_sal &lt; 3000 then  update emp set sal = sal + 200 where empno = &amp;&amp;员工号;  end if;  dbms_output.put_line(&#x27;工资为：&#x27;||v_sal);  exception when no_data_found then    dbms_output.put_line(&#x27;没有该员工！&#x27;);end;
运行上述代码，输入一个empno，比如7369，就可以在DBMS输出中看到结果，注意，需要先启用DBMS输出。

2. 使用游标为了处理select语句返回的多行数据，开发人员可以使用显式游标。使用显式游标表哭定义游标、打开游标、提取游标和关闭游标。
1、游标的使用
declare  cursor emp_cursor is select ename,sal from emp where deptno=10;  v_ename emp.ename%type;  v_sal emp.sal%type;begin  open emp_cursor;  loop    fetch emp_cursor into v_ename,v_sal;    exit when emp_cursor%notfound;    dbms_output.put_line(&#x27;姓名：&#x27;||v_ename||&#x27;，工资：&#x27;||v_sal);  end loop;  close emp_cursor;end;

2、在游标中，使用fetch…bulk collect into语句提取所有数据
declare  cursor emp_cursor is    select ename from emp where deptno=10;  type ename_table_type is table of emp.ename%type;  ename_table ename_table_type;begin  open emp_cursor;  fetch emp_cursor bulk collect into ename_table;  for i in 1..ename_table.count loop    dbms_output.put_line(ename_table(i));  end loop;  close emp_cursor;end;

3、使用游标属性
declare   cursor emp_cursor is    select ename from emp where deptno=10;  type ename_table_type is table of emp.ename%type;  ename_table ename_table_type;begin  if not emp_cursor%isopen then    open emp_cursor;  end if;  fetch emp_cursor bulk collect into ename_table;  dbms_output.put_line(&#x27;提取的总行数：&#x27;||emp_cursor%rowcount);  close emp_cursor;end;
4、基于游标定义记录变量5、带参数的游标6、使用游标更新数据7、使用游标删除数据8、使用游标for循环9、若类型REF游标10、强类型REF游标
3. 小结PL/SQL程序设计有什么用处？不知道。只是感觉很复杂，也没有学习的欲望，等到确实有需求了再来学习吧！
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>人脉管理系统功能需求（二）</title>
    <url>/dev-connection-management-system-feature-request-2/</url>
    <content><![CDATA[1. 名片1、扫描纸质名片，生成电子名片
2、输入信息，制作电子名片
3、名片可以发送给他人，当两个人或者更多人距离比较近时，一个人点击发送名片，其他人保持接收名片状态，就可以实现名片的发送。
4、名片同步。当他人的名片发生变化时，用户点击名片同步到本地，就可以从服务器获取最新的名片信息；当自己的名片、自己制作的名片、自己添加的名片附加信息发生变化时，用户点击名片同步到云端，就可以在服务器端变更数据。PS：每个用户ID和自己的名片ID相同。

5、名片附加信息。朋友的生日、朋友的纪念日、下次应该和他联系的时间、毕业的学校、所属的圈子（高尔夫、同学等）、爱好、照片、合作关系。。。这些，都可以手动输入到名片附加信息中。部分信息关联到日程，比如好友生日，联系时间等。
6、名片搜索
7、时间轴。选择日期，事件（会面、吃饭、游玩），地点，名片（一张或多张）等信息，保存记录。一段时间之后，就和名片上的朋友有了用来记录交往过程时间轴。
2. 短信群发1、姓名替换。短信群发，别人会感觉你不够用心。如果在短信中使用到了对方的名字，这条短信的意义，就完全不一样了。比如，我们写一条短信：“[姓名]，天冷加衣！”点击群发，每个联系人收到的都是替换过姓名的短信。张三同学收到的短信就是“张三，天冷加衣！”，李四同学收到的就是“李四，天冷加衣！”。
3. 日程1、好友生日提醒
2、联系提醒
3、会面提醒
4、任务提醒
4. 目标规划1、制订目标例：拉到100万投资。
2、为目标制订任务
3、安排任务到日程
5. 社交技能1、谈资话题
2、社交礼仪
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
  </entry>
  <entry>
    <title>人脉管理系统功能需求（一）</title>
    <url>/dev-connection-management-system-feature-request-1/</url>
    <content><![CDATA[1. 前言本项目来自第六届中国大学生服务外包创新创业大赛。
2. 项目说明2.1. 问题说明与某人的联系程度，以及亲密度不是十分容易统计，通过人脉管理系统可以清除了解和某人的亲密度。
2.2. 用户期望智翔集团期望：对人脉的特征进行匹配性的关怀，极大提升关怀效果,极大降低为实施关怀所付出的时间精力成本，从而维护、推动人脉关系的发展。
该平台规划为开放式平台，平台支持个人对人脉进行排程，互动建议，谈资话题搜集，短信群发，人脉微博监控等等。


3. 任务要求参赛者，设计一套基于互联网，以人脉管理为核心的解决方案，该解决方案包括以下要求：
3.1. 策划方案项目需求分析。项目计划。原型图设计。未来的市场推广方案及计划。
3.2. 业务模型用户的登陆，退出与注册。
用户对自己的人脉进行特点分析，并进行针对性的人脉互动，以达到人脉关系促进的作用。
为用户对人脉进行排程，互动建议，谈资话题搜集，短信群发，人脉微博监控用户与其他用户进行分享或互动，收集用户对产品使用的各种心得、意见、想法等。
为用户提供名片识别、名片设计、名片交换、云名片管理、备份、同步、分享以及人脉云端管理等等功能；具有优越的用户体验，便捷的手势操作，给手机用户以全新、愉悦的使用体验。
为用户提供相遇信息，他们可交换照片并迅速创建一份基本档案。之后再点击某人照片，便会显示两人的邂逅历史，以及见面时的时间、地点、通讯录、话题、照片甚至当时的笔记等。
3.3. 技术方案及产品原型基于主流浏览器 PC 端、手机端。
提供整体技术架构解决方案，方案突出大规模访问及海量数据存储的系统规划解决方案。
实现该产品的原型系统。
3.4. 实施方案请提供该解决方案的实施计划。
3.5. 提交内容需求文档。原型设计。项目源代码。
4. 功能需求初稿4.1. 好友4.1.1. 好友列表1、好友列表类似于手机的通讯录，在姓名后面紧跟着亲密度（条形显示），可以通过右侧的亲密度迅速定位。2、可以选择排序方式，比如按亲密度升序，按亲密度降序。3、可以选择显示方式，使用此应用的好友或者未使用此应用的好友。4、对于未使用此应用的好友，可以通过发短信的方式邀请使用。5、点开一个好友，可以看到好友的基本信息、名片（如果已经获得）、时间轴、二度人脉等。也可以选择通话、短信、发消息。6、可以特别关心某好友。
4.1.2. 即时通讯（聊天）和好友聊天的时候，也许会出现没有话题聊的场面。这时候，选择聊天界面的聊天帮助，选择随机推荐话题、分类查看话题、根据聊天记录智能推荐话题，出现相关话题。
4.1.3. 时间轴点开时间轴，可以看到显示两人的邂逅历史，以及见面时的时间、地点、通讯录、话题、照片甚至当时的笔记等。
4.1.4. 查看二度人脉点开二度人脉，可以看到好友的好友的信息（如果对方设置允许）。可以选择发送好友请求，发送名片请求。
4.1.5. 短信群发，名片群发选择一些好友，群发短信，群发名片。可以选择定时发送。（和手机自带的短信功能也没啥差别。。。）
4.1.6. 向好友推荐好友自己拥有好友A和B，但是A和B不是好友，这时可以点击好友推荐，选择通过个人信息或者名片，然后选择A和B。A的个人信息或者名片就发送给B，B的个人信息或者名片就发送给A。同样的方式，可以选择更多的好友。
4.1.7. 搜索好友1、条件搜索。选择昵称、性别、行业、职位、范围（所有人或者圈子内的人）等，搜索到符合条件的用户，进行好友请求。
2、精确搜索。输入对方账号进行搜索。
4.1.8. 偶遇设置偶遇的条件，比如距离、年龄、职业等，当出现符合条件的用户时，会提醒用户是否添加好友，是否去见面。（偶遇排除好友）
4.1.9. 好友名片管理1、包含在应用内不是好友的名片2、名片的添加、删除、搜索
4.1.10. 亲密度统计1、使用此应用的人和未使用此应用的人分开统计。2、对于使用者，根据通话记录、短信、本应用内联系信息、微博来统计。3、对于未使用者，根据通话记录、短信来统计。4、根据联系次数、联系时间、联系方式绘制直观的图表。比如最近一年和某个好友的电话联系次数走势图。
4.2. 个人4.2.1. 基本信息昵称，等级，积分，性别，职业，签名，生日，手机，邮箱等。
4.2.2. 个人名片管理系统提供一些名片模板，用户通过选择这些模板，填写相应信息，生成名片。名片可以通过扫二维码的方式添加，也可以通过发送名片的方式发送给好友或二度人脉。
4.2.3. 空间（个人圈子）1、个人有自己的谈资话题，即好友的谈论。2、发起活动，好友可以看到。
4.2.4. 基本设置1、是否开启好友生日提醒功能2、是否开启联系提醒。3、是否开启附近好友提醒。4、是否开启偶遇。
4.2.5. 隐私设置1、是否允许好友发送自己的名片给二度人脉2、是否允许好友发送自己的名片给陌生人3、是否接收二度人脉的名片4、是否接收陌生人的名片5、是否可以通过二度人脉看到自己的信息6、是否随时定位自己的位置。7、是否允许被陌生人搜索。8、是否允许系统查看聊天记录。
4.2.6. 同步管理1、同步名片到云端，同步名片到本地。2、同步通讯录到云端，同步通讯录到本地。3、同步短信到云端，同步短信到本地。
4.2.7. 提醒1、联系提醒一周、一月、一年没有联系过的好友，设置阶段性提醒。
2、附近好友提醒如果和某好友距离在100米之内，就发出提醒，询问是否去见面。
4.2.8. 日程1、生日好友的生日会自动同步到自己的日程表中，生日前一天或者当天会有闹钟提醒。2、约会点开日程表，输入约会信息，前一天或者当天会有闹钟提醒。3、圈子聚会圈子管理员发起了活动，如果用户确认参加，聚会的时间地点信息会出现在日程中，前一天或者当天会有闹钟提醒。
4.2.9. 话题选择感兴趣的话题分类，积累聊天话题，避免没有话题聊的尴尬。此功能可以和即时通讯（聊天）功能结合。
4.3. 人脉圈4.3.1. 管理1、每个用户都有权利创建人脉圈，创建人脉圈之后，该用户自动成为该人脉圈的管理员，他可以邀请自己的好友加入自己的人脉圈，任命其他的人为管理员。
2、管理员可以设置人脉圈的主题，如亲友圈，朋友圈，同事圈等。
3、管理员可以设置人脉圈的管理制度，可以是“独裁制”，“民主选举制”,“独裁制”暨管理始终不变，“民主选举制”暨管理员定期选举产生。
4、管理员可以发起活动，可以是线下的聚会等活动，聚会现场可以二维码签到；也可以是线上的游戏或讨论。我们鼓励管理员记录人脉圈中活动情况，如：聚会的照片，参与的人员，发生的主要事件，从而生成人脉圈的历史。
5、管理员对人脉圈的隐私信息有设置和修改的权力，如：人脉圈是否公开等。
4.3.2. 谈资话题1、每个人脉圈都有各自的谈资话题模块，人脉圈中的所有的都可以在其中发表各自关于该人脉圈的话题，其他用户可以点赞，评论，@。2、管理员对于人脉圈谈资话题有管理权利。3、人脉圈中用户可以针对自己的需求对谈资话题进行搜索。4、用户的话题被评论有相关的推送提示。
4.3.3. 圈子拓展1、每个用户都可以根据自己的喜好搜索相关的人脉圈，如果该人脉圈为公开的，用户就可以浏览圈子的相关信息，也可以查看圈子中的人员组成。2、人脉圈的管理员，可以向其他的人脉圈发出联结申请，若两个人脉圈成为联结关系，双方的管理员可以自由交流。3、管理员可以引荐自己圈子里的人给联结圈子的人，引荐的用户不会立即成为联结圈子的成员，而是将信息公布给联结圈子的成员，供联结圈子的用户拓宽自己的人脉（有需求的用户可以自己联系他成为自己的好友），能否成为圈子的成员决定权在于管理者。4、两个圈子发起共同活动，设置可以两个人脉圈合并成为一个新的人脉圈。5、在圈子中可以随意私聊，添加好友。
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
  </entry>
  <entry>
    <title>Oracle实验记录——模式对象</title>
    <url>/dev-oracle-experiment-pattern-object/</url>
    <content><![CDATA[1. 表表示数据库中最常用的模式对象，用户的数据在数据库中是以表的形式存储的。表通常由一个或多个列组成，每个列表示一个属性，而表中的一行则表示一条记录。
在创建表时可以为表指定存储空间，如果不指定，Oracle会将该表存储到默认表空间中。根据需要可以将表从一个表空间移动到另一个表空间。create table userbase(username varchar2(16),passwd varchar2(16));alter table userbase move tablespace user_data;
重命名表alter table userbase rename to newuserbase;或者rename userbase to newuserbase;


2. 索引索引是数据库中用于存放表的每一条记录的位置的一种对象，其主要目的是为了加快数据的读取速度和完整性检查。不过，创建索引需要占用许多存储空间，而且在向表中添加和删除记录时，数据库需要花费额外的开销来更新索引。创建索引：create index index_username on userbase(username) tablespace user_data;删除索引：drop index index_username;创建唯一索引：create unique index unique_index_username on userbase(username) tablespace user_data;创建组合索引：create index complex_index_username on userbase(username,passwd) tablespace user_data;创建反向键索引：create index reverse_index_username on userbase(username) reverse tablespace user_data;
3. 视图视图是一个虚拟表，它并不存储真实的数据，它的行和列的数据来自于定义视图的子查询语句中所引用的表，这些表通常也称为视图的基表。视图可以建立在一个或多个表（或其他视图）上，它不占用实际的存储空间，只是在数据字典中保存它的定义信息。
create table userdetail(username varchar2(16),realname varchar2(16));创建视图语法如下：
create or replace view view_user(v_username,v_passwd,v_realname) as select b.username,b.passwd,d.realnamefrom userbase b,userdetail dwhere b.username=d.username;
以上命令如果提示没有创建视图权限，请使用system登陆，执行grant create view to scott;
4. 序列在Oracle中，可以使用序列自动生成一个整数序列，主要用来自动为表中的数据类型的主键列提供有序的唯一值，这样就可以避免在向表中添加数据时，手工指定主键值。序列和表没有关系。
而且使用手工指定主键值这种方式时，由于主键值不允许重复，因此它要求操作人员在指定主键值时自己判断新添加的值是否已经存在，这显然是不可取的。
创建序列语法如下：
create sequence userbase_sequenceminvalue 1maxvalue 1024start with 1increment by 1cache 10;
删除序列：drop sequence userbase_sequence;
添加id列：alter table add id int;
插入数据：insert into userbase values(&#39;voidking&#39;,&#39;voidking&#39;,userbase_sequence.nextval);
5. 同义词Oracle支持为表、索引或视图等模式对象定义别名，也就是为这些对象创建同义词。Oracle中的同义词主要分为如下两类。1、公有同义词：在数据库中所有用户都可以使用。2、私有同义词：由创建它的用户私人拥有。不过，用户可以控制其他用户是否有权使用自己的同义词。
创建同义词语法：create public synonym synonym_userbase for userbase;
权限不够，使用system用户登录，执行grant create synonym to scott;
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——数据查询和函数的使用</title>
    <url>/dev-oracle-experiment-data-query-and-function/</url>
    <content><![CDATA[我们使用scott用户的几张表来演示。
1. 查看表结构desc emp;
2. 查询所有列select * from dept;
3. 查询指定列select ename, sal, job, deptno from emp;

4. 取消重复行select distinct deptno, job from emp; 
5. 统计行数select count(*) from emp;
6. 条件查询1、查询SMITH所在部门，工作，薪水select deptno,job,sal from emp where ename = &#39;SMITH&#39;; 2、查询薪水大于3000的员工信息select * from emp where sal &gt; 3000;
7. 显示每个雇员的年工资select sal*13+nvl(comm, 0)*13 &quot;年薪&quot; , ename, comm from emp;
8. 显示列别名select ename &quot;姓名&quot;, sal*12 as &quot;年收入&quot; from emp; 
9. ||select ename || &#39; is a &#39; || job from emp;
10. like%表示0到多个字符，_表示任意单个字符。1、显示首字符为S的员工姓名和工资？select ename,sal from emp where ename like &#39;S%&#39;;
2、显示第三个字符为大写O的所有员工的姓名和工资select ename,sal from emp where ename like &#39;__O%&#39;;
11. inselect * from emp where empno in (7844, 7839,123,456);
12. is nullselect * from emp where mgr is null; 
13. 逻辑操作符查询工资高于500或者是岗位为 MANAGER 的雇员，同时还要满足他们的姓名首字母为大写的J。select * from emp where (sal &gt;500 or job = &#39;MANAGER&#39;) and ename like &#39;J%&#39;;
14. order by1、按照工资的从低到高的顺序显示雇员的信息select * from emp order by sal;2、按照部门号升序而雇员的工资降序排列select * from emp order by deptno,sal desc;3、按年薪排序select ename, (sal+nvl(comm,0))*12 &quot;年薪&quot; from emp order by &quot;年薪&quot; asc;
15. 分组函数1、显示所有员工中的最高工资和最低工资select max(sal),min(sal) from emp;
2、最高工资那个人select ename,sal from emp where sal=(select max(sal) from emp);
3、显示所有员工的平均工资和工资总和select avg(sal),sum(sal) from emp;
4、计算总共有多少员工select count(*) from emp;
16. group by1、显示每个部门的平均工资和最高工资select avg(sal),max(sal),deptno from emp group by deptno;
2、显示每个部门的每种岗位的平均工资和最低工资select min(sal), avg(sal), deptno, job from emp group by deptno, job;
17. having显示平均工资低于 2000的部门号和它的平均工资select avg(sal), max(sal), deptno from emp group by deptno having avg(sal) &lt; 2000;
18. 多表查询1、显示雇员名，雇员工资及所在部门的名字select e.ename, e.sal, d.dname from emp e, dept d where e.deptno = d.deptno;
2、显示部门号为 10 的部门名、员工名和工资select d.dname, e.ename, e.sal from emp e, dept d where e.deptno = d.deptno and e.deptno = 10;
3、显示各个员工的姓名，工资及工资的级别select e.ename, e.sal, s.grade from emp e, salgrade s where e.sal between s.losal and s.hisal;
4、显示雇员名，雇员工资及所在部门的名字，并按部门排序select e.ename, e.sal, d.dname from emp e, dept d where e.deptno = d.deptno order by e.deptno;
19. 自连接显示某个员工的上级领导的姓名select worker.ename, boss.ename from emp worker,emp boss where worker.mgr = boss.empno and worker.ename = &#39;FORD&#39;;
20. 单行子查询1、查询出SMITH的部门号select deptno from emp where ename = &#39;SMITH&#39;;
2、显示与SMITH同部门的所有员工select * from emp where deptno = (select deptno from emp where ename = &#39;SMITH&#39;);
21. 多行子查询查询和部门10的工作相同的雇员的名字、岗位、工资、部门号select distinct job from emp where deptno = 10; select * from emp where job in (select distinct job from emp where deptno = 10);
22. all显示工资比部门 30的所有员工的工资高的员工的姓名、工资和部门号select ename, sal, deptno from emp where sal &gt; all (select sal from emp where deptno = 30);select ename, sal, deptno from emp where sal &gt; (select max(sal) from emp where deptno = 30);
23. any显示工资比部门30的任意一个员工的工资高的员工姓名、工资和部门号select ename, sal, deptno from emp where sal &gt; any (select sal from emp where deptno = 30);select ename, sal, deptno from emp where sal &gt; (select min(sal) from emp where deptno = 30);
24. 多列子查询查询与 SMITH 的部门和岗位完全相同的所有雇员select deptno, job from emp where ename = &#39;SMITH&#39;;select * from emp where (deptno, job) = (select deptno, job from emp where ename = &#39;SMITH&#39;);
25. from子句中使用子查询显示高于自己部门平均工资的员工的信息select deptno, avg(sal) mysal from emp group by deptno;
select e.ename, e.deptno, e.sal, ds.mysal from emp e, (select deptno, avg(sal) mysal from emp group by deptno) ds where e.deptno = ds.deptno and e.sal &gt; ds.mysal;
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——数据库的安全性</title>
    <url>/dev-oracle-experiment-database-safety/</url>
    <content><![CDATA[1. 创建数据库表空间（永久表空间）create tablespace test datafile &#x27;D:\oracle\oradata\test.dbf&#x27;size 100mautoextend on next 100mmaxsize unlimited;

2. 创建临时表空间create temporary tablespace test_temp tempfile &#x27;D:\oracle\oradata\test_temp.dbf&#x27;size 100mautoextend on next 32mmaxsize 1024m;


3. 创建用户create user user1 identified by user1default tablespace test quota 10m  on users account lock;
PS:创建用户时，如果没有指定默认表空间和临时表空间，则该用户将使用系统自带的users表空间作为默认表空间，自带的temp表空间作为临时表空间。
4. 解锁用户alter user user1 account unlock;
5. 授予系统权限grant create session to user1;grant create table to user1;grant unlimited tablespace to user1;
6. 授予对象权限conn scott/tiger;grant select on emp to user1;grant update(ename) on emp to user1;
7. 创建角色Oracle三个标准角色：1、Connect：拥有CONNECT角色的用户，可以与服务器建立连接会话，不可以创建表。2、Resource：RESOURCE角色允许用户创建表、过程、触发器、序列等权限。3、DBA：DBA角色拥有所有的系统权限——包括无限制的空间限额和给其他用户授予各种权限的能力。PS：如果新创建的用户需要登录到OEM，则必须给用户授予select_catalog_role角色后才可以登录，否则只有管理员身份的用户才可以登录到OEM。
conn system/voidkingcreate role myrole;grant create session to myrole;grant create table to myrole;grant myrole to user1;
PS：有些系统权限比较特殊，是不能放到角色中的，如：unlimited tablaspace；这些权限很高、很特殊，必须直接赋予给用户。
8. 回收用户的权限revoke create table from user1;
9. 删除用户drop user user1;drop user user1 cascade;//用户如果有其他对象，加上cascade连同该用户的所有对象一并删除。
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——创建和管理表空间</title>
    <url>/dev-oracle-experiment-create-and-manage-table-space/</url>
    <content><![CDATA[1. 创建临时表空间create temporary tablespace user_temp  tempfile &#x27;D:\oracle\oradata\user_temp.dbf&#x27; size 10m  autoextend on  next 5m maxsize 50m  extent management local;  

PS：临时表空间主要用于存储用户在执行order by等语句进行排序或汇总时产生的临时数据，它是所有用户公有的。默认情况下，所有用户都使用temp作为临时表空间。但是也允许使用其他表空间作为临时表空间，这需要在创建用户时进行指定。


2. 创建数据表空间创建数据表空间 user_data
create tablespace user_data  datafile &#x27;D:\oracle\oradata\user_data.dbf&#x27; logging size 10m  autoextend on  next 5m maxsize 50m  extent management local; 

3. 创建用户并指定表空间创建用户 voidking，密码为 voidking，指定表空间为user_data 
create user voidking identified by voidkingdefault tablespace user_data  temporary tablespace user_temp; 

4. 修改用户的默认表空间修改用户 voidking 的默认表空间为 user_dataalter user voidking default tablespace user_data;
5. 给用户授予权限给 voidking 用户授权grant connect,resource,dba to voidking;
6. 重命名表空间重命名表空间 user_data 为 new_user_dataalter tablespace user_data rename to new_user_data;
7. 增加新的数据文件在 user_data 表空间下新增数据文件
alter tablespace user_dataadd datafile &#x27;D:\oracle\oradata\user_data2.dbf&#x27;size 2m autoextend on next 1m maxsize unlimited;

8. 删除表空间的数据文件删除 user_data 表空间下的数据文件alter tablespace user_data drop datafile &#39;D:\oracle\oradata\user_data2.dbf&#39;;
9. 设置数据文件的状态数据文件主要有三种状态：online、offline、offline drop。alter database datafile &#39;D:\oracle\oradata\user_data.dbf&#39; online;
10. 删除表空间//删除表空间drop tablespace user_data including contents and datafiles;//无法删除，表空间被占用drop tablespace user_temp including contents and datafiles;//查看user_temp是不是某些用户的默认临时表空间select username,temporary_tablespace from dba_users;alter user voidking temporary tablespace user_temp2;alter database default temporary tablespace user_temp2;//成功删除drop tablespace user_temp including contents and datafiles;


11. 设置默认表空间//设置默认临时表空间alter database default temporary tablespace user_temp;//设置默认永久表空间alter database default tablespace user_data;

12. 查看表空间1、查看表空间的名称和大小
SELECT t.tablespace_name, round(SUM(bytes / (1024 * 1024)), 0) ts_size FROM dba_tablespaces t, dba_data_files d WHERE t.tablespace_name = d.tablespace_name GROUP BY t.tablespace_name; // orselect tablespace_name ,sum(bytes) / 1024 / 1024 as MB　from dba_data_files group by tablespace_name;


2、查看表空间物理文件的名称及大小 
SELECT tablespace_name, file_id, file_name, round(bytes / (1024 * 1024), 0) total_space FROM dba_data_files ORDER BY tablespace_name; 

3、查看所有表空间对应的文件
select tablespace_name,file_name from dba_data_files;

4、查看回滚段名称和大小
SELECT segment_name, tablespace_name, r.status, (initial_extent / 1024) initialextent, (next_extent / 1024) nextextent, max_extents, v.curext curextent FROM dba_rollback_segs r, v$rollstat v WHERE r.segment_id = v.usn(+) ORDER BY segment_name; 

5、查看控制文件
SELECT NAME FROM v$controlfile; 

6、查看日志文件
SELECT MEMBER FROM v$logfile; 

7、查看表空间的使用情况 
SELECT SUM(bytes) / (1024 * 1024) AS free_space, tablespace_name FROM dba_free_space GROUP BY tablespace_name; SELECT a.tablespace_name, a.bytes total, b.bytes used, c.bytes free, (b.bytes * 100) / a.bytes &quot;% USED &quot;, (c.bytes * 100) / a.bytes &quot;% FREE &quot; FROM sys.sm$ts_avail a, sys.sm$ts_used b, sys.sm$ts_free c WHERE a.tablespace_name = b.tablespace_name AND a.tablespace_name = c.tablespace_name; 

8、查看数据库库对象
SELECT owner, object_type, status, COUNT(*) count# FROM all_objects GROUP BY owner, object_type, status; 

9、查看数据库的版本
SELECT version FROM product_component_version WHERE substr(product, 1, 6) = &#x27;Oracle&#x27;; 

10、查看数据库的创建日期和归档方式 
SELECT created, log_mode, log_mode FROM v$database; 

13. 修改数据文件大小修改数据文件大小为 20Malter database datafile &#39;D:\oracle\oradata\user_data.dbf&#39;  resize 20m;
14. 使用OEM管理表空间开始，所有程序，Oracle-OraDb11g_home1，Database Control-orcl。使用sys登陆，连接身份sysdba。服务器，存储，表空间。
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——利用SQL命令创建表</title>
    <url>/dev-oracle-experiment-sql-commond-create-table/</url>
    <content><![CDATA[1. 创建表create table userbase(username varchar2(8),passwd varchar2(16));
2. 插入一行insert into userbase values(&#39;voidking&#39;,&#39;voidking&#39;);
3. 查看数据select * from userbase;


4. 修改表1、追加新的列alter table userbase add(realname varchar2(8));
2、修改现有的列、为新追加的列定义默认值alter table userbase modify(realname varchar2(8) default &#39;haojin&#39;);insert into userbase(username, passwd) values(&#39;voidking&#39;,&#39;voidking&#39;);select * from userbase;
3、删除一个列alter table userbase drop column realname;
4、禁用列alter table userbase set unused column passwd;
5、重命名列alter table userbase rename column username to name;
5. 清空表truncate table userbase;
6. 删除表drop table userbase;
7. 添加注释comment on table userbase is &#39;这张表用来测试&#39;;
8. 定义和管理数据完整性约束1、非空约束create table userbase(username varchar2(8) not null,passwd varchar2(16));
2、主键约束create table userbase(username varchar2(8) primary key,passwd varchar2(16));alter table userbase add constraint primary_key primary key(username);
3、唯一性约束create table userbase(username varchar2(8) primary key not null ,passwd varchar2(16) unique);alter table userbase add constraint unique_value unique(passwd);
4、外键约束create table userdetail(username varchar2(8) primary key not null ,realname varchar2(8));alter table userbase add constraint fk_username foreign key(username) references userdetail(username);
insert into userdetail values(&#39;voidking&#39;,&#39;haojin&#39;);insert into userbase values(&#39;voidking&#39;,&#39;voidking&#39;);
delete from userbase where username=&#39;voidking&#39;;//不会删除userdetail的内容delete from userdetail where username=&#39;voidking&#39;;//约束限制，无法删除
alter table userbase add constraint fk_username foreign key(username) references userdetail(username) on delete cascade;delete from userbase where username=&#39;voidking&#39;;//不会删除userdetail的内容delete from userdetail where username=&#39;voidking&#39;;//userdetail和userbase里的内容都被删除了
5、检查约束alter table userdetail add(sex varchar(4) default &#39;男&#39; check(sex=&#39;男&#39; or sex=&#39;女&#39;));alter table userdetail add(sex varchar(4));alter table userdetail modify(sex check(sex=&#39;男&#39; or sex=&#39;女&#39;));
6、禁用和激活约束insert into userbase values(&#39;test&#39;,&#39;voidking&#39;);//执行失败alter table userbase disable constraint fk_username;insert into userbase values(&#39;test&#39;,&#39;voidking&#39;);//执行成功alter table userbase enable constraint fk_username;
7、删除约束alter table userdetail drop primary key;//删除失败alter table userdetail drop primary key cascade;//删除成功，同时删除了fk_usernamealter table userbase disable constraint fk_username;
8、查看约束信息select constraint_name,constraint_type,status,validated from user_constraints where table_name=&#39;USERBASE&#39;;select * from user_cons_columns where table_name=&#39;USERBASE&#39;;
9. 在sqldeveloper中创建表开始，所有程序，Oracle-OraDb11g_home1，应用程序开发，SQL Developer。
10. 在OEM中创建表开始，所有程序，Oracle-OraDb11g_home1，Database Control-orcl。
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>64位Oracle11gR2无法启动sqldeveloper</title>
    <url>/dev-64bit-oracle-r2-cannot-start-sqldeveloper/</url>
    <content><![CDATA[1. 详细问题描述安装好Oracle11gR2之后，启动SOL Developer：开始，所有程序，Oracle-OraDb11g_home1，应用程序开发，SQL Developer。第一次使用，需要选择与Oracle配套安装的jave.exe的路径，郝同学的安装路径为D:\Oracle\dbhome\jdk\bin\java.exe。此时，错误出现了，Unable to find a java Virtual Machineto point to a location of a java virtual machine,please refer to the oracle9i Jdeveloper Install guide(jdev\install.html)


2. 解决思路经百度，原来Oracle在制造64位版的时候没注意Oracle11gR2所带的SQL Developer是1.5.5.59.69版，不支持64位版的JDK，恰好64位Oracle带的JDK是64位的。
知道了这个就好办了，下载安装一个32位的jdk，重新设置一下路径。
3. 我的实际操作在使用32位Oracle的小伙伴那里，从dbhome目录下，把整个jdk文件夹拷贝出来，重命名为jdk32。拷贝到自己电脑上的dbhome文件夹下。然后，打开sqldeveloper.conf（路径为D:\Oracle\dbhome\sqldeveloper\sqldeveloper\bin\sqldeveloper.conf），修改SetJavaHome D:\Oracle\dbhome\jdk，为SetJavaHome D:\Oracle\dbhome\jdk32。至此，SQL Developer已经可以正常使用了。
但是，初始的开始菜单中的SQL Developer，链接到的是一个bat文件，这个bat文件，又打开了sqldeveloper.exe。郝同学窃以为此步画蛇添足，而且打开了两个窗口，让我很不爽。于是，郝同学果断删掉了开始菜单中的SQL Developer这个超链，然后右击Oracle-OraDb11g_home1下的应用程序开发，属性，复制下位置，打开此位置（C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Oracle - OraDb11g_home1），进入应用程序开发文件夹。然后，把sqldeveloper.exe的快捷方式拷贝进刚才打开的应用程序开发文件夹。
4. 题外记Oracle11g的安装包压缩文件有两个，win64_11gR2_database_1of2.zip和win64_11gR2_database_2of2.zip，安装时，要把它们解压到同一个文件夹。解压完成，点击setup.exe，根据喜好设置一下，等待一会儿就安装成功了。
当年，第一次接触Oracle，使用的是Oracle10g，各种安装报错，泪流满面。年轻不懂事，不止安装了database，还安装了client。想想真是多余，用来学习，安装database就够了，咱又不连接远程服务器。哪怕连接远程服务器，使用database里的工具也足够了，比如sqldeveloper。当年，还独立安装了一款PL/SQL Developer，解决完各种报错，微微一笑，这个工具真不错！如今，郝同学会毫不犹豫地会选择database里的自带工具！一言以蔽之，Oracle的安装和使用更简单了。（侧面反映了郝同学对于Oracle的使用更加得心应手，哇哈哈哈）
5. 参考文档http://caogenit.com/caogenxueyuan/yingyongfangxiang/shujuku/2343.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——数据的导入和导出</title>
    <url>/dev-oracle-experiment-data-in-and-out/</url>
    <content><![CDATA[以下命令，都是在操作系统命令行中使用。
1. EXP数据导出1、full方式：导出整个数据库exp system/Admin123 file=d:/test_1.dmp full=y
2、owner方式：导出指定的用户模式exp scott/tiger file=d:/test_2.dmp owner=scott
3、tables方式：导出指定的表exp scott/tiger file=d:/test_3.dmp tables=(emp,dept)

2. IMP数据导入1、full方式：导入整个数据库imp system/Admin123 file=d:/test_1.dmp full=y
2、owner方式：导入指定的用户模式imp scott/tiger file=d:/test_2.dmp owner=scott
3、tables方式：导入指定的表imp scott/tiger file=d:/test_3.dmp tables=emp,dept
3. EXPDP数据泵导出3.1. 数据泵工具导出的步骤1、启动sql plus，以system/manager 登陆conn system/manager as sysdba
2、创建目录对象（Tips：手动在D盘下创建一个temp文件夹）create directory mypump as &#39;D:\temp&#39;;
3、授权grant read,write on directory mypump to scott;
4、在操作系统命令行中，执行导出expdp scott/tiger schemas=scott directory=mypump dumpfile=expdp_test1.dmp;
3.2. 数据泵导出的各种模式1、tables：指定表模式导出expdp scott/tiger tables=emp,dept directory=mypump dumpfile=expdp_test2.dmp
2、按条件查询导出expdp scott/tiger tables=emp directory=mypump dumpfile=exopdp_test3.dmp query=&#39;&quot;where sal &lt; 2000&quot;&#39;
3、tablespaces：指定要导出表空间列表expdp system/Admin123 directory=mypump dumpfile=expdp_tablespace.dmp tablespaces=users
4、schemas：指定用户模式导出expdp scott/tiger schemas=scott directory=mypump dumpfile=expdp_test1.dmp
5、full：指定整个数据库模式导出expdp system/Admin123 directory=mypump dumpfile=full.dmp full=y
6、使用exclude，include导出数据使用include选项，导出用户中包括指定类型的指定对象导出scott用户下以B开头的所有表expdp scott/tiger directory=mypump dumpfile=include_1.dmp include=table:\&quot;like\&#39;B%\&#39;\&quot;
导出scott用户下的所有存储过程expdp scott/tiger directory=mypump dumpfile=include_2.dmp schemas=scott include=procedure
exclude导出用户中排除指定类型的指定对象导出scott用户下除view类型以外的所有对象expdp scott/tiger directory=mypump dumpfile=exclude_1.dmp schemas=scott exclude=view
4. IMPDP数据泵导入1、tables：指定表模式导入impdp scott/tiger tables=emp,dept directory=mypump dumpfile=expdp_test2.dmp
2、schemas：指定用户模式导入impdp scott/tiger schemas=scott directory=mypump dumpfile=expdp_test1.dmp
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——基本命令</title>
    <url>/dev-oracle-experiment-base-command/</url>
    <content><![CDATA[1、显示员工信息表emp的内容select * from emp;
2、将sql命令存入emp.sql文件save d:\emp.sql
3、清空缓存区del
4、调入emp.sql文件get d:\emp.sql
5、再次执行同样的sql命令@ d:\emp.sql或者start d:\emp.sql


6、利用替换变量接受雇员的姓名select * from emp where ename=&amp;cont_name;
7、利用替换变量接受雇员的雇佣日期（格式：03-12月-81）select * from emp where hiredate=&#39;&amp;d&#39;;
8、定义替换变量，在sql语句里使用该替换变量，使用完后将该变量删除define salary=1800;select * from emp where sal&gt;&amp;salary;undefine salary
9、将查询结果假脱机输出到文件d:\spool_temp.txt中spool d:\spool_temp.txtselect * from emp;select * from dept;spool off
10、为列deptno设置标题为“部门代码”column deptno heading &#39;部门代码&#39;select * from emp;
11、为sal列定制格式。要求在每个值前加$符号作为前缀，并保留一个小数位column sal format $99,999.0select * from emp;
12、用“/”替换所有空值column comm null &#39;/&#39;select * from emp;
13、显示所有列的当前设置column
14、清除所有列的设置clear column
15、限制重复行select * from emp;select * from emp order by deptno;
可能有许多雇员属于同一个部门，因此在DepartmentID列将有重复值出现break on deptno;select * from emp order by deptno;
显示break的设置break
清除break的设置clear break
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>Oracle实验记录——登陆解锁</title>
    <url>/dev-oralce-experiment-login-unlock/</url>
    <content><![CDATA[1、使用SYSTEM登陆SQL Plus
2、通过数据字典dba_users，查看Oracle账户的锁定状态select username,account_status from dba_users;
3、为scott账户解锁alter user scott account unlock;
4、为scott账户设置口令alter user scott identified by tiger;
5、查看现在scott的状态select username,account_status from dba_users where username=&#39;SCOTT&#39;;
6、在SQL Plus中使用scott账户连接数据库conn scott/tiger;
7、显示当前用户show user;
8、查看表信息select empno,ename,job,sal from emp where sal&lt;2500;
9、列出缓冲区内容list或者l
10、编辑当前行change /epno/empno

11、运行当前命令run或者/
12、增加一行inputorder by sal
13、在一行上增加内容append  desc注意，两个单词之间有两个空格
14、删除一行del
15、用系统编辑器编辑命令edit
16、保存命令（sql文件）save d:\hello.sql
17、运行命令（sql文件）start d:\hello.sql或者@ d:\hello.sql
18、清空缓冲区clear buffer
19、列出表结构desc emp
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>校内网服务器IP问题</title>
    <url>/dev-school-network-host-ip/</url>
    <content><![CDATA[帮助叶老师做精品课程网站的时候，使用过两个服务器，IP先后分别是202.119.160.48和202.119.160.253。不止如此，学校里还有很多服务器，每个都有不同的IP，而且都可以通过公网访问！
这里的问题是：公网IP地址不是很紧张吗？为啥能分到那么多公网IP？或者是使用了什么技术让IP地址增加？
解答：没有使用啥高深的技术，IP地址并没有想象的那么紧张，这些确实都是公网IP。宽带接入时可以申请独立IP，在阿里云、腾讯云等云厂商购买服务器时也可以同时购买公网IP。
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
      </tags>
  </entry>
  <entry>
    <title>DDNS入门篇</title>
    <url>/dev-ddns-start/</url>
    <content><![CDATA[1. 什么是DDNS？动态 DNS（DDNS）是一项在 IP 地址发生变化时可以自动更新 DNS 记录的服务。域名将网络 IP 地址转换为人类可读的名称，便于识别和使用。将名称映射到 IP 地址的信息以表格形式记录在 DNS 服务器上。在互联网服务供应商（ISP）提供动态IP地址（即每次连接到互联网时分配不同IP地址）的情况下，每当 IP 地址发生变化时，DDNS 服务都会更新 DNS 服务器记录。借助 DDNS，域名管理变得更容易、更高效。
DDNS的优点：

简化访问：即使IP地址经常改变，也可以通过固定的域名轻松访问我们的网络或设备。
节省成本：DDNS是维持稳定网络通信的经济有效的方法，这避免了需要为静态IP地址付费的必要性。

参考文档：

什么是 DDNS？
《DNS入门篇》



2. DNS 和 DDNS 之间有什么区别？DNS 服务是一种全球分布式服务，可将人类可读的名称转换为数字 IP 地址。DNS 服务器将域名请求转换为 IP 地址。这些地址控制最终用户在 Web 浏览器中键入域名时将访问哪个服务器。
动态 DNS（DDNS）是 DNS 的扩展，可自动实时更新与域名关联的 IP 地址。它扩展 DNS 的功能。借助 DDNS，即使在动态 IP 地址环境中，组织和个人也可以保持连接和可访问性。
DNS 得到所有 DNS 服务商的普遍支持，在全球范围内用于将域名解析为 IP 地址。
而 DDNS 要求 DNS 服务商支持 DDNS，并且要求用户配置 DDNS 客户端，与 DDNS 服务商合作完成 DDNS 功能。
3. DDNS的工作原理当我们本地网络的IP地址发生变化时，DDNS客户端会获取到本地网络的最新IP，请求DNS服务商的API，修改DNS记录，更新域名记录以反映新的IP地址。因此，即使我们本地网络的IP地址在不断变化，我们也可以使用固定的域名来访问我们的网络或设备。
4. 如何使用DDNS？使用DDNS服务的一般步骤如下：
1、申请域名：在域名服务商平台申请一个域名，比如GoDaddy、DNSPod、阿里云等。
2、选择DNS服务商：很多DNS服务商支持DDNS服务，比如DNSPod、阿里云、Cloudflare等。
3、DNS服务商平台配置：在DNS服务商平台上添加域名，并配置 DNS 解析
4、配置DDNS客户端：许多网络设备（如路由器、摄像头、NAS设备等）都支持DDNS服务，直接配置即可；在原本不支持DDNS服务的设备上，我们可以自行安装配置DDNS客户端软件。
5、配置端口映射：公网IP通常是分配给路由器的，如果想要直接访问到内网主机上服务，需要在路由器上配置一个端口映射。
5. DDNS客户端DDNS客户端有很多，这里推荐一个ddns-go，使用简单，功能齐全：

支持Mac、Windows、Linux系统，支持ARM、x86架构
支持主流域名服务商
支持接口/网卡/命令获取IP
支持同时配置多个DNS服务商
支持多个域名同时解析
支持多级域名
支持WebUI配置

6. Linux安装配置DDNS参考文档：绿联 NAS 域名直连 DDNS-Go+IPv6 内网穿透并开启 HTTPS
1、安装ddns-go
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root jeessy/ddns-go

2、浏览器访问ddns-go管理页面假设内网地址为 192.168.56.101，那么URL为 http://192.168.56.101:9876
3、配置ddns-go（1）在DNS服务商平台获取token，填入token（2）取消勾选ipv4；勾选ipv6，填入计划分配的域名 x.voidking.com（3）填入webhook，以便ipv6变更时通知（可选）（4）保存，保存后会自动在DNS服务商平台添加一条解析
要在5分钟内完成配置，否则就需要重启ddns-go
4、主机上ping域名
ping x.voidking.com

如果可以正常ping通，说明配置没问题。
5、网关配置检查并修改网关（路由器）防火墙配置，普通路由器将防火墙等级调到低，使得外网可以访问到局域网主机。不过，防火墙等级调低会带来一定的安全风险，最好是能配置防火墙规则（很多路由器不支持）。
6、外网ping域名
ping x.voidking.com

如果可以正常ping通，就证明配置成功，可以通过域名访问内网主机了。
7. 后记使用DDNS的优点是可以充分利用带宽，因为是直连。
某些情况下无法使用DDNS（比如路由器不支持端口映射），那么退而求其次的方法是使用内网穿透技术，可以参考文档《使用frp进行内网穿透》
]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>dns</tag>
      </tags>
  </entry>
  <entry>
    <title>DNS入门篇</title>
    <url>/dev-dns-start/</url>
    <content><![CDATA[1. DNS简介
网域名称（英语：Domain Name，简称：Domain），简称域名、网域，是由一串用点分隔的字符组成的互联网上某一台计算机或计算机组的名称，用于在数据传输时标识计算机的电子方位。域名可以说是一个IP地址的代称，目的是为了便于记忆后者。例如，wikipedia.org是一个域名。人们可以直接访问wikipedia.org来代替IP地址，然后域名系统（DNS）就会将它转化成便于机器识别的IP地址。这样，人们只需要记忆wikipedia.org这一串带有特殊含义的字符，而不需要记忆没有含义的数字。


域名的核心是域名系统（英语：Domain Name System，缩写：DNS），域名系统中的任何名称都是域名。在域名系统的层次结构中，各种域名都隶属于域名系统根域的下级。域名的第一级是顶级域，它包括通用顶级域，例如.com、.net和.org；以及国家和地区顶级域，例如.us、.cn和.tk。顶级域名下一层是二级域名，一级一级地往下。现在，还有一些新兴的中文域名，例如.在线等。这些域名向人们提供注册服务，人们可以用它创建公开的互联网资源或运行网站。顶级域名的管理服务由对应的域名注册管理机构（域名注册局）负责，注册服务通常由域名注册商负责。


域名系统（英语：Domain Name System，缩写：DNS）是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库，能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前，对于每一级域名长度的限制是63个字符，域名总长度则不能超过253个字符。

参考文档：

域名
Domain Name System
域名系统
DNS 原理入门



2. DNS记录类型域名与IP之间的对应关系，称为”记录”（record）。根据使用场景，”记录”可以分成不同的类型（type）。常见的DNS记录类型如下：

A：IPv4地址记录（IPv4 Address），返回域名指向的IP地址。
NS：域名服务器记录（Name Server），返回保存下一级域名信息的服务器地址。该记录只能设置为域名，不能设置为IP地址。
MX：邮件记录（Mail eXchange），返回接收电子邮件的服务器地址。
CNAME：规范名称记录（Canonical Name），返回另一个域名，即当前查询的域名是另一个域名的跳转，详见下文。
PTR：逆向查询记录（Pointer Record），只用于从IP地址查询域名，详见下文。
AAAA：IPv6地址记录（IPv6 Address），返回域名指向的IPv6地址。

一般来说，为了服务的安全可靠，至少应该有两条NS记录，而A记录和MX记录也可以有多条，这样就提供了服务的冗余性，防止出现单点失败。
3. DNS解析流程
DNS解析流程，简单来说就是分级查询。

用户请求本地域名服务器（DNS），如果有结果就直接返回；如果没有结果，就请求根域名服务器
请求根域名服务器，拿到顶级域名（一级域名）服务器的NS记录和A记录
请求顶级域名服务器，拿到二级域名服务器的NS记录和A记录
请求二级域名服务器，拿到三级或四级域名的NS记录和A记录

查看分级查询过程：
dig +trace www.baidu.com

本地DNS服务器，还可以配置上游DNS服务器。如果本地DNS服务器无法解析，那么会将该查询请求转发到指定的上游DNS服务器。上游DNS服务器将尝试解析该查询，并返回答案给自建DNS服务器。自建DNS服务器接收到上游DNS服务器的响应后，将答案返回给发起原始查询的客户端。这样就可以让上游DNS服务器进行递归查询，从而减少本地自建DNS服务器的递归查询数量，提高性能。
注意：本地DNS是否请求了上游DNS，通过dig或者nslookup命令是看不出来的，一般需要通过本地DNS的日志来验证。
4. DNS配置4.1. 配置文件主机上，我们通过配置文件 /etc/resolv.conf 指定DNS服务器。例如：
nameserver 10.96.0.10

4.2. search和ndots举个例子：
nameserver 10.96.0.10search svc.cluster.local cluster.localoptions ndots:5

当域名的点的数量小于ndots的数量时会依次加上后缀（search）去DNS服务器查找，如果都找不到再直接去DNS服务器查找域名。
4.3. nsswitch.confnsswitch全称为: network service switch，是一个通用框架，是各种类型存储交互的公共实现，实现名称解析服务。nsswitch.conf 决定系统获取解析的顺序。例如：
hosts:      files dns
以上配置，在域名解析时，就会先读取本地 /etc/hosts ，再请求dns服务器。
4.4. ubuntu20对于ubuntu20，dns的配置方法是比较特殊的。参考文档Ubuntu20.04 系统中设置dns并生效
1、修改dns的配置/etc/systemd/resolved.conf 中修改
DNS=180.76.76.76 114.114.114.114

2、重启systemd-resolved
systemctl enable systemd-resolvedsystemctl restart systemd-resolved

3、使用新的resolv.conf
cat /run/systemd/resolve/resolv.confmv /etc/resolv.conf&#123;,.bak&#125;ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf

5. DNS解析查询命令5.1. 查询顺序说明
nslookup/dig会直接查询DNS服务器
ping、host、curl等命令实际上会查看本地hosts，再根据resolv.conf中的配置查询域名。

5.2. dig查看域名解析记录
dig www.voidking.com

指定DNS查看域名解析记录
dig @180.76.76.76 www.voidking.comdig @180.76.76.76 -p 53 www.voidking.com

指定DNS查看域名指定类型的解析记录
dig @180.76.76.76 www.voidking.com Adig @180.76.76.76 www.voidking.com AAAA

查看每一级域名的NS记录
dig ns comdig ns voidking.com

反向查询DNS记录（PTR，根据IP查询域名）
dig -x 192.168.1.100

PTR相关知识参考文档什么是 DNS PTR 记录？
5.3. nslookup查看域名解析记录
nslookup www.voidking.com

指定DNS查看域名解析记录
nslookup www.voidking.com 180.76.76.76nslookup www.voidking.com 180.76.76.76 -port=53

指定DNS查看域名指定类型的解析记录
nslookup -type=A www.voidking.com 180.76.76.76nslookup -type=AAAA www.voidking.com 180.76.76.76nslookup -query=A www.voidking.com 180.76.76.76nslookup -query=AAAA www.voidking.com 180.76.76.76

5.4. host查看域名解析记录
host www.voidking.com

6. 公共DNS服务器6.1. 百度DNS180.76.76.76114.114.114.114

6.2. GoogleDNS8.8.8.88.8.4.4

6.3. 114DNS114.114.114.114114.114.115.115

6.4. 阿里DNS223.5.5.5223.6.6.6

6.5. OneDNS112.124.47.27114.215.126.16

6.6. DNS派101.226.4.6123.125.81.6

6.7. OpenDNS208.67.222.222208.67.220.220



]]></content>
      <categories>
        <category>engineering</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>dns</tag>
      </tags>
  </entry>
  <entry>
    <title>常用正则表达式</title>
    <url>/dev-common-regular-expression/</url>
    <content><![CDATA[正则表达式用于字符串处理、表单验证等场合，实用高效。现将一些常用的表达式收集于此，以备不时之需。
匹配中文字符的正则表达式： [\u4e00-\u9fa5]
匹配双字节字符(包括汉字在内)：[^\x00-\xff]
应用：计算字符串的长度（一个双字节字符长度计2，ASCII字符计1）
String.prototype.len=function()&#123;return this.replace([^\x00-\xff]/g,&quot;aa&quot;).length;&#125;

匹配空行的正则表达式：\n[\s| ]*\r
匹配HTML标记的正则表达式：/&lt;(.*)&gt;.*&lt;\/\1&gt;|&lt;(.*) \/&gt;/
匹配首尾空格的正则表达式：(^\s*)|(\s*$)


匹配中文字符的正则表达式：[\u4e00-\u9fa5]评注：匹配中文还真是个头疼的事，有了这个表达式就好办了
匹配双字节字符(包括汉字在内)：[^\x00-\xff]评注：可以用来计算字符串的长度（一个双字节字符长度计2，ASCII字符计1）
匹配空白行的正则表达式：\n\s*\r评注：可以用来删除空白行
匹配HTML标记的正则表达式：&lt;(\S*?)[^&gt;]*&gt;.*?&lt;/\1&gt;|&lt;.*? /&gt;评注：网上流传的版本太糟糕，上面这个也仅仅能匹配部分，对于复杂的嵌套标记依旧无能为力
匹配首尾空白字符的正则表达式：^\s*|\s*$评注：可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等)，非常有用的表达式
匹配Email地址的正则表达式：\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*评注：表单验证时很实用
匹配网址URL的正则表达式：[a-zA-z]+://[^\s]*评注：网上流传的版本功能很有限，上面这个基本可以满足需求
匹配帐号是否合法(字母开头，允许5-16字节，允许字母数字下划线)：^[a-zA-Z][a-zA-Z0-9_]&#123;4,15&#125;$评注：表单验证时很实用
匹配国内电话号码：\d&#123;3&#125;-\d&#123;8&#125;|\d&#123;4&#125;-\d&#123;7&#125;评注：匹配形式如 0511-4405222 或 021-87888822
匹配腾讯QQ号：[1-9][0-9]&#123;4,&#125;评注：腾讯QQ号从10000开始
匹配中国邮政编码：[1-9]\d&#123;5&#125;(?!\d)评注：中国邮政编码为6位数字
匹配身份证：\d&#123;15&#125;|\d&#123;18&#125;评注：中国的身份证为15位或18位
匹配ip地址：\d+\.\d+\.\d+\.\d+评注：提取ip地址时有用
匹配特定数字：^[1-9]\d*$　 　 //匹配正整数^-[1-9]\d*$ 　 //匹配负整数^-?[1-9]\d*$　　 //匹配整数^[1-9]\d*|0$　 //匹配非负整数（正整数 + 0）^-[1-9]\d*|0$　　 //匹配非正整数（负整数 + 0）^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$　　 //匹配正浮点数^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$　 //匹配负浮点数^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$　 //匹配浮点数^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$　　 //匹配非负浮点数（正浮点数 + 0）^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$　　//匹配非正浮点数（负浮点数 + 0）评注：处理大量数据时有用，具体应用时注意修正
匹配特定字符串：^[A-Za-z]+$　　//匹配由26个英文字母组成的字符串^[A-Z]+$　　//匹配由26个英文字母的大写组成的字符串^[a-z]+$　　//匹配由26个英文字母的小写组成的字符串^[A-Za-z0-9]+$　　//匹配由数字和26个英文字母组成的字符串^\w+$　　//匹配由数字、26个英文字母或者下划线组成的字符串
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
  </entry>
  <entry>
    <title>JavaScript正则表达式</title>
    <url>/dev-javascript-regex/</url>
    <content><![CDATA[1. 前言正则表达式（Regular Expression）主要是用来描述一个句法规则的模式。其实说的通俗一点，就是利用字符和元字符的组合，对一些符合既定句法的模式进行模糊匹配。它的主要功能是文本查询和字符串操作。本文讨论一下JavaScript中的正则表达式用法。


2. 定义正则表达式1、定义正则表达式有两种形式，一种是普通方式，一种是构造函数方式。2、普通方式：var reg=/表达式/附加参数表达式：一个字符串，代表了某种规则，其中可以使用某些特殊字符，来代表特殊的规则，后面会详细说明。附加参数：用来扩展表达式的含义，目前主要有三个参数：g：代表可以进行全局匹配。i：代表不区分大小写匹配。m：代表可以进行多行匹配。上面三个参数，可以任意组合，代表复合含义，当然也可以不加参数。例子：
var reg=/a*b/;var reg=/abc+f/g;
3、构造函数方式：var reg=new RegExp(“表达式”,”附加参数”);其中“表达式”与“附加参数”的含义与上面那种定义方式中的含义相同。例子：
var reg=new RegExp(&quot;a*b&quot;);var reg=new RegExp(&quot;abc+f&quot;,&quot;g&quot;);
4、普通方式与构造函数方式的区别普通方式中的表达式必须是一个常量字符串，而构造函数中的表达式可以是常量字符串，也可以是一个js变量，例如根据用户的输入来作为表达式参数等等：
var reg=new RegExp(document.forms[0].exprfiled.value,&quot;g&quot;);

3. 表达式模式1、表达式模式，是指表达式的表达方式与样式， 即 var reg=/表达式/附加参数 中的“表达式”怎样去描述？2、从规范上讲，表达式模式分为简单模式和复合模式。3、简单模式：是指通过普通字符的组合来表达的模式，例如
var reg=/abc0d/;
可见简单模式只能表示具体的匹配。4、复合模式：是指含有通配符来表达的模式，例如：
var reg=/a+b?\w/;
其中的+、?和\w都属于通配符，代表着特殊的含义。因此复合模式可以表达更为抽象化的逻辑。复合模式中各个通配符的含义及其使用请阅读下文给出的参考文档。
4. 表达式操作1、表达式操作，在这里是指和表达式相关的方法，我们将介绍六个方法。2、表达式对象（RegExp）方法：
（1）exec(str)，返回str中与表达式相匹配的第一个字符串，而且以数组的形式表现，当然如果表达式中含有捕捉用的小括号，则返回的数组中也可能含有()中的匹配字符串，例如：
var regx=/\d+/;var rs=regx.exec(&quot;3432ddf53&quot;);
返回的rs值为：{3432}
var regx2=new RegExp(&quot;ab(\d+)c&quot;);var rs2=regx2.exec(&quot;ab234c44&quot;);
返回的rs2值为：{ab234c,234}另外，如果有多个合适的匹配，则第一次执行exec返回一个第一个匹配，此时继续执行exec，则依次返回第二个第三个匹配。例如：
var regx=/user\d/g;var rs=regx.exec(&quot;ddduser1dsfuser2dd&quot;);var rs1=regx.exec(&quot;ddduser1dsfuser2dd&quot;);
则rs的值为{user1}，rs1的值为{user2}，当然注意regx中的g参数是必须的，否则无论exec执行多少次，都返回第一个匹配。后面还有相关内容涉及到对此想象的解释。
（2）test(str)，判断字符串str是否匹配表达式，返回一个布尔值。例如：
var regx=/user\d+/g;var flag=regx.test(&quot;user12dd&quot;);
flag的值为true。
3、String对象方法
（1）match(expr)，返回与expr相匹配的一个字符串数组，如果没有加参数g，则返回第一个匹配，加入参数g则返回所有的匹配例子：
var regx=/user\d/g;var str=&quot;user13userddduser345&quot;;var rs=str.match(regx);
rs的值为：{user1,user3}
（2）search(expr)，返回字符串中与expr相匹配的第一个匹配的index值。例子：
var regx=/user\d/g;var str=&quot;user13userddduser345&quot;;var rs=str.search(regx);
rs的值为：0
（3）replace(expr,str)，将字符串中匹配expr的部分替换为str。另外在replace方法中，str中可以含有一种变量符号$，格式为$n，代表匹配中被记住的第n的匹配字符串（注意小括号可以记忆匹配）。例子：
var regx=/user\d/g;var str=&quot;user13userddduser345&quot;;var rs=str.replace(regx,&quot;00&quot;);
rs的值为：003userddd0045例子2：
var regx=/u(se)r\d/g;var str=&quot;user13userddduser345&quot;;var rs=str.replace(regx,&quot;$1&quot;);
rs的值为：se3userdddse45对于replace(expr,str)方法还要特别注意一点，如果expr是一个表达式对象则会进行全局替换（此时表达式必须附加参数g，否则也只是替换第一个匹配），如果expr是一个字符串对象，则只会替换第一个匹配的部分，例如：
var regx=&quot;user&quot;var str=&quot;user13userddduser345&quot;;var rs=str.replace(regx,&quot;00&quot;);
rs的值为： 0013userddduser345
（4）split(expr)，将字符串以匹配expr的部分做分割，返回一个数组，而且表达式是否附加参数g都没有关系，结果是一样的。例子：
var regx=/user\d/g;var str=&quot;user13userddduser345&quot;;var rs=str.split(regx);
rs的值为：{3userddd,45}
5. 表达式相关属性1、表达式相关属性，是指和表达式相关的属性，如下面的形式：
var regx=/myexpr/;var rs=regx.exec(str);
其中，和表达式自身regx相关的属性有两个，和表达式匹配结果rs相关的属性有三个，下面将逐一介绍。2、和表达式自身相关的两个属性：
（1）lastIndex，返回开始下一个匹配的位置，注意必须是全局匹配（表达式中带有g参数）时，lastIndex才会有不断返回下一个匹配值，否则该值为总是返回第一个下一个匹配位置，例如：
var regx=/user\d/;var rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var lastIndex1=regx.lastIndex;rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var lastIndex2=regx.lastIndex;rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var lastIndex3=regx.lastIndex; 
上面lastIndex1为9，第二个lastIndex2也为9，第三个也是9；如果regx=/user/d/g，则第一个为9，第二个为18，第三个为0。
（2）source，返回表达式字符串自身。例如：
var regx=/user\d/;var rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var source=regx.source;
source的值为user\d3、和匹配结果相关的三个属性：
（1）index，返回当前匹配的位置。例如：
var regx=/user\d/;var rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var index1=rs.index;rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var index2=rs.index; rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var index3=rs.index; 
index1为4，index2为4，index3为4，如果表达式加入参数g，则index1为4，index2为13，index3会报错（index为空或不是对象）。
（2）input，用于匹配的字符串。例如：
var regx=/user\d/;var rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var input=rs.input;
input的值为sdsfuser1dfsfuser2。
（3）[0]，返回匹配结果中的第一个匹配值，对于match而言可能返回一个多值的数字，则除了[0]外，还可以取[1]、[2]等等。例如：
var regx=/user\d/;var rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var value1=rs[0];rs=regx.exec(&quot;sdsfuser1dfsfuser2&quot;);var value2=rs[0]; 
value1的值为user1,value2的值为user2
6. 实际应用1、实际应用一描述：有一表单，其中有一个“用户名”input域要求：汉字，而且不能少于2个汉字，不能多于4个汉字。实现：
&lt;script&gt;function checkForm(obj)&#123;     var username=obj.username.value;     var regx=/^[\u4e00-\u9fa5]&#123;2,4&#125;$/g;     if(!regx.test(username))&#123;               alert(&quot;Invalid username!&quot;);               return false;      &#125;     return true;&#125;&lt;/script&gt;&lt;form name=&quot;myForm&quot; onSubmit=&quot;return checkForm(this)&quot;&gt;    &lt;input type=&quot;text&quot; name=&quot;username&quot;/&gt;    &lt;input type=&quot;submit&quot; vlaue=&quot;submit&quot;/&gt;&lt;/form&gt;
2、实际应用二描述：给定一个含有html标记的字符串，要求将其中的html标记去掉。实现：
&lt;script&gt;function toPlainText(htmlStr)&#123;   var regx=/&lt;[^&gt;]*&gt;|&lt;\/[^&gt;]*&gt;/gm;   var str=htmlStr.replace(regx,&quot;&quot;);   return str;&#125;&lt;/script&gt;&lt;form name=&quot;myForm&quot;&gt;    &lt;textarea id=&quot;htmlInput&quot;&gt;&lt;/textarea&gt;    &lt;input type=&quot;button&quot; value=&quot;submit&quot; onclick=&quot;toPlainText(document.getElementById(&#x27;htmlInput&#x27;).value&quot;/&gt;&lt;/form&gt;

7. 参考文档js正则表达式语法http://blog.csdn.net/zaifendou/article/details/5746988
正则表达式30分钟入门教程http://demo.voidking.com/reprint/正则表达式/正则表达式30分钟入门教程.htm
常用的正则表达式http://demo.voidking.com/reprint/正则表达式/常用的正则表达式.htm
正则表达式速查表http://demo.voidking.com/reprint/正则表达式/正则表达式速查表.htm
正则表达式测试器http://demo.voidking.com/reprint/正则表达式/正则表达式测试器.htm
正则表达式在线测试器详情请见：http://demo.voidking.com/reprint/regexpal/
deerchao大侠正则表达式原文地址http://www.jb51.net/tools/zhengze.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>前端</tag>
        <tag>javascript</tag>
      </tags>
  </entry>
  <entry>
    <title>word通配符的使用</title>
    <url>/hobby-word-wildcard/</url>
    <content><![CDATA[1. 常见的几种通配符的使用方法任意单个字符：“?”可以代表任意单个字符，输入几个“?”就代表几个未知字符。如：输入“? 国”就可以找到诸如“中国”、“美国”、“德国”等字符；输入“???国”可以找到“孟加拉国”等字符。
任意多个字符：“*”可以代表任意多个字符。如：输入“*国”就可以找到“中国”、“美国”、 “孟加拉国”等字符。
指定字符之一：“[]”框内的字符可以是指定要查找的字符之一，如：输入“[中美]国”就可以找到“中国”、“美国”。 又如：输入“th[iu]g”，就可查找到“thigh”和“thug”。 输入“[学硕博]士”，查找到的将会是学士、士、硕士、博士。


指定范围内的任意单个字符：“[x-x]”可以指定某一范围内的任意单个字符，如：输入“[a-c]mend”的话，Word查找工具就可以找到“amend”、“bmend”、“cmend”等字符内容。
排除指定范、排除指定范围内的任意单个字符： “[!x-x]”可以用来排除指定范围内的任意单个字符，如：输入“[!a-c]”的话，word程序就可以找到“good”、“see”、“these”等目标字符，而所有包含字符a、b、c之类的内容都不会在查找结果中出现。
指定前一字符的个数：“{n}”可以用来指定要查找的字符中包含前一字符的个数，如：输入“cho{1} se”就是说包含1个前一字符“o”，可以找到“chose”，输入“cho{2}se”就是说包含2个前一字符“o”，可以找到， “choose”。
指定前一字符、指定前一字符数范围：“{x,x}”可以用指定要查找字符中前一字符数范围，如：输入“cho{1,2}”，则说明包含前一字符“o”数目范围是1-2个，则可以找到“chose”、“choose”。
一个以上的前一字符：“@”可以用来指定要查找字符中包含一个以上的前一字符，如：输入”，就可以找到， “chose”、“choose”等字符。
指定起始字符串：“&lt;”可以用来指定要查找字符中的起始字符串，如：输入“&lt;ag”，就说明要查找的字符的起始字符为“ag”，可以找到 “ago”、“agree”、“again”等字符。输入“&lt;te”的话，可能查到“ten”、“tea”等。
指定结尾字符串： “&gt;”可以用来指定要查找字符中的结尾字符串，如：输入“er&gt;”，就说明要查找的字符的结尾字符为“er”，可以找到 “ver”、“her”、“lover”等等。输入“en&gt;”， 就说明要查找到以“en”结尾的所有目标对象，可能找到“ten”、“pen”、“men”；输入“up&gt;”，就说明要查找到以“up”结尾的所有目标对象，例如会找到“setup”、“cup”等等。
2. 去除回车想必大家都有过这种经历，从网页上或者txt文档上复制一段文字到Word中，本该是一段完整内容的文字，被回车分成了一行行的（如下图），编辑起来非常不方便，一个个的删除又太麻烦，太浪费时间，这个时候大家不妨来试试下面的方法。1、选中该段文字，Ctrl + H ，打开查找替换窗口；2、单击“更多”按钮，在使用通配符前面选中；3、在查找内容处输入([!。：……？！）])^13{1,}，在替换为处输入\1，单击全部替换；4、关闭查找替换窗口，我们就会奇迹般地发现，原先被回车乱七八糟的内容，变为了完整的一段。
3. 为完形填空中添加下划线在制作英语试卷的阅读理解时，最麻烦的就是在英语的文章中输入带下划线的数字了。常规的方法是用“格式刷”工具一个一个地复制格式，能不能用格式替换的方法来实现呢？答案是肯定的。
在输入这些带有下划线的数字时无需设置任何格式或在其左右添加过多的空格，输入完成后选中该段文本，按“Ctrl+H”组合键打开“查找和替换”对话框，切换到“替换”选项卡，单击“高级”按钮，勾选“使用通配符”复选框，在“查找内容”框中输入“[0-9]{1，}”(其中“[0-9]{1，}”表示1个以上的数字);在“替换为”框中输入“^32^&amp;^32”(即在查找到的数字前后各加一个半角空格，其中“^32”为半角空格，“^&amp;”为查找到的数字，如图1)。
单击对话框下方的“格式”按钮，选中“字体”命令，在弹出的对话框中选择需要的“下划线线形”，确定后返回替换对话框中，单击“全部替换”按钮可以看到替换后的效果了(如图2)。
小提示：当前操作是对选中的文本进行操作的，在替换完毕后系统会提示要不要查找文档的其余部分，此时应选“否”。
4. 将数字间的句号改为小数点在文本录入时经常会遇到将数字间的小数点误输入成句号“。”的情况，在替换时要注意如果句号出现在数字中间那就需要将其转换为小数点即“.”;如果句号出现在数字的末尾就无需替换，所以不能简单地查找句号然后再替换为小数点。
使用通配符查找的方法就可以轻松解决这个问题了。打开替换对话框，勾选“使用通配符”复选框，在“查找内容”框中输入“([0-9]{1，})。([0-9]{1，})”，其含义是查找数字中间含有“。”的字符串;在“替换为”框中输入“\1.\2”(其中“\1”和“\2”分别代表原数字字符串中“。”左右的两个数字表达式，如图3)，单击“全部替换”按钮，数字间的“。”就全部替换为小数点了。
5. 删除中英文混排中的空格如果遇到中英文混排的文档中有许多的空格，有时连续的空格还不止一个，如何去掉中文中的空格同时保留英语单词间的空格呢？由于涉及的条件比较复杂，一次替换的确有些困难，所以在多次尝试后发现用下面的两步替换就可以实现了。
首先要将文档中的多个连续的空格变成一个半角空格，方法是在勾选“使用通配符”的替换对话框中查找“^32{1，}”(其含义是一个以上的空格)，在“替换为”框中输入“^32”，这样替换后连续的多个空格就替换为一个半角空格了。
然后将“查找内容”更改为“([!a-zA-Z])( )([!a-z，A-Z])”(即查找非英文字符间的空格，其中“[!a-zA-Z]”的含义是除大小写字母以外的其他任意字符)，在“替换为”框中输入“\1\3”，单击“全部替换”按钮后就可以看到除英文间的空格保留之外，其余多余的空格都被删除了(如图4)。
6. 小结word中的通配符替换功能的确强大，用好它可以帮我们解决日常办公中的许多难题。
说到通配符，不得不提到正则表达式。基本区别，就是通配符是系统命令使用，一般用来匹配文件名；而正则表达式是操作字符串，以行为单位来匹配字符串使用的。但是，这个区别，放在windows中似乎不太合适，因为word恰恰使用通配符来处理字符串。
linux中，通配符多用在文件名上，比如查找find、ls、cp；在文本过滤工具里，都是用正则表达式，比如grep、awk、sed，是针对文件的内容的。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
        <tag>word</tag>
      </tags>
  </entry>
  <entry>
    <title>word格式刷</title>
    <url>/hobby-word-format-brush/</url>
    <content><![CDATA[1. 前言格式刷位于“格式”工具栏上，相当好认，就是一把“刷子”。用它“刷”格式，可以快速将指定段落或文本的格式延用到其他段落或文本上，让我们免受重复设置之苦。 
2. 复制文字格式1．选中要引用格式的文本。 
2．单击“格式”工具栏上的“格式刷”按钮，此时鼠标指针显示为“I”形旁一个刷子图案。 
3．按住左键刷（即拖选）要应用新格式的文字。 


3. 复制段落格式1．选中要引用格式的整个段落（可以不包括最后的段落标记），或将插入点定位到此段落内，也可以仅选中此段落末尾的段落标记。 
2．单击“格式”工具栏上的“格式刷”按钮。
3．在应用该段落格式的段落中单击，如果同时要复制段落格式和文本格式，则需拖选整个段落（可以不包括最后的段落标记）。 
4. 提示1．单击“格式刷”按钮，使用一次后，按钮将自动弹起，不能继续使用；如要连续多次使用，可双击“格式刷”按钮。如要停止使用，可按键盘上的Esc键，或再次单击“格式刷”按钮。执行其他命令或操作（如“复制”），也可以自动停止使用格式刷。 
2．复制格式的组合键：Ctrl+Shift+C、Ctrl+Shift+V。而且Ctrl+Shift+V还有一个特点，只要曾经复制过某种格式，就可以反复使用此快捷键将此格式应用到其他段落或文字上，不受其间其他操作的影响，直到复制了一种新的格式。 
3．使用上述方法，可以在不同的Word文档间进行格式复制。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
        <tag>word</tag>
      </tags>
  </entry>
  <entry>
    <title>桌面美化</title>
    <url>/hobby-desktop-beautify/</url>
    <content><![CDATA[1. 前言爱拉风的小伙伴，总喜欢把电脑鼓捣的花里胡哨。郝同学也研究过一段时间，美其名曰“个性”，今天和大家探讨一下（win7为例）。
2. 我的桌面郝同学的桌面经历过几次变革，下面这张图，是现在的样子（使用rainmeter制作）。
3. 微软主题在桌面上右击，个性化，联机获取更多主题。下载安装替换即可。
这种主题，其实只是桌面背景的替换，美化效果不是很明显。
4. 第三方主题安装第三方主题，是最简便的一种美化桌面的方式。用时少，见效好，大部分小伙伴喜闻乐见。
4.1. 安装1、百度“win7主题”，选择一个主题下载网站进入。2、挑选喜欢的主题，下载。一般是压缩包，解压后会有一个以“.exe”结尾的文件，安装即可。
4.2. 免安装“安装”就意味着，往注册表写入东西，还要创建很多不必要的文件和文件夹。所以，只要条件允许，郝同学喜欢使用绿色软件，也就是免安装软件。主题也有办法跳过安装实现美化桌面。
原理：主题安装，实际上也就是往Themes、Wallpaper和Cursors文件夹里面写入文件，然后更改设置。我们只要获得win7主题中写入这三个文件夹的文件，就可以实现免安装。怎么搞？虚拟机安装win7主题，然后从这三个文件夹把文件拷贝出来，放入客户机对应的文件夹，稍加设置即可。
主题存放目录：C:\Windows\Rsources\ThemesC:\Windows\Web\WallpaperC:\Windows\Cursors
下面是郝同学使用过的主题，免安装版，分享给大家：http://yunpan.cn/cKDN8aRdgqEtX  访问密码 15ce
这次分享不包含鼠标光标，关于鼠标光标的使用和制作，将会在下一篇文章详细讨论。

4.3. 问题使用ghost镜像安装的系统，安装第三方主题，一般没有问题。但是，正版Win7系统（纯净版系统），安装了win7主题之后，任务栏和开始菜单都变成白色的，很难看，与主题预览图不一样,窗口颜色那里显示是Windows经典。解决办法是：主题破解。
在此附上win7主题破解工具：http://yunpan.cn/cKDN59AfDPUqR  访问密码 ce5e
5. rainmeterrainmeter（雨滴），个人比较满意的一款桌面美化工具，我们来研究一下它的使用方法。以下部分内容来自百度贴吧。
5.1. 步骤一：下载安装官网地址：http://rainmeter.cn/cms/ ，下载安装即可。需要注意的是，最好勾选开机启动。
5.2. 步骤二：关闭皮肤运行Rainmeter，Rainmeter会自动加载软件自带的皮肤，就是四个黑不溜丢的皮肤。一般人是不会喜欢这四个皮肤的，只要在这四个黑不溜丢的皮肤上点击鼠标右键。找到“关闭皮肤”、逐一关掉即可。
5.3. 皮肤分享以下是郝同学当年搜集的一些rainmeter皮肤，分享给大家：http://yunpan.cn/cKDh9Hcmuy6qK  访问密码 2bba
5.4. 步骤三：使用皮肤假设大家已经下载好了自己喜欢的皮肤，这时我们就可以使用皮肤了。1、假设你的系统用户名是Administrator，那么打开C:\Users\Administrator\Documents\Rainmeter\Skins，把下载好的皮肤（包括文件夹）复制进这个文件夹。我们假设你们下载了郝同学分享的皮肤，那么，就把“我的rainmeter”文件夹复制到Skins文件夹下。
2、打开rainmeter，就可以看到刚才复制到Skins的皮肤，如果没有，请刷新。
3、双击那些“*.ini”文件，皮肤就会出现在桌面上，再次双击，消失。
4、选中一个“*.ini”文件，编辑，自定义皮肤的各项属性。我们以“线条0.INI”为例，修改一下名称和路径，就可以成为适用于自己电脑的快捷方式。
5.5. 进阶今天我们主要讨论的，只是rainmeter的使用。如果你想深入学习，比如皮肤的制作等，就需要系统的学习了。这里分享一本《rainmeter中文手册第三版》：http://yunpan.cn/cKzsFRKGVKDcX  访问密码 b570
6. 小结美无止境，更多方法和工具，等待我们去发现。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>word排版高级进阶</title>
    <url>/hobby-word-senior/</url>
    <content><![CDATA[1. 前言在公司实习的时候，写过需求分析文档、概要设计文档、详细设计文档。前些天，帮助小伙伴整理了一下毕业设计论文。而这些长篇文档，都需要用到一些高级的word技巧。下面，我们以毕业设计论文为例，一起来学习一下。郝同学使用的WPS，Office基本相同。本文为图文教程，加载会比较慢，请小伙伴稍等一会。
2. 要求
目录的内容和编排目录只需编排到二级标题，三级标题及以下内容可不编入目录目录题头（格式：三号黑体居中，字间空一格）目录内容（格式：四号宋体，行距24磅）

摘要（1）中文题目：设计（论文）题目（格式：小二号黑体居中）副标题（格式：小三号黑体）题目可以分为1或2行居中打印（2）中文摘要和中文关键词：摘要题头（格式：三号黑体居中，字间空一格）摘要内容（格式：小四号楷体，1.5倍行距）关键词（格式：四号黑体）关键词内容（格式：小四号楷体）摘要题头与中文题目之间空一行，关键词与摘要内容之间空一行。（3）英文题目：设计（论文）英文题目（格式：小二号新罗马体（加粗）居中）副标题（格式：三号新罗马体加粗）题目可以分为1或2行居中打印。（4）英文摘要和英文关键词：英文摘要题头—“Abstract”（格式：三号新罗马体（加粗）居中）英文摘要内容（格式：小四号新罗马体， 1.5倍行距）英文关键词—“Key words”（格式：四号新罗马体加粗）英文关键词内容（格式：小四号新罗马体，1.5倍行距）英文摘要题头与英文题目之间空一行，英文关键词与英文摘要内容之间空一行。（5）中、英文摘要分为两页。

正文字体（1）正文：打印中文用宋体小四号字，英文用新罗马体12号字，行距为固定值20磅，首行缩进。版面上空2.5cm，下空2cm，左空2.5cm，右空2cm。（2）一级标题（章，如“1”，也可用“一”）（格式：三号黑体居中）（3）二级标题（节，如“1.1”，也可用“（一）”）（格式：小三号黑体居左）（4）三级标题（小节，如“1.1.1”，也可用“1”）（格式：四号黑体居左）（5）四级标题（节内小节，如“1.1.1.1”，也可用“（1）”）（注：小四号宋体居左）建议标题最好不要超过三级，否则适得其反，会引起格式混乱。

页眉和页脚（1）页眉页眉采用下列形式：


（2）页脚目录和摘要等内容的页脚为居中、连续的大写罗马数字页码（Ⅰ、Ⅱ、Ⅲ、Ⅳ ……）。正文及其以后部分，其页脚为居中、连续的阿拉伯数字页码（1、2、3 ……）。不宜采用分章的非连续页码。（3）分章节显示页眉页脚的方法，见《毕业设计（论文）撰写规范模板》。

图、表、公式、计量单位和数字用法的规定（1）图：每幅图应有“图序”和“图题”，“图序”和“图题”应放在图位下方居中处。插图一般按全文编排，如图1、图2……，插图较多时可按章排序，如图1.1、图1.2……。“图题”、“图号”字体用小五号黑体。（2）表格：每个表格应有自己的“表序”和“表题”，“表序”和“表题”应写在表格上方居中排放，表序后空一格书写“表题”。“表序”采用阿拉伯数字编排序号，如表1，表2等，表格较多时可按章排序，如表1.1，表2.3等。表格允许下页续写，续写表题可省略，但“表头”应重复写，并在右上方写“续表××”。表内同一栏的数字必须上下对齐。表内必须按规定的符号注明单位。“表序”、“表题”字体用小五号黑体。（3）公式：公式书写应在文中另起一行，居中书写，公式的编号用圆括号括起放在公式右边行末，按章顺序编排，公式与编号之间不加虚线。（4）计量单位和数字：毕业设计（论文 ）中的量和单位必须符合中华人民共和国的国家标准GB3100～GB3102-93。毕业设计（论文 ）中的测量、统计数据一律用阿拉伯数字；在叙述中，一般不宜用阿拉伯数字。

注释注释是对论著中某一特定内容所作的补充说明或进一步解释。注释一般用页末注即将注文放于加注页下端，不可用中注（夹在正文中注）。注释只限于写在注释符号出现的同页。注释格式见下例。例如：



参考文献参考文献应按文中引用的先后顺序，以阿拉伯数字连续编号，在正文引用的相应位置右上角，用[ ]加序号标出。参考文献的有关信息置于文末。参考文献文末著录格式为：专著: [序号]作者.书名[M].出版地:出版者，出版年.译著: [序号]国名或地区（加圆括弧）原作者.书名[M].译者.出版地:出版社,出版年.期刊文章: [序号]作者.篇名[J].期刊名,出版年,卷（期）.报纸文章: [序号]作者.篇名[N].报纸名,年-月-日（版次）.论文集: [序号]作者.篇名[A].编著者.论文集名[C].出版地:出版者,出版年.学位论文: [序号]作者.题名[D].保存地:保存单位,年份.国际、国家标准: [序号]标准编号-发布年,标准名称[S].电子文献：[序号]作者.电子文献名.[电子文献及载体类型标识[电子文献及载体类型说明：
电子文献类型及其标识：数据库—DB，计算机程序—CP，电子公告—EB
电子文献载体类型及其标识：磁带(magnetic tape)—MT，磁盘(disk)—DK，光盘(CD-ROM)—CD，联机网络(online)—OL
举例：[DB/OL]—联机网上数据库(database online)[DB/MT]—磁带数据库(database on magnetic tape)[M/CD]—光盘图书(monograph on CD-ROM)[CP/DK]—磁盘软件(computer program on disk)[J/OL]—网上期刊(serial online)[EB/OL]—网上电子公告(electronic bulletin board online)]].电子文献出处或可获得地址.

3. 效果图有图有真相，先上几幅图，给大家看看最终的效果。
4. 样式样式，其实就是模板。郝同学认为，样式是word排版最重要的一个技巧。学会样式，天下无敌！通过上面的几幅图，我们看到，文档中出现了三级标题，标题前面有编号。“1”、“1.1”、“1.1.1”等等，这些都是自己敲？别傻了！一个个敲，一个个设置，崩溃是早晚的。。。这时候，就需要样式出马了！而且，设置好了样式，待会我们可以直接生成目录。
4.1. 步骤一：样式设置的位置打开word文档，开始，看右上角。如下图：
4.2. 步骤二：编号设置1、右击“标题1”，单击“修改样式”2、单击“样式”，单击“编号”3、单击“多级编号”，发现，没有自己需要的编号样式。这时，选中一个接近目标样式的样式，单击“自定义”。4、初始样式如下。5、把编号设置后面的点去掉，编号之后接空格。设置好1级编号样式之后，我们选中2级编号样式，设置类似。之后设置3级编号样式。6、3级编号样式都设置好了，如图。
4.3. 步骤三：格式设置什么黑体，居中啥的，修改样式即可。
4.4. 步骤四：使用通过前三个步骤，我们已经设置好了样式。使用的时候，只需要把光标移动到需要设置样式的那一行，然后单击样式即可。
5. 页眉页脚有了页眉页脚的文档，看起来上档次。
5.1. 步骤一：插入页眉页脚章节，页眉和页脚，然后就可以给页面插入页眉和页脚了。
5.2. 步骤二：插入分节符1、找到页眉或者页脚需要不同样式的页面，光标移动到页面尾部。2、章节，拆分章节，下一页分节符。（或者：插入，分隔符，下一页分节符）3、选择是否取消各节之间的关联，就可以按自己需要设置各节页眉和页脚以及页码等格式。双击页眉或者页脚，单击“页眉页脚选项”，去掉和前节相关联的勾。
5.3. 步骤三：设置给不同的节分别设置页眉和页脚。在这个例子中，页眉输入文字，页脚插入页码。
6. 目录没有目录的文档不是好文档！
6.1. 步骤一：插入目录在封面页后面，插入一个空白页。引用，插入目录，根据需要设置目录选项。
6.2. 步骤二：目录级别由步骤一，我们看出，目录是自动生成的。但是，我们发现，只有那些分级标题可以生成目录，像摘要、引言等并不会出现在目录中。怎么搞？
在引用页，插入目录的旁边，有一个目录级别选项。如果我们想把摘要添加到目录，只需要把光标移动到摘要这一行，然后单击“目录级别”，选中“1级目录（1）”，就可以了。
6.3. 步骤三：更新目录步骤二完成后，目录不会变化。这时，我们点击“更新目录”，“更新整个目录”，“摘要”就出现在了目录里。虽然最理想的步骤是：步骤二、步骤一，不用步骤三。但是通常，我们都会用到步骤三。
7. 小结不要问郝同学，黑体怎么设置？居中怎么设置？字体大小怎么设置？。。。这些简单的东西，默认大家已经掌握。加上上面三个技巧，相信你距离高手更近了一步。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>word</tag>
      </tags>
  </entry>
  <entry>
    <title>安装系统之双系统</title>
    <url>/hobby-install-os-double-os/</url>
    <content><![CDATA[1. 前言安装双系统，和安装单一系统类似。有时候会失败，如果实在没办法了，就安装虚拟机吧，也是个不错的选择。
2. windows下安装windowswindows下安装windows，前提，要有两个以上主分区，而且都是活动的。安装成功后，在当前系统下。单击计算机，右击，属性，高级系统设置，高级，启动和故障恢复下的设置。设置默认操作系统和操作系统列表显示时间。
3. windows下安装linux在硬盘上划分出一块空白区域，不要格式化。然后，从启动盘开始安装linux，安装到那一块空白区域。
4. windows下安装mac这个，有难度。尤其是非Intel处理器，基本都会失败。Intel处理器的小伙伴，可以搞一搞。话说郝同学前前后后花费了一周左右，也没有安装成功，惭愧惭愧，继续努力。。。
5. linux下安装linux关键在于启动配置文件的修改。
6. linux下安装windows挂载、分区、引导等等，总之很复杂。建议使用wine，模拟windows环境，然后就可以一步步往下安装了。
7. Mac下安装windowsmac下安装windows比较简单，看来苹果公司非常人性化。前提，准备好纯净系统镜像。以下内容摘自百度经验，亲测可用。
7.1. 步骤一：进入苹果系统，左上角，前往，实用工具，BootCamp 助理，继续

7.2. 步骤二：1.选第一项，创建win7或更高版本的安装磁盘，这是用苹果自带工具做U盘启动。需要把win7 64位纯净版ISO镜像文件放在桌面，插入U盘（最好是2.0的U盘）大概需要15分钟左右（不要以为卡死了，等着就好，一次没有做好的话重新做就可以了，做U盘启动会格式化U盘，注意备份）U盘启动做好之后选第二项
2.第二项，从Apple下载最新的windows支持软件此为苹果电脑的win7驱动，根据网速大概20分钟，别着急，注意路径（装好win7后要安装此驱动，可以下到U盘上）；（注意：13款的air装win7必须把驱动下载到做U盘启动的U盘内，否则进入win7后触摸板和鼠标将不能使用，或者使用这个方法，见链接教程：http://www.macx.cn/thread-2098963-1-1.html）驱动下载好之后选第三项
3.第三项，安装win7或者更高版本，下一步；
4.划分win7系统和mac系统空间大小，可以均等分割，也可以根据自己的使用习惯分；
5.点击安装，系统会分区，然后系统会自动重启，进入win7安装界面，根据提示操作（如果提示：请使用64位安装系统、或者重启后显示no bootable device–insert boot disk and press any key，说明可能刚才做的U盘启动不识别，重新做一次，如果还是不行，换U盘重新做）
7.3. 步骤三：选择自定义——选择分区BOOTCAMP（千万选对了，千万别把mac系统给格式化了）——驱动器选项 高级——格式化——确定
然后就开始装系统了，大概需要30分钟，之间会重启多次，然后就进入系统了，win7就装好了
7.4. 步骤四：安装win7驱动，在win7系统下找到刚才的下载的驱动（应该是个文件夹BootCamp），打开，有个setup.exe程序，双击根据提示安装就可以了，大概5分钟
8. 小结上面已经把安装双系统的重点点明了，搞不定的话，百度更详细的教程吧。这次，郝同学就不展开了，内容实在太多。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>安装系统之不同平台</title>
    <url>/hobby-install-os-various-platform/</url>
    <content><![CDATA[1. 前言Windows、Linux、MacOS、手机rom，不同的平台，安装方式有一些差别。
2. Windows这几篇关于安装系统的短文，大都是关于windows的。
3. LinuxLinux的版本非常多，主流的版本，郝同学都试用过。个人比较喜欢的有CentOS、Ubuntu、Fedora。很多教程喜欢用RetHat，郝同学想说，作为一个商业版linux，RedHat的各种限制会让你吐血。。。如果想学习linux，从CentOS入门吧！至于教程，力荐《鸟哥的linux私房菜》。
3.1. 镜像下载不同版本的linux镜像，在各自的官网上就可以下载到。也是以“.iso”结尾的文件。
3.2. 硬盘安装使用频率较低，如果你现在使用的系统是linux，想要安装双系统或者安装新的linux，可以从硬盘安装。
3.3. U盘安装从U盘安装，是linux最主流的安装方法。这个方法的重点在于启动盘的制作，给出一个软件，不要告诉别人。秘密入口：Universal USB Installer使用这个软件，把下载的linux镜像刻录到U盘，就做成了启动盘。
3.4. 光盘安装从光盘安装，是成功率最高的linux安装方法。用UltraISO或其他软件，把下载的linux镜像刻录到光盘，就做成了启动盘。
4. MacOS郝同学只在虚拟机上玩过Mac。
当时，网上很难找到“*.iso”的mac镜像，能找到的，都是“*.dmg”，这时候，重点来了！把dmg格式的镜像转换成iso镜像！使用的软件，还是UltraISO！怎么样？发现这个软件的强大了吧！

5. 手机rom我们经常可以听到“刷机”这个词，其实刷机，就是给手机重装系统。电脑安装系统，我们使用镜像；手机安装系统，我们使用rom。
问题来了，rom到底是个啥玩意？它的全称是啥？
ROM是只读内存（Read-Only Memory）的简称，是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除。通常用在不需经常变更资料的电子或电脑系统中，资料并且不会因为电源关闭而消失。
手机中，ROM结构是由：系统空间+用户安装程序空间+用户存储空间。也就是说，手机系统存储在rom当中，刷rom，就是改变rom中的系统。而很多人把待刷系统的安装包称为rom，算是一个词义的延伸，相当于镜像。
刷机比重装系统危险，因为手机有可能会变砖！虽然网上有很多救砖教程，但是，考虑到手机型号多如繁星，真的不敢保证你的手机一定能救回来。
刷机方法很多，这里郝同学写一个通用的方法：卡刷！1、下载需要的rom，一般是*.zip文件。2、拷贝rom到手机外置内存卡。3、开机时，按住开机键+音量减/开机键+音量加/开机键+音量减+音量加，进入recovery模式。4、选择rom，开始刷机。PS：刷第三方的rom的时候，大都需要先刷recovery。
6. 小结不同平台，安装系统有差别，常用的也都是那三种方式。所谓见微知著，举一反三，一通百通。。。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>安装系统之激活</title>
    <url>/hobby-install-os-active/</url>
    <content><![CDATA[1. 前言纯净系统安装后肯定需要激活，除非是升级系统；ghost系统安装后，一般不需要激活，个别情况下需要激活。
2. winXP现在，安装winXP的小伙伴已经极少极少。考虑到使用虚拟机的情况，还是分享一下激活密钥和激活工具吧。
正版密钥：MRX3F-47B9T-2487J-KWKMF-RPWBYHTXH6-2JJC4-CDB6C-X38B4-C3GF3 RT4H2-8WYHG-QKK6K-WWHJ2-9427X DYPVX-43TRT-YDBGB-7YQJX-CWXW7 HGM7B-YF7T7-8R7RF-Y6RPY-XTQ77 Windows XP专业版最新注册码 DJQJB-PC83T-FTGJC-CQTCK-RJD8D VMMBM-8WK8W-H44YH-37B4M-KX8QR XRCTF-Y68KJ-VVFTR-7BDFP-4PW7G 6RV7B-FYWR2-PW3C6-DDWDR-68X9C KYMTD-BV7KP-RRM33-P3XKJ-RDKVD X3WYK-H7CR8-KQBMV-7DP6X-W6YQQ CQWK3-CCYJY-TQDFV-2HJDR-W3B2M 2RXYJ-VQWXM-J2V2R-CVXQT-Y6MPY X7TVH-VJTFG-BK22B-XXG6D-27326 VYGXV-YM8VB-4RVQX-QXBMX-G3WV7 4DP2D-CXW4C-TRYDH-CW4CT-PT23X R6M6K-HT7G7-XG4K4-66PGK-9V2RM 86VYW-4RHCG-CCC7Y-64MWM-V8B68 VPM77-Y3YJW-W4MFC-CQTCK-D2XGK K3JD6-DK6G4-YH32B-QT7VP-R8WC7 JRMCK-J3V37-YVCYH-MDJ37-94BHP KJ3XK-3B6KW-XK62M-VDC7W-DJ6V9 WindowsXP可以无限次激活的号码 CXGDD-GP2B2-RKWWD-HG3HY-VDJ7J RK7J8-2PGYQ-P47VV-V6PMB-F6XPQ
激活工具分享：http://yunpan.cn/cKvVDSmX7xBJU  提取码 788e


3. win7下面这个工具，是郝同学用过的最好用的win7激活工具，屡试不爽！激活工具分享：http://yunpan.cn/cKvjwPMn4NCDA  提取码 a281
4. win8win8是今后的趋势系统，下面这个工具，是郝同学认为最好用的win8/win8.1激活工具！激活工具分享：http://yunpan.cn/cKvxrq86aFTkX  提取码 8b0b
5. 小结激活方法不止一种，以上提供的工具，都是郝同学亲测可用的。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>安装系统之纯净系统和ghost系统</title>
    <url>/hobby-install-os-pureos-and-ghostos/</url>
    <content><![CDATA[1. 前言微软的原生系统，被称为纯净系统；什么中关村、雨林木风、绿茶系统、新萝卜家园、深度技术等等，出品的系统，都是ghost系统。
2. 纯净系统纯净系统，里面没有QQ、迅雷、酷狗等软件。安装完成之后，桌面上只有一个图标——回收站。纯净系统的安装，和ghost系统类似，也有三种方式。
2.1. 硬盘安装下载纯净系统镜像，解压，双击setup.exe，按照提示安装。
2.2. U盘安装在winpe下双击setup.exe，按照提示安装。

2.3. 光盘安装从光盘启动后，按照提示安装。
3. ghost系统ghost系统，里面有很多自带的软件。网上给的教程，基本上都是ghost系统的安装教程。郝同学也写了一篇，请移步http://voidking.com/【专业】安装系统之三种方式/
4. 小结如果没有特殊需求，建议安装ghost系统，简单方便。郝同学也制作过ghost系统，制作方法很简单。但是，如果想要制作出想雨林木风那样的系统镜像，还是有难度的。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>安装系统之驱动</title>
    <url>/hobby-install-os-drive/</url>
    <content><![CDATA[1. 基本概念设备驱动程序，是一种可以使计算机和设备通信的特殊程序，是硬件厂商根据操作系统编写的配置文件。相当于硬件的接口，操作系统只有通过这个接口，才能控制硬件设备的工作，假如某设备的驱动程序未能正确安装，便不能正常工作。
因此，驱动程序被比作“ 硬件的灵魂”、“硬件的主宰”、和“硬件和系统之间的桥梁”等。
2. 常见驱动问题2.1. 界面变丑还记得第一次安装完系统，开机之后，我靠，那些又大又模糊的图标是怎么回事？！分辨率无法调节是怎么回事？！后来终于懂得，显卡驱动没装好。
2.2. 无法使用网线联网网卡驱动没有安装好。
2.3. 无法使用无线联网无线网卡驱动没有安装好。

3. 驱动安装工具windows和linux的驱动安装方法不同，linux更加复杂。以下工具适用于windows。
3.1. 系统自带单击计算机，右击，管理，设备管理器。
3.2. 驱动精灵感觉最顺手的一个驱动管理软件。
3.3. 驱动人生也是很好用的一个驱动管理软件。
4. 小结在学习汇编的时候，曾经想要开发驱动程序，后来发现，太难了！！！驱动，会安装就够了！
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>安装系统之分区</title>
    <url>/hobby-install-os-partition/</url>
    <content><![CDATA[初学者很容易把重新分区和重装系统搞混，这里简要说明一下。大部分内容摘自百度百科。
1. 基本概念MBR（Master Boot Record）下的硬盘分区有三种，主分区、扩展分区、逻辑分区。
一个硬盘，主分区至少有1个，最多4个，扩展分区可以没有，最多1个。且主分区+扩展分区总共不能超过4个。
扩展分区不能直接使用，扩展分区可以分为若干个逻辑分区，最多分为23个逻辑分区。
逻辑分区的信息都被保存在扩展分区内，而主分区和扩展分区的信息被保存在硬盘的MBR内。
在windows下，激活的主分区是硬盘的启动分区，他是独立的，也是硬盘的第一个分区，正常分的话就是C区。 
在linux下，主分区和逻辑分区都可以用来放系统，引导os开机，grub会兼容windows系统开机启动。在linux中，第一块硬盘分区为hda分区，主分区编号为hda1-4，逻辑分区从5开始。

2. 分区格式2.1. windowsFAT16、FAT32、NTFS、exFAT等。应用最广泛的是NTFS，我们使用的电脑分区，基本上都是这个格式的。FAT32格式也很常用，常用于做启动盘，但是单个文件大小不能超过4G。
安装linux的时候，前些年，必须从FAT32分区或者linux专用分区读取安装数据，否则无法识别。4G的限制，导致很多linux安装起来很费劲。近两年，不少linux版本，已经可以支持读取NTFS分区上的数据，更加人性化了。
2.2. linuxExt2、Ext3、Linux swap、VFAT、Ext4等。最开始搞linux的时候，每种类型的分区应该分多大，对系统运行有很大的影响，不能马虎。现在好了，安装linux的时候，系统会给出建议分区大小，基本不需要修改，更加平民化。
3. 初始分区新买的电脑，大都有自带的系统，常见的有win8和linux。这些电脑，都是分好区的，但是，分区的方式不一定是自己满意的。
比如，win8下，你想要D盘200G，但是，自带系统分了100G。这时，可以在当前系统下修改分区，也可以进去winpe之后修改分区。
比如，linux下，你想改成win8系统。这时，可以通过winpe重新分区，然后安装win8系统。

4. 分区方式分区分为两种：有损分区和无损分区。
4.1. 有损分区我们常说的分区，一般指的是有损分区。有损分区会清除掉硬盘上所有的数据，所以，分区之前，请备份好数据。
4.2. 无损分区无损分区指的是，在分区过程中不会损失数据的分区方式。假设，我们想把F盘分成两个盘：F盘和Z盘。这时候，可以把数据集中在F盘，空闲的部分分出来，成为Z盘。
5. 分区工具以下工具适用于windows。linux下，需要分区时查找一下分区命令就好。
5.1. 系统自带分区工具单击计算机，右击，管理，磁盘管理。这个工具支持无损分区。
5.2. DiskGenius郝同学最喜欢的一款分区工具，非常强大。专业版支持硬盘格式转换，牛逼的掉渣。最适合在winpe下使用。
5.3. 无损分区助手顾名思义，这款软件软件适用于无损分区。
6. 小结分区，很有学问，细节上的东西，郝同学不再展开，感兴趣的小伙伴自行百度。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>安装系统之三种方式</title>
    <url>/hobby-install-os-three-method/</url>
    <content><![CDATA[1. 前言安装系统的教程有很多，最常用三种方式：硬盘安装，U盘安装，光盘安装。
2. 硬盘安装从硬盘安装系统，是最简单的一种方式。这种方式，前提是原系统可以正常开机。
2.1. 步骤一：下载镜像我们以安装win8为例。1、打开浏览器，然后在地址栏输入一个搜索引擎网址，比如百度。2、在搜索框输入“win8镜像”，单击第一个搜索结果。3、选择一个镜像，进入下载页4、找到下载链接，下载镜像，大小一般在2g到4g，文件名称为*.iso。5、解压下载好的镜像。

2.2. 步骤二：安装系统1、双击autorun.exe，选择打开ghost安装器。2、在ghost安装器界面，选中需要安装系统的盘符，以及刚才解压出来的win8.gho映像。单击“执行”，安装系统就开始了。3、系统重启几次后，系统安装完成。这个过程，10到30分钟。
3. U盘安装从U盘安装系统，是最常用的一种方式。如果你的系统彻底崩溃了，或者你想分区后再重装系统，这种方式是很好的选择。
3.1. 步骤一：下载镜像这个步骤的操作方法和硬盘安装步骤一相同。
3.2. 步骤二：制作启动盘1、将U盘插入USB接口。2、下载启动盘制作工具，我们以U大师为例。进入U大师官网，然后下载U大师。
3、安装U大师，并启动。4、制作启动盘。
3.3. 步骤三：拷贝映像步骤一中，我们解压出的文件中有一个win8.gho，复制win8.gho映像到U盘。
3.4. 步骤四：从U盘启动1、将做好的启动盘插入USB接口。2、点完开机键，狂按F12（一般的品牌机，选择启动项的键都是F12）。出现启动项选择界面，一般可供选择的有光驱、硬盘、网络、可移动磁盘（U盘）。这里我们选择从U盘启动。3、如果按F12没有出现启动项选择界面，请参考下列启动按键。4、如果还是没有出现启动项选择界面，或者出现了启动项选择界面，但是没有从U盘启动这个选项。这时，就需要设置BIOS。不同机器的BIOS差别较大，不展开讲了，给出两个参考，分别来自百度经验和U大师。
3.5. 步骤五：进入winpe1、假定步骤四我们已经成功完成，这时候，我们会看到下面的界面。
2、有时间的话，这上面的选项你可以挨个试试。这里我们选择【01】进入。
3.6. 步骤六：安装系统1、winpe系统会自动弹出一个软件——U大师智能快速装机。安装源我们选择U盘里的win8.gho。
2、单击上图中的开始，安装系统开始。
3、系统重启几次后，系统安装完成。这个过程，10到30分钟。
4. 光盘安装从光盘安装系统，是最传统的一种方式。这种方式的前提是，你的电脑要有光驱。
如果，你有系统盘（无论是自己做的，还是从商店买的），那么直接插入光驱，然后从步骤三开始往下看。我们这里假设你没有系统盘，有一张4g的空白光盘。
4.1. 步骤一：下载镜像这个步骤的操作方法和硬盘安装步骤一相同，需要注意的是，这里不需要解压。
4.2. 步骤二：制作系统盘1、将空白盘插入光驱。2、下载安装光盘刻录软件，这里郝同学使用的是UltraISO。3、打开UltraISO，将下载的镜像文件*.iso刻录到空白光盘。4、刻录完成后，系统盘也就制作完成了。
4.3. 步骤三：从光盘启动1、将系统盘插入光驱。2、点完开机键，狂按F12。出现启动项选择界面，我们选择从CD-ROM启动。
4.4. 步骤四：安装系统选择“安装win8系统到C盘”，开始安装系统。系统自动重启几次，安装完成。在这个界面，我们也可以进入winpe，然后分区或者安装系统。
5. 小结所谓的不同方式，说白了，不过是安装源的存储介质的区别。
还有其他的安装方式，下面再给出两个：1、移动硬盘这种安装方式和从U盘安装类似，甚至，你可以把系统安装在移动硬盘，随身携带。
2、网络在安装linux的时候，可以通过网络获取最新版。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>安装系统之概述</title>
    <url>/hobby-install-os-start/</url>
    <content><![CDATA[1. 前言12年，开始接触安装系统。三个年头了，安装的系统，数量过百是肯定的。遇到的各种奇葩问题，数不胜数。winXP，win7，win8，各种版本的Linux，MacOS，安卓手机第三方rom……都研究过一段时间。今天，简单总结一下安装系统的方法，希望对想学习的小伙伴有点帮助。


2. 操作系统理解
3. 三种方式安装系统的教程有很多，最常用的有三种方式：硬盘安装，U盘安装，光盘安装。更多内容参考安装系统之三种方式
4. 分区初学者很容易把重新分区和重装系统搞混，实际上，它们是相关但完全不同的两件事。更多内容参考安装系统之分区
5. 驱动驱动是连接操作系统和硬件的程序。驱动安装不好，计算机会出现各种问题。更多内容参考安装系统之驱动
6. 纯净系统和ghost系统微软的原生系统，被称为纯净系统；什么中关村、雨林木风、绿茶系统、新萝卜家园、深度技术等等，出品的系统，都是ghost系统。更多内容参考安装系统之纯净系统和ghost系统
7. 激活纯净系统安装后肯定需要激活，除非是升级系统；ghost系统安装后，一般不需要激活。更多内容参考安装系统之激活
8. 不同平台Windows、Linux、MacOS、手机rom，不同的平台，安装方式有一些差别。更多内容参考安装系统之不同平台
9. 双系统安装双系统，和安装单一系统类似。更多内容参考安装系统之双系统
10. 常见问题安装系统，难得一帆风顺。遇见问题，解决问题！更多内容参考安装系统之常见问题
11. 结束语几篇短文，不可能面面俱到，有任何问题，欢迎给郝同学留言。
如果想换系统了，那么，备份文件，然后大胆地去折腾吧！重装系统后，也许，你的电脑无法开机了；或者，你的电脑没有声音了；或者，你的电脑无法联网了；或者，你的电脑分区变奇怪了……也许，你的手机无法拍照了；或者，你的手机无法打电话了；或者，你的手机会自动关机……这都没什么，郝同学遇到过更多奇葩的问题，有些几乎无法用语言描述！解决这些问题的过程，你会学到的东西不止可以解决这些问题。
不下水，是永远无法学会游泳的。也许会被呛到，也许会喝几口池水，也许会被人笑话……没关系，最后，我们获得的技能必然是牛逼的，一切都是值得的！
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>Axure入门篇</title>
    <url>/dev-axure-start/</url>
    <content><![CDATA[1. 前言寒假，打算做一个自己使用的安卓app。那就学习安卓吧？这个是必须的，但是首先，我要明确需求。说的通俗一点，我要先明确一下，完成后的产品大概是什么样子。
用软件工程的思想来解释，就是使用原型开发方法。利用快速开发工具构建一个系统原型，以便分析用户需求和界面需求。
而Axure，就是一个快速开发工具。一周左右，利用Axure完成了自己的原型设计，在此总结一下。
2. Axure入门这款软件的界面如下：
2.1. 菜单栏和工具栏不多解释，会使用word，这两栏一看就懂。


2.2. 页面导航页面的目录，方便我们找到页面。
2.3. 工作区每个页面对应一个工作区，我们在工作区进行设计。
2.4. 组件组件是我们进行设计的“原料”。和做菜一样，各种原料经过处理成为一道菜，各种组件经过组合成为一个页面。
2.5. 母版假设每个页面都有一个logo，上面写着“voidking”。等到完成之后，突然觉得不合适，想改成“帅哥”，这时候工作量太大了！
那么怎么避免这个问题？很简单，把这个logo作为母版！只要修改了母版，那么，所有使用了这个母版的页面，logo内容都会被修改。
2.6. 页面交互举个例子，我们想要某个页面一闪而过。也就是说，不需要用户对这个页面进行任何操作，就跳转到下一个页面。这时候，就需要在页面交互这里进行设置。
2.7. 组件交互举个例子，我们想要通过单击操作跳转到另一个页面。这个时候，就需要在组件交互这里这里进行设置。
3. Axure实践3.1. 细致程度细致程度分为草图、低保真原型、高保真原型。这次原型设计，只是做了一个低保真原型。
3.2. 额外技能首先，你要学会PS。否则，你会发现，寸步难行！其次，你要学会寻找资源。无论是安卓组件，还是需要的图片素材，都是需要寻找的。所以，学会使用搜索引擎，受益一生。最后，你要学会学习。在做原型的过程中，肯定有某些功能，你不知道如何实现，这时候，就需要你去学习。
3.3. 部分成果不要认为自己不会，大胆去使用吧，用不了几个小时，你就会掌握这款工具。具体操作过程省略，给大家展示几张图片。
4. Axure高级应用学会使用Axure很简单，但是，要实现某些细节的效果（高保真原型），就需要高级的技巧了。比如完全模拟iPhone发送短信的过程，比如完全模拟微信聊天的过程等等。高级的内容，还没有学习。给大家推荐几个学习教程：网易云课堂Axure教程系列多贝公开课Axure教程系列《Axure快速原型设计》，提取码 f77c
5. 小结类似Axure这样的工具有很多，重要的不是学会使用Axure，而是掌握设计的思路。
]]></content>
      <categories>
        <category>engineering</category>
        <category>android</category>
      </categories>
      <tags>
        <tag>android</tag>
        <tag>axure</tag>
      </tags>
  </entry>
  <entry>
    <title>搭建个人网站之概述</title>
    <url>/dev-build-personal-website-start/</url>
    <content><![CDATA[1. 搭建个人网站之概述每个学计算机的小伙伴，都想过搭建个人网站吧？毕竟，这是一件拉风的事情。也有几个学弟向我请教过，一个个教，很麻烦。在此，写一篇短文，给后来者扫平一些障碍。
不喜欢使用各种拗口的术语，尽量举例、类比、比喻、画图，说的不够专业的地方，小伙伴们见谅！
1.1. 域名voidking.com、baidu.com、qq.com、jd.com、csdn.net等等，这些，就是所谓的域名（二级域名）。在浏览器地址栏，输入这些域名，就能访问到相应的网站。简单介绍一下原理：访问请求先被发送到DNS服务器，然后这些域名会被解析成IP地址，实际访问的，就是IP地址。


没有域名可不可以做网站？可以！但是，访问的时候就麻烦了。一些服务器，会提供给你三级域名，比如voidking.gitcafe.com；而另外一些，只能通过IP地址访问，非常不人道！所以，想做网站的小伙伴，先注册个域名吧，这笔投资不要省！也许若干年后，你的域名会被十倍百倍的价格收购，或者成为一个品牌！
注册域名的网站很多，国内，最欣赏万网；国外，最欣赏godaddy。这两个网站，都使用过，相较而言，还是万网更适合国内用户。
前些年，万网的问题较多，我对它也存在一些质疑。但是，现在，万网在阿里旗下，无论是其访问速度、安全性、解析速度、迁移、客服等方面，都做的非常好。
godaddy的口碑非常好，很多前辈都推荐。但是个人体验发现，godaddy的访问速度和万网相比就是个渣，这让国内的我难以忍受。另外，域名解析界面真心难找，解析速度也没有万网快。值得夸奖的是，客服很有水平。还记得，曾经遇到域名消失问题，在我的poor English描述下，客服帮助我完美解决了问题！
总而言之，在哪注册域名都可以，个人推荐万网。
1.2. 服务器没有域名，别人可以访问到你的网站，但是没有服务器，就真的不行了！除非你打算只是局域网使用！服务器的用途，除了部署你的网站外，还可以部署很多其他服务。
使用过很多服务器，简单描述一下它们！
1.2.1. 免费服务器BAE、CAE、GAE、JAE、SAE等等，之前都是免费的，现在，只有GAE、JAE、SAE还算得上免费。他们的用法比较死板，基本都是建立某个类型的应用，然后把代码部署上去。重点在于免费，或者免费一段时间，对于学生党，还是值得花时间研究一下的。
此外，openshift空间也不错，但是国内的访问速度，实在是无法忍受！曾经搭建过一个wordpress项目，那速度。。。还是先去泡杯茶吧。。。
还有heroku、appfog、mongolab等等。
1.2.2. 收费服务器收费的服务器，那就多了去了。推荐阿里云，使用中，各项性能都很满意。各种集成系统镜像可供选择，安全性也很好。而且，备案服务做得非常好！突然感觉自己有做广告的嫌疑，阿里云没给我钱，我是不是吃亏了？
1.3. 备案为什么需要备案？证明你的网站是合法的！万一犯了事，方便抓捕。。。我不会告诉你，使用国外空间不要备案；我更不会告诉你，国内空间想要使用域名，必须备案！
不久前，万网备案系统和阿里云备案系统合一，备案要求也更加严格。曾经备案过两次，每次需要的时间，前前后后大概半个月。
1.4. 技术难度系数从高到低：全部自己做 &gt; hexo &gt; octopress &gt; wordpress。
1.4.1. 全部自己做自己做网站，可以选各种语言，后台Jave EE、JSP、ASP、PHP、Node.js等等，前台Html/XHtml、CSS、JavaScript、JQuery、AngularJs等等。自己做网站，适合计算机专业的小伙伴。
1.4.2. hexo这是一个静态化博客系统，也就是说，它的页面只需要html，不需要服务器的编译，不需要数据库等。现在你看到的这篇小文，就是使用hexo做的。准确的说，这并不是一个网站，只是一个个人主页。可以存储在github、gitcafe，或者任何可访问的空间。
1.4.3. octopress这也是一个静态化博客系统。并没有使用过这个博客系统，但是网上教程很多，口碑不错哦！也可以存储在任何可访问的空间。
1.4.4. wordpress非常强大的一个博客系统，主题非常多，总有一款适合你。甚至，可以做企业官网！
1.5. 小结这篇文章，没有展开论述每一个点，有不懂的地方，百度会告诉你答案。同时欢迎大家留言讨论，一定尽快回复！
1.6. 参考文档
2014年十大优秀免费空间排行
使用github + Octopress 搭建免费博客

]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>网站</tag>
        <tag>域名</tag>
        <tag>服务器</tag>
      </tags>
  </entry>
  <entry>
    <title>网页标签添加图标</title>
    <url>/dev-page-label-add-icon/</url>
    <content><![CDATA[1. 前言这个标题是啥意思？如下图，每个网站都有自己的标签图标：


2. 制作图标图标，值得你花费时间用心选择、设计。每当你看到那个蓝色的脚印，都会想起百度！我们难以超越百度，但是，我们也能拥有自己的“品牌”，哪怕没人知道，也很爽，不是吗？
2.1. 在线生成百度在线favicon生成，你会找到很多在线制作favicon图标的网站。上传一张图片，就可以自动生成你想要的favicon.ico图标，下载即可。
2.2. 软件生成制作favicon的软件有很多，这里推荐一款魔法ICO v2.00。
3. 使用方法假设我们已经得到了favicon图标，这时，应该怎样使用它呢？
把favicon.ico上传到服务器放在网站根目录下，然后在首页文件中段插入：
&lt;link rel=&quot;shortcut icon&quot; href=&quot;favicon.ico&quot;&gt;&lt;link rel=&quot;Bookmark&quot; href=&quot;favicon.ico&quot;&gt;

第一句用来正常显示图标，第二句用来添加书签时显示图标。

如果你希望出现动画效果的favicon图标，那就上传animated_favicon1.gif（动画图标）并且添加如下的HTML标签：
&lt;link rel=&quot;shortcut icon&quot; href=&quot;favicon.ico&quot; &gt;&lt;link rel=&quot;icon&quot; href=&quot;animated_favicon1.gif&quot; type=&quot;image/gif&quot; &gt;
4. 小结整个过程还是很简单，记录一下，希望对不熟悉的小伙伴有点帮助。
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>html</tag>
      </tags>
  </entry>
  <entry>
    <title>操作系统课设</title>
    <url>/dev-os-curriculum-design/</url>
    <content><![CDATA[1. 选题：文件管理系统每个同学，必须完成两个题目，以下是我的两个选题。
1、银行家算法的模拟掌握死锁相关的概念和解决方案，理解银行家算法的工作原理，设计合适的数据结构和算法，模拟实现银行家算法。
2、一个简单文件管理器的实现掌握操作系统关于文件管理的各种原理，熟悉常用的文件操作，编写程序实现文件的常规操作。
对于选题1，找到了前辈的作品，改一改，验收！当然，被老师吐槽一顿是难免的。
对于选题2，文件管理系统，这个就有意思了，借鉴了大神cattong 的思路，下面和大家讨论下。


2. 语言：Java编程三年，个人感觉，最友好的语言就是Java了。本次课设，毫无争议选择了Java。
3. 设计思路3.1. 界面很简洁的界面，上面输入命令，下面显示结果。废话不多说，上代码！
package com.voidking.file;import java.awt.BorderLayout;import java.awt.FlowLayout;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.JTextField;public class MainFrame extends JFrame &#123;	public static void main(String[] args) &#123;				MainFrame thisClass = new MainFrame();		thisClass.setVisible(true);	&#125;		public MainFrame() &#123;		super();		initialize();	&#125;		private void initialize() &#123;		this.setTitle(&quot;资源管理&quot;);		this.setSize(500, 400);		this.setLocationRelativeTo(null);		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);		contentPane = new JPanel();		contentPane.setLayout(new BorderLayout());		contentPane.add(getCmdPane(), BorderLayout.NORTH);		contentPane.add(getEchoArea(), BorderLayout.CENTER);		this.add(contentPane);		cmd = new Command(this);		display(&quot;C:&gt;&quot;);	&#125;	private static final long serialVersionUID = 1L;	private JPanel contentPane = null;	private JPanel cmdPane = null;	private JLabel jLabel = null;	private JTextField cmdText = null;	private JTextArea echoArea = null;	private JScrollPane areaScroll = null;	private Command cmd = null;	private JPanel getCmdPane() &#123;		if (cmdPane == null) &#123;			jLabel = new JLabel();			jLabel.setText(&quot;命 令 行:&quot;);			cmdPane = new JPanel();			cmdPane.setLayout(new FlowLayout());			cmdPane.add(jLabel, null);			cmdPane.add(getCmdText(), null);		&#125;		return cmdPane;	&#125;	private JTextField getCmdText() &#123;		if (cmdText == null) &#123;			cmdText = new JTextField();			cmdText.setColumns(25);			// 添加键盘监听事件;			cmdText.addKeyListener(new java.awt.event.KeyAdapter() &#123;				public void keyTyped(java.awt.event.KeyEvent e) &#123;				&#125;								public void keyPressed(java.awt.event.KeyEvent e) &#123;					if (e.getKeyCode() == 10) &#123;						// 对文本进行处理;						cmd.operate(cmdText.getText().trim());					&#125;				&#125;			&#125;);		&#125;		return cmdText;	&#125;	private JScrollPane getEchoArea() &#123;		if (echoArea == null) &#123;			echoArea = new JTextArea();			echoArea.setEditable(false);			areaScroll = new JScrollPane();			areaScroll.setViewportView(echoArea);		&#125;		return areaScroll;	&#125;	// 在回显框显示结果;	public void display(String str) &#123;		echoArea.append(str + &quot;\n&quot;);		echoArea.setCaretPosition(echoArea.getText().length());		cmdText.setText(&quot;&quot;);	&#125;&#125;
3.2. 命令处理在界面初始化函数initialize()中，我们创建了一个Command类的实例：cmd对象。然后，在getCmdText()函数中监听文本框中的内容，调用cmd对象的函数进行处理。
Command类，封装了所有的命令处理函数。结构简单明了，不多解释，直接上代码！
package com.voidking.file;import java.awt.Desktop;import java.io.File;public class Command &#123;	private String currentPath;	private MainFrame mainFrame = null;	private final String cmd[] = &#123; &quot;cd&quot;, &quot;dir&quot;, &quot;mkdir&quot;, &quot;rmdir&quot;, &quot;mkfile&quot;, &quot;rmfile&quot;,			&quot;exit&quot; ,&quot;mkpath&quot;,&quot;open&quot;&#125;;	private final int cmdInt[] = &#123; 1, 2, 3, 4, 5, 6, 7 ,8,9&#125;;	Command(MainFrame mainFrame) &#123;		currentPath = &quot;C:&quot;;		this.mainFrame = mainFrame;	&#125;	public String[] separate(String operation) &#123;		String[] str = operation.split(&quot; &quot;);		//主要解决文件夹或文件中含有空格的情况;		if(str.length&gt;2)&#123;			String[] tempStr=new String[2];			tempStr[0]=str[0];			tempStr[1]=str[1];			for(int i=2;i&lt;str.length;i++)				tempStr[1]+=&quot; &quot;+str[i];			return tempStr;		&#125;		return str;	&#125;	/*	 * 根据参数operation执行相应的操作;	 */	public void operate(String operation) &#123;		String[] str = separate(operation);		String mycmd = &quot;&quot;;		int mycmdInt = 0;		String path = &quot;&quot;;		if (str.length == 1) &#123;			mycmd = str[0];			//切换盘符			if (mycmd.indexOf(&quot;:&quot;) != -1) &#123;				File newFile = new File(mycmd);				if (newFile.exists()) &#123;					currentPath = mycmd;					mainFrame.display(getPath());					return;				&#125;			&#125;		&#125;		if (str.length &gt;= 2) &#123;			mycmd = str[0];			path = str[1];		&#125;		for (int i = 0; i &lt; cmd.length; i++) &#123;			if (mycmd.equalsIgnoreCase(cmd[i])) &#123;				mycmdInt = cmdInt[i];			&#125;		&#125;				switch (mycmdInt) &#123;		case 1:			cd(currentPath, path);			break;		case 2:			dir(currentPath);			break;		case 3:			mkdir(path);			break;		case 4:			rmdir(path);			break;		case 5:			mkfile(path);			break;		case 6:			rmfile(path);			break;		case 7:			exit();			break;		case 8:			mkpath(path);			break;		case 9:			open(path);			break;					default:			mainFrame.display(&quot;无效的命令!&quot;);		&#125;		mainFrame.display(getPath());	&#125;	/*	 * 获得当前所在目录;	 */	public String getPath() &#123;		return currentPath + &quot;&gt;&quot;;	&#125;	/*	 * 获得路径path下的文件;	 */	public String[] listAll(String path) &#123;				try &#123;			File f = new File(path);			String[] fileName;			if (f.isDirectory()) &#123;				fileName = f.list();				mainFrame.display(&quot;共有&quot; + fileName.length + &quot;个文件&quot;);				for (int i = 0; i &lt; fileName.length; i++)					mainFrame.display(&quot;    &quot; + fileName[i]);				return fileName;			&#125; else if (f.isFile()) &#123;				mainFrame.display(&quot;这是一个文件&quot;);				return null;			&#125; else &#123;				//System.out.println(path);				return null;			&#125;		&#125; catch (Exception e) &#123;			return null;		&#125;	&#125;    public String[] listDirectory(String path)&#123;    	File f = new File(path);		String[] fileName;		if (f.isDirectory()) &#123;			fileName = f.list();			//for (int i = 0; i &lt; fileName.length; i++)				//System.out.println(&quot;/&quot;+fileName[i]);			return fileName;		&#125; else &#123;			//System.out.println(path+&quot;是文件&quot;);			return null;		&#125;    &#125;	/*	 * 判断这个路径是否正确;	 */	public boolean isRightPath(String path) &#123;		File file = new File(path);		if (file.isDirectory() || file.isFile())			return true;		else			return false;	&#125;	/*	 * 进行cd操作;cd&lt;目录名&gt; 功能:进入下一个目录;	 */	public void cd(String path, String file) &#123;		String temp = path + &quot;\\&quot; + file;		if (!isRightPath(temp)) &#123;			mainFrame.display(&quot;没有找到这个文件夹&quot;);		&#125; else &#123;			if (!file.equals(&quot;&quot;))				currentPath += &quot;\\&quot; + file;		&#125;	&#125;	/*	 * 进行dir操作;dir [&lt;目录名&gt;] 功能: 显示目录下的所有文件;	 */	public void dir(String path) &#123;		if (path != null)			listAll(path);	&#125;	/*	 * mkdir &lt;目录名&gt; 功能: 创建新目录	 */	public void mkdir(String directory) &#123;		if (!currentPath.equals(&quot;&quot;)) &#123;			String temp = currentPath + &quot;\\&quot; + directory;			File newFile = new File(temp);			if (!newFile.exists()) &#123;				try &#123;					if (newFile.isDirectory() == false) &#123;						newFile.mkdirs();						mainFrame.display(&quot;文件夹创建成功!&quot;);					&#125; else &#123;						mainFrame.display(&quot;文件夹创建出错!&quot;);					&#125;				&#125; catch (Exception e) &#123;					mainFrame.display(&quot;出错信息:&quot; + e.getMessage());				&#125;			&#125; else &#123;				mainFrame.display(&quot;文件夹已经存在&quot;);			&#125;		&#125;	&#125;	/*	 * rmdir &lt;目录名&gt; 功能: 删除目录;	 */	public void rmdir(String directory) &#123;		if (!currentPath.equals(&quot;&quot;)) &#123;			String temp = currentPath + &quot;\\&quot; + directory;			File file = new File(temp);			if (file.exists()) &#123;				if (file.delete()) &#123;					mainFrame.display(&quot;文件夹删除成功!&quot;);				&#125; else &#123;					mainFrame.display(&quot;文件夹删除操作出错!&quot;);				&#125;			&#125; else &#123;				mainFrame.display(&quot;文件夹不存在&quot;);			&#125;		&#125;	&#125;	/*	 * mkfile &lt;文件名&gt; 功能: 新建文件	 */	public void mkfile(String file) &#123;		if (!currentPath.equals(&quot;&quot;)) &#123;			String temp = currentPath + &quot;\\&quot; + file;			File newFile = new File(temp);			if (newFile.exists()) &#123;				mainFrame.display(&quot;文件已经存在!&quot;);			&#125; else &#123;				try &#123;					newFile.createNewFile();					mainFrame.display(&quot;文件创建成功!&quot;);				&#125; catch (Exception e) &#123;					mainFrame.display(&quot;文件创建失败:&quot; + e.getMessage());				&#125;			&#125;		&#125;	&#125;	/*	 * rmfile &lt;文件名&gt; 功能:删除文件;	 */	public void rmfile(String file) &#123;		if (!file.equals(&quot;&quot;)) &#123;			String temp = currentPath + &quot;\\&quot; + file;			File dfile = new File(temp);			if (dfile.exists()) &#123;				if (dfile.delete()) &#123;					mainFrame.display(&quot;文件删除成功!&quot;);				&#125; else &#123;					mainFrame.display(&quot;文件删除操作出错!&quot;);				&#125;			&#125; else &#123;				mainFrame.display(&quot;文件不存在&quot;);			&#125;		&#125;	&#125;	/*	 * 进行exit操作; 功能:退出文件系统;	 */	public void exit() &#123;		mainFrame.display(&quot;退出系统&quot;);		System.exit(1);	&#125;		/*	 * 新建文件，使用绝对路径	 */	public void mkpath(String file)&#123;				if (!currentPath.equals(&quot;&quot;)) &#123;			String temp = file;			File newFile = new File(temp);			if (newFile.exists()) &#123;				mainFrame.display(&quot;文件已经存在!&quot;);			&#125; else &#123;				try &#123;					newFile.createNewFile();					mainFrame.display(&quot;文件创建成功!&quot;);				&#125; catch (Exception e) &#123;					mainFrame.display(&quot;文件创建失败:&quot; + e.getMessage());				&#125;			&#125;		&#125;	&#125;		/*	 * 使用系统默认软件打开指定文件	 */	public void open(String file)&#123;				if (!currentPath.equals(&quot;&quot;)) &#123;			String temp = currentPath + &quot;\\&quot; + file;			File newFile = new File(temp);			if (newFile.exists()) &#123;				try &#123;					Desktop.getDesktop().open(newFile);				&#125; catch (Exception e) &#123;					e.printStackTrace();				&#125;				mainFrame.display(&quot;打开文件成功！&quot;);			&#125; else &#123;				mainFrame.display(&quot;文件不存在，无法打开&quot;);			&#125;		&#125;	&#125;&#125;

4. 小结怎么这么简单？没错，代码是很简单，但是，想到这个思路不容易，再次感谢cattong前辈。
5. 源代码分享http://yunpan.cn/cjqdEABf3dxkj  访问密码 7159http://yunpan.cn/cjqdWLKJk4TQL  访问密码 63de
6. 参考文档http://www.oschina.net/project/tag/193?lang=19&amp;show=hotshttp://blog.csdn.net/cattong/article/details/1844916
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>XMPP学习笔记——调试控制台</title>
    <url>/dev-xmpp-note-debug-console/</url>
    <content><![CDATA[1. 前言开发人员总是喜欢不断地加工并完善自己的工具，在开发XMPP应用程序的过程中，我们将需要一款工具来辅助研究和查看协议流量。要是不使用查看源代码命令，或者不能轻易加工URL来测试远程站点的功能，那么很少有Web开发人员能够轻松工作。
对于XMPP节，这样的工具可用来查看协议流量并轻易地创建要发送的节。现在，我们就来做一个这样的工具。
Peek可用来帮助我们研究XMPP扩展如何运转以及为何有些扩展并不能按照预期执行操作。我们可以从应用程序中剪切并粘贴XMPP节构建代码，看看服务器响应究竟是什么。如果不熟悉特定的协议扩展，那么可以输入实例，看看服务器如何响应不同的输入。


2. 操作步骤其实这个程序和之前的Hello很像，步骤类似。
2.1. peek.html新建peek.html，内容如下：
&lt;!DOCTYPE HTML&gt;&lt;html&gt;  &lt;head&gt;    &lt;meta http-equiv=&quot;Content-type&quot; content=&quot;text/html;charset=UTF-8&quot;&gt;    &lt;title&gt;Peek&lt;/title&gt;    &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;./style/jquery-ui.css&quot; &gt;	&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;./style/jquery-ui.theme.css&quot; &gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;./js/jquery.js&quot;&gt;&lt;/script&gt;	&lt;script type=&quot;text/javascript&quot; src=&quot;./js/jquery-ui.js&quot;&gt;&lt;/script&gt;	    &lt;script type=&quot;text/javascript&quot; src=&quot;./js/strophe.js&quot;&gt;&lt;/script&gt;       &lt;script type=&quot;text/javascript&quot; src=&#x27;./js/strophe.flxhr.js&#x27;&gt;&lt;/script&gt;	&lt;script type=&quot;text/javascript&quot; src=&#x27;./js/flXHR.js&#x27;&gt;&lt;/script&gt;    &lt;link rel=&#x27;stylesheet&#x27; type=&#x27;text/css&#x27; href=&#x27;./style/peek.css&#x27;&gt;    &lt;script src=&#x27;./js/peek.js&#x27;&gt;&lt;/script&gt;  &lt;/head&gt;  &lt;body&gt;    &lt;h1&gt;Peek&lt;/h1&gt;    &lt;div id=&#x27;console&#x27;&gt;&lt;/div&gt;    &lt;textarea id=&#x27;input&#x27; class=&#x27;disabled&#x27;              disabled=&#x27;disabled&#x27;&gt;	&lt;/textarea&gt;    &lt;div id=&#x27;buttonbar&#x27;&gt;      &lt;input id=&#x27;send_button&#x27; type=&#x27;button&#x27; value=&#x27;Send Data&#x27;             disabled=&#x27;disabled&#x27; class=&#x27;button&#x27;&gt;      &lt;input id=&#x27;disconnect_button&#x27; type=&#x27;button&#x27; value=&#x27;Disconnect&#x27;             disabled=&#x27;disabled&#x27; class=&#x27;button&#x27;&gt;    &lt;/div&gt;    &lt;!-- login dialog --&gt;    &lt;div id=&#x27;login_dialog&#x27; class=&#x27;hidden&#x27;&gt;      &lt;label&gt;JID:&lt;/label&gt;&lt;input type=&#x27;text&#x27; id=&#x27;jid&#x27;&gt;      &lt;label&gt;Password:&lt;/label&gt;&lt;input type=&#x27;password&#x27; id=&#x27;password&#x27;&gt;    &lt;/div&gt;  &lt;/body&gt;&lt;/html&gt;

2.2. peek.css新建peek.css，内容如下：
body &#123;    font-family: Helvetica;&#125;h1 &#123;    text-align: center;&#125;#console &#123;    padding: 10px;    height: 300px;    border: solid 1px #aaa;    background-color: #000;    color: #eee;    font-family: monospace;    overflow: auto;&#125;#input &#123;    width: 100%;    height: 100px;    font-family: monospace;&#125;.incoming &#123;    background-color: #111;&#125;textarea.disabled &#123;    background-color: #bbb;&#125;#buttonbar &#123;    margin: 10px;&#125;#disconnect_button &#123;    float: left;    width: 100px;&#125;#send_button &#123;    float: right;    width: 100px;&#125;/* xml styles */.xml_punc &#123; color: #888; &#125;.xml_tag &#123; color: #e77; &#125;.xml_aname &#123; color: #55d; &#125;.xml_avalue &#123; color: #77f; &#125;.xml_text &#123; color: #aaa &#125;.xml_level0 &#123; padding-left: 0; &#125;.xml_level1 &#123; padding-left: 1em; &#125;.xml_level2 &#123; padding-left: 2em; &#125;.xml_level3 &#123; padding-left: 3em; &#125;.xml_level4 &#123; padding-left: 4em; &#125;.xml_level5 &#123; padding-left: 5em; &#125;.xml_level6 &#123; padding-left: 6em; &#125;.xml_level7 &#123; padding-left: 7em; &#125;.xml_level8 &#123; padding-left: 8em; &#125;.xml_level9 &#123; padding-left: 9em; &#125;

2.3. peek.js新建peek.js，内容如下：
var Peek = &#123;    connection: null,    show_traffic: function (body, type) &#123;        if (body.childNodes.length &gt; 0) &#123;            var console = $(&#x27;#console&#x27;).get(0);            var at_bottom = console.scrollTop &gt;= console.scrollHeight -                 console.clientHeight;;            $.each(body.childNodes, function () &#123;                $(&#x27;#console&#x27;).append(&quot;&lt;div class=&#x27;&quot; + type + &quot;&#x27;&gt;&quot; +                                      Peek.pretty_xml(this) +                                     &quot;&lt;/div&gt;&quot;);            &#125;);            if (at_bottom) &#123;                console.scrollTop = console.scrollHeight;            &#125;        &#125;    &#125;,    pretty_xml: function (xml, level) &#123;        var i, j;        var result = [];        if (!level) &#123;             level = 0;        &#125;        result.push(&quot;&lt;div class=&#x27;xml_level&quot; + level + &quot;&#x27;&gt;&quot;);        result.push(&quot;&lt;span class=&#x27;xml_punc&#x27;&gt;&amp;lt;&lt;/span&gt;&quot;);        result.push(&quot;&lt;span class=&#x27;xml_tag&#x27;&gt;&quot;);        result.push(xml.tagName);        result.push(&quot;&lt;/span&gt;&quot;);        // attributes        var attrs = xml.attributes;        var attr_lead = []        for (i = 0; i &lt; xml.tagName.length + 1; i++) &#123;            attr_lead.push(&quot;&amp;nbsp;&quot;);        &#125;        attr_lead = attr_lead.join(&quot;&quot;);        for (i = 0; i &lt; attrs.length; i++) &#123;            result.push(&quot; &lt;span class=&#x27;xml_aname&#x27;&gt;&quot;);            result.push(attrs[i].nodeName);            result.push(&quot;&lt;/span&gt;&lt;span class=&#x27;xml_punc&#x27;&gt;=&#x27;&lt;/span&gt;&quot;);            result.push(&quot;&lt;span class=&#x27;xml_avalue&#x27;&gt;&quot;);            result.push(attrs[i].nodeValue);            result.push(&quot;&lt;/span&gt;&lt;span class=&#x27;xml_punc&#x27;&gt;&#x27;&lt;/span&gt;&quot;);            if (i !== attrs.length - 1) &#123;                result.push(&quot;&lt;/div&gt;&lt;div class=&#x27;xml_level&quot; + level + &quot;&#x27;&gt;&quot;);                result.push(attr_lead);            &#125;        &#125;        if (xml.childNodes.length === 0) &#123;            result.push(&quot;&lt;span class=&#x27;xml_punc&#x27;&gt;/&amp;gt;&lt;/span&gt;&lt;/div&gt;&quot;);        &#125; else &#123;            result.push(&quot;&lt;span class=&#x27;xml_punc&#x27;&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&quot;);            // children            $.each(xml.childNodes, function () &#123;                if (this.nodeType === 1) &#123;                    result.push(Peek.pretty_xml(this, level + 1));                &#125; else if (this.nodeType === 3) &#123;                    result.push(&quot;&lt;div class=&#x27;xml_text xml_level&quot; +                                 (level + 1) + &quot;&#x27;&gt;&quot;);                    result.push(this.nodeValue);                    result.push(&quot;&lt;/div&gt;&quot;);                &#125;            &#125;);                        result.push(&quot;&lt;div class=&#x27;xml xml_level&quot; + level + &quot;&#x27;&gt;&quot;);            result.push(&quot;&lt;span class=&#x27;xml_punc&#x27;&gt;&amp;lt;/&lt;/span&gt;&quot;);            result.push(&quot;&lt;span class=&#x27;xml_tag&#x27;&gt;&quot;);            result.push(xml.tagName);            result.push(&quot;&lt;/span&gt;&quot;);            result.push(&quot;&lt;span class=&#x27;xml_punc&#x27;&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&quot;);        &#125;                return result.join(&quot;&quot;);    &#125;,    text_to_xml: function (text) &#123;        var doc = null;        if (window[&#x27;DOMParser&#x27;]) &#123;            var parser = new DOMParser();            doc = parser.parseFromString(text, &#x27;text/xml&#x27;);        &#125; else if (window[&#x27;ActiveXObject&#x27;]) &#123;            var doc = new ActiveXObject(&quot;MSXML2.DOMDocument&quot;);            doc.async = false;            doc.loadXML(text);        &#125; else &#123;            throw &#123;                type: &#x27;PeekError&#x27;,                message: &#x27;No DOMParser object found.&#x27;            &#125;;        &#125;        var elem = doc.documentElement;        if ($(elem).filter(&#x27;parsererror&#x27;).length &gt; 0) &#123;            return null;        &#125;        return elem;    &#125;&#125;;$(document).ready(function () &#123;    $(&#x27;#login_dialog&#x27;).dialog(&#123;        autoOpen: true,        draggable: false,        modal: true,        title: &#x27;Connect to XMPP&#x27;,        buttons: &#123;            &quot;Connect&quot;: function () &#123;                $(document).trigger(&#x27;connect&#x27;, &#123;                    jid: $(&#x27;#jid&#x27;).val(),                    password: $(&#x27;#password&#x27;).val()                &#125;);                                $(&#x27;#password&#x27;).val(&#x27;&#x27;);                $(this).dialog(&#x27;close&#x27;);            &#125;        &#125;    &#125;);    $(&#x27;#disconnect_button&#x27;).click(function () &#123;        Peek.connection.disconnect();    &#125;);    $(&#x27;#send_button&#x27;).click(function () &#123;        var input = $(&#x27;#input&#x27;).val();        var error = false;        if (input.length &gt; 0) &#123;            if (input[0] === &#x27;&lt;&#x27;) &#123;                var xml = Peek.text_to_xml(input);                if (xml) &#123;                    Peek.connection.send(xml);                    $(&#x27;#input&#x27;).val(&#x27;&#x27;);                &#125; else &#123;                    error = true;                &#125;            &#125; else if (input[0] === &#x27;$&#x27;) &#123;                try &#123;                    var builder = eval(input);                    Peek.connection.send(builder);                    $(&#x27;#input&#x27;).val(&#x27;&#x27;);                &#125; catch (e) &#123;                    console.log(e);                    error = true;                &#125;            &#125; else &#123;                error = true;            &#125;        &#125;        if (error) &#123;            $(&#x27;#input&#x27;).animate(&#123;backgroundColor: &quot;#faa&quot;&#125;);        &#125;    &#125;);    $(&#x27;#input&#x27;).keypress(function () &#123;        $(this).css(&#123;backgroundColor: &#x27;#fff&#x27;&#125;);    &#125;);&#125;);$(document).bind(&#x27;connect&#x27;, function (ev, data) &#123;    var conn = new Strophe.Connection(        &quot;http://bosh.metajack.im:5280/xmpp-httpbind&quot;);    conn.xmlInput = function (body) &#123;        Peek.show_traffic(body, &#x27;incoming&#x27;);    &#125;;    conn.xmlOutput = function (body) &#123;        Peek.show_traffic(body, &#x27;outgoing&#x27;);    &#125;;    conn.connect(data.jid, data.password, function (status) &#123;        if (status === Strophe.Status.CONNECTED) &#123;            $(document).trigger(&#x27;connected&#x27;);        &#125; else if (status === Strophe.Status.DISCONNECTED) &#123;            $(document).trigger(&#x27;disconnected&#x27;);        &#125;    &#125;);    Peek.connection = conn;&#125;);$(document).bind(&#x27;connected&#x27;, function () &#123;    $(&#x27;.button&#x27;).removeAttr(&#x27;disabled&#x27;);    $(&#x27;#input&#x27;).removeClass(&#x27;disabled&#x27;).removeAttr(&#x27;disabled&#x27;);&#125;);$(document).bind(&#x27;disconnected&#x27;, function () &#123;    $(&#x27;.button&#x27;).attr(&#x27;disabled&#x27;, &#x27;disabled&#x27;);    $(&#x27;#input&#x27;).addClass(&#x27;disabled&#x27;).attr(&#x27;disabled&#x27;, &#x27;disabled&#x27;);&#125;);

3. 输入测试及运行结果
3.1. 控制出席打开Peek应用程序，登陆到XMPP服务器，输入&lt;presence/&gt;，然后点击Send按钮。
3.2. 设置状态为已离开$pres().c(&#x27;show&#x27;).t(&quot;away&quot;).up().c(&#x27;status&#x27;).t(&quot;reading&quot;);

3.3. 探测版本$iq(&#123;type:&quot;get&quot;,id:&quot;version&quot;,to:&quot;jabber.org&quot;&#125;).c(&quot;query&quot;,&#123;xmlns:&quot;jabber:iq:version&quot;&#125;);
或者
&lt;iq type=&#x27;get&#x27; id=&#x27;version1&#x27; to=&#x27;jabber.org&#x27;&gt;&lt;query xmlns=&#x27;jabber:iq:version&#x27;/&gt;&lt;/iq&gt;

没有反应，不知道为什么。
3.4. iq节错误$iq(&#123;type:&quot;get&quot;,id:&quot;version2&quot;,to:&quot;gmail.com&quot;&#125;).c(&quot;query&quot;,&#123;xmlns:&quot;jabber:iq:version&quot;&#125;);

&lt;iq type=&#x27;get&#x27; id=&#x27;info1&#x27; to=&#x27;bad-room-123@conference.jabber.org&#x27;&gt;	&lt;query xmlns=&#x27;http://jabber.org/protocol/disco#info&#x27; /&gt;&lt;/iq&gt;

3.5. message节错误$msg(&#123;to:&#x27;voidking@voidking.lit&#x27;,type:&#x27;chat&#x27;&#125;).c(&#x27;body&#x27;).t(&#x27;What think you of books ?&#x27;);

3.6. presence节错误不知道怎么发送presence节错误信息，省略先。
4. 结束语这篇文章也是摘自《XMPP高级编程》，void也不详细解释代码了，看不懂的地方，请小伙伴参考这本书。
5. 参考文档《XMPP高级编程（作者Jack Moffitt）》《XMPP The Definitive Guide（作者Peter Saint-Andre, Kevin Smith, and Remko Tron?on）》前辈们的技术博客……
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>jquery</tag>
        <tag>xmpp</tag>
      </tags>
  </entry>
  <entry>
    <title>XMPP学习笔记——Hello</title>
    <url>/dev-xmpp-note-hello/</url>
    <content><![CDATA[1. 前言首先声明，我们将要写的这个小程序，是属于XMPP客户端的。以后要写的程序，也都是XMPP客户端的。
马上开始第一个XMPP程序了，真有点小激动呢！接下来，各位同学和void一起试试手吧！
2. 准备编程之前，我们需要准备好环境和依赖的工具。
这个程序，我们需要Tomcat服务器、jquery.js、jquery-ui.js、jquery-ui.css、jquery-ui.theme.css、strophe.js、strophe.flxhr.js、flXHR.js以及依赖的文件。哦，还有一个XMPP账号。
下面简单介绍一下环境的准备和依赖工具的下载，最后提供打包好的工具。

2.1. 本地服务器因为Hello程序是一个仅由HTML、CSS和JavaScript代码组成的Web应用程序，所以运行和测试它是非常简单的。
尽管flXHR库可用来执行跨域请求而无需任何服务器设置，但它并不允许我们在file://这样的URL下面运行，却请求http://URL 的应用程序。这意味着我们要使用Web服务器通过HTTP来访问Hello程序。
这里，void使用的是tomcat，任何其他Web服务器也可以胜任。或者，你也可以直接将代码上传到你的公网服务器。
2.2. jQueryjQuery库使得HTML和CSS的处理变得异常简单，它在XML（XMPP节）的操作方面也非常方便。
最新版本请到jQuery官网下载。
2.3. jQuery UIjQuery UI库提供了我们所需要的一些常见的用户界面构造块，包括对话框和标签页。
最新版本请到jQuery UI官网下载。
PS：最新的jquery.js和最新的jquery-ui.js可能会有兼容性问题，保险起见，最好使用jQuery UI官网提供的jquery.js。
2.4. StropheStrophe库使得编写XMPP客户端应用程序变得极其简单，而且它已有多种编程语言版本。当然，我们将使用Javascript版本。
最新版本请到Strophe官网下载。
2.5. flXHRStrophe能够使用flXHR（标准 XMLHttpRequest API的Flash替代技术）来简化Javascript同源策略的处理。通常，Javascript应用程序不能与外部服务器通信，但借助Flash和flXHR，Strophe能够克服这个限制。
最新版本请到flXHR官网下载。但是，void登不上去这个网站，也ping不到它的服务器，不知道是被屏蔽了还是下线了。
这里提供flXHR开源项目下载地址，很古老，三年前的，莫非这个项目有了更好的替代品？不再更新了？不管了，我们先用着。经过测试，这个项目还是可以使用的。
国内github的下载速度真的让人绝望，尤其下载稍大的项目，看着几K的下载速度，哭的心都有了：大哥，你是要我等到天荒地老吗？
void通过网盘离线下载成功，需要的同学自己取吧，下载地址http://yunpan.cn/cAtFDZVWCJ6QX （提取码：1429）。
flXHR.js依赖的文件较多，这个程序中具体用到的有flensed.js、checkplayer.js、flXHR.swf、flXHR.vbs、swfobject.js、updateplayer.swf。
2.6. XMPP账号假设，qq开源，你要写一个qq客户端，想要测试能否连接到qq的服务器，首先要有个qq号吧？一个道理，做测试之前，我们要先有一个XMPP账号。
虽然http://register.jabber.org 已经下线，但是我们可以找到很多其他免费的公共XMPP服务器，这里给出一个列表https://xmpp.net/directory.php 。
这里绝大部分服务器是国外的，速度比较慢，于是void在自己的阿里云上搭建了一个openfire服务器。当然，你也可以本地搭建服务器，然后通过局域网访问。
找到或者搭建好XMPP服务器，然后，下载安装spark，看看能否注册和登陆，测试是否可用。
至此，假设我们已经拥有了一个XMPP账号。没有搞定的小伙伴请留言。
2.7. 工具打包下载上面提到的工具，以及Hello程序的代码，打包到360网盘了，需要的小伙伴自取http://yunpan.cn/cAnXNmvewvGme  （提取码 baaf）
3. 具体步骤这个程序的功能是：向XMPP服务器发送一条信息并将响应显示出来。下面我们来一步一步完成它！
3.1. 目录结构接下来的文件放置位置，请下载“工具打包下载”一节的压缩文件，用作参考。文件位置不对的话，需要修改代码，相信你们都懂得。
3.2. hello.html新建文件hello.html，内容如下：
&lt;!DOCTYPE HTML&gt;&lt;html&gt;  &lt;head&gt;    &lt;title&gt;Hello&lt;/title&gt;        &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;./style/jquery-ui.css&quot; &gt;	&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;./style/jquery-ui.theme.css&quot; &gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;./js/jquery.js&quot;&gt;&lt;/script&gt;	&lt;script type=&quot;text/javascript&quot; src=&quot;./js/jquery-ui.js&quot;&gt;&lt;/script&gt;	    &lt;script type=&quot;text/javascript&quot; src=&quot;./js/strophe.js&quot;&gt;&lt;/script&gt;       &lt;script type=&quot;text/javascript&quot; src=&#x27;./js/strophe.flxhr.js&#x27;&gt;&lt;/script&gt;	&lt;script type=&quot;text/javascript&quot; src=&#x27;./js/flXHR.js&#x27;&gt;&lt;/script&gt;    &lt;link rel=&#x27;stylesheet&#x27; type=&quot;text/css&quot; href=&#x27;./style/hello.css&#x27;&gt;    &lt;script type=&quot;text/javascript&quot; src=&#x27;./js/hello.js&#x27;&gt;&lt;/script&gt;  &lt;/head&gt;  &lt;body&gt;    &lt;h1&gt;Hello&lt;/h1&gt;    &lt;div id=&#x27;log&#x27;&gt;    &lt;/div&gt;    &lt;!-- login dialog --&gt;    &lt;div id=&#x27;login_dialog&#x27; class=&#x27;hidden&#x27;&gt;      &lt;label&gt;JID:&lt;/label&gt;&lt;input type=&#x27;text&#x27; id=&#x27;jid&#x27;&gt;      &lt;label&gt;Password:&lt;/label&gt;&lt;input type=&#x27;password&#x27; id=&#x27;password&#x27;&gt;    &lt;/div&gt;  &lt;/body&gt;&lt;/html&gt;

3.3. hello.css新建hello.css文件，内容如下：
body &#123;    font-family: Helvetica;&#125;h1 &#123;    text-align: center;&#125;.hidden &#123;    display: none;&#125;#log &#123;    padding: 10px;&#125;

3.4. hello.js新建hello.js文件，内容如下：
var Hello = &#123;    connection: null,    start_time: null,    log: function (msg) &#123;        $(&#x27;#log&#x27;).append(&quot;&lt;p&gt;&quot; + msg + &quot;&lt;/p&gt;&quot;);    &#125;,    send_ping: function (to) &#123;        var ping = $iq(&#123;            to: to,            type: &quot;get&quot;,            id: &quot;ping1&quot;&#125;).c(&quot;ping&quot;, &#123;xmlns: &quot;urn:xmpp:ping&quot;&#125;);        Hello.log(&quot;Sending ping to &quot; + to + &quot;.&quot;);        Hello.start_time = (new Date()).getTime();        Hello.connection.send(ping);    &#125;,    handle_pong: function (iq) &#123;        var elapsed = (new Date()).getTime() - Hello.start_time;        Hello.log(&quot;Received pong from server in &quot; + elapsed + &quot;ms.&quot;);        Hello.connection.disconnect();                return false;    &#125;&#125;;$(document).ready(function () &#123;    $(&#x27;#login_dialog&#x27;).dialog(&#123;        autoOpen: true,        draggable: false,        modal: true,        title: &#x27;Connect to XMPP&#x27;,        buttons: &#123;            &quot;Connect&quot;: function () &#123;                $(document).trigger(&#x27;connect&#x27;, &#123;                    jid: $(&#x27;#jid&#x27;).val(),                    password: $(&#x27;#password&#x27;).val()                &#125;);                                $(&#x27;#password&#x27;).val(&#x27;&#x27;);                $(this).dialog(&#x27;close&#x27;);            &#125;        &#125;    &#125;);&#125;);$(document).bind(&#x27;connect&#x27;, function (ev, data) &#123;    var conn = new Strophe.Connection(        &quot;http://bosh.metajack.im:5280/xmpp-httpbind&quot;);    conn.connect(data.jid, data.password, function (status) &#123;        if (status === Strophe.Status.CONNECTED) &#123;            $(document).trigger(&#x27;connected&#x27;);        &#125; else if (status === Strophe.Status.DISCONNECTED) &#123;            $(document).trigger(&#x27;disconnected&#x27;);        &#125;    &#125;);    Hello.connection = conn;&#125;);$(document).bind(&#x27;connected&#x27;, function () &#123;    // inform the user    Hello.log(&quot;Connection established.&quot;);    Hello.connection.addHandler(Hello.handle_pong, null, &quot;iq&quot;, null, &quot;ping1&quot;);    var domain = Strophe.getDomainFromJid(Hello.connection.jid);        Hello.send_ping(domain);&#125;);$(document).bind(&#x27;disconnected&#x27;, function () &#123;    Hello.log(&quot;Connection terminated.&quot;);    // remove dead connection object    Hello.connection = null;&#125;);
上面Javascript的代码由三个基本部分组成。首先是应用程序的命名空间对象，应用程序的状态和函数都在此定义。位于命名空间对象之后的是文档准备就绪事件处理程序，一旦浏览器准备就绪它就会初始化应用程序。最后是自定义事件处理程序，它负责处理那些不是由元素或用户交互触发的程序。
3.4.1. 命名空间对象我们使用命名空间对象，尽可能避免使用全局变量，这样可以将问题降到最少。上面代码中的Hello对象就是一个例子。
3.4.2. 文档就绪事件处理程序一旦DOM可供JavaScript代码使用，就会立即引发文档准备就绪事件。一般而言，最好将初始化代码放在这里。
3.4.3. 自定义事件处理程序jQuery库是的创建和使用自定义事件变得非常容易，这些自定义事件通常用来提高代码的可读性并减少组件之间的耦合度。
3.5. 发布上面三个文件，详解请读书《XMPP高级编程》（提取码 20fb），内容太多，不码字了。
完成之后，把整个工程文件夹拷贝到tomcat的webapp文件夹下。然后，启动tomcat，就可以访问了。
4. 最终效果
5. 结束语如果最终效果是你期望的，恭喜你，至少你已经知其然了！
6. 参考文档《XMPP高级编程（作者Jack Moffitt）》《XMPP The Definitive Guide（作者Peter Saint-Andre, Kevin Smith, and Remko Tron?on）》前辈们的技术博客……
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>jquery</tag>
        <tag>xmpp</tag>
      </tags>
  </entry>
  <entry>
    <title>XMPP学习笔记——概述篇</title>
    <url>/dev-xmpp-note-start/</url>
    <content><![CDATA[1. XMPP是什么？XMPP，可扩展消息和出席（存在）协议（eXtensible Messageing and Presence Protocol）。顾名思义，这是一个关于收发消息的规范。
最初研发IMPP（即时信息和出席协议，Instant Messaging and Presence Protocol）是为了创建一种标准化的协议，但是今天，IMPP已经发展成为基本协议单元，定义所有即时通信协议应该支持的核心功能集。
XMPP和SIMPLE（针对即时信息和出席扩展的会话发起协议，Session Initiation Protocol for Instant Messaging and Presence Leveraging Extensions）两种协议是架构，有助于实现IMPP协议所描述的规范。
PRIM（出席和即时信息协议，Presence and Instant Messaging Protocol）最初是基于即时通信的协议，与XMPP 和SIMPLE 类似，但是已经不再使用。

2. XMPP简史1996年之后，Mirabilis、AOL、Yahoo、微软等互联网公司陆续推出了个人通信产品。但是，这些产品各自绑定到各自专用的协议和网络。也就是说，ICQ的用户不能和MSN的用户交谈。
很多开发人员希望整合客户端，但是屡屡受挫。开放的、去中心化的IM网络和协议思想在这个时候就诞生了，经过发展，后来便有了XMPP。
3. XMPP网络任何XMPP网络都是由若干角色组成的，这些角色可以分为服务器、客户端、组件和服务器插件。
3.1. 服务器XMPP服务器是XMPP网络的交通系统，它的任务就是为XMPP节提供路由。
常见的XMPP服务器有Openfire、Ejabberd、Tigase、M-Link、Jabber-XCP等。
3.2. 客户端大多数的XMPP实体是客户端，它们通过客户端-服务器协议连接到XMPP服务器。
3.3. 组件并不只是客户端能连接到XMPP服务器，大多数服务器还支持外部服务器组件，这些组件通过添加某种新服务来增强服务器的行为。在外界看来，组件就像一个子服务器。

3.4. 插件许多XMPP服务器支持插件扩展，这些插件通常使用与服务器自身相同的语言编写，并在服务器的进程内运行。他们的作用很大程度上与外部组件重叠，但插件还能够访问内部服务器数据结构并改变核心服务器行为。
4. XMPP寻址XMPP网络上每个实体都有一个或多个地址（JID，jabber identifier）。JID有多种不同形式，但它们通常看上去就像是电子邮件。
JID有两种类型，裸JID和完整JID，而裸JID是完整JID去除资源部分后的地址。例如，某个客户端的完整JID是&#x76;&#111;&#x69;&#x64;&#x6b;&#x69;&#x6e;&#103;&#x40;&#x76;&#x6f;&#105;&#x64;&#107;&#x69;&#x6e;&#103;&#46;&#108;&#x69;&#x74;/library，那么它的裸JID就是&#118;&#x6f;&#x69;&#100;&#107;&#x69;&#110;&#103;&#64;&#x76;&#111;&#x69;&#x64;&#107;&#105;&#x6e;&#103;&#46;&#108;&#105;&#116;。
5. 节在XMPP中，各项工作都是通过在一个XMPP流上发送和接收XMPP节来完成的。核心XMPP工具集由三种基本节组成，这三种节分别为&lt;presence&gt;、&lt;message&gt;、&lt;iq&gt;。
XMPP流由两份XML文档组成，通信的每个方向均有一份文档。这份文档有一个根元素&lt;stream:stream&gt;，这个根元素的子元素由可路由的节以及与流相关的顶级子元素构成。下面给出一段简短的XMPP会话：
&lt;stream:stream&gt;	&lt;iq type=&#x27;get&#x27;&gt;		&lt;query xmlns=&#x27;jabber:iq:roster&#x27;/&gt;	&lt;/iq&gt;		&lt;presence/&gt;		&lt;message to=&#x27;voidking@voidking.lit&#x27; from=&#x27;haojin@gmail.com/ballroom&#x27; type=&#x27;chat&#x27;&gt;		&lt;body&gt;I cannot talk of books in a ball-room; my head is always full of something else.&lt;/body&gt;	&lt;/message&gt;		&lt;presence type=&#x27;unavailable&#x27;/&gt;&lt;/stream:stream&gt;
简单解释下：（1）haojin登陆后，将自己的第一节（一个&lt;iq&gt;元素）发送出去，这个&lt;iq&gt;元素请求haojin的联系人列表。（2）接下来，他使用&lt;presence&gt;节通知服务器他在线并且可以访问。（3）当haojin注意到voidking也在线时，他发送了一条&lt;message&gt;节给voidking。（4）最后，haojin发送了一个&lt;presence&gt;节告诉服务器自己不可访问并关闭&lt;stream:stream&gt;元素，然后结束会话。
5.1. 通用属性所有三种节都支持一组通用的属性：（1）from（2）to（3）type（4）id
5.2. presence节&lt;presence&gt;节控制并报告实体的可访问性，“在线”、“离线”、“离开”、“请勿打扰”。此外，这个节还用来建立和终止向其他实体发布出席订阅。
5.2.1. 普通presence节普通&lt;presence&gt;节不含type属性，或者type属性值为unavailable或error。type属性没有available值，因为可以通过缺少type属性来指出这种情况。
用户通过发送不带to属性，直接发往服务器的&lt;presence&gt;节来操纵自己的出席状态。
&lt;presence/&gt;&lt;presence type=&#x27;unavailable&#x27;/&gt;&lt;presence&gt;	&lt;show&gt;away&lt;/show&gt;	&lt;status&gt;at the ball&lt;/status&gt;&lt;presence/&gt;&lt;presence&gt;	&lt;status&gt;touring the countryside&lt;/status&gt;	&lt;priority&gt;10&lt;/priority&gt;&lt;/presence&gt;&lt;presence&gt;	&lt;priority&gt;10&lt;/priority&gt;&lt;/presence&gt;
简单解释：（1）&lt;show&gt;元素用来传达用户的可访问性，只能出现在&lt;presence&gt;节中。该元素的值可以为：away、chat、dnd和xa，分别表示离开、有意聊天、不希望被打扰和长期离开。（2）&lt;status&gt;元素时一个人类可读的字符串，在接收者的聊天客户端中，这个字符串一般会紧挨着联系人名字显示。（3）&lt;priority&gt;元素用来指明连接资源的优先级，介于-128~127，默认值为0。
5.2.2. 扩展presence节开发人员希望能够扩展&lt;presence&gt;节以包含更详细的信息，比如用户当前听的歌或个人的情绪。因为&lt;presence&gt;节会广播给所有联系人，并且在XMPP网络流量中占据了很大的份额，所以不鼓励这种做法。这类扩展应该交给额外信息传送协议来处理。
5.2.3. 出席订阅用户的服务器会自动地将出席信息广播给那些订阅该用户出席信息的联系人。类似的，用户从所有他已经出席订阅的联系人那里接收到出席更新信息。与一些社交网络和IM系统不同，在XMPP中，出席订阅是有方向的。我订阅了你的出席信息，但是你并不一定订阅了我的出席信息。可以通过设置type的值来识别出席订阅节：subscribe、unsubscribe、subscribed、unsubscribed。
5.2.4. 定向出席定向出席是一种直接发给另一个用户或其他实体的普通&lt;presence&gt;节，这种节用来向那些没有进行出席订阅（通常因为只是临时需要出席信息）的实体传达出席状态信息。
5.3. message节&lt;message&gt;节用来从一个实体向另一个实体发送消息。这些可以是简单的聊天信息，也可以是任何结构化信息。例如，绘制指令、游戏状态和新游戏变动状况。
&lt;message&gt;属于发送后不管的类型，没有内在的可靠性，就像电子邮件一样。在有些情况下（比如向不存在的服务器发送信息），发送者可能会收到一个错误提示节，从中了解出现的问题。可以通过在应用程序协议中增加确认机制来实现可靠传送。
5.3.1. 消息类型&lt;message&gt;节有几种不同的类型，这些类型由type属性指出，可取的值有：chat、error、normal、groupchat和headline。该属性是可选的，如果没有指定type值，默认为normal。
5.3.2. 消息内容尽管&lt;message&gt;节可以包含任意扩展元素，但&lt;body&gt;和&lt;thread&gt;元素是为向消息中添加内容提供的正常机制。这两种子元素均是可选的。
5.4. IQ节&lt;iq&gt;节表示的是Info/Query，它为XMPP通信提供请求和响应机制。它与HTTP协议的基本工作原理非常相似，允许获取和设置查询，与HTTP的GET和POST动作类似。&lt;iq&gt;节有四种，通过type属性区分，其中两种请求get和set，两种响应result和error。每一个IQ-get或IQ-set节均必须接收响应的IQ-result和IQ-error节。此外，每一对&lt;iq&gt;必须匹配id属性。
5.5. error节所有三种XMPP节都有一个error类型，而且错误提示节的每种类型的内容都是按照同一模式排列。错误提示节具有定义明确的的结构，通常包含原节（肇事节）的内容、通用错误信息以及应用程序特有的错误条件和信息（可选）。
6. 连接生命周期通过三种基本节，实际上可以完成XMPP中任何任务，但发送XMPP节通常需要建立一个经过身份验证的XMPP会话。
6.1. 连接在发送任何节之前，需要建立XMPP流。在XMPP流存在之前，必须建立通往XMPP服务器的连接。
6.2. 流的建立一旦建立通往XMPP服务器的连接，XMPP流就启动了。通过向服务器发送起始元素&lt;stream:stream&gt;，就可打开XMPP流。服务器通过发送响应流起始标记&lt;stream:stream&gt;进行相应。一旦双向建立XMPP流，就可以来回发送各种元素。
6.3. 身份验证XMPP允许进行TLS（Transport Layer Security，传输层安全）加密，而且大多数客户端默认使用该功能。一旦服务器通告TLS支持后，客户端就启动TLS连接并将当前套接字升级为一个加密套接字而不断开连接。一旦TLS加密确立，就会创建一对新的XMPP流。
6.4. 连接断开当用户结束XMPP会话时，会终止会话并断开连接。一般终止会话方式是，首先发送无效出席信息，然后关闭&lt;stream:stream&gt;元素。
7. XMPP通信流程(1)节点连接到服务器；(2)服务器利用本地目录系统中的证书对其认证；(3)节点指定目标地址，让服务器告知目标状态；(4)服务器查找、连接并进行相互认证；(5)节点之间进行交互．
8. XMPP优势与HTTP相比，XMPP具有如下的优势：（1）能够“推送”数据，而不是“拉”。（2）防火墙友好（3）牢固的身份验证和安全机制（4）为许多不同的问题提供大量即开即用的工具
9. XMPP不足每种协议都有各自的优缺点，在许多场合中XMPP并不是完成任务的最佳工具或受到某种限制。（1）有状态协议（2）社区和部署不及HTTP广泛（3）对于简单的请求，其开销比HTTP大（4）仍然需要专门的实现
10. 桥接XMPP和Web虽然有几款浏览器正在试验一些功能以利用XMPP，但主流浏览器目前还没有内置XMPP协议支持。但通过某种巧妙的编程和一些服务器端的帮助，我们可以在HTTP连接之上建立高效的XMPP会话通道。
是这种高效通道成为可能的是一项名为HTTP长轮询的技术。通过联合使用一个简单的基于HTTP的管理协议以及XMPP连接管理器，我们可以将XMPP（以及它的所有功能）带入到HTTP应用程序中。
10.1. 长轮询如果读者曾在一个专横的老板下面工作，他会不停地问：“软件还没有写完吗？”这其实就是轮询。
尽管服务器不会被激怒，但是如果太多客户端过快地轮询，服务器也可能变得缓慢。如果不停地被打扰，那么完成的工作量就会减少。但是为了获取快速更新，轮询的间隔必须相当短，最低的延迟就是轮询间隔的长度。
轮询的另一个问题是大多数轮询请求并没有接收到新数据，就像给专横雇主的答复一样，服务器对“软件还没有写完吗？”问题的答案总是“还没有”。
一些聪明的家伙，发明了一种巧妙的技术来解决这个问题。它们并不立即响应该请求，而是在新数据没有准备好时将其挂起一段时间。
例如，如果服务器上新数据已经准备就绪，那么服务器会立即应答。如果尚无新数据，那么服务器将保持连接的打开状态，并持有所有应答。一旦新数据到达，它最终会响应该请求。如果在一定时间内没有新数据到达，服务器可以发回一个空的响应，这样就不会一次性锁住过多打开的连接。一旦一个请求返回，客户端就会立即发送一个新的请求，整个过程重新开始。
因为每个轮询请求都可能打开较长的时间，所以这种技术被称为长轮询。
轮询和长轮询的唯一真正改变之处是，不让客户端等待重新发送请求，而是让服务器等待直到其有新数据需要通告才响应请求。
该技术的一个缺点是服务器需要更聪明才可以处理这些长轮询请求，而这正是连接管理器的作用所在。
10.2. 管理连接XMPP连接可以持续任意长的时间，但HTTP请求却相当短命。连接管理器负责维护第三方的XMPP连接并通过HTTP长轮询技术来提供对连接的访问。
浏览器和连接管理器使用一种名为BOSH的简单协议通过HTTP进行通信。实际上，BOSH帮助HTTP客户端建立一个新的XMPP会话，然后将XMPP节包装到一个特殊的&lt;body&gt;元素中通过HTTP来回传送。它还提供了一些安全功能以确保XMPP会话不会轻易地被劫持。连接管理器和XMPP服务器通信就像它是一个普通的客户端一样。
这样一来，HTTP应用程序就能够控制一个真正的XMPP会话。由于长轮询及时提供的高效率和低延迟，因此它的性能相当好，足矣匹敌原生连接。
10.3. 让Javascript理解XMPP协议利用HTTP长轮询，我们就拥有了从服务器获取低延迟数据更新的技术。将该技术与连接管理器组合起来，我们就能够通过一系列HTTP请求来发送和接受XMPP数据。最后，我们还需要简化该技术在Web的原生编程语言Javascript中的实现。
Strophe库的创建宗旨，就是为了让使用Javascript编写XMPP应用程序，能够想采用任何其他语言一样简单，将托管连接的底层细节全部隐藏起来。就Strophe的用户而言，它看上去就跟在任何其他环境中所使用的原生XMPP连接一样。
11. 构建XMPP应用程序11.1. 浏览器平台Web浏览器可能是有史以来部署最广泛、使用最多的应用程序平台。XMPP为Web应用程序带来了一套新技术和抽象概念，同时带来的是实时、交互式和协作式应用程序的巨大潜力。
对于XMPP开发人员来说，将Web浏览器定位为目标平台有着巨大的意义。Web应用程序跨平台、易于部署，而且有着巨大的用户基础。此外，Web技术大量使用HTML，而用于HTML的工具通常也都能很好地用于XML，因而也能用于XMPP。
11.2. 基础设施就像Web通常需要一个Web服务器和应用程序服务器或框架，XMPP应用程序也需要一些基础设施。
XMPP连接要求有一台XMPP服务器，而且通常在该服务器上会有一个账户。XMPP应用程序还需要与连接管理器进行通信，这是因为浏览器目前还无法原生地理解XMPP协议。最后，应用程序使用的任何服务也必须由XMPP服务器提供。
11.3. 协议设计如果不是在创建现有XMPP服务（比如多人聊天或传统IM功能），那么用户可能在进行某种协议涉及来实现自己的梦想。XMPP提供了大量的工具可作为工作基础，而且通常，将这些工具进行简单组合，就足够满足大多数应用程序的需要。下面有几条指南：（1）组合现有协议。（2）保持简洁。（3）避免扩展出席。（4）参与社区。
12. 参考文档《XMPP高级编程（作者Jack Moffitt）》《XMPP The Definitive Guide（作者Peter Saint-Andre, Kevin Smith, and Remko Tron?on）》前辈们的技术博客……
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>jquery</tag>
        <tag>xmpp</tag>
      </tags>
  </entry>
  <entry>
    <title>jQuery学习笔记——Ajax篇</title>
    <url>/dev-jquery-ajax/</url>
    <content><![CDATA[1. 显示欢迎信息Ajax的优点我们已经说过了，上个例子先！
&lt;!--显示欢迎信息.html--&gt;&lt;html&gt;	&lt;head&gt;	&lt;title&gt;显示欢迎信息&lt;/title&gt;	&lt;meta charset=&quot;UTF-8&quot;&gt;	&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.11.1.js&quot;&gt;&lt;/script&gt;	&lt;script type=&quot;text/javascript&quot;&gt;		$(function()&#123;			$(&quot;#submit&quot;).click(function()&#123;				var name = $(&quot;.uname&quot;).val();				var data = &quot;uname=&quot; + name;				$.ajax(&#123;					type:&quot;POST&quot;,					url:&quot;http://demo.voidking.com/welcome.php&quot;,					data:data,					success: function(html)&#123;						$(&quot;#message&quot;).html(html);					&#125;				&#125;);				return false;			&#125;);		&#125;);	&lt;/script&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;form&gt;			&lt;label&gt;Enter your name&lt;/label&gt;			&lt;input type=&quot;text&quot; name=&quot;uname&quot; class=&quot;uname&quot; /&gt;&lt;br/&gt;			&lt;input type=&quot;submit&quot; id=&quot;submit&quot;/&gt;		&lt;/form&gt;		&lt;div id=&quot;message&quot;&gt;		&lt;/div&gt;	&lt;/body&gt;&lt;/html&gt;

&lt;!--welcome.php--&gt;&lt;?php	$name = $_POST[&#x27;uname&#x27;];	echo &quot;welcome &quot;.$name;?&gt;
把ajax实例01.html、welcome.php、jquery-1.11.1.js上传到自己的服务器。打开浏览器访问“显示欢迎信息.html”，输入一个名字，就能看到效果了。
为什么要上传服务器，搞的这么麻烦？大哥，ajax主要就是用来和服务器交互的好不好？如果你觉得麻烦，可以直接访问我的服务器查看效果:VoidKing编程实例，接下来的例子也可以在这个网页找到。
当然，你也可以直接本地打开“显示欢迎信息.html”，但是提交数据没有反应，因为同源策略限制交互。怎么破？简单点，本地搭建一个php服务器！高大上点，自行百度，修改代码！


2. 执行认证&lt;!DOCTYPE html&gt;&lt;html&gt;	&lt;head&gt;        &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot;/&gt;        &lt;title&gt;ajax实例02&lt;/title&gt;        &lt;script src=&quot;jquery-1.11.1.js&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;          &lt;script type=&quot;text/javascript&quot;&gt;			$(function() &#123;   				$(&#x27;#submit&#x27;).click(function () &#123;           					var name = $(&#x27;.uname&#x27;).val();							var pwd = $(&#x27;.passwd&#x27;).val();						var data=&#x27;uname=&#x27;+name+&#x27;&amp;password=&#x27;+pwd;					$.ajax(&#123;						type:&quot;GET&quot;,						url:&quot;logincheck.php&quot;, 						data:data,             						success:function(html) &#123;										$(&quot;#message&quot;).html(html);								&#125;					&#125;);							return false;					&#125;);			&#125;);		&lt;/script&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;p&gt;用户名输入guest，密码输入jquery，会出现欢迎信息，否则显示未注册&lt;/p&gt;&lt;br/&gt;		&lt;form&gt;  		&lt;label&gt;Enter your Name&lt;/label&gt;  		&lt;input type=&quot;text&quot;  name=&quot;uname&quot; class=&quot;uname&quot;/&gt;  &lt;br/&gt;		&lt;label&gt;Enter your Password&lt;/label&gt;  		&lt;input type=&quot;password&quot;  name=&quot;password&quot; class=&quot;passwd&quot;/&gt;  &lt;br/&gt;		&lt;input type=&quot;submit&quot; id=&quot;submit&quot;/&gt;  		&lt;/form&gt;  		&lt;div id=&quot;message&quot;&gt;&lt;/div&gt;	&lt;/body&gt;&lt;/html&gt;
&lt;!--logincheck.php--&gt;&lt;?php   $name = trim($_GET[&#x27;uname&#x27;]); $pswd = trim($_GET[&#x27;password&#x27;]);    if(($name==&quot;guest&quot;) &amp;&amp; ($pswd==&quot;jquery&quot;))  echo &quot;Welcome &quot;.  $name;else  echo &quot;Sorry you are not authorized&quot;;?&gt;  

3. 验证用户名&lt;html&gt;	&lt;head&gt;		&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;title&gt;验证用户名&lt;/title&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.11.1.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(function() &#123;   				$(&#x27;.error&#x27;).hide();				$(&#x27;#submit&#x27;).click(function () &#123;           					var name = $(&#x27;.uname&#x27;).val();							var data=&#x27;uname=&#x27;+name;					$.ajax(&#123;						type:&quot;POST&quot;,						url:&quot;validateuser.php&quot;, 						data:data,             						success:function(html) &#123;							$(&#x27;.error&#x27;).show();									$(&#x27;.error&#x27;).text(html);										&#125;					&#125;);							return false;					&#125;);			&#125;);		&lt;/script&gt;		&lt;/head&gt;	&lt;body&gt;		&lt;p&gt;验证用户名，只能是数字、字母和下划线&lt;/p&gt;&lt;br/&gt;		&lt;form&gt;  			&lt;span class=&quot;label&quot;&gt;Enter your Name&lt;/span&gt;  			&lt;input type=&quot;text&quot;  name=&quot;uname&quot; class=&quot;uname&quot;/&gt;  &lt;span class=&quot;error&quot;&gt; &lt;/span&gt;&lt;br&gt;			&lt;input type=&quot;submit&quot; id=&quot;submit&quot;/&gt;  		&lt;/form&gt;  	&lt;/body&gt;&lt;/html&gt;
&lt;!--validateuser.php--&gt;&lt;?php   $name = $_POST[&#x27;uname&#x27;]; if (!eregi(&quot;^[a-zA-Z0-9_]+$&quot;, $name)) &#123;	echo &quot;Invalid User name!&quot;;&#125;else&#123;	echo &quot;Great user name!&quot;;&#125;?&gt;  

4. 使用自动完成&lt;html&gt;	&lt;head&gt;		&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;title&gt;使用自动完成&lt;/title&gt;				&lt;style type=&quot;text/css&quot;&gt;			.listbox &#123;				position: relative;				left: 10px;				margin: 10px;				width: 200px;				background-color: #000;				color: #fff;				border: 2px solid #000;			&#125;			.nameslist &#123;				margin: 0px;				padding: 0px;			list-style:none;			&#125;			.hover &#123;				background-color: cyan;				color: blue;			&#125;		&lt;/style&gt;				&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.11.1.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(function() &#123;  				$(&#x27;.listbox&#x27;).hide(); 				$(&#x27;.userid&#x27;).keyup(function () &#123;       					var uid = $(&#x27;.userid&#x27;).val();						var data=&#x27;userid=&#x27;+uid;					$.ajax(&#123;						type:&quot;POST&quot;,						url:&quot;autocomplete.php&quot;, 						data:data,             						success:function(html) &#123;							$(&#x27;.listbox&#x27;).show();							$(&#x27;.nameslist&#x27;).html(html);							$(&#x27;li&#x27;).hover(function()&#123;								$(this).addClass(&#x27;hover&#x27;);							&#125;,							function()&#123;								$(this).removeClass(&#x27;hover&#x27;);							&#125;);							$(&#x27;li&#x27;).click(function()&#123;								$(&#x27;.userid&#x27;).val($(this).text());								$(&#x27;.listbox&#x27;).hide(); 							&#125;);						&#125;					&#125;);						return false;					&#125;);			&#125;);		&lt;/script&gt;		&lt;/head&gt;	&lt;body&gt;		&lt;p&gt;输入名字的第一个字符时，弹出建议框。&lt;/p&gt;&lt;br/&gt;		&lt;form&gt;  			&lt;span class=&quot;label&quot;&gt;Enter user id&lt;/span&gt;  			&lt;input type=&quot;text&quot;  name=&quot;userid&quot; class=&quot;userid&quot;/&gt; 			&lt;div class=&quot;listbox&quot;&gt;				&lt;div class=&quot;nameslist&quot;&gt;				&lt;/div&gt;			&lt;/div&gt;		&lt;/form&gt;   	&lt;/body&gt;&lt;/html&gt;

&lt;?php   	$name = $_POST[&#x27;userid&#x27;]; 	$connect = mysql_connect(&quot;localhost&quot;, &quot;demo&quot;, &quot;voidking&quot;) or die(&quot;Please, check your server connection.&quot;);	mysql_select_db(&quot;demo&quot;);	$query = &quot;SELECT name from user where name like &#x27;$name%&#x27;&quot;;	$results = mysql_query($query) or die(mysql_error());	if($results)	&#123;		while ($row = mysql_fetch_array($results)) &#123;			extract($row);			echo &#x27;&lt;li&gt;&#x27; . $name. &#x27;&lt;/li&gt;&#x27;;		&#125;	&#125;?&gt;  

5. 导入HTML&lt;!--导入HTML--&gt;&lt;html&gt;	&lt;head&gt;		&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;title&gt;导入HTML&lt;/title&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.11.1.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(function() &#123;   				$(&#x27;.list&#x27;).click(function () &#123;           					$(&#x27;#message&#x27;).load(&#x27;namesinfo.html&#x27;);					return false;   				&#125;);			&#125;);		&lt;/script&gt;		&lt;/head&gt;	&lt;body&gt;		&lt;p&gt;单击超链接，从另一个文件中导入一些HTML内容到当前网页中。&lt;/p&gt;&lt;br/&gt;		&lt;p&gt;We are going to organize the Conference on IT on 2nd Feb 2010&lt;/p&gt;		&lt;a href=&quot;http://demo.voidking.com&quot; class=&quot;list&quot;&gt;Participants&lt;/a&gt;		&lt;div id=&quot;message&quot;&gt;&lt;/div&gt;	&lt;/body&gt;&lt;/html&gt;

&lt;!--namesinfo.html--&gt;&lt;p&gt;The list of the persons taking part in conference &lt;/p&gt;&lt;ul&gt;&lt;li&gt;Jackub&lt;/li&gt;&lt;li&gt;Jenny&lt;/li&gt;&lt;li&gt;Jill&lt;/li&gt;&lt;li&gt;John&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We wish them All the Best&lt;/p&gt;

6. 取得JSON数据&lt;!--取得JSON数据--&gt;&lt;html&gt;	&lt;head&gt;		&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;title&gt;取得JSON数据&lt;/title&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.11.1.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(function() &#123;   				$(&#x27;#submit&#x27;).click(function () &#123;      					$.ajax(&#123;						type:&quot;GET&quot;,						url:&quot;drinkinfo.json&quot;,    						dataType:&quot;json&quot;,						success: function (data) &#123;   							var drinks=&quot;&lt;ul&gt;&quot;;							$.each(data, function(i,n)&#123;								drinks+=&quot;&lt;li&gt;&quot;+n[&quot;optiontext&quot;]+&quot;&lt;/li&gt;&quot;;							&#125;);							drinks+=&quot;&lt;/ul&gt;&quot;;              							$(&#x27;#message&#x27;).append(drinks);							&#125;					&#125;);   					return false;					&#125;);					&#125;);		&lt;/script&gt;		&lt;/head&gt;	&lt;body&gt;		&lt;p&gt;从JSON文件中异步地导入信息到当前的网页中。&lt;/p&gt;&lt;br/&gt;		&lt;p&gt;For information from JSON file click the button given below :&lt;br&gt;		&lt;input type=&quot;submit&quot; id=&quot;submit&quot;/&gt;  		&lt;div id=&quot;message&quot;&gt;&lt;/div&gt;  	&lt;/body&gt;&lt;/html&gt;
drinkinfo.json文件内容如下：
[	&#123;&quot;optiontext&quot; : &quot;Tea&quot;, &quot;optionvalue&quot; : &quot;Tea&quot;&#125;,	&#123;&quot;optiontext&quot; : &quot;Coffee&quot;, &quot;optionvalue&quot; : &quot;Coffee&quot;&#125;,	&#123;&quot;optiontext&quot; : &quot;Juice&quot;, &quot;optionvalue&quot; : &quot;Juice&quot;&#125;]
矮油我靠，在服务器上打开浏览器，读取正常；在本地打开浏览器，居然无法读取，404错误！
经过测试，只要把文件名更改一下就好了。即把drinkinfo.json改成drinkinfo.txt。真是个奇葩问题，个人猜测是浏览器传输文件有格式限制。
7. 取得XML数据&lt;html&gt;	&lt;head&gt;		&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;title&gt;取得XML数据&lt;/title&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.11.1.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(document).ready(function() &#123;   				$(&#x27;#submit&#x27;).click(function () &#123;      					$.ajax(&#123;						type:&quot;GET&quot;,						url:&quot;student.xml&quot;,    						dataType:&quot;xml&quot;,						success: function (sturec) &#123;    							var stud=&quot;&lt;ul&gt;&quot;;							$(sturec).find(&#x27;student&#x27;).each(function()&#123;								var name = $(this).find(&#x27;first-name&#x27;).text()								stud+=&quot;&lt;li&gt;&quot;+name+&quot;&lt;/li&gt;&quot;;							&#125;);							stud+=&quot;&lt;/ul&gt;&quot;; 							$(&#x27;#message&#x27;).append(stud);						&#125;					&#125;);   					return false;					&#125;);					&#125;);		&lt;/script&gt;		&lt;/head&gt;	&lt;body&gt;		&lt;p&gt;从student.xml文件中异步导入信息到当前的网页中。&lt;/p&gt;&lt;br/&gt;		&lt;p&gt;To see the Names of the students extracted from XML file click the button given below :&lt;/p&gt;		&lt;input type=&quot;submit&quot; id=&quot;submit&quot;/&gt;  		&lt;div id=&quot;message&quot;&gt;&lt;/div&gt;	&lt;/body&gt;&lt;/html&gt;
student.xml文件内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;&lt;school&gt;  &lt;student&gt;    &lt;roll&gt;101&lt;/roll&gt;    &lt;name&gt;      &lt;first-name&gt;Anil&lt;/first-name&gt;      &lt;last-name&gt;Sharma&lt;/last-name&gt;    &lt;/name&gt;    &lt;address&gt;      &lt;street&gt;        22/10 Sri Nagar Road      &lt;/street&gt;      &lt;city&gt;        Ajmer      &lt;/city&gt;      &lt;state&gt;        Rajasthan      &lt;/state&gt;    &lt;/address&gt;    &lt;marks&gt;      85    &lt;/marks&gt;   &lt;/student&gt;    &lt;student&gt;    &lt;roll&gt;102&lt;/roll&gt;    &lt;name&gt;      &lt;first-name&gt;Manoj&lt;/first-name&gt;      &lt;last-name&gt;Arora&lt;/last-name&gt;    &lt;/name&gt;    &lt;address&gt;      &lt;street&gt;        H.No 11-B Alwar Gate      &lt;/street&gt;      &lt;city&gt;        Ajmer      &lt;/city&gt;      &lt;state&gt;        Rajasthan      &lt;/state&gt;    &lt;/address&gt;    &lt;marks&gt;      92    &lt;/marks&gt;  &lt;/student&gt;&lt;/school&gt;

8. 结束语以上例子，全部摘自《jQuery攻略》，不详解，看代码！
9. 参考文档李炎恢的jQuery视频教程《jQuery攻略（作者B.M.Harwani）》《jQuery实战（作者Bear Bibeault、Yehuda Katz）》《jQuery高级编程（作者 Cesar Otero、Rob Larsen）》《jQuery Javascript 与CSS开发入门经典（作者Richard York）》
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>javascript</tag>
        <tag>jquery</tag>
        <tag>ajax</tag>
      </tags>
  </entry>
  <entry>
    <title>jQuery学习笔记——事件篇</title>
    <url>/dev-jquery-event/</url>
    <content><![CDATA[1. 前言任何基于GUI的现代应用程序都是基于事件驱动的，Web应用程序也不例外。
所有事件驱动的应用程序都采用相同的工作模式：建立事件机制、等待相关事件发生（比如鼠标单击）、对该事件做出相应。
2. 浏览器事件模型忆苦思甜，先了解一下传统的事件模型的缺点，我们才能明白改革开放是多么好的政策！
2.1. DOM第0级事件模型也许你听说过网景事件模型、基本事件模型、浏览器事件模型，但是大多数人称其为DOM第0级事件模型。
为什么称其为第0级事件模型呢？因为，虽然该模型并不是一个正式的标准，但是所有主流的浏览器都与之兼容。而且，所有的现代浏览器依然支持这种模型。


2.2. 插播广告几乎所有的标准、协议、规范，都是在一门技术出现甚至成熟之后，才会形成。所以，我们也不能责怪W3C组织，没有及时给网景推出的事件模型一个名分，毕竟技术要领先于标准。
2.3. DOM第2级事件模型等一下？void，你是不是把DOM第1级事件模型漏掉了？没有漏掉，因为没有DOM第1级事件模型。DOM级别1于1998年10月成为W3C推荐标准，但是，该标准中并没有定义事件相关的内容。直到2000年11月，DOM级别2被引入时，W3C才真正为事件处理建立了标准模型。
这个模型得到了所有标准兼容的现代浏览器的支持，比如Firefox、Safari、Opera等。IE浏览器特立独行，它支持DOM第2级事件模型的一个功能子集。
2.4. IE事件模型刚才说到，IE不支持DOM第2级事件模型。IE为每个DOM元素定义了一个名为attachEvent()的方法，而不是addEventListener()。
2.5. 小结以上你可以认为是废话……总而言之，想要使用事件模型，不得不考虑到兼容问题，非常麻烦！这时，我们的jQuery事件模型华丽登场！
3. jQuery事件模型，jQuery把不一致的代码从页面代码中提取出来，将其隐藏在API中，因此，我们终于不用再去考虑兼容性问题！
3.1. 使用jQuery绑定事件处理器&lt;html&gt;	&lt;head&gt;			&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.10.2.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(function()&#123;				$(&quot;img&quot;).bind(&quot;click&quot;,function()&#123;alert(&quot;hello voidking&quot;);&#125;);			&#125;);		&lt;/script&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;img src=&quot;./head.jpg&quot; alt=&quot;头像&quot;/&gt;			&lt;/body&gt;&lt;/html&gt;
&lt;html&gt;	&lt;head&gt;			&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.10.2.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(function()&#123;				$(&quot;#voidking&quot;)				.bind(&quot;click&quot;,function()&#123;					alert(&quot;hello voidking once!&quot;);				&#125;)				.bind(&quot;click&quot;,function()&#123;					alert(&quot;hello voidking twice!&quot;);				&#125;)				.bind(&quot;click&quot;,function()&#123;					alert(&quot;hello voidking three times!&quot;);				&#125;);			&#125;);		&lt;/script&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;img id=&quot;voidking&quot; src=&quot;./head.jpg&quot; alt=&quot;头像&quot;/&gt;			&lt;/body&gt;&lt;/html&gt;
由上面两段代码我们发现，绑定事件很简单，只需要一个bind()函数。而且，一个事件可以对应多个处理器。除了bind()函数，jQuery还为创建特定的事件处理器提供了一些便捷方法。比如one、focusin、focusout等。
3.2. 删除事件处理器创建了一个事件处理器，那么它在页面剩余的生命周期里就是有效的。但是一些特殊的交互，要求根据一定标准删除处理器。这时我们就要用到unbind()函数，jQuery考虑的很周到啊！
&lt;html&gt;	&lt;head&gt;			&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.10.2.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(function()&#123;				$(&quot;#voidking1&quot;).bind(&quot;click.voidking&quot;,function()&#123;					alert(&quot;hello voidking1&quot;);					$(&quot;*&quot;).unbind(&#x27;click.voidking&#x27;);				&#125;);								$(&quot;#voidking2&quot;).bind(&quot;click.voidking&quot;,function()&#123;					alert(&quot;hello voidking2&quot;);					$(&quot;*&quot;).unbind(&#x27;click.voidking&#x27;);				&#125;);							&#125;);		&lt;/script&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;img id=&quot;voidking1&quot; src=&quot;./head.jpg&quot; alt=&quot;头像&quot;/&gt;		&lt;img id=&quot;voidking2&quot; src=&quot;./head.jpg&quot; alt=&quot;头像&quot;/&gt;			&lt;/body&gt;&lt;/html&gt;
上面这个例子中，我们实现了一个效果：有任何一张图片被点击了一次，那么两张图都不可以再被点击。
3.3. Event实例使用bind()方法，无论是什么浏览器，Event实例都会作为第一个参数传入函数。
那么，怎么处理不同浏览器中Event实例中属性的差异呢？原来，jQuery定义了一个jQuery.Event对象，真正传入的函数的，是这个对象。这个对象复制了大部分原始Event的属性，忽略了Event实例的差异性。就像是一个接口，我们不需要管它怎么实现，只要调用就好了！
3.4. 预先管理事件处理器当混合使用Ajax时，我们可能在页面的生命周期内频繁引入DOM元素或删除它们。在管理这些动态元素的事件处理器时，就绪处理器就起不到多大作用了，因为这些动态元素在就绪处理器执行时还不存在。
jQuery提供了live()方法，该方法允许预先为那些不存在的元素创建事件处理器。它的语法和bind()非常相似，看上去似乎比bind()更加强大，那么，用live()代替bind()不就好了吗？不是的，首先，“live”事件不是原生的“普通”事件。其次，live()方法只能应用于选择器，不能应用于衍生而来的包装集。
这里不再举例，下面的Ajax会详细探讨。
live()创建的选择器可以使用die()方法来解除绑定，语法和unbind()相似。
3.5. 触发事件处理器触发事件，听起来就很难懂的样子，我们换个说法：模拟用户动作。比如点击，我们可以用代码去模拟用户点击，达到的效果与真实的鼠标点击是一样的。
&lt;html&gt;	&lt;head&gt;			&lt;meta charset=&quot;UTF-8&quot;&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.10.2.js&quot;&gt;&lt;/script&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			$(function()&#123;				$(&quot;#voidking&quot;).bind(&quot;click&quot;,function()&#123;					alert(&quot;hello voidking&quot;);				&#125;);									$(&quot;#voidking&quot;).click();//模拟用户单击事件				$(&quot;#voidking&quot;).trigger(&quot;click&quot;);//模拟用户单击事件				$(&quot;p&quot;).bind(&quot;myEvent&quot;, function (event, message1, message2) &#123;					alert(message1 + &#x27; &#x27; + message2);				&#125;);				$(&quot;p&quot;).trigger(&quot;myEvent&quot;, [&quot;hello&quot;,&quot;trigger&quot;]);	//弹出两次						$(&#x27;p&#x27;).triggerHandler(&quot;myEvent&quot;,[&quot;hello&quot;,&quot;triggerHandler&quot;]); //弹出一次，只触发第一个p元素								$(&quot;input&quot;).select(function()&#123;								&#125;);				$(&quot;#trigger&quot;).click(function()&#123;					$(&quot;input&quot;).trigger(&quot;select&quot;);//触发select事件，执行select默认操作和select处理器内操作				&#125;);				$(&quot;#triggerHandler&quot;).click(function()&#123;					$(&quot;input&quot;).triggerHandler(&quot;select&quot;);//触发select事件，但是不会执行select默认操作				&#125;);			&#125;);			&lt;/script&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;img id=&quot;voidking&quot; src=&quot;./head.jpg&quot; alt=&quot;头像&quot;/&gt;			&lt;p&gt;&lt;/p&gt;		&lt;p&gt;&lt;/p&gt;		&lt;input type=&quot;text&quot;  value=&quot;hello voidking&quot; /&gt;		&lt;br /&gt;		&lt;button id=&quot;trigger&quot;&gt;激活select事件，同时选中文本&lt;/button&gt;&lt;/br&gt;		&lt;button id=&quot;triggerHandler&quot;&gt;激活select事件，不选中文本&lt;/button&gt;	&lt;/body&gt;&lt;/html&gt;
上面这个例子，基本上涵盖了触发事件管理器的所有知识点。这里不详细解释了，自己看注释，不懂的请百度或留言。
3.6. 更多toggle: 这个方法在jQuery 1.8中宣告过时，在jQuery 1.9中已经移除。为了写出一个图片大中小切换效果，各种百度谷歌，最终没搞出来。开始怀疑，toggle过时了，经过查看官方文档，果然！我靠，各位大神，你们13、14年写的教程，居然使用过时的代码……还有你，w3school，该更新了大哥！
4. 结束语以上总结不可能面面俱到，差不多了，就写到这儿吧！
5. 参考文档李炎恢的jQuery视频教程《jQuery攻略（作者B.M.Harwani）》《jQuery实战（作者Bear Bibeault、Yehuda Katz）》《jQuery高级编程（作者 Cesar Otero、Rob Larsen）》《jQuery Javascript 与CSS开发入门经典（作者Richard York）》
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>javascript</tag>
        <tag>jquery</tag>
      </tags>
  </entry>
  <entry>
    <title>jQuery学习笔记——选择器篇</title>
    <url>/dev-jquery-selector/</url>
    <content><![CDATA[1. 选择器语法jQuery选择元素的语法为：$(selector,[content]);
如果第一个参数是选择器，那么第二个参数就是指示该操作的上下文，默认为整个DOM文档。上下文参数可以是DOM元素的引用，也可以包含jQuery选择器的字符串，或者是DOM元素包装集。
上文hello voidking中的html文件内容修改为：
&lt;html&gt;&lt;head&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.10.2.js&quot;&gt;&lt;/script&gt;	&lt;script type=&quot;text/javascript&quot;&gt;		$(function()&#123;			$(&quot;p&quot;,&quot;div#voidking&quot;).css(&quot;color&quot;,&quot;red&quot;);		&#125;);	&lt;/script&gt;&lt;/head&gt;&lt;body&gt;	&lt;div id=&quot;voidking&quot;&gt;		&lt;p&gt;			welcome to jQuery world !		&lt;/p&gt;	&lt;/div&gt;	&lt;p&gt;		welcome to jQuery world !	&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

在浏览器中打开，我们发现，只有第一行是红色的。这是因为，$(“p”,”div#voidking”)指定的范围是：id值为voidking的元素内。
2. CSS基本选择器2.1. id选择器获取一个id为voidking的元素。
在CSS中：
#voidking&#123;	color:red;&#125;
在jQuery中：
$(&quot;#voidking&quot;).css(&quot;color&quot;,&quot;red&quot;);
2.2. class选择器获取所有class为voidking的元素。
在CSS中：
.voidking&#123;	color:red;&#125;
在jQuery中：
$(&quot;.voidking&quot;).css(&quot;color&quot;,&quot;red&quot;);
2.3. 标签选择器获取所有标签名为div的元素。
在CSS中：
div&#123;	color:red;&#125;
在jQuery中：
$(&quot;div&quot;).css(&quot;color&quot;,&quot;red&quot;);
2.4. 群组选择器获取标签名为span、em和class为voidking的所有元素。
在CSS中：
span,em,.voidking&#123;	color:red;&#125;
在jQuery中：
$(&quot;span,em,.voidking&quot;).css(&quot;color&quot;,&quot;red&quot;);

2.5. 后代选择器获取标签名为ul下的标签名为li下的标签名为a的所有元素。
在CSS中：
ul li a&#123;	color:red;&#125;
在jQuery中：
$(&quot;ul li a&quot;).css(&quot;color&quot;,&quot;red&quot;);
或者
$(&quot;ul&quot;).find(&quot;li&quot;).find(&quot;a&quot;).css(&quot;color&quot;, &quot;red&quot;);

2.6. 通配选择器获取所有元素，一般不使用。
在CSS中：
*&#123;	color:red;&#125;
在jQuery中：
$(&quot;*&quot;).css(&quot;color&quot;,&quot;red&quot;);

2.7. 组合上面的六种选择器，已经可以满足大部分的选择需要。而它们还可以结合起来使用。
$(&quot;div.voidking,ul li a&quot;).css(&quot;color&quot;,&quot;red&quot;);

3. CSS高级选择器3.1. 子选择器选择id为voidking的元素下，子标签为p的元素。
在CSS中：
#voidking &gt; p&#123;	color:red;&#125;
在jQuery中：
$(&quot;#voidking &gt; p&quot;).css(&quot;color&quot;,&quot;red&quot;);
或者
$(&quot;#voidking&quot;).children(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);

3.2. 同级下一个选择器选择和id为voidking的元素同级的，下一个标签为p的元素。
在CSS中：
#voidking + p&#123;	color:red;&#125;
在jQuery中：
$(&quot;#voidking + p&quot;).css(&quot;color&quot;,&quot;red&quot;);
或者
$(&quot;#voidking&quot;).next(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);

3.3. 同级所有下面选择器选择和id为voidking的元素同级的，下面所有标签为p的元素。
在CSS中：
#voidking ~ p&#123;	color:red;&#125;
在jQuery中：
$(&quot;#voidking ~ p&quot;).css(&quot;color&quot;,&quot;red&quot;);
或者
$(&quot;#voidking&quot;).nextAll(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);
3.4. PS在find()、children()、next()、nextAll()四个函数中，如果不传入参数，默认为”*”。
建议使用方法而不是符号，理论上讲，使用方法的效率高于使用符号，而且，使用方法更加易读易懂。
接下来的选择器就没有类似于” “、”&gt;”、”+”、”~”这样的符号了，全部由函数来完成。
3.5. 同级上一个选择器选择和id为voidking的元素同级的，上一个标签为p的元素。
$(&quot;#voidking&quot;).prev(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);
3.6. 同级所有上面选择器选择和id为voidking的元素同级的，上面所有标签为p的元素。
$(&quot;#voidking&quot;).prevAll(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);
3.7. 同级上下所有选择器$(&quot;#voidking&quot;).siblings(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);
等价于
$(&quot;#voidking&quot;).prevAll(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);$(&quot;#voidking&quot;).nextAll(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);
3.8. 非指定选择器同级上、下非指定元素选定，遇到则停止。
$(&quot;#voidking&quot;).prevUntil(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);$(&quot;#voidking&quot;).nextUntil(&quot;p&quot;).css(&quot;color&quot;, &quot;red&quot;);

4. 属性选择器4.1. 精确匹配$(&#x27;[id=&#x27;voidking&#x27;]&#x27;).css(&quot;color&quot;,&#x27;red&#x27;);
$(&#x27;div[id=&#x27;voidking&#x27;]&#x27;).css(&quot;color&quot;,&#x27;red&#x27;);

4.2. 精确不匹配$(&quot;p[class != &#x27;voidking&#x27;]&quot;).css(&quot;color&quot;,&#x27;red&#x27;);

4.3. 匹配开头$(&quot;[id ^= &#x27;void&#x27;]&quot;).css(&quot;color&quot;,&quot;red&quot;);

4.4. 匹配结尾$(&quot;[id $= &#x27;king&#x27;]&quot;).css(&quot;color&quot;,&quot;red&quot;);

4.5. 其他


CSS选择器
jQuery选择器
描述



elem[id]
$(“elem[id]”)
选择具有id属性的元素


elem[id |= ‘void’]
$(“elem[id\
= ‘void’]”)


elem[class ~= ‘voidking’]
$(“elem[id ~= ‘voidking’]”)
选择具有class属性，且属性值是一个以空格分格的列表，其中包含voidking的元素


elem[id *= ‘oidki’]
$(“elem[id *= ‘oidki’]”)
选择具有id属性,且属性值中包含”oidki”字串的元素


5. 过滤选择器5.1. 位置选择器


jQuery选择器
描述



$(“li:first”)
返回匹配集合的第一个元素


$(“li:last”)
返回匹配集合的最后一个元素


$(“li:odd”)
返回匹配集合的奇数成员


$(“li:even”)
返回匹配集合的偶数成员


$(“li:eq(3)”)
返回匹配集合的索引值等于3的元素（第4个元素）


$(“li:not(3)”)
返回匹配集合的索引值不等于3的所有元素


$(“li:gt(2)”)
返回匹配集合的索引值大于2的所有元素


$(“li:lt(3)”)
返回匹配集合的索引值小于3的所有元素


5.2. 基本过滤选择器基本过滤选择器包括位置选择器，比位置选择器多了一些东东：| 过滤器 |  描述 || ——-  | ——- || :animated |  选择当前正在执行动画的所有元素 || :header |  选择所有的标题元素，比如h1、h2、h3等 |
5.3. 过滤表单元素


过滤器
描述



:text
选择所有类型为text的元素


:password
选择所有类型为password的元素


:radio
选择所有类型为radio的元素


:checkbox
选择所有类型为checkbox的元素


:checked
匹配所有已被选中的元素


:image
选择所有类型为image的元素


:file
选择所有类型为file的元素


:submit
选择所有类型为submit的元素


:reset
选择所有类型为reset的元素


:button
选择所有button元素和类型为botton的元素


:input
选择所有input、textarea、select和button元素


:selected
选择所有类型已选中的元素


:enabled
选择所有可用元素


:disabled
选择所有不可用元素


5.4. 可见性过滤器


过滤器
描述



:visible
选择所有可见元素


:hidden
选择所有隐藏元素


5.5. 内容过滤器


过滤器
描述



:contains()
选择所有包含特定文本内容的元素


:has()
选择至少含有一个元素与制定选择器匹配的元素


:empty
选择所有不包含子元素或文本的空元素


:parent
选择所有含有子元素或文本节点的元素


5.6. 关系过滤器


过滤器
描述



:first-child
选择每个父元素的第一个子元素


:last-child
选择每个父元素的最后一个子元素


:nth-child
选择每个父元素的第nth-child()个子元素


:only-child
选择具有唯一一个子元素的元素


6. 自定义选择器有些时候，jQuery提供的选择器不够用，我们就需要自己创建选择器。比如，我们需要选择具有绿色背景的元素：
&lt;html&gt;&lt;head&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.10.2.js&quot;&gt;&lt;/script&gt;	&lt;script type=&quot;text/javascript&quot;&gt;		$(function()&#123;			//通过扩展$.expr[&quot;:&quot;]实现自定义选择器			$.expr[&quot;:&quot;].greenbg = function(element)&#123;				return $(element).css(&quot;background-color&quot;) === &quot;green&quot;;			&#125;;				//此处兼容性问题。输出在Firefox和IE中有所不同，Firefox值为0，IE值为1。			alert($(&quot;:greenbg&quot;).length);			$(&quot;:greenbg&quot;).text(&quot;hello voidking&quot;);		&#125;);	&lt;/script&gt;&lt;/head&gt;&lt;body&gt;	&lt;div style=&quot;width:200 ; height:200 ; background-color:green;&quot; &gt;&lt;/div&gt;	&lt;div style=&quot;width:200 ; height:200 ; background-color:red;&quot; &gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;
7. 结束语我靠，选择器这块实在是太难搞了！一天一夜才完成这份总结。内容纯手打，代码测试通过！
8. 参考文档李炎恢的jQuery视频教程《jQuery攻略（作者B.M.Harwani）》《jQuery实战（作者Bear Bibeault、Yehuda Katz）》《jQuery高级编程（作者 Cesar Otero、Rob Larsen）》《jQuery Javascript 与CSS开发入门经典（作者Richard York）》
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>javascript</tag>
        <tag>jquery</tag>
      </tags>
  </entry>
  <entry>
    <title>jQuery学习笔记——概述篇</title>
    <url>/dev-jquery-start/</url>
    <content><![CDATA[1. jQuery是什么jQuery是一个兼容多浏览器的Javascript库，类似于C语言中的”.h”文件和Java中的”.jar” 文件。
核心理念是write less,do more(写得更少,做得更多)。
jQuery是免费、开源的，使用MIT许可协议。jQuery的语法设计可以使开发者更加便捷，例如操作文档对象、选择DOM元素、制作动画效果、事件处理、使用Ajax以及其他功能。除此以外，jQuery提供API让开发者编写插件。其模块化的使用方式使开发者可以很轻松的开发出功能强大的静态或动态网页。
2. jQuery能够做什么1、简化编程。jQuery最大的优点，是简化了Javascript编程。本来需要很多行代码才能完成的功能，使用jQuery，时常一两行就够了。
2、跨平台。使用Javascript开发，是一件痛苦的事情，因为你不得不考虑到各种浏览器的兼容问题。而使用jQuery，你编写的程序可以很容易地实现跨浏览器平台。

3. jQuery简史jQuery在2006年1月由美国人John Resig在纽约的barcamp发布，吸引了来自世界各地的众多JavaScript高手加入，由Dave Methvin率领团队进行开发。
如今，jQuery已经成为最流行的javascript库，在世界前10000个访问最多的网站中，有超过55%在使用jQuery。
在写这篇文章的时候，在谷歌搜索jQuery，返回大约 85,900,000条结果。jQuery每天都有新的官方插件和第三方插件产生，它们不断扩展jQuery的核心功能。
4. hello voidkingC语言写的第一个程序是helloworld，今天，void自恋一把，第一个程序就和自己打招呼了！
1、官网下载jQuery文件（这里我使用的是jquery-1.10.2.js）。
2、新建index.html文件，内容如下：
&lt;html&gt;&lt;head&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.10.2.js&quot;&gt;&lt;/script&gt;	&lt;script type=&quot;text/javascript&quot;&gt;		$(function()&#123;			alert(&quot;hello voidking&quot;);			$(&quot;p&quot;).css(&quot;color&quot;,&quot;red&quot;);		&#125;);	&lt;/script&gt;&lt;/head&gt;&lt;body&gt;	&lt;p&gt;		welcome to jQuery world !	&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;

3、打开浏览器，看到弹出的对话框了吧？大功告成！下面的内容，会对此程序作出解释。
5. $jQuery的一切功能都源自”$”对象，即一个美元符号对象（或美元符号方法），它可以用”jQuery”来代替。
美元符号既是一个对象，也是一个方法。这是因为它具有很多成员属性和方法可以调用，同时可以把它当成一个函数来调用。
6. 延迟加载我们在hellovoidking的代码中，使用$(function(){});进行首尾包裹，那么为什么要包裹这段代码呢？原因是，jQuery库文件是在body元素之前加载的，我们必须等待所有的DOM元素加载后，延迟支持DOM操作，否则就无法获取到。
为了延迟等待加载，JavaScript提供了一个事件为load，方法如下：
window.onload = function()&#123;&#125;; 
而jQuery提供的方法如下：
$(document).ready(function()&#123;&#125;); 
什么东东？和hello voidking中的代码不一样啊？原来，jQuery的延迟加载方法可以简写为：
$(function()&#123;&#125;);

7. jQuery选择器jQuery最核心的组成部分就是：选择器引擎。用iPhone式的格言来说，“让选择器完成一切”就是jQuery的座右铭。
在使用jQuery的任何方法时，首先要做的就是选择页面中要操作的那些元素。
jQuery选择器继承了CSS的语法，可以对DOM元素的标签名、属性名、状态等进行快速准确的选择，并且不必担心浏览器的兼容性。jQuery选择器实现了CSS1~CSS3的大部分规则之外，还实现了一些自定义的选择器，用于各种特殊状态的选择。
选择器部分内容很多，我会在下一篇文章中详细研究探讨。
8. 事件处理对于事件的概念，我在《Javascript学习笔记——基础篇》中已经解释过，这里不再赘述。jQuery的事件处理，内容也很多，我会在接下来的文章中探究。
9. AjaxjQuery的Ajax也是重点，必须的展开。我靠，肿么这么多！别吐槽了帅哥，写教程的void都快哭了，赶着去搞XMPP呢！
10. 调试个人喜欢的工具是：火狐浏览器 + firebug。
11. 结束语jQuery博大精深，还有什么单元测试、插件、特效制作啥的，void就不多说了，感兴趣的小伙伴自己查找资料吧！
12. 参考文档李炎恢的jQuery视频教程《jQuery攻略（作者B.M.Harwani）》《jQuery实战（作者Bear Bibeault、Yehuda Katz）》《jQuery高级编程（作者 Cesar Otero、Rob Larsen）》《jQuery Javascript 与CSS开发入门经典（作者Richard York）》
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>javascript</tag>
        <tag>jquery</tag>
      </tags>
  </entry>
  <entry>
    <title>Javascript学习笔记——实践篇</title>
    <url>/dev-jquery-practice/</url>
    <content><![CDATA[1. 注释本文中的文件名，写在了代码注释中。既然用到了三种文件，就把这三种文件的注释方法先说明一下。
1.1. HTML注释语法&lt;!--注释的内容--&gt;,示例如下：
&lt;!--欢迎来到VoidKing的主页--&gt;

1.2. CSS注释语法/*注释的内容*/，示例如下：
/*这是注释*//*这也是注释可以分段*/

1.3. Javascript注释和C语言相同，语法//注释的内容或者/*注释的内容*/，示例如下：
//这是注释/*这也是注释*/
2. 使用Javascript在html页面中使用Javascript，有三种方法：
2.1. body中body中的Javascript代码，相当于C语言中位于main函数内代码，格式如下：
&lt;!--index.html--&gt;&lt;html&gt;	&lt;head&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;script type=&quot;text/javascript&quot;&gt;		document.write(&quot;voidking.com&quot;);		alert(&quot;voidking.com&quot;);		&lt;/script&gt;	&lt;/body&gt;&lt;/html&gt;
2.2. head中head中的Javascript代码，相当于C语言中位于main函数外的代码，一般是封装好的函数。格式如下：
&lt;!--index.html--&gt;&lt;html&gt;	&lt;head&gt;		&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;charset=utf-8&quot; /&gt;		&lt;script type=&quot;text/javascript&quot;&gt;		function hello()&#123;			var str=prompt(&quot;VoidKing的网址是什么？&quot;,&quot;请在这里输入&quot;);			if(str!=null &amp;&amp; str!=&quot;&quot;)			&#123;				alert(&quot;你输入的是：&quot;+str);			&#125;			else			&#123;				alert(&quot;你什么也没有输入！&quot;);			&#125;		&#125;		&lt;/script&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			hello();		&lt;/script&gt;	&lt;/body&gt;&lt;/html&gt;
2.3. 外部head中的Javascript代码不易维护，所以多数情况下我们会使用外部引用来代替。格式如下：
&lt;!--index.html--&gt;&lt;html&gt;	&lt;head&gt;		&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;charset=utf-8&quot; /&gt;		&lt;script type=&quot;text/javascript&quot; src=&quot;javascript.js&quot;&gt;&lt;/script&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;script type=&quot;text/javascript&quot;&gt;			hello();		&lt;/script&gt;	&lt;/body&gt;&lt;/html&gt;
//javascript.jsfunction hello()&#123;			var str=prompt(&quot;VoidKing的网址是什么？&quot;,&quot;请在这里输入&quot;);			if(str!=null &amp;&amp; str!=&quot;&quot;)			&#123;				alert(&quot;你输入的是：&quot;+str);			&#125;			else			&#123;				alert(&quot;你什么也没有输入！&quot;);			&#125;		&#125;
3. 使用CSS上面我们已经说完了使用Javascript的三种方式，爱思考的小伙伴肯定想到了CSS的使用方法，在这里，我也总结一下。
3.1. 内联样式当特殊的样式需要应用到个别元素时，就可以使用内联样式。格式如下：
&lt;!--index.html--&gt;&lt;html&gt;	&lt;head&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;p style=&quot;color:red;margin-left:20px&quot;&gt;		This is a paragraph.		&lt;/p&gt;	&lt;/body&gt;&lt;/html&gt;

3.2. 内部样式当单个文件需要特别样式的时候，就可以使用内部样式表。格式如下:
&lt;!--index.html--&gt;&lt;html&gt;	&lt;head&gt;		&lt;style type=&quot;text/css&quot;&gt;		body&#123;background-color:red&#125;		p&#123;margin-left:20px&#125;		&lt;/style&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;p&gt;		This is a paragraph.		&lt;/p&gt;	&lt;/body&gt;&lt;/html&gt;
3.3. 外部样式当样式需要被应用到很多页面的时候，外部样式表最适合。格式如下：
&lt;!--index.html--&gt;&lt;html&gt;	&lt;head&gt;		&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;style.css&quot;/&gt;	&lt;/head&gt;	&lt;body&gt;		&lt;p&gt;		This is a paragraph.		&lt;/p&gt;	&lt;/body&gt;&lt;/html&gt;
/*style.css*/body&#123;background-color:red&#125;p&#123;margin-left:20px&#125;

4. 借花献佛本来想自己整理出一批好的例子，但是，无意中发现了一个网站，已经做得非常好了。所以，void在这里就偷懒一下。例子精品，还附有教程讲解，分享给大家——梦之都！
5. 结束语这篇文章比预期的用时要短上很多，主要因为，我把痛苦的工作全部交给梦之都了，好机智地说！还想写个提高篇来着，但是能力有限，不能误人子弟啊！所以，Javascript笔记至此结束，接下来，该是jQuery了！
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>javascript</tag>
      </tags>
  </entry>
  <entry>
    <title>Javascript学习笔记——基础篇</title>
    <url>/dev-javascript-base/</url>
    <content><![CDATA[1. 什么是JavascriptJavascript是一种基于对象的脚本语言。
基于对象（Object-Based）不提供抽象、继承、重载等有关面向对象语言的功能。而是把其他语言创建的对象统一起来，形成一个对象系统，以供使用。
脚本语言最大的特点就是不需要编译和链接。传统编程语言有四个步骤“编写-&gt;编译-&gt;链接-&gt;运行”，而脚本语言只有两个步骤“编写 -&gt; 运行”。
脚本语言是解释执行而非编译执行。windows下，命令提示符界面，就是输入脚本语言的shell；经常见到的“*.bat”批处理文件，就是脚本文件。而在linux系统里面，脚本、脚本编程的概念更是常见。
shell，提供用户使用界面的软件。在windows中，win+R，输入cmd，出现的那个黑黝黝的窗口就是一个shell；打开任务管理器，看到的那个explorer.exe程序，也是一个shell，它叫做GUI shell。在linux中，如果不使用图形用户界面，那么，你所看到的，就是一个shell，一般的linux系统都会提供几种shell供你选择；而如果使用图形用户界面，你看到的界面，就是一个GUI shell。

2. 什么是对象一切都是对象。比如“你”就是一个对象，你拥有姓名、性别、身高、体重等等属性信息，也有跳跃、奔跑、吃饭、睡觉等等方法。
Javascript中使用的对象，分为内置对象和自定义对象。
常见的内置对象有Array、String、Math、Date、Document、Form、Anchor、Link、Image、Windows、Screen、Navigator、Location、History、Frame、DOM、RegExp、StyleSheet、Event、FileSystemObject、Drive、Folder、File、XMLHttpRequest、Error……
3. Javascript出现的原因1、表单验证。早期的验证全部在服务端，比如注册时需要输入邮箱，而邮箱有固定格式，中间有一个“@”。为了验证一个邮箱格式，就需要发送请求给服务端，服务端的压力很大，多么令人蛋疼。为了减轻服务器的压力，需要一种可以运行在客户端的精小高效的语言。
2、网页交互性。随着互联网的发展，单纯的网页浏览已经无法满足用户的需求，越来越多的用户渴望与网页进行交互。为了改善用户的交互体验，需要开发一种语言，而这种语言运行在服务端是不合适的。
火药的发明，最初并没有想到可以用来打仗。Javascript的作者也没想到，Javascript能够发展到现在这么强大：（1）在浏览器的状态栏或者警告框里，向访问者显示信息。（2）验证表单内容。（3）当访问者将鼠标指针移动到图像上面时，自动替换图像。（4）创建与访问者交互的广告栏，而不仅仅是显示一幅图像。（5）检测可用浏览器或其属性，并且只在支持它们的浏览器上运行高级功能。（6）检测已安装的插件，并在需要某一插件时通知访问者。（7）在不需要访问者重新加载网页的情况下，修改整个或部分网页。（8）显示从远程服务器检索到的数据，或者与远程服务器交互数据。……总结一下，大致分为表单验证、网页特效、浏览器检测。
4. 语法汉语有语法，英语有语法，编程语言也有语法，Javascript也不例外。
语法，简而言之，就是语言表达的规则。你说“我真帅！”，大家理解你的意思，但是你说“帅真我！”，就没有人明白了！同样的，只有遵循一定的规则，浏览器才能明白你写的某句代码的意思。
4.1. 数据类型数值型、字符串、布尔型、null、undefined、对象、数组
4.2. 变量和常量和C、Java基本相同，不多解释，如果你没有学过编程，就按照数学中的理解就好了。
4.3. 运算符和表达式和C、Java基本相同，不懂的同学请自行百度、读书，此处不展开了。
4.4. 流程和C、Java基本相同，学过C、Java的同学请自行跳过本节。
其实所有的编程语言的执行流程都可以分为四种：（1）顺序。排队买饭，就是一个顺序流程。
（2）条件。假设有两个窗口，窗口一卖饭，窗口二卖汤。如果你想打饭，就去窗口一；如果你想打汤，就去窗口二。这里的“想打饭”和“想打汤”就是条件。
（3）循环。排队买饭，终于排到我了，这时我发现钱不够，于是我回宿舍拿钱；之后回来排队，终于又排到我了，钱还是不够，于是我又回宿舍拿钱……这个过程，就是循环。终于有一次，我的钱拿够了，买到了饭，这个循环就结束了。
（4）其他。比如continue、break、异常处理等。continue，排队买饭，还没有排到我，我发现钱没带，于是提前结束本次排队，回宿舍拿钱，回来后重新排队。break，排队买饭，还没有排到我，突然不想买了，于是结束排队。异常处理，排队买饭，突然收到一个紧急电话，必须去跑1000米。这个紧急电话，就是一个异常，跑1000米，就是处理。
4.5. 函数和程序函数是用来实现一个功能的程序块。
举个例子，打电话告诉老爸缺钱了。这个过程，有两个动作，“打电话”和“告诉”。这里的“打电话”和“告诉”就是两个函数，也就是两个程序块。
程序块是什么？那些用大括号括起来的代码就是程序块。
程序呢？程序就是命令序列的集合。通俗一点说，程序就是做一件事情的步骤的集合。
“打电话”是一个程序，可以分为这样几个步骤：掏出手机，找到号码，拨号，等待接通。
当然你可以分的更细，比如“掏出手机”也可以看成一个程序，分为这样几个步骤：伸手拿到手机，提高一厘米，提高一厘米，提高一厘米……
4.6. 闭包闭包是啥玩意？各种专业文档中给出了void根本看不懂的定义。这里借用阮一峰的理解：闭包就是能够读取和保持其他函数内部变量的函数。
1、作用域要理解闭包，首先必须理解Javascript的变量作用域。变量作用域无非两种：全局变量和局部变量。
Javascript特有“链式作用域”结构。js寻找变量的定义会从最近的区域开始，本地找不到就往上一层区域，直到找到命名空间的顶端，在浏览器的世界里顶端就是window对象了。所以，父对象的所有变量，对子对象都是可见的，反之则不成立。
2、一个例子
&lt;html&gt;	&lt;head&gt;		&lt;/head&gt;	&lt;body&gt;		&lt;script &gt;		function f1()&#123;				var n = &quot;hello voidking&quot;;				function f2()&#123;					alert(n);				&#125;				return f2;				&#125;				var result = f1();				result();		&lt;/script&gt;	&lt;/body&gt;&lt;/html&gt;
运行这段代码，浏览器弹出了“hello voidking”对话框。
这段代码中的f2，就是一个闭包。
虽然外部函数已经返回，但是内部函数仍然记得外部函数定义的变量。
由于在Javascript中，只有函数内部的子函数才能读取局部变量，因此可以把闭包简单理解成“定义在一个函数内部的函数”。
在本质上，闭包就是将函数内部和函数外部连接起来的一座桥梁。功能类似于Java对象中的set和get函数。
3、传引用闭包可以更新外部变量的值。
Javascript中函数的传参是传值，而不是传引用。而在闭包中，其存储的是一个对外部变量的引用。
5. 正则表达式说到正则表达式，很多人会想到通配符。正则表达式和通配符的不同在哪里呢？
1、通配符是系统级的，在bash shell界面下可以直接使用；正则表达式是需要工具支持的，比如grep、sed、awk等工具。2、通配符一般用来查找文件、文件夹；正则表达式的匹配更加精确，主要用来文本过滤和字符串的操作。
Javascript对于正则表达式有很好的支持，也就是说，我们可以很方便的进行文本过滤和字符串操作。比如开篇提到的邮箱格式验证，我们就可以用正则表达式来实现。
6. DOMDOM，文档对象模型（Document Object Model）。
DOM独立于语言和平台，是一套标准接口，用来对XML和HTML文档进行增删查改。
DOM规范的核心就是树模型，对于要解析的HTML文档，解析器会把HTML文档加载到内存中，在内存中为HTML文件建立逻辑形式的节点树。而每一个节点，代表一个可以进行交互的对象。
7. XMLXML，可扩展标记语言（eXtensible Markup Language）。
它的格式很HTML很像，但是，HTML的标记是固定的，不区分大小写；而XML的标记是自定义的，区分大小写的。
XML文档结构分为3个部分：序言、主体和尾声。其中尾声可有可无。如果把一个HTML文档看做是一个主体（一棵树），那么，XML和HTML文档结构的不同，就在于XML多了一个序言，而且，主体可能不止一棵树。
8. 事件编程语言中的事件，我们可以简单理解为事情。
当一件事情发生，我们可能会产生反应，也许不会。
比如，你们的老师让你们写一篇实验报告，这就是一个事件。这个事件发生后，你就会去写实验报告，这就是反应。也许你会吐槽，告诉远方的某位姑娘，老师多么无聊，让你们写实验报告。这时，虽然姑娘也知道了这件事，但是微微一笑（没有去写报告吧），或者连反应都没有。而更多的不知道这个事件的人，当然就当做没发生。
Javascript中的事件，提供了与窗口以及当前加载文档交互的基础。而Javascript对事件的处理，也就是通常所说的反应。
9. CookieCookie（小甜饼）是由浏览器存储在客户端系统的文本，而且与浏览器的每次请求一同发送到服务器。使用Cookie可以方便地帮助Web服务器保存有关访客的信息。
Cookie常用于以下场合：（1）保存用户登录状态（2）跟踪用户行为（3）定制界面（4）创建购物车
10. AjaxAjax，异步Javascript和XML（Asynchronous Javascript and XML），被称为远程脚本技术。
Javascript在早期，和服务器通信的方法只有一个——提交表单，远程脚本技术使两者之间的通信变得更加丰富。它可以使Javascript超越客户端的界限，使其能够处理Web服务器上的文件。
特点：局部更新，节省带宽，提高加载速度。
11. 结束语看完上面的概念，是不是在想：什么玩意儿？看不懂没关系，知道有那么一回事就行，下一篇实践篇将会告诉你Javascript到底怎么玩。以上内容也许理解失误的地方，感谢大家留言指正。
12. 参考文档李炎恢的Javascript视频教程《Javascript完全学习手册（作者张银鹤等）》《Javascript基础教程（第七版，作者Tom Negrino、Dori Smith）》《Javascript开发技术详解（作者李峰、晁阳）》一些技术大牛的博客……
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>javascript</tag>
      </tags>
  </entry>
  <entry>
    <title>《超越Java》</title>
    <url>/dev-beyond-java/</url>
    <content><![CDATA[

题记：没有语言可以永远保持领先地位，Java统治的时代将会结束，这不是“会不会”的问题，而是“什么时候”的问题。

不久前，我以为，Java会是最有前途的编程语言！不久前，我以为，Node.js非常强大！不久前，我以为，编程很苦！
现在，我发现，Java可以超越！现在，我发现，Node.js在很多方面比不上Fib.js！现在，我发现，编程依旧很苦！
之前写过一篇《发展方向的思考》，现在看来，如此幼稚。那么，我的发展方向又在哪里呢？不再定下什么具体的语言，或者前端后端之类。我的发展方向，在于掌握学习的能力！没有什么语言和技术是不会过时的，我要掌握的，是一种快速有效学习的能力！比如，接触一门新的语言、新的工具，能够在一周或者更短的时间内上手！
简而言之四个字：学会学习！


1. Java从哪里来？到哪里去？1991年，sun公司的James Gosling领导的绿色计划，着力开发一种分布式系统结构，使其能够在各种消费性电子产品上运行，比如机顶盒。刚开始的时候使用C++，后来发现C++的各种不足，非常蛋疼，于是研发了Oak（橡树）来代替C++。
1994年，因特网迅速发展，工业界对适合在网络异构环境下使用的语言有一种非常急迫的需求。这时James决定对Oak进行小规模的改造，于是，1995年Java诞生。（名字取义自开发团队最喜欢喝的咖啡产地——Java。）
Java拥有巨大潜力，发展至今，已经成为全世界最流行的编程语言，未来也将无处不在。但是，它不可能一直保持领先地位，终究会被其他更优秀的语言取代。
2. Java的现状1、Java已经结束了领导趋势（大概在2005年），不再是创新的源头。2、Java不止一个接班者，未来Java仍然是无处不在的语言。3、Java已经远离它的基础，大型企业的问题获取很容易解决，但是简单的问题反而很难解决。Ruby on Rails 在某些例子中，效率远远高于Java。4、复杂，门槛高。建立一个简单的web应用程序，使用最普遍的框架，你必须学会一箩筐的知识。5、不自然的延伸，比如AOP或依赖注入。6、Java泛型的实现不足取。
1、Java社区很多很活跃。2、拥有最多的商业厂商和项目。3、开源项目非常多。4、可移植性好。（JVM比Java更重要）
3. 什么是分布式系统？“分布式的”意味着计算的成本或性能取决于数据和控制的通信。
如果一个系统的部件局限在一个地方，它就是集中式的：如果它的部件在不同地方，部件之间要么不存在或仅存在有限的合作，要么存在紧密的合作，它是分散式的。
当一个分散式系统不存在或仅存在有限的合作时，它就被称作网络的；否则它就被称作分布式的，表示在不同地方的部件之间存在紧密的合作。在给出分布式系统具体定义的模型中，分布式系统可以用硬件、控制、数据这三个维度加以检验。
分布式系统=分布式硬件+分布式控制+分布式数据
分布式系统有很多不同的定义，但其中没有一个是令人满意或者能够被所有人接受的。介绍分布式系统，对它的特点的下列大致的描述足够了：
一个分布式系统是一些独立的计算机的集合，但是对这个系统的用户来说，系统就象一台计算机一样。
这个定义有两个方面的含义：第一，从硬件角度来讲，各个计算机都是自治的；第二，从软件角度来讲，用户将整个系统看作是一台计算机。这两者都是必需的，缺一不可。
4. POJO和EJBPOJO（Plain Old Java Objects）简单的Java对象，实际就是普通JavaBeans，是为了避免和EJB混淆所创造的简称。
使用POJO名称是为了避免和EJB混淆起来, 而且简称比较直接. 其中有一些属性及其getter setter方法的类,没有业务逻辑，有时可以作为VO(value -object)或dto(Data Transform Object)来使用.当然,如果你有一个简单的运算属性也是可以的,但不允许有业务方法,也不能携带有connection之类的方法。
JavaBean定义：一种用Java语言写的可重用组件（是具有某种功能，或者处理某个业务的对象）。
特点：共有类、无参构造、私有属性（共有的get、set方法）、可以有方法，但不作为业务逻辑
优点：Write once，run anywhere，reuse everywhere。（WORA）
EJB（Enterprise JavaBean）定义：EJB 从技术上而言不是一种”产品”，而是一种描述了构建应用组件要解决的标准。可扩展 (Scalable)、分布式 (Distributed)、事务处理 (Transactional)、数据存储 (Persistent)、安全性 (Secure)。EJB有EntityBean、SessionBean、MessageDrivernBean三种。
EJB的详解参见点滴之水的博客，讲的非常好。
5. Java纯面向对象？Java的基本类型并非继承自Object，所以Java是混种语言，而非纯面向对象语言。当初基本类型的引入，完全是为了吸引C++开发者。
6. 系统编程语言系统编程语言，顾名思义，这个语言和操作系统联系很密切。比如C++，它可以精确的控制每个内存单元。但是在应用程序的开发中，根本不需要处理这些底层的细节，比如使用Java开发一个超级玛丽游戏。因此，按照我的理解，系统编程语言是可以处理操作系统细节的编程语言。
7. 面向对象编程的三大特点封装、继承和多态。为什么是这三大特点，而不是别的？我想，是因为，面向对象编程语言先出现，之后大家才总结出来这三大特点。而不是定好了三大特点，才开始设计面向对象编程语言。
以下内容引用自Veda原型
7.1. 封装封装是对象和类概念的主要特性。
封装，也就是把客观事物封装成抽象的类，并且类可以把自己的数据和方法只让可信的类或者对象操作，对不可信的进行信息隐藏。
7.2. 继承面向对象编程(OOP)语言的一个主要功能就是“继承”。继承是指这样一种能力：它可以使用现有类的所有功能，并在无需重新编写原来的类的情况下对这些功能进行扩展。
通过继承创建的新类称为“子类”或“派生类”，被继承的类称为“基类”、“父类”或“超类”。继承的过程，就是从一般到特殊的过程。
要实现继承，可以通过“继承”（Inheritance）和“组合”（Composition）来实现。在某些OOP语言中，一个子类可以继承多个基类。但是一般情况下，一个子类只能有一个基类，要实现多重继承，可以通过多级继承来实现。
继承概念的实现方式有三类：实现继承、接口继承和可视继承。1、实现继承是指使用基类的属性和方法而无需额外编码的能力；2、接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力；3、可视继承是指子窗体（类）使用基窗体（类）的外观和实现代码的能力。
在考虑使用继承时，有一点需要注意，那就是两个类之间的关系应该是“属于”关系。例如，Employee 是一个人，Manager 也是一个人，因此这两个类都可以继承 Person 类。但是 Leg 类却不能继承 Person 类，因为腿并不是一个人。
抽象类仅定义将由子类创建的一般属性和方法，OO开发范式大致为：划分对象→抽象类→将类组织成为层次化结构(继承和合成) →用类与实例进行设计和实现几个阶段。
7.3. 多态多态性（polymorphism）是允许你将父对象设置成为和一个或更多的他的子对象相等的技术，赋值之后，父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说，就是一句话：允许将子类类型的指针赋值给父类类型的指针。
实现多态，有二种方式，覆盖，重载。1、覆盖(override)，是指子类重新定义父类的虚函数的做法。2、重载(overload)，是指允许存在多个同名函数，而这些函数的参数表不同（或许参数个数不同，或许参数类型不同，或许两者都不同）。
其实，重载的概念并不属于“面向对象编程”，重载的实现是：编译器根据函数不同的参数表，对同名函数的名称做修饰，然后这些同名函数就成了不同的函数（至少对于编译器来说是这样的）。
比如有两个同名函数：function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的：int_func、str_func。对于这两个函数的调用，在编译器间就已经确定了，是静态的（记住：是静态）。也就是说，它们的地址在编译期就绑定了（早绑定），因此，重载和多态无关！真正和多态相关的是“覆盖”。当子类重新定义了父类的虚函数后，父类指针根据赋给它的不同的子类指针，动态（记住：是动态！）的调用属于子类的该函数，这样的函数调用在编译期间是无法确定的（调用的子类的虚函数的地址无法给出）。因此，这样的函数地址是在运行期绑定的（晚绑定）。结论就是：重载只是一种语言特性，与多态无关，与面向对象也无关！引用一句Bruce Eckel的话：“不要犯傻，如果它不是晚绑定，它就不是多态。”
那么，多态的作用是什么呢？我们知道，封装可以隐藏实现细节，使得代码模块化；继承可以扩展已存在的代码模块（类）；它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用！多态的作用，就是为了类在继承和派生的时候，保证使用“家谱”中任一类的实例的某一属性时的正确调用。
8. 面向对象和基于对象的区别它们最大的区别在于：基于对象，没有继承！
9. OOP和AOP以下内容来自丁成云的博客
面向对象的编程（OOP）方法是在面向过程的编程方法基础上进行的改进，而面向方面编程（AOP）方法又是在面向对象编程（OOP）方法的基础上进行改进而来的一种创新的软件开发方法。
9.1. 区别：面向领域不同。AOP和OOP是面向不同领域的两种设计思想。
OOP（面向对象编程）针对问题领域中以及业务处理过程中存在的实体及其属性和操作进行抽象和封装，面向对象的核心概念是纵向结构的，其目的是获得更加清晰高效的逻辑单元划分。
而AOP则是针对业务处理过程中的切面进行提取，例如，企业开发中经常会面临的种种非功能性需求（操作日志、权限控制、性能监测等等），用面向对象的思路，将业务操作对象的核心功能和对它的其他服务性功能代码分离，即某一个操作在各个模块中都有涉及，这个操作就可以看成“横切”存在于系统当中。在许多情况下，这些操作都是与业务逻辑相关性不强或者不属于逻辑操作的必须部分，而面向对象的方法很难对这种情况做出处理。 
AOP则将这些操作与业务逻辑分离，使程序员在编写程序时可以专注于业务逻辑的处理，而利用AOP将贯穿于各个模块间的横切关注点自动耦合进来。 
AOP所面对的是处理过程中的某个步骤或阶段，对不同的阶段领域加以隔离，已获得逻辑过程中各部分之间低耦合性的隔离效果，其与面向方面编程在目标上有着本质的差异。 
AOP的核心思想就是将应用程序中的业务逻辑处理部分同对其提供支持的通用服务，即所谓的“横切关注点”进行分离，这些“横切关注点”贯穿了程序中的多个纵向模块的需求。
9.2. 关系：AOP是OOP的延续和补充。
AOP与OOP并不是相互竞争的两种技术,人们不是为了代替OOP而提出AOP,事实上AOP与OOP两者互相之间是一个很好的补充和完善。
OOP面向对象编程关注的是将需求功能“垂直”划分为不同的并且相对独立,封装良好的类,并让它们有着属于自己的行为。至于对象间的关系则依靠继承和多态等来定义，即OOP引入封装、继承和多态性等概念来建立一种对象层次结构，用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候，OOP则显得无能为力。也就是说，OOP允许你定义从上到下的关系，但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中，而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码，如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切（cross-cutting）代码，在OOP设计中，它导致了大量代码的重复，而不利于各个模块的重用。
而AOP技术则恰恰相反，它利用一种称为“横切”的技术，将OOP构建的庞大的类体系结构进行进一步的“水平”切割，并将那些影响了多个类的公共行为封装到一个可重用模块，封装成“Aspect”，即方面。所谓“方面”，简单地说，就是将那些与业务无关，却为业务模块所共同调用的逻辑或责任封装起来，便于减少系统的重复代码，降低模块间的耦合度，并有利于未来的可操作性和可维护性。 
AOP代表的是一个横向的关系，如果说“对象”是一个空心的圆柱体，其中封装的是对象的属性和行为；那么面向方面编程的方法，就仿佛一把利刃，将这些空心圆柱体剖开，以获得其内部的消息。而剖开的切面，也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原，不留痕迹。
面向方面编程则是希望能够将OOP构建的庞大的类体系结构进行进一步的“水平”切割,将通用需求功能从不相关的类当中分离出来, 封装成方面。
10. SOA面向服务架构。
11. REST表述性状态转移（英文：Representational State Transfer，简称REST）是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。它是一种针对网络应用的设计和开发方式，可以降低开发的复杂性，提高系统的可伸缩性。
12. 复杂的问题驱动更高的抽象机器语言 -&gt;  汇编 -&gt; 结构化编程（C） -&gt; 面向对象编程（Java） -&gt; ?
13. 元编程元编程（Metaprogramming）是指某类计算机程序的编写，这类计算机程序编写或者操纵其他程序（或者自身）作为它们的数据，或者在运行时完成部分本应在编译时完成的工作。很多情况下比手工编写全部代码相比工作效率更高。编写元程序的语言称之为元语言，被操作的语言称之为目标语言。一门语言同时也是自身的元语言的能力称之为反射。
14. 泛型编程泛型编程（Generic Programming）最初提出时的动机很简单直接：发明一种语言机制，能够帮助实现一个通用的标准容器库。
15. 四个准则下一个成功的编程语言必须满足四个主要的四个准则：1、需要建立一个明显的社区。只有让采用者安心，他才会去使用此技术。2、需要具备可移植性，Java虚拟机已经提高了后继语言的门槛。3、需要提供经济上的动力。目前，生产力对我来说，看起来就像是经济上的动机，但还是有一些其他的动机在引诱着我，像无线运算以及数据搜索。4、它需要展示技术优点，这才是最重要的一点。
16. 知识欠缺Ruby、Perl、AOP（aspect-oriented programming）、Ruby on Rails、Python、Smalltalk、ObjC、Subversion……
17. 结束语这是一本冲击思维，开启世界的书！上面的摘录，无法展现本书的十之一二。
本书并非是评判java的不足，而是从某种高度上讲述了java以及另外几种语言的优缺点、适合领域，并谈了下一代语言要具备什么特点。值得每个程序员认真读一下！
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>笔记</tag>
      </tags>
  </entry>
  <entry>
    <title>IoC-DI的Java编程实现</title>
    <url>/dev-ioc-di-experiment/</url>
    <content><![CDATA[1. 实验要求加深对控制反转和依赖注入的理解，使用Spring框架实现一个小的demo。
2. 实验题目IoC-DI的Java编程实现及Spring程序设计与实现
3. 实验原理IoC（控制反转）是一种软件设计模式，遵从了DIP（依赖倒置原则）。DI（依赖注入），是IoC的实现方式。它提供一种机制，将需要依赖（低层模块）对象的引用传递给被依赖（高层模块）对象。IoC容器：依赖注入的框架，用来映射依赖，管理对象创建和生存周期（DI框架）。Spring框架：IoC容器。Spring的IOC容器主要使用DI方式实现的。不需要主动查找，对象的查找、定位和创建全部由容器管理。

设计如下：数据层提供接口IPersonDao，业务逻辑层提供接口IPersonService。IPersonService的实现PersonService中通过Spring容器调用IPersonDao。测试类PersonServiceTest，通过Spring容器调用IPersonService。
目录结构：
4. 实验代码//IPersonDao.javapackage com.voidking.ioc.dao;public interface IPersonDao &#123;		public void save();	&#125;

//PersonDaopackage com.voidking.ioc.dao.impl;import com.voidking.ioc.dao.IPersonDao;public class PersonDao implements IPersonDao &#123;	@Override	public void save() &#123;		System.out.println(&quot;------------from PersonDao.save()&quot;);	&#125;&#125;

//IPersonService.javapackage com.voidking.ioc.service;public interface IPersonService &#123;		public void processSave();&#125;

//PersonServiceTest.javapackage com.voidking.ioc.service;import org.junit.Before;import org.junit.Test;import org.springframework.beans.factory.BeanFactory;import org.springframework.context.support.ClassPathXmlApplicationContext;public class PersonServiceTest &#123;	private BeanFactory factory = null;		@Before	public void before() &#123;		factory = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);	&#125;		@Test	public void testSpring() &#123;		IPersonService personService = (IPersonService) factory.getBean(&quot;personService&quot;);		personService.processSave();	&#125;&#125;

//PersonService.javapackage com.voidking.ioc.service.impl;import com.voidking.ioc.dao.IPersonDao;import com.voidking.ioc.service.IPersonService;public class PersonService implements IPersonService &#123;	private IPersonDao personDao;		public void setPersonDao(IPersonDao personDao) &#123;		this.personDao = personDao;	&#125;	@Override	public void processSave() &#123;		System.out.println(&quot;-------------from PersonService.processSave()&quot;);				personDao.save();	&#125;&#125;


applicationContext.xml配置文件内容：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;	xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot; xmlns:context=&quot;http://www.springframework.org/schema/context&quot;	xsi:schemaLocation=&quot;     http://www.springframework.org/schema/beans      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd     http://www.springframework.org/schema/tx      http://www.springframework.org/schema/tx/spring-tx-3.0.xsd     http://www.springframework.org/schema/aop      http://www.springframework.org/schema/aop/spring-aop-3.0.xsd     http://www.springframework.org/schema/context     http://www.springframework.org/schema/context/spring-context-3.0.xsd&quot;&gt;	&lt;bean id=&quot;personDao&quot; class=&quot;com.voidking.ioc.dao.impl.PersonDao&quot;&gt;&lt;/bean&gt;		&lt;bean id=&quot;personService&quot; class=&quot;com.voidking.ioc.service.impl.PersonService&quot;&gt;		&lt;property name=&quot;personDao&quot; ref=&quot;personDao&quot;&gt;&lt;/property&gt;	&lt;/bean&gt;	&lt;/beans&gt;

pom.xml配置文件内容：
&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;  xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd&quot;&gt;  &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;  &lt;groupId&gt;com.voidking.ioc&lt;/groupId&gt;  &lt;artifactId&gt;ioc&lt;/artifactId&gt;  &lt;packaging&gt;war&lt;/packaging&gt;  &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;  &lt;name&gt;ioc Maven Webapp&lt;/name&gt;  &lt;url&gt;http://maven.apache.org&lt;/url&gt;    &lt;dependencies&gt;    &lt;dependency&gt;      &lt;groupId&gt;junit&lt;/groupId&gt;      &lt;artifactId&gt;junit&lt;/artifactId&gt;      &lt;version&gt;4.11&lt;/version&gt;    &lt;/dependency&gt;        &lt;!-- 添加Spring依赖 --&gt;    &lt;dependency&gt;		&lt;groupId&gt;org.springframework&lt;/groupId&gt;		&lt;artifactId&gt;spring-core&lt;/artifactId&gt;		&lt;version&gt;3.1.1.RELEASE&lt;/version&gt;	&lt;/dependency&gt;		&lt;dependency&gt;		&lt;groupId&gt;org.springframework&lt;/groupId&gt;		&lt;artifactId&gt;spring-beans&lt;/artifactId&gt;		&lt;version&gt;3.1.1.RELEASE&lt;/version&gt;	&lt;/dependency&gt;		&lt;dependency&gt;		&lt;groupId&gt;org.springframework&lt;/groupId&gt;		&lt;artifactId&gt;spring-context&lt;/artifactId&gt;		&lt;version&gt;3.1.1.RELEASE&lt;/version&gt;	&lt;/dependency&gt;        &lt;dependency&gt;		&lt;groupId&gt;org.springframework&lt;/groupId&gt;		&lt;artifactId&gt;spring-jdbc&lt;/artifactId&gt;		&lt;version&gt;3.1.1.RELEASE&lt;/version&gt;	&lt;/dependency&gt;	  &lt;/dependencies&gt;  &lt;build&gt;    &lt;finalName&gt;ioc&lt;/finalName&gt;  &lt;/build&gt;&lt;/project&gt;

5. 源代码下载https://github.com/voidking/ioc.git
6. 参考文档http://www.cnblogs.com/niuniu1985/archive/1646375.htmlhttp://www.cnblogs.com/liuhaorain/p/3747470.htmlhttp://blog.csdn.net/m13666368773/article/details/8053138
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>spring</tag>
      </tags>
  </entry>
  <entry>
    <title>Notepad++使用技巧</title>
    <url>/hobby-notepadpp-skill/</url>
    <content><![CDATA[1. Notepad++比较文本内容参考文本内容比较 Notepad++。
插件，Plugin Manager，Show Plugin Manager，找到Compare，Install。
打开需要对比的两个文件，插件，Compare，Compare。或者直接使用Alt+D。


2. NotePad++高亮Markdown参考写了一个Notepad++的markdown插件、notepad++安装markdown插件和How to use markdown in notepad++，安装markdown插件。
1、访问Edditoria的github，下载markdown-plus-plus。
2、拷贝userDefineLang.xml到Notepad++的安装目录（郝同学的是D:\Program Files\Notepad++），重启Notepad++。可以看到，在语言选项中，多出了Markdown。
3、出现了问题，安装完成没有效果，重启也无效。原来，notepad++有一个缓存文件夹：C:\Users\Administrator\AppData\Roaming\Notepad++，直接在这个文件夹中替换userDefineLang.xml，再次打开一个.md文件查看，格式果然出现了变化。所以，如果你要替换自定义的markdown语言格式，建议你同时替换上述两个文件夹下的文件。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>markdown</tag>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>为什么MVC不是一种设计模式？</title>
    <url>/dev-mvc-design-pattern/</url>
    <content><![CDATA[GoF (Gang of Four，四人组，《Design Patterns: Elements of Reusable Object-Oriented Software》/《设计模式》一书的作者：Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)并没有把MVC提及为一种设计模式，而是把它当做“一组用于构建用户界面的类集合”。
在他们看来，它其实是其它三个经典的设计模式的演变：观察者模式(Observer)(Pub/Sub),策略模式(Strategy)和组合模式(Composite)。
根据MVC在框架中的实现不同可能还会用到工厂模式(Factory)和装饰器(Decorator)模式。
models表示应用的数据，而views处理屏幕上展现给用户的内容。为此，MVC在核心通讯上基于推送/订阅模型(惊讶的是在很多关于MVC的文章中并没有提及到)。当一个model变化时它对应用其它模块发出更新通知(“publishes”)，订阅者(subscriber)——通常是一个Controller，然后更新对应的view。观察者——这种自然的观察关系促进了多个view关联到同一个 model。
对于感兴趣的开发人员想更多的了解解耦性的MVC(根据不同的实现)，这种模式的目标之一就是在一个主题和它的观察者之间建立一对多的关系。当这个主题改变的时候，它的观察者也会得到更新。Views和controllers的关系稍微有点不同。Controllers帮助views对不同用户的输 入做不同的响应，是一个非常好的策略模式列子。
摘自文档：http://damoqiongqiu.iteye.com/blog/1949256
]]></content>
      <categories>
        <category>engineering</category>
        <category>architecture</category>
      </categories>
  </entry>
  <entry>
    <title>团队博客项目（二）</title>
    <url>/dev-team-blog-project-2-hello/</url>
    <content><![CDATA[1. hello voidking1.1. 查看效果1、单击webstorm右上角的绿色三角形（或者shift+f10），运行项目。
2、打开浏览器，输入localhost:3000，有没有看到welcome to express ？
1.2. 修改文字下面我们把welcome to express修改为hello voidking！1、打开views文件夹下的index.ejs，诶？这不是html代码吗？发现有个&lt;%= title%&gt;，这是个啥玩意？这时，我们就要解释一下模板引擎了。专业一点说，模板引擎是一个可以根据模板生成html代码的工具。通俗一点讲，模板引擎就像是一个函数，不同的x值对应不同的y值。比如y=x+1，当x=1时y=2。这里的x就相当于&lt;%= title%&gt;，y就相当于html页面。懂了？不懂拉倒，自己慢慢想，这不是重点。2、知道了原理，修改就简单了，不就是给x赋值嘛！打开routes文件夹下的index.js文件，看到这段代码：
router.get(&#x27;/&#x27;, function(req, res) &#123;  res.render(&#x27;index&#x27;, &#123; title: &#x27;Express&#x27; &#125;);&#125;);
修改如下：
router.get(&#x27;/&#x27;, function(req, res) &#123;  res.render(&#x27;index&#x27;, &#123; title: &#x27;voidking&#x27; &#125;);&#125;);
看懂了吧，把“voidking”赋值给了title，仅此而已。

3、运行，看到效果没？“welcome to voidking”，怎么改成“hello voidking”？当然是修改index.ejs文件了！“welcome to” 就相当于y=x+1中的1，是固定的。
1.3. 一点解释也许你已经运行成功了，但是你还是不理解这些代码。下面我简单解释下，不懂没关系，我会在接下来的教程中详细解释。1、bin文件夹，不用管。
2、node_modules文件夹，存放包。包又是啥玩意？类比吧，node.js中的包（文件夹） = java中的包（jar文件）。
3、public文件夹，存放图片等资源文件。
4、routes文件夹，存放路由文件，也就是业务层处理的文件。
5、views文件夹，存放交互层文件，这次项目中，也就是ejs模板文件。
6、app.js文件，程序入口。不要费心去找“main”函数了，它就是！
7、package.json文件，包的配置文件，相当于java的maven工程中的pom.xml文件。也许咱们这个项目用了很多第三方的包，那么，别人要运行这个项目，必须也安装了这些包。所以，package.json文件的主要目的，就是记录依赖的第三方包。这样，代码是不是很容易迁移？没错！具体写法这里不展开了，下次专门说一下。
8、细心的朋友可能发现了，业务层和交互层都有了，那么数据层在哪？别着急，马上我们会新建一个models文件夹，存放数据层文件。
表达的不好，请见谅。推荐一本好书——《node.js开发指南》，byvoid写的，这次项目我主要参考的就是这本书。不懂的地方，看书吧！
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
      </tags>
  </entry>
  <entry>
    <title>团队博客项目（一）——基础框架的搭建</title>
    <url>/dev-team-blog-project-1-basic-frame/</url>
    <content><![CDATA[1. 开发环境下载安装node.js。安装完成后，进入命令提示符界面，输入node --version，如果能出现node的版本号，则说明安装成功。
2. 开发工具的选用java程序开发，我喜欢eclipse；node.js程序开发，我选择的工具是webstorm。至于软件下载破解啥的，都是基本功，在此不赘述。
3. 生成基本目录和文件1、打开webstorm，file -&gt; new project
2、输入project name，选择location，project type选择node.js express app
3、node.js interpreter和npm excutable的位置默认就好（自动读取），version of express-generator选择最新版4.9.0，template engine选择ejs，css engine选择plain css。
4、ok，this window，如果接下来弹出configure node.js v0.10.31 core modules sources，直接点cancel就好，用不到。
5、好了，项目基础框架搭建好了，看下效果：
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
      </tags>
  </entry>
  <entry>
    <title>团队博客项目总结</title>
    <url>/dev-team-blog-project-summary/</url>
    <content><![CDATA[1. 20天这次项目的任务是：开发一个适用于团队的博客。从9月27接手任务，到10月17项目上线，20天左右。加班加点，一路走过，收获颇丰。现在，项目验收完毕，是时候吹吹牛了！
2. node.js + express + ejs + mongodb + bootstrap起初，有组员提出使用Java EE，因为我们之前用它开发了复合人才网，比较熟悉。这个提议被我否定了，理由是杀鸡用牛刀。最终，我们决定使用从来都没有接触过的node.js，边学边做，这也符合我们做开发的最终目的——学习。

什么都不懂，根本无从下手，那就学习吧！找了很多资料来看，接触到了很多有趣的东西。大概一周后，我才对node.js有了比较清晰的认识。
学一点，用一点，经过艰辛的探索、测试，我们才选出了最终的框架——node.js + express + ejs + mongodb + bootstrap。
例会三天一次，交流经验，分配任务，交流经验，分配任务……互相借鉴彼此实现的功能模块，两条线，基本上都已经完成。
3. 收获最大的收获，在于扩宽了眼界，对于编程工具的选用，有了更多的思路。
4. 其他我在考虑，是否要把做项目的步骤写下来，毕竟这是一笔宝贵的经验。但是郝同学实在是懒到不行，不能确定是否可以坚持下来，写教程太累了。（10分钟后……）决定了，写！！！权当复习了，也为后来者提供一点点帮助。慢慢写，不着急，欧耶！

最后，附上几张效果图吧！
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>node</tag>
      </tags>
  </entry>
  <entry>
    <title>引擎是什么？</title>
    <url>/hobby-what-is-engine/</url>
    <content><![CDATA[1. 关于引擎的思考经常看到引擎这个词，比如搜索引擎，游戏引擎，浏览器引擎，javascript引擎，node.js的javascript引擎是V8……百度之，搜到的都是机械上的引擎解释。那么，在猿类的世界里，引擎到底是啥东西？
2. 结论个人认同最佳解释：引擎是最核心的方法、组件或者模块。
3. 理由经过向大神请教，以及群组讨论，得到了很多见解。不保证正确性，下面记录一些个人认同的观点。

最核心的方法，比如搜索引擎就表示你是用的什么搜索方式，众所周知，GOOGLE的搜索要比百度好，就是引擎做得好。


最核心的组件，比如游戏引擎是直接影响着你是3D还是2D。看一款游戏好不好，其实引擎就决定了。QQ斗地主和魔兽世界就完全不同，虽然两者都叫游戏；某些网游基本都一样玩法，就是引擎一样。


linux系统很多，但是使用的linux内核一样，内核就相当于引擎。计算机种类很多，但是很多计算机都使用Intel的CPU，CPU就相当于引擎。汽车种类很多，但是很多汽车都是用的同样的发动机（引擎）。汽车的质量也是看发动机，差的发动机肯定是做不出来法拉利赛车的。

4. 附录
引擎一般用C++编写。


引擎更重要的是方法和理念。

5. 2016.11.08更新一位老师给出的解释：给出一些东西，返回另一些东西，这就是引擎。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>思考</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：Node.js学习笔记（二）</title>
    <url>/dev-nodejs-note-2/</url>
    <content><![CDATA[整理自：慕课网
1. 伪造模板数据跑通前后端交互流程本篇博客用到的代码，小伙伴们直接复制粘贴就可以了，你们可知道郝同学一行行敲得多么辛苦T_T，复制粘贴前给个赞啊！
1.1. 工程结构helloworld/  -bower_components/  -node_modules/  -views/    -includes/      -head.jade      -header.jade    -pages/      -index.jade      -detail.jade      -admin.jade      -list.jade    -layout.jade  -app.js

1.2. 操作步骤我们在上次helloworld工程的基础上接着做！1、在views文件夹下新建文件夹includes和pages2、在views文件夹下，删除index.jade，新建layout.jade，输入内容：
doctypehtml	head		meta(charset=&quot;utf-8&quot;)		title #&#123;title&#125;		include ./includes/head	body		include ./includes/header		block content
3、在includes文件夹下，新建文件head.jade和header.jade，分别输入内容：
link(href=&quot;/bootstrap/dist/css/bootstrap.min.css&quot;,rel=&quot;stylesheet&quot;)script(src=&quot;/jquery/dist/jquery.min.js&quot;)script(src=&quot;/bootstrap/dist/js/bootstrap.min.js&quot;)
.container	.row		.page-header			h1 #&#123;title&#125;			small 天印电影
4、在pages文件夹下，新建文件index.jade、detail.jade、admin.jade和list.jade，分别输入内容：
extends ../layoutblock content	.container		.row			each item in movies				.col-md-2					.thumbnail						a(href=&quot;/movie/#&#123;item._id&#125;&quot;)							img(src=&quot;#&#123;item.poster&#125;&quot;,alt=&quot;#&#123;item.title&#125;&quot;)						.caption							h3 #&#123;item.title&#125;							p: a.btn.btn-primary(href=&quot;/movie/#&#123;item._id&#125;&quot;,role=&quot;button&quot;)								观看预告片
extends ../layoutblock content	.container		.row			.col-md-7				embed(src=&quot;#&#123;movie.flash&#125;&quot;,allowFullScreen=&quot;true&quot;,quality=&quot;high&quot;,width=&quot;720&quot;,height=&quot;500&quot;,align=&quot;middle&quot;,type=&quot;application/x-shock&quot;)			.col-md-5				dl.dl-horizontal					dt 电影名字					dd= movie.title					dt 导演					dd= movie.doctor					dt 国家					dd= movie.country					dt 语言					dd= movie.language					dt 上映年份					dd= movie.year					dt 简介					dd= movie.summary
extends ../layoutblock content	.container		.row			form.form-horizontal(method=&quot;post&quot;,action=&quot;/admin/movie/new&quot;)				.form-group					label.col-sm-3.control-label(for=&quot;inputTitle&quot;) 电影名字					.col-sm-10						input#inputTitle.form-control(type=&quot;text&quot;,name=&quot;movie[title]&quot;,value=&quot;#&#123;movie.title&#125;&quot;)				.form-group					label.col-sm-3.control-label(for=&quot;inputDoctor&quot;) 导演					.col-sm-10						input#inputTitle.form-control(type=&quot;text&quot;,name=&quot;movie[doctor]&quot;,value=&quot;#&#123;movie.doctor&#125;&quot;)				.form-group					label.col-sm-3.control-label(for=&quot;inputCountry&quot;) 国家					.col-sm-10						input#inputTitle.form-control(type=&quot;text&quot;,name=&quot;movie[country]&quot;,value=&quot;#&#123;movie.country&#125;&quot;)				.form-group					label.col-sm-3.control-label(for=&quot;inputLanguage&quot;) 语言					.col-sm-10						input#inputTitle.form-control(type=&quot;text&quot;,name=&quot;movie[language]&quot;,value=&quot;#&#123;movie.language&#125;&quot;)				.form-group					label.col-sm-3.control-label(for=&quot;inputYear&quot;) 上映年份					.col-sm-10						input#inputTitle.form-control(type=&quot;text&quot;,name=&quot;movie[year]&quot;,value=&quot;#&#123;movie.year&#125;&quot;)				.form-group					label.col-sm-3.control-label(for=&quot;inputSummary&quot;) 简介					.col-sm-10						input#inputTitle.form-control(type=&quot;text&quot;,name=&quot;movie[summary]&quot;,value=&quot;#&#123;movie.summary&#125;&quot;)										.form-group					label.col-sm-3.control-label(for=&quot;inputPoster&quot;) 海报地址					.col-sm-10						input#inputTitle.form-control(type=&quot;text&quot;,name=&quot;movie[poster]&quot;,value=&quot;#&#123;movie.poster&#125;&quot;)				.form-group					label.col-sm-3.control-label(for=&quot;inputFlash&quot;) 片源地址					.col-sm-10						input#inputTitle.form-control(type=&quot;text&quot;,name=&quot;movie[flash]&quot;,value=&quot;#&#123;movie.flash&#125;&quot;)																.form-group					.col-sm-offset-2.col-sum-10					button.btn.btn-default(type=&quot;submit&quot;) 录入
extends ../layoutblock content	.container		.row			table.table.table-hover.table-bordered				thead					tr						th 电影名称						th 导演						th 国家						th 上映年份						//- th 录入时间						th 查看						th 修改						th 删除				tbody					each item in movies						tr(class=&quot;item-id-#&#123;item._id&#125;&quot;)							td #&#123;item.title&#125;							td #&#123;item.doctor&#125;							td #&#123;item.country&#125;							td #&#123;item.year&#125;							//- td #&#123;moment(item.meta.createdAt).format(&#x27;MM/DD/YYYY&#x27;)&#125;							td: a(target=&quot;_blank&quot;,href=&quot;../movie/#&#123;item._id&#125;&quot;) 查看							td: a(target=&quot;_blank&quot;,href=&quot;../admin/update/#&#123;item._id&#125;&quot;) 修改							td								button.btn.btn-danger.del(type=&quot;button&quot;,data-id=&quot;#&#123;item._id&#125;&quot;) 删除													
5、修改helloworld文件夹下app.js，主要是加入模拟数据，内容如下：
var express = require(&#x27;express&#x27;);var path = require(&#x27;path&#x27;);var port = process.env.PORT || 3000 ;var app = express();app.set(&#x27;views&#x27;,&#x27;./views/pages&#x27;);app.set(&#x27;view engine&#x27;,&#x27;jade&#x27;);app.use(express.bodyParser());app.use(express.static(path.join(__dirname, &#x27;bower_components&#x27;)));app.listen(port);console.log(&#x27;helloworld started on port &#x27; + port);app.get(&#x27;/&#x27;, function(req, res)&#123;  res.render(&#x27;index&#x27;,&#123;  title:&#x27;首页&#x27;,  movies: [	  &#123;		title: &#x27;机械战警&#x27;,		_id: 1,		poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,	  &#125;,	  &#123;		title: &#x27;机械战警&#x27;,		_id: 2,		poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,	  &#125;    ]  &#125;)&#125;)app.get(&#x27;/movie/:id&#x27;, function(req, res)&#123;  res.render(&#x27;detail&#x27;,&#123;  title:&#x27;详情页&#x27;,  movie: &#123;	doctor: &#x27;未知&#x27;,	country: &#x27;美国&#x27;,	title: &#x27;机械战警&#x27;,	year: &#x27;2014&#x27;,	poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,	language: &#x27;英语&#x27;,	flash: &#x27;http://player.youku.com/player.php/sid/XNjM3Njc3MTY4/v.swf&#x27;,	summary: &#x27;无&#x27;,  &#125;  &#125;)&#125;)app.get(&#x27;/admin/movie&#x27;, function(req, res)&#123;  res.render(&#x27;admin&#x27;,&#123;  title:&#x27;后台录入页&#x27;,  movie: &#123;	doctor: &#x27;&#x27;,	country: &#x27;&#x27;,	title: &#x27;&#x27;,	year: &#x27;&#x27;,	poster: &#x27;&#x27;,	language: &#x27;&#x27;,	flash: &#x27;&#x27;,	summary: &#x27;&#x27;,  &#125;  &#125;)&#125;)app.get(&#x27;/admin/list&#x27;, function(req, res)&#123;  res.render(&#x27;list&#x27;,&#123;  title:&#x27;列表页&#x27;,  movies: [	  &#123;		_id: 1,		doctor: &#x27;未知&#x27;,		country: &#x27;美国&#x27;,		title: &#x27;机械战警&#x27;,		year: &#x27;2014&#x27;,		poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,		language: &#x27;英语&#x27;,		flash: &#x27;http://player.youku.com/player.php/sid/XNjM3Njc3MTY4/v.swf&#x27;,		summary: &#x27;无&#x27;,	  &#125;,	  &#123;		_id: 2,		doctor: &#x27;未知&#x27;,		country: &#x27;美国&#x27;,		title: &#x27;机械战警&#x27;,		year: &#x27;2014&#x27;,		poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,		language: &#x27;英语&#x27;,		flash: &#x27;http://player.youku.com/player.php/sid/XNjM3Njc3MTY4/v.swf&#x27;,		summary: &#x27;无&#x27;,	  &#125;  ]  &#125;)&#125;)
1.3. 报错处理至此，已经敲完了Scott大神给的代码，马上就能看到效果了，好激动！哎呦我靠！报错，我改！又报错，我再改！还报错……诶？坏了，改不出来了！记录错误如下：百度“nodejs使用app.use(express.bodyParser());出错”，得到如下结论：1、node.js和windows的兼容性不如POSIX操作系统，因此npm提供给windows的第三方模块较少。2、bodyParser以前是集成在express中的，现在需要单独安装。无论哪个结论，解决办法都是安装body-parser：
npm install body-parser
然后在代码中如下使用：

var bodyParser = require(‘body-parser’);app.use(bodyParser.urlencoded({ extended: false }))app.use(bodyParser.json());

即把app.js的内容修改为如下内容：
var express = require(&#x27;express&#x27;);var path = require(&#x27;path&#x27;);var port = process.env.PORT || 3000 ;var app = express();var bodyParser = require(&#x27;body-parser&#x27;);app.set(&#x27;views&#x27;,&#x27;./views/pages&#x27;);app.set(&#x27;view engine&#x27;,&#x27;jade&#x27;);app.use(bodyParser.urlencoded(&#123; extended: false &#125;))app.use(bodyParser.json());app.use(express.static(path.join(__dirname, &#x27;bower_components&#x27;)));app.listen(port);console.log(&#x27;helloworld started on port &#x27; + port);app.get(&#x27;/&#x27;, function(req, res)&#123;  res.render(&#x27;index&#x27;,&#123;  title:&#x27;首页&#x27;,  movies: [	  &#123;		title: &#x27;机械战警&#x27;,		_id: 1,		poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,	  &#125;,	  &#123;		title: &#x27;机械战警&#x27;,		_id: 2,		poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,	  &#125;    ]  &#125;)&#125;)app.get(&#x27;/movie/:id&#x27;, function(req, res)&#123;  res.render(&#x27;detail&#x27;,&#123;  title:&#x27;详情页&#x27;,  movie: &#123;	doctor: &#x27;未知&#x27;,	country: &#x27;美国&#x27;,	title: &#x27;机械战警&#x27;,	year: &#x27;2014&#x27;,	poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,	language: &#x27;英语&#x27;,	flash: &#x27;http://player.youku.com/player.php/sid/XNjM3Njc3MTY4/v.swf&#x27;,	summary: &#x27;无&#x27;,  &#125;  &#125;)&#125;)app.get(&#x27;/admin/movie&#x27;, function(req, res)&#123;  res.render(&#x27;admin&#x27;,&#123;  title:&#x27;后台录入页&#x27;,  movie: &#123;	doctor: &#x27;&#x27;,	country: &#x27;&#x27;,	title: &#x27;&#x27;,	year: &#x27;&#x27;,	poster: &#x27;&#x27;,	language: &#x27;&#x27;,	flash: &#x27;&#x27;,	summary: &#x27;&#x27;,  &#125;  &#125;)&#125;)app.get(&#x27;/admin/list&#x27;, function(req, res)&#123;  res.render(&#x27;list&#x27;,&#123;  title:&#x27;列表页&#x27;,  movies: [	  &#123;		_id: 1,		doctor: &#x27;未知&#x27;,		country: &#x27;美国&#x27;,		title: &#x27;机械战警&#x27;,		year: &#x27;2014&#x27;,		poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,		language: &#x27;英语&#x27;,		flash: &#x27;http://player.youku.com/player.php/sid/XNjM3Njc3MTY4/v.swf&#x27;,		summary: &#x27;无&#x27;,	  &#125;,	  &#123;		_id: 2,		doctor: &#x27;未知&#x27;,		country: &#x27;美国&#x27;,		title: &#x27;机械战警&#x27;,		year: &#x27;2014&#x27;,		poster: &#x27;http://ihelloworld.qiniudn.com/%40%2Fimgs%2FiNJIT.jpg&#x27;,		language: &#x27;英语&#x27;,		flash: &#x27;http://player.youku.com/player.php/sid/XNjM3Njc3MTY4/v.swf&#x27;,		summary: &#x27;无&#x27;,	  &#125;  ]  &#125;)&#125;)
1.4. 效果图吼吼，终于搞定了！有图有真相！
1.5. 结束操作过程中的其他小错误，在此不作记录，有任何问题欢迎留言！
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>好好学Node.js</tag>
        <tag>nodejs</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Node.js：Node.js学习笔记（一）</title>
    <url>/dev-nodejs-note-1/</url>
    <content><![CDATA[整理自：慕课网
1. helloworld工程的搭建1.1. 技术及工具
node.js + mongodb(工具mongoose) + express + jade + moment.js + npm + jquery + bootstrap + grunt

我靠，那么多！淡定，暂时，咱们这个工程只要 node.js + express + jade 。
1.2. 准备工作下载安装node.js。
1.3. 工程结构helloworld/  -node_modules/  -bower_components/  -views/    -index.jade  -app.js
上两张图，直观感受下：![工程结构][1]![工程结构][2]

1.4. 具体操作1.4.1. 新建helloworld文件夹在F盘下新建个文件夹叫做“ helloworld ”，然后打开命令提示符，cd命令进入F盘下的helloworld文件夹。
1.4.2. 执行以下命令：npm install expressnpm install jadenpm install mongodbnpm install bower -gbower install bootstrap
1.4.3. 查看文件夹看看helloworld文件夹，诶？结构和你给的不一样啊！那就对了，咱接着搞！
1.4.4. 新建文件在helloworld文件夹下，新建文件app.js，输入内容如下：
var express = require(&#x27;express&#x27;);var port = process.env.PORT || 3000 ;var app = express();app.set(&#x27;views&#x27;,&#x27;./views&#x27;);app.set(&#x27;view engine&#x27;,&#x27;jade&#x27;);app.listen(port);console.log(&#x27;helloworld started on port &#x27; + port);app.get(&#x27;/&#x27;, function(req, res)&#123;  res.render(&#x27;index&#x27;,&#123;title:&#x27;helloworld&#x27;&#125;);&#125;);
至于这段代码的意思，请自行观看视频哈！在views文件夹下，新建文件index.jade（这个文件就相当于html文件），输入内容如下：
doctypehtml	head		meta(charset=&quot;utf-8&quot;)		title #&#123;title&#125;	body		h1 #&#123;title&#125;
至此，大功告成！
1.4.5. 查看效果在helloworld文件夹下执行命令：
node app.js
在浏览器中打开：http://localhost:3000有没有看到“helloworld”这个出现在了浏览器上？没看到？请留言！PS：你也可以新建admin.jade，也输入和index.jade相同的代码。然后在app.js添加如下代码：
app.get(&#x27;/admin/&#x27;, function(req, res)&#123;  res.render(&#x27;admin&#x27;,&#123;title:&#x27;admin&#x27;&#125;);&#125;);
访问地址：http://localhost:3000/admin/这时你会看到“admin”出现在了浏览器上！
1.5. 结束好了，今天的笔记就写到这里！有任何问题欢迎留言。  [1]: http://ihelloworld.qiniudn.com/@/imgs/nodejs%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84.jpg  [2]: http://ihelloworld.qiniudn.com/@/imgs/nodejs%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84%E5%88%86%E6%94%AF.jpg
]]></content>
      <categories>
        <category>engineering</category>
        <category>nodejs</category>
      </categories>
      <tags>
        <tag>好好学Node.js</tag>
        <tag>nodejs</tag>
      </tags>
  </entry>
  <entry>
    <title>如何引入另一个html文件</title>
    <url>/dev-how-to-load-in-another-html-file/</url>
    <content><![CDATA[1. iframe方式&lt;iframe name=&quot;content_frame&quot; width=100% height=30 marginwidth=0 marginheight=0 src=&quot;import.htm&quot; mce_src=&quot;import.htm&quot; &gt;&lt;/iframe&gt;

你会看到一个外部引入的文件，但会发现有一个类似外框的东西将其包围，可使用
&lt;iframe name=&quot;content_frame&quot; marginwidth=0 marginheight=0 width=100% height=30 src=&quot;import.htm&quot; mce_src=&quot;import.htm&quot; frameborder=0&gt;&lt;/iframe&gt;

但你会发现还会有点问题，就是背景色不同，你只要在引入的文件import.htm中使用相同的背景色也可以如果想引入的文件过长时不出现滚动条的话在import.htm中的body中加入scroll=no
2. object方式&lt;object style=&quot;border:0px&quot; type=&quot;text/x-scriptlet&quot; data=&quot;import.htm&quot; width=100% height=30&gt;&lt;/object&gt;

3. behavior的download方式&lt;span id=showImport&gt;&lt;/span&gt;&lt;IE:Download ID=&quot;oDownload&quot; STYLE=&quot;behavior:url(#default#download)&quot; /&gt;&lt;script&gt;function onDownloadDone(downDate)&#123;	showImport.innerHTML=downDate&#125;oDownload.startDownload(@#import.htm@#,onDownloadDone)&lt;/script&gt;
]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>html</tag>
      </tags>
  </entry>
  <entry>
    <title>jQuery本地无法加载解决办法</title>
    <url>/dev-jquery-cannot-local-load/</url>
    <content><![CDATA[1. 问题描述环境：win7+apache2.2+php5.2
症状：jquery包文件(jquery-1.4.2.min.js)在本地测试的时候加载失败，通过firebug查看，发现文件被加载了两次，但每次都加载不完全。直接引用外部的jquery包就OK，比如http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js


2. 解决办法参考Can’t load jquery on Apache，将httpd.conf中的
#EnableMMAP off#EnableSendfile off
注释去掉，重启apache，在firefox下马上就生效了，在ie下需要删除下缓存。
这两个参数在httpd.conf有作了一些注释
## EnableMMAP and EnableSendfile: On systems that support it, # memory-mapping or the sendfile syscall is used to deliver# files.  This usually improves server performance, but must# be turned off when serving from networked-mounted # filesystems or if support for these functions is otherwise# broken on your system.#

]]></content>
      <categories>
        <category>engineering</category>
        <category>frontend</category>
      </categories>
      <tags>
        <tag>jquery</tag>
      </tags>
  </entry>
  <entry>
    <title>初见Markdown</title>
    <url>/dev-first-see-markdown/</url>
    <content><![CDATA[1. 什么是 MarkdownMarkdown 是一种方便记忆、书写的纯文本标记语言，用户可以使用这些标记符号以最小的输入代价生成极富表现力的文档：譬如您正在阅读的这份文档。它使用简单的符号标记不同的标题，分割不同的段落，粗体 或者 斜体 某些文字，更棒的是，它还可以
2. 书写一个质能守恒公式[^LaTeX]$$E=mc^2$$
3. 高亮一段代码[^code]@requires_authorizationclass SomeClass:    passif __name__ == &#x27;__main__&#x27;:    # A comment    print &#x27;hello world&#x27;

4. 高效绘制 流程图st=&gt;start: Startop=&gt;operation: Your Operationcond=&gt;condition: Yes or No?e=&gt;endst-&gt;op-&gt;condcond(yes)-&gt;econd(no)-&gt;op

5. 高效绘制 序列图Alice-&gt;Bob: Hello Bob, how are you?Note right of Bob: Bob thinksBob--&gt;Alice: I am good thanks!

6. 绘制表格


项目
价格
数量



计算机
$1600
5


手机
$12
12


管线
$1
234


7. 更详细语法说明想要查看更详细的语法说明，可以参考我们准备的 Cmd Markdown 简明语法手册，进阶用户可以参考 Cmd Markdown 高阶语法手册 了解更多高级功能。
总而言之，不同于其它 所见即所得 的编辑器：你只需使用键盘专注于书写文本内容，就可以生成印刷级的排版格式，省却在键盘和工具栏之间来回切换，调整内容和格式的麻烦。Markdown 在流畅的书写和印刷级的阅读体验之间找到了平衡。 目前它已经成为世界上最大的技术分享网站 GitHub 和 技术问答网站 StackOverFlow 的御用书写格式。

1. 什么是 Cmd Markdown您可以使用很多工具书写 Markdown，但是 Cmd Markdown 是这个星球上我们已知的、最好的 Markdown 工具——没有之一 ：）因为深信文字的力量，所以我们和你一样，对流畅书写，分享思想和知识，以及阅读体验有极致的追求，我们把对于这些诉求的回应整合在 Cmd Markdown，并且一次，两次，三次，乃至无数次地提升这个工具的体验，最终将它演化成一个 编辑/发布/阅读 Markdown 的在线平台——您可以在任何地方，任何系统/设备上管理这里的文字。
1.1. 1. 实时同步预览我们将 Cmd Markdown 的主界面一分为二，左边为编辑区，右边为预览区，在编辑区的操作会实时地渲染到预览区方便查看最终的版面效果，并且如果你在其中一个区拖动滚动条，我们有一个巧妙的算法把另一个区的滚动条同步到等价的位置，超酷！
1.2. 2. 编辑工具栏也许您还是一个 Markdown 语法的新手，在您完全熟悉它之前，我们在 编辑区 的顶部放置了一个如下图所示的工具栏，您可以使用鼠标在工具栏上调整格式，不过我们仍旧鼓励你使用键盘标记格式，提高书写的流畅度。

1.3. 3. 编辑模式完全心无旁骛的方式编辑文字：点击 编辑工具栏 最右测的拉伸按钮或者按下 Ctrl + M，将 Cmd Markdown 切换到独立的编辑模式，这是一个极度简洁的写作环境，所有可能会引起分心的元素都已经被挪除，超清爽！
1.4. 4. 实时的云端文稿为了保障数据安全，Cmd Markdown 会将您每一次击键的内容保存至云端，同时在 编辑工具栏 的最右侧提示 已保存 的字样。无需担心浏览器崩溃，机器掉电或者地震，海啸——在编辑的过程中随时关闭浏览器或者机器，下一次回到 Cmd Markdown 的时候继续写作。
1.5. 5. 离线模式在网络环境不稳定的情况下记录文字一样很安全！在您写作的时候，如果电脑突然失去网络连接，Cmd Markdown 会智能切换至离线模式，将您后续键入的文字保存在本地，直到网络恢复再将他们传送至云端，即使在网络恢复前关闭浏览器或者电脑，一样没有问题，等到下次开启 Cmd Markdown 的时候，她会提醒您将离线保存的文字传送至云端。简而言之，我们尽最大的努力保障您文字的安全。
1.6. 6. 管理工具栏为了便于管理您的文稿，在 预览区 的顶部放置了如下所示的 管理工具栏：

通过管理工具栏可以：
 发布：将当前的文稿生成固定链接，在网络上发布，分享 新建：开始撰写一篇新的文稿 删除：删除当前的文稿 导出：将当前的文稿转化为 Markdown 文本或者 Html 格式，并导出到本地 列表：所有新增和过往的文稿都可以在这里查看、操作 模式：切换 普通/Vim/Emacs 编辑模式
1.7. 7. 阅读工具栏
通过 预览区 右上角的 阅读工具栏，可以查看当前文稿的目录并增强阅读体验。
工具栏上的五个图标依次为：
 目录：快速导航当前文稿的目录结构以跳转到感兴趣的段落 视图：互换左边编辑区和右边预览区的位置 主题：内置了黑白两种模式的主题，试试 黑色主题，超炫！ 阅读：心无旁骛的阅读模式提供超一流的阅读体验 全屏：简洁，简洁，再简洁，一个完全沉浸式的写作和阅读环境
1.8. 8. 阅读模式在 阅读工具栏 点击  或者按下 Ctrl+Alt+M 随即进入独立的阅读模式界面，我们在版面渲染上的每一个细节：字体，字号，行间距，前背景色都倾注了大量的时间，努力提升阅读的体验和品质。
1.9. 9. 标签、分类和搜索在编辑区任意行首位置输入以下格式的文字可以标签当前文档：
标签： 未分类
标签以后的文稿在【文件列表】（Ctrl+Alt+F）里会按照标签分类，用户可以同时使用键盘或者鼠标浏览查看，或者在【文件列表】的搜索文本框内搜索标题关键字过滤文稿，如下图所示：

1.10. 10. 文稿发布和分享在您使用 Cmd Markdown 记录，创作，整理，阅读文稿的同时，我们不仅希望它是一个有力的工具，更希望您的思想和知识通过这个平台，连同优质的阅读体验，将他们分享给有相同志趣的人，进而鼓励更多的人来到这里记录分享他们的思想和知识，尝试点击  (Ctrl+Alt+P) 发布这份文档给好友吧！

再一次感谢您花费时间阅读这份欢迎稿，点击  (Ctrl+Alt+N) 开始撰写新的文稿吧！祝您在这里记录、阅读、分享愉快！
作者 @ghosert2014 年 07月 07日    
[^LaTeX]: 支持 LaTeX 编辑显示支持，例如：$\sum_{i=1}^n a_i=0$， 访问 MathJax 参考更多使用方法。
[^code]: 代码高亮功能支持包括 Java, Python, JavaScript 在内的，四十一种主流编程语言。
]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>markdown</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Hexo：Hexo参考文档</title>
    <url>/dev-hexo-document/</url>
    <content><![CDATA[hexo系列教程：（一）hexo介绍http://zipperary.com/hexo-guide-1/

hexo系列教程：（二）搭建hexo博客http://zipperary.com/hexo-guide-2/

hexo系列教程：（三）hexo博客的配置、使用http://zipperary.com/hexo-guide-3/

hexo系列教程：（四）hexo博客的优化技巧http://zipperary.com/hexo-guide-4/



hexo系列教程：（五）hexo博客的优化技巧续http://zipperary.com/hexo-guide-5/

hexo 主题优化http://zipperary.com/hexo-theme-alteration/

把flickr相册嵌入网页http://zipperary.com/flickr-on-page/

怎样在博文中嵌入图片、音乐、视频？http://zipperary.com/media-on-hexo/

前端之drop-shadow效果http://zipperary.com/box-and-shadow-effect/

为Hexo添加多说评论功能(正文评论和列表评论数)http://blog.csdn.net/jelope/article/details/19678633

Markdown指南http://zipperary.com/introduction-to-markdown/

轻松搞定Markdownhttp://zipperary.com/easy-markdown/

初识node.jshttp://zipperary.com/learn-nodejs/

购买域名、设置DNShttp://zipperary.com/domain-name-and-dns/

如何绑定二级域名http://zipperary.com/secondary-dns/

托管博客到gitcafehttp://zipperary.com/hexo-to-gitcafe/

托管博客到STDYUNhttp://zipperary.com/blog-to-stdyun/

将hexo博客部署到baehttp://zipperary.com/hexo-on-bae/

hexo你的博客http://ibruce.info/hexo-your-blog/?utm_source=tuicoolhttp://www.tuicool.com/articles/AfQnQjy

初识hexo，安装及配置http://md5sum.cc/2014-05-05/first-met-hexo/

使用hexo搭建静态博客http://m.chinaz.com/article/354945.shtml

google字库导致hexo modernist首页加载变慢http://ibruce.info/fonts-googleapis-lead-to-slow/?utm_source=tuicoolhttp://www.tuicool.com/articles/iAVjE3

通过本地加载ga.js文件提高Google Anlytics性能http://www.jb51.net/yunying/70962.html

引用新浪微软谷歌的CDN加载jQueryhttp://www.hicc.cc/quote-sina-microsoft-googles-cdn-load-jquery.html

]]></content>
      <categories>
        <category>engineering</category>
        <category>hexo</category>
      </categories>
      <tags>
        <tag>hexo</tag>
        <tag>好好学Hexo</tag>
      </tags>
  </entry>
  <entry>
    <title>iNJIT微信公众平台</title>
    <url>/dev-injit-wechat/</url>
    <content><![CDATA[南工程大计院科技立项，纯服务性质微信平台，功能内测中，感谢亲的宝贵意见！
查课表、查学科成绩、查CET成绩、查空教室、查馆藏图书，一个都不能少！
工程外卖、每日挖宝、失物招领、圈里约TA、二手交易，吃喝玩乐样样有！
工程联盟、表白墙、微信小游戏（无需下载）、吐槽灌水，无聊的日子真无聊！
男神女神、我爱抽奖、讲座报名、蹭课、搭讪、大学生活、校内活动、文字控、近期影讯、微招聘……开发进行中！
小伙伴们有什么好的建议，欢迎给我们留言!扫一扫关注iNJIT，公众号搜索injiti，更多好玩功能，等你来发现！


]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>微信</tag>
      </tags>
  </entry>
  <entry>
    <title>7个源码搜索网站</title>
    <url>/hobby-7-source-code-search-engine-website/</url>
    <content><![CDATA[不管你是初学一门计算机语言或技术，还是想学习别人的经验和创意。做为一个开发人员，都会经常需要到搜索一些代码，下面是7个地方可以帮助你快速寻找到你需要的代码。原文7 Places to Find the Code You Need。


1. GitHub Code Search热门的开源代码库和和版本控制服务。 GitHub 在最近启动了 Code Search.即使它才启动不久，但凭借GitHub 的受欢迎度和成千上万的代码库，GitHub的这项代码搜索服务已经非常受欢迎。
2. KrugleKrugle是一个为开发人员量身定做的代码搜索引擎，他们宣称可搜索超过25亿行的开放源代码，是互联网最大的源代码搜索引擎之一。
和传统搜索引擎不同，Krugle专门用来搜索代码，不管是压缩包文件，还是展示在blog、网页上的代码，只要是在网络开放的空间内，Krugle都能找到，并且Krugle还可以自动生成某个特定代码的API报告 ( 尚处实验阶段 ) 。
Krugle 能提供和代码相关问题的解答。 
允许开发人员对搜索结果进行评论，并通过创建tag的方式来帮助其他人更好的找到所需的代码，实现共享。
Krugle具有较快的搜索速度，能对和代码相关的例如各种技术主题、 API 、示例、文档进行快速搜索。
3. KodersKoders是一个专为开放代码的搜索引擎。目前可以搜索到424,227,372套程序代码，并提供了全站更新订阅。你还可以在你的网站上共享它的代码搜索服务。每天有30000多开发者在Koders上搜索766000000行的代码，包括高达30种编程语言和28种软件license。
4. CodaseHuihong Luo 等人在硅谷创办一家名为 Codase.com 的程序源码搜索引擎，用户可以通过项目、类、方法及字段等来搜索程序源代码；最初只针对 Linux 平台的 C/C++ 开源项目源码搜索，目前已包含了 Windows 平台及 Java 语言相关的源码搜索，计划不久将提供对 C#、Visual Basic、Perl、PHP、Python、Ruby 等程序设计语言的支持；据 Codase 称现在已可搜索超过 250,000,000 行的源代码，并致力成为最为专业的源码搜索引擎。
5. SnipplrSnipplr是一个开放的源代码技巧分享社区，号称Code2.0。和一般的源码分享网站不同，它针对的并不是大型网站源码，而是一些编程的代码技巧。比如针对IE的CSS代码Hack，javascript的隐藏和显示效果代码，CSS圆角效果代码等等。具有分类和Tag支持，以及搜索，评论和收藏等等功能。
6. DZone Snippets拥有超过13,000 用户提供的 5,000多代码摘录, DZone Snippets 是一个寻找代码示例的好地方.
7. Google Code Search这个就不用多做介绍了。
8. 其他https://searchcode.com/
]]></content>
      <categories>
        <category>computer</category>
      </categories>
  </entry>
  <entry>
    <title>Windows的一些技巧</title>
    <url>/hobby-windows-skill/</url>
    <content><![CDATA[1. realtek高清晰音频管理器，关闭图标1、开始，运行，输入 msconfig ，启动2、取消勾选 realtek高清晰音频管理器 的开机自启动项，OK3、重启后生效
2. 菜单栏消失1、开始，控制面板，外观和个性化2、文件夹选项，查看，始终显示菜单
3. 删除用户1、开始，控制面板，性能和维护2、管理工具，计算机管理3、本地用户和组，删掉不想要的用户
4. 切换管理员账户1、右击计算机图标，管理2、本地用户和组，用户，找到 administrator，帐户已禁用去掉勾3、切换用户（关机图标旁边）


5. 设置自动关机设置在18:30自动关机
at 18:30 shutdown -s

查看任务列表
at

取消自动关机
at /del# orat 1 /del

6. 修改盘符1、计算机右击，管理2、磁盘管理，选中需要修改的卷，右击，更改驱动器号和路径
7. 去除用户账户控制1、控制面板，用户账户（大图标）2、更改用户账户控制设置，将按钮调到最低（从不通知）
8. 隐藏NVIDIA 控制面板1、双击（或者右击）打开 NVIDIA控制面板，或者在控制面板里面打开2、点击 桌面，去掉那些勾（有个右下角的，还有个桌面的，去掉就可以了）
9. 移动桌面1、进入C盘，Users（或者用户）2、找到当前用户文件夹（一般是Administrator），找到桌面文件夹3、右键，属性，位置4、把 C:\Users\Administrator\Desktop 改为你想存放的位置，比如 D:\桌面 ，然后点移动就OK
10. 能上QQ不能打开网页大概率是DNS服务器配置错误。
11. win7修改文件名不能使用搜狗输入法修改完hosts文件后，出现这个问题。解决办法：重启。
12. ISO镜像制作使用UltraISO
13. pagefile.sys转移1、右击计算机，属性2、高级系统设置，高级，性能设置3、性能选项窗口中，高级，虚拟内存更改
14. hiberfil.sys删除powercfg -h off

15. 右击文件夹出现Runtime Errorwinmount这款软件的问题，卸载了就OK了，或者设置去掉右键菜单。
16. 在cmd里结束进程tasklisttaskkill /f /t /im firefox.exe

17. 网络管理命令ping/?nslookup

18. 使用SYSTEM超级管理员账户（最高权限）taskkill /f /im explorer.exeat 20:00 /interactive %systemroot%\explorer.exe

19. 批量修改文件名1、生成 rename.xls 
dir /b&gt;rename.xls

2、使用wps打开rename.xls，在B1单元格输入1.jpg，其余行自动生成。
3、在C1单元格输入公式=”ren “&amp;A1&amp;” “&amp;B1，其余行自动生成。
4、复制C列，粘贴到记事本，重命名为 rename.bat 文件。
5、双击 rename.bat 文件。
20. 利用系统加密文件夹(适用XP)md f:\my..\start f:\my..\

21. win7库的使用1、计算机，库，音乐库，点击蓝色的n个位置。2、计算机，库，右击音乐库，属性。
22. 豆丁文档免费下载。百度快照或者豆丁文档下载器
23. 捉弄人（慎用）截屏，设置为桌面
24. 如何在ppt中批量插入图片插入，图片，分页插入图片
25. 校园视频网破解云窗(校园视频网)破解版，安徽师范大学2010级计算机王宇辉开发。http://wangyuhui.com.cn
26. 文件合成copy /b test.jpg + test.rar finish.jpg

27. win7运行ewb问题win7运行ewb时，拖动原件以后背景会变成某一时刻的桌面背景
解决办法：在exe上右键，属性，兼容性，两个禁用选一下，点应用。
28. 该文件没有与之关联的程序来执行该操作win10右键显示设置和个性化，出现“该文件没有与之关联的程序来执行该操作……”。
解决办法：1、WIN+R 打开运行，输入regedit2、地址栏输入 HKEY_CURRENT_USER\Software\Classes\ms-settings3、把ms-settings重命名为ms-settings1
29. 将文件夹取消与SVN关联Windows Registry Editor Version 5.00[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSVN]@=&quot;删除该目录下面.svn文件&quot;[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSVN\command]@=&quot;cmd.exe /c \&quot;TITLE Removing SVN Folders in %1 &amp;&amp; COLOR 9A &amp;&amp; FOR /r \&quot;%1\&quot; %%f IN (.svn) DO RD /s /q \&quot;%%f\&quot; \&quot;&quot; 

（1）把上面这段文字保存为“取消SVN文件夹.reg”文件。（2）双击这个文件，导入到注册表。（3）右键一个文件夹的时候，会多出来一个菜单”删除该目录下面.svn文件”，执行该命令即可。
30. Win11查找C盘大文件查找C盘中的大文件，以管理员用户运行以下命令：
DiskUsage C:\ /minFileSize=100000000 /h

31. 查找端口对应的进程并kill1、查找端口对应的进程ID
netstat -ano | findstr &quot;8080&quot;

2、查看进程名称（PID 要替换成实际的进程ID）
tasklist | findstr &quot;PID&quot;

3、kill进程
taskkill /f /t /im 进程名.exe# ortaskkill /pid PID /f



]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>加密</tag>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>Eclipse配置Maven</title>
    <url>/dev-maven-eclipse-setting/</url>
    <content><![CDATA[1. 前言Eclipse的自带Maven，在一般情况下就够用了。但是，由于版本要求或者高级功能要求，我们不得不自己安装配置Maven。
2. 下载安装详见《Maven本地安装jar文件》
3. Eclipse配置Eclipse，Window，Preferences。


1、Maven，Installations，Add，然后选择Maven的安装路径。
2、Maven，User Settings，User Settings选择Maven下的setting.xml。
3、编辑settings.xml，释放&lt;localRepository&gt;&lt;/localRepository&gt;节点，中间加上本地Repository路径。
4、Java，Installed JREs，Edit，在Default VM arguments中设置-Dmaven.multiModuleProjectDirectory=$M2_HOME
4. 后记在使用Eclipse自带Maven的过程中，如果出现了一些无法理解的错误，不妨试试自己配置一个Maven。
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>maven</tag>
      </tags>
  </entry>
  <entry>
    <title>Maven项目显示叉号</title>
    <url>/dev-maven-project-fork/</url>
    <content><![CDATA[1. 问题描述明明没有错误的Maven项目，导入到Eclipse之后，项目名的旁边会出现一个叉号。虽然不影响项目的使用，但是，看起来总有一点不爽。
2. 解决办法单击项目，然后alt+F5，Update Maven Project。大部分的项目，都可以通过这个方法去掉叉号。暂时不懂得原理，随着学习的深入，会懂得的。
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>maven</tag>
      </tags>
  </entry>
  <entry>
    <title>Maven本地安装jar文件</title>
    <url>/dev-maven-local-install-jar/</url>
    <content><![CDATA[1. 下载安装Maven平时使用的，是Eclipse的Eclipse IDE for Java EE Developers 版本，自带Maven。但是，想要使用Maven本地安装jar文件，就需要自己安装Maven。
下载地址： http://maven.apache.org/download.cgi
1、解压到自己喜欢的目录（这里郝同学放到D:\Server路径下）。2、添加环境变量M2_HOME，值为D:\Server\apache-maven-3.3.33、在Path中添加;%M2_HOME%\bin;。
打开命令提示符，输入mvn -v，如果能够看到maven版本号，说明安装成功。
2. Oracle驱动jar包安装以安装Oracle驱动jar包为例。由于Oracle授权问题，Maven不提供Oracle JDBC Driver，为了在Maven项目中应用Oracle JDBC driver,必须手动添加到本地仓库。
2.1. 下载jar包JDBC、SQLJ、Oracle JPublisher 和通用连接池 (UCP)JDBC and Universal Connection Pool (UCP)
郝同学使用的是Oracle11g，下载下来的jar包名为ojdbc14.jar。通过解压缩软件看到，jar包中有一个META-INF/MANIFEST.MF文件。打开这个文件，我们看到
Manifest-Version: 1.0Specification-Title:    Oracle JDBC driver classes for use with JDK14Sealed: trueCreated-By: 1.4.2_08 (Sun Microsystems Inc.)Implementation-Title:   ojdbc14.jarSpecification-Vendor:   Oracle CorporationSpecification-Version:  Oracle JDBC Driver version - &quot;10.2.0.3.0&quot;Implementation-Version: Oracle JDBC Driver version - &quot;10.2.0.3.0&quot;Implementation-Vendor:  Oracle CorporationImplementation-Time:    Tue Feb 27 15:23:24 2007Name: oracle/sql/converter/Sealed: falseName: oracle/sql/Sealed: falseName: oracle/sql/converter_xcharset/Sealed: false
等下我们要用到version信息：10.2.0.3.0。

2.2. 安装命令打开命令提示符，进入到ojdbc.jar所在的文件夹下，执行以下命令：
mvn install:install-file -DgroupId=com.oracle -DartifactId=ojdbc14 -Dversion=10.2.0.3.0 -Dpackaging=jar -Dfile=ojdbc14.jar
看到BUILD SECCESS就说明大功告成了！
也许你会问，Dfile参数可不可以使用绝对路径？答：不可以。我在使用绝对路径的时候失败了，不知道为什么。
其实，上面的DgroupId、DartifactId、Dversion全部可以按照自己喜好来定义，只是，在配置pom.xml的时候会用到。所以，我们还是尽可能规范一些。
2.3. 本地仓库路径默认为C:\Users\Administrator\.m2\repository，在这个路径下，你可以看到本地已经安装的jar包。
2.4. pom.xml中配置&lt;!-- Oracle驱动 --&gt;&lt;dependency&gt;	&lt;groupId&gt;com.oracle&lt;/groupId&gt;	&lt;artifactId&gt;ojdbc14&lt;/artifactId&gt;	&lt;version&gt;10.2.0.3.0&lt;/version&gt;&lt;/dependency&gt;

3. SQLServer驱动jar包安装类似于Oracle驱动jar包安装，记录命令如下：
mvn install:install-file -DgroupId=com.microsoft.sqlserver -DartifactId=jdbc -Dversion=1.1.1 -Dpackaging=jar -Dfile=sqljdbc4.jar
pom.xml配置如下：
&lt;dependency&gt;	&lt;groupId&gt;com.microsoft.sqlserver&lt;/groupId&gt;	&lt;artifactId&gt;sqljdbc4&lt;/artifactId&gt;	&lt;version&gt;1.1.1&lt;/version&gt;&lt;/dependency&gt;

4. 参考文档在Maven仓库中添加Oracle JDBC驱动
maven3 手动安装本地jar到仓库
MAVEN安装JAR文件到本地仓库
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>maven</tag>
      </tags>
  </entry>
  <entry>
    <title>Maven之pom.xml</title>
    <url>/dev-maven-pom/</url>
    <content><![CDATA[1. pom.xml速览&lt;project&gt;    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;    &lt;groupId&gt;...&lt;/groupId&gt;    &lt;artifactId&gt;...&lt;/artifactId&gt;    &lt;version&gt;...&lt;/version&gt;    &lt;packaging&gt;...&lt;/packaging&gt;    &lt;dependencies&gt;...&lt;/dependencies&gt;    &lt;parent&gt;...&lt;/parent&gt;    &lt;dependencyManagement&gt;...&lt;/dependencyManagement&gt;    &lt;modules&gt;...&lt;/modules&gt;    &lt;properties&gt;...&lt;/properties&gt;      &lt;build&gt;...&lt;/build&gt;    &lt;reporting&gt;...&lt;/reporting&gt;      &lt;name&gt;...&lt;/name&gt;    &lt;description&gt;...&lt;/description&gt;    &lt;url&gt;...&lt;/url&gt;    &lt;inceptionYear&gt;...&lt;/inceptionYear&gt;    &lt;licenses&gt;...&lt;/licenses&gt;    &lt;organization&gt;...&lt;/organization&gt;    &lt;developers&gt;...&lt;/developers&gt;    &lt;contributors&gt;...&lt;/contributors&gt;      &lt;issueManagement&gt;...&lt;/issueManagement&gt;    &lt;ciManagement&gt;...&lt;/ciManagement&gt;    &lt;mailingLists&gt;...&lt;/mailingLists&gt;    &lt;scm&gt;...&lt;/scm&gt;    &lt;prerequisites&gt;...&lt;/prerequisites&gt;    &lt;repositories&gt;...&lt;/repositories&gt;    &lt;pluginRepositories&gt;...&lt;/pluginRepositories&gt;    &lt;distributionManagement&gt;...&lt;/distributionManagement&gt;    &lt;profiles&gt;...&lt;/profiles&gt;  &lt;/project&gt;  

POM包括了所有的项目信息：

groupId：项目或者组织的唯一标志，并且配置时生成的路径也是由此生成，如org.codehaus.mojo生成的相对路径为：/org/codehaus/mojo
artifactId：项目的通用名称
version：项目的版本
packaging：打包的机制，如pom, jar, maven-plugin, ejb, war, ear, rar, par
classifier：分类



2. POM关系2.1. 依赖&lt;dependencies&gt;      &lt;dependency&gt;        &lt;groupId&gt;junit&lt;/groupId&gt;        &lt;artifactId&gt;junit&lt;/artifactId&gt;        &lt;version&gt;4.0&lt;/version&gt;        &lt;type&gt;jar&lt;/type&gt;        &lt;scope&gt;test&lt;/scope&gt;        &lt;optional&gt;true&lt;/optional&gt;      &lt;/dependency&gt;      ...  &lt;/dependencies&gt;  


groupId, artifactId, version:描述了依赖的项目唯一标志
type:相应的依赖产品包形式，如jar，war
scope:用于限制相应的依赖范围，包括以下的几种变量：


compile ：默认范围，用于编译provided：类似于编译，但支持你期待jdk或者容器提供，类似于classpathruntime:在执行时，需要使用test:用于test任务时使用system:需要外在提供相应得元素。通过systemPath来取得


systemPath: 仅用于范围为system。提供相应的路径
optional: 标注可选，当项目自身也是依赖时。用于连续依赖时使用

独占性外在告诉maven你只包括指定的项目，不包括相关的依赖。此因素主要用于解决版本冲突问题。
&lt;dependencies&gt;      &lt;dependency&gt;        &lt;groupId&gt;org.apache.maven&lt;/groupId&gt;        &lt;artifactId&gt;maven-embedder&lt;/artifactId&gt;        &lt;version&gt;2.0&lt;/version&gt;        &lt;exclusions&gt;          &lt;exclusion&gt;            &lt;groupId&gt;org.apache.maven&lt;/groupId&gt;            &lt;artifactId&gt;maven-core&lt;/artifactId&gt;          &lt;/exclusion&gt;        &lt;/exclusions&gt;      &lt;/dependency&gt; &lt;/dependencies&gt;
表示项目maven-embedder需要项目maven-core，但我们不想引用maven-core。
2.2. 聚合为了能够使用一条命令就能构建account-email和account-persist两个模块，我们需要建立一个额外的名为account-aggregator的模块，然后通过该模块构建整个项目的所有模块。account-aggregator本身也是个 Maven项目，它的 POM如下
&lt;project&gt;	&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;	&lt;groupId&gt;com.juvenxu.mvnbook.account&lt;/groupId&gt;	&lt;artifactId&gt;account-aggregator&lt;/artifactId&gt;	&lt;version&gt;1.0.0-SNAPSHOT&lt;/version&gt;	&lt;packaging&gt; pom &lt;/packaging&gt;	&lt;name&gt;Account Aggregator&lt;/name&gt;	 &lt;modules&gt;		&lt;module&gt;account-email&lt;/module&gt;		&lt;module&gt;account-persist&lt;/module&gt;	 &lt;/modules&gt;&lt;/project&gt;

注意：packaging的类型为pom ，module的值是一个以当前POM为主目录的相对路径。
2.3. 继承2.3.1. 父模块配置&lt;project&gt;	&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;	&lt;groupId&gt;com.juvenxu.mvnbook.account&lt;/groupId&gt;	&lt;artifactId&gt; account-parent &lt;/artifactId&gt;	&lt;version&gt;1.0.0-SNAPSHOT&lt;/version&gt;	&lt;packaging&gt;pom&lt;/packaging&gt;	&lt;name&gt;Account Parent&lt;/name&gt;		&lt;dependencyManagement&gt;...&lt;/dependencyManagement&gt;  &lt;/project&gt;



2.3.2. 子模块配置&lt;project&gt;	&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;		&lt; parent &gt;		&lt;groupId&gt;com.juvenxu.mvnbook.account&lt;/groupId&gt;		&lt;artifactId&gt; account-parent &lt;/artifactId&gt;		&lt;version&gt;1.0.0-SNAPSHOT&lt;/version&gt;		&lt; relativePath &gt;../account-parent/pom.xml&lt;/ relativePath&gt;	&lt;/ parent &gt;		&lt;artifactId&gt; account-email &lt;/artifactId&gt;	&lt;name&gt;Account Email&lt;/name&gt;  ...&lt;/project&gt;

注意：1、子模块没有声明groupId和version，这两个属性继承至父模块。但如果子模块有不同与父模块的 groupId、version ，也可指定；2、不应该继承artifactId,如果groupId ，version，artifactId 完全继承的话会造成坐标冲突；另外即使使用不同的 groupId或version，同样的 artifactId也容易产生混淆。
2.3.3. 聚合使用继承后，parent也必须像子模块一样加入到聚合模块中。也就是在在聚合模块的 pom中加入&lt;module&gt;account-parent&lt;/module&gt;。聚合的 POM如下：
&lt;project&gt;	&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;	&lt;groupId&gt;com.juvenxu.mvnbook.account&lt;/groupId&gt;	&lt;artifactId&gt;account-aggregator&lt;/artifactId&gt;	&lt;version&gt;1.0.0-SNAPSHOT&lt;/version&gt;	&lt;packaging&gt; pom &lt;/packaging&gt;	&lt;name&gt;Account Aggregator&lt;/name&gt;	&lt;modules&gt;		&lt;module&gt;account-email&lt;/module&gt;		&lt;module&gt;account-persist&lt;/module&gt;		&lt;module&gt; account-parent&lt;/module&gt;	&lt;/modules&gt;&lt;/project&gt;

2.3.4. 可继承的元素groupId ：项目组 ID ，项目坐标的核心元素；version ：项目版本，项目坐标的核心元素；description ：项目的描述信息；organization ：项目的组织信息；inceptionYear ：项目的创始年份；url ：项目的 url 地址develoers ：项目的开发者信息；contributors ：项目的贡献者信息；distributionManagerment ：项目的部署信息；issueManagement ：缺陷跟踪系统信息；ciManagement ：项目的持续继承信息；scm ：项目的版本控制信息；mailingListserv ：项目的邮件列表信息；properties ：自定义的 Maven 属性；dependencies ：项目的依赖配置；dependencyManagement ：醒目的依赖管理配置；repositories ：项目的仓库配置；build ：包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等；reporting ：包括项目的报告输出目录配置、报告插件配置等。

我们知道dependencies是可以被继承的，这个时候我们就想到让我们的发生了共用的依赖元素转移到parent中，这样我们又进一步的优化了配置。可是问题也随之而来，如果有一天我创建了一个新的模块，但是这个模块不需要这些parent的依赖，这时候如何处理？
是的，maven的依赖管理就是来解决这个问题的：dependencyManagement。从上面的列表中我们发现dependencyManagement也是可以被继承的，这恰恰满足了我们的需要，它既能够让子模块继承到父模块的依赖配置，又能保证子模块依赖使用的灵活性。
dependencyManagement的特性：在dependencyManagement中配置的元素既不会给parent引入依赖，也不会给它的子模块引入依赖，仅仅是它的配置是可继承的。
最佳实践：这时候我们就可以在父POM中声明这些依赖：
&lt;properties&gt;	&lt;target.version&gt;2.5.6&lt;/target.version&gt;&lt;/properties&gt;&lt;dependencyManagement&gt;	&lt;dependencies&gt;		&lt;dependency&gt;			&lt;groupId&gt;your groupId&lt;/groupId&gt;			&lt;artifactId&gt;your artifactId&lt;/artifactId&gt;			&lt;version&gt;$&#123;target.version&#125;&lt;/version&gt;		&lt;/dependency&gt;	&lt;/dependencies&gt;&lt;/dependencyManagement&gt;

子模块的POM继承这些配置：子模块继承这些配置的时候，仍然要声明groupId和artifactId，表示当前配置是继承于父POM的，从而直接使用父POM的版本对应的资源。
&lt;dependencies&gt;	&lt;dependency&gt;		&lt;groupId&gt;your groupId&lt;/groupId&gt;		&lt;artifactId&gt;your artifactId&lt;/artifactId&gt;	&lt;/dependency&gt;&lt;/dependencies&gt;
这个可以有效的避免多个子模块使用依赖版本不一致的情况，有助于降低依赖冲突的几率。
注：只有子模块配置了继承的元素，才会真正的有效，否则maven是不会加载父模块中声明的元素。
pluginManagement和dependencyManagement相类似，它是用来进行插件管理的。
2.4. 聚合和继承的关系区别 ：1、对于聚合模块来说，它知道有哪些被聚合的模块，但那些被聚合的模块不知道这个聚合模块的存在。2、对于继承关系的父POM来说，它不知道有哪些子模块继承与它，但那些子模块都必须知道自己的父 POM是什么。
共同点 ：1.聚合 POM与继承关系中的父POM的packaging都是pom2.聚合模块与继承关系中的父模块除了 POM之外都没有实际的内容。注：在现有的实际项目中一个 POM既是聚合POM，又是父 POM，这么做主要是为了方便。
3. 参考文档Maven POM
maven核心，pom.xml详解
maven 配置篇 之pom.xml(一）
史上最全的maven pom.xml文件教程详解
Maven聚合与继承
Maven详解之聚合与继承
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>maven</tag>
      </tags>
  </entry>
  <entry>
    <title>Maven概述</title>
    <url>/dev-maven-start/</url>
    <content><![CDATA[1. Maven是什么Maven这个单词来自于意第绪语，意为知识的积累，最早在Jakata Turbine项目中它开始被用来试图简化构建过程。
Maven是基于项目对象模型(POM，Project Object Model)，可以通过一小段描述信息来管理项目的构建，报告和文档的软件项目管理工具。
2. Maven能做什么Maven采用了一种被称之为POM的概念来管理项目，所有的项目配置信息都被定义在一个叫做pom.xml的文件中，通过该文件，Maven可以管理项目的整个声明周期，包括编译，构建，测试，发布，报告等等。目前Apache下绝大多数项目都已经采用Maven进行管理。而Maven本身还支持多种插件，可以方便更灵活的控制项目。
郝同学使用Maven，最深切的体会是，不用手动导入jar包，写写配置文件就搞定了，真方便！


3. jar包依赖使用maven不需要上网单独下载jar包，只需要在配置文件pom.xml中配置jar包的依赖关系，就可以自动的下载jar包到我们的项目中。这样，别人开发或者使用这个工程时，不需要来回的拷贝jar包，只需要复制这个pom.xml就可以自动的下载这些jar包。
而且，我们自己下载jar包，还有可能造成版本的不一致，这样在协同开发的过程中就有可能造成代码运行的不一致。通过使用maven精确的匹配jar包，就不会出现这种问题了。
4. 项目坐标Maven通过特定的标识来定义项目名称，这样既可以唯一的匹配其他的jar包，也可以通过发布，使别人能使用自己的发布产品。这个标识就被叫做坐标，长的其实很普通，就是简单的xml而已：
&lt;groupId&gt;com.voidking.pandawork&lt;/groupId&gt;&lt;artifactId&gt;pandawork-start&lt;/artifactId&gt;&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;&lt;packaging&gt;jar&lt;/packaging&gt;&lt;name&gt;pandawork-start&lt;/name&gt;&lt;url&gt;http://maven.apache.org&lt;/url&gt;
groupId：所述的项目名称，由于有的项目并不是一个jar包构成的，而是由很多的jar包组成的。因此这个groupId就是整个项目的名称。
artifactId：包的名称。
version：版本号。
packaging：包的类型，一般都是jar，也可以是war之类的。如果不填，默认就是jar。
name和url，一个是名称，一个是maven的地址。主要就是上面的几个参数。
当想要依赖什么jar的时候就可以通过下面的方式依赖：
&lt;dependencies&gt;  &lt;dependency&gt;    &lt;groupId&gt;junit&lt;/groupId&gt;    &lt;artifactId&gt;junit&lt;/artifactId&gt;    &lt;version&gt;3.8.1&lt;/version&gt;    &lt;scope&gt;test&lt;/scope&gt;  &lt;/dependency&gt;&lt;/dependencies&gt;

各个属性的内容基本上都是一样的。
这里要注意的是jar包的命名规则：
artifactId-version[-classifier].packaging
比如上面的pom.xml生成的jar包名字就是：pandawork-start-0.0.1-SNAPSHOT.jar。
这里的classifier是可选的，但是有的项目可能还需要导出附属的一些文件，如javadoc，source等等，那么这个地方就需要配置一个字符串。一般都是JDKXXX之类的。
5. 测试驱动Maven是测试驱动的开发思路，因此工程创建初期，就包含两个文件夹，main和test。一个用于放置开发的java文件，一个用于写test单元测试。这样每次开发的时候，提前设计单元测试，就能帮助减少BUG。
6. Maven生命周期Maven有三套相互独立的生命周期，请注意这里说的是“三套”，而且“相互独立”。这三套生命周期分别是：

Clean Lifecycle 在进行真正的构建之前进行一些清理工作。
Default Lifecycle 构建的核心部分，编译，测试，打包，部署等等。
Site Lifecycle 生成项目报告，站点，发布站点。

再次强调一下它们是相互独立的，你可以仅仅调用clean来清理工作目录，仅仅调用site来生成站点。当然你也可以直接运行 mvn clean install site 运行所有这三套生命周期。
6.1. Clean每套生命周期都由一组阶段(Phase)组成，我们平时在命令行输入的命令总会对应于一个特定的阶段。比如，运行mvn clean ，这个的clean是Clean生命周期的一个阶段。Clean生命周期一共包含了三个阶段：

pre-clean  执行一些需要在clean之前完成的工作
clean  移除所有上一次构建生成的文件
post-clean  执行一些需要在clean之后立刻完成的工作

mvn clean中的clean就是上面的clean，在一个生命周期中，运行某个阶段的时候，它之前的所有阶段都会被运行，也就是说，mvn clean 等同于 mvn pre-clean clean ，如果我们运行 mvn post-clean ，那么 pre-clean，clean 都会被运行。这是Maven很重要的一个规则，可以大大简化命令行的输入。
6.2. SiteSite生命周期的各个阶段：

pre-site     执行一些需要在生成站点文档之前完成的工作
site    生成项目的站点文档
post-site     执行一些需要在生成站点文档之后完成的工作，并且为部署做准备
site-deploy     将生成的站点文档部署到特定的服务器上这里经常用到的是site阶段和site-deploy阶段，用以生成和发布Maven站点，这可是Maven相当强大的功能，Manager比较喜欢，文档及统计数据自动生成，很好看。

6.3. DefaultMaven的最重要的是Default生命周期，绝大部分工作都发生在这个生命周期中，这里，我只解释一些比较重要和常用的阶段：

validate
generate-sources
process-sources
generate-resources
process-resources     复制并处理资源文件，至目标目录，准备打包。
compile     编译项目的源代码。
process-classes
generate-test-sources 
process-test-sources 
generate-test-resources
process-test-resources     复制并处理资源文件，至目标测试目录。
test-compile     编译测试源代码。
process-test-classes
test     使用合适的单元测试框架运行测试。这些测试代码不会被打包或部署。
prepare-package
package     接受编译好的代码，打包成可发布的格式，如 JAR 。
pre-integration-test
integration-test
post-integration-test
verify
install     将包安装至本地仓库，以让其它项目依赖。
deploy     将最终的包复制到远程的仓库，以让其它开发人员与项目共享。

基本上，根据名称我们就能猜出每个阶段的用途，关于其它阶段的解释，请参考 http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
记住，运行任何一个阶段的时候，它前面的所有阶段都会被运行，这也就是为什么我们运行mvn install 的时候，代码会被编译，测试，打包。
7. Maven常用命令
mvn -h不会用时，可寻求帮助。

mvn archetype:create -DgroupId=packageName -DartifactId=projectName创建Maven的普通java项目

mvn archetype:create -DgroupId=packageName -DartifactId=webappName -DarchetypeArtifactId=maven-archetype-webapp创建Maven的Web项目

mvn compile编译源代码

mvn test-compile编译测试代码

mvn test运行测试

mvn install在本地Repository中安装jar

mvn clean install

mvn jetty:run调用 Jetty 插件的 Run 目标在 Jetty Servlet 容器中启动 web 应用 


8. 学习资料Maven官网
将maven源改为国内阿里云镜像
Maven教程
maven pom.xml文件详解
Maven那点事儿（Eclipse版）
Maven生命周期详解
mvn常用命令
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>maven</tag>
      </tags>
  </entry>
  <entry>
    <title>SSH概述</title>
    <url>/dev-ssh-start/</url>
    <content><![CDATA[1. 名词解释SSH：SSH不是一个框架，而是多个框架（Struts+Spring+Hibernate）的集成，是目前较流行的一种Web应用程序开源集成框架，用于构建灵活、易于扩展的多层Web应用程序。
Struts：它通过采用 Java Servlet/JSP 技术，实现了基于JavaEE Web应用的MVC设计模式的应用框架，是MVC经典设计模式中的一个经典产品。
Struts2：它是Struts的下一代产品，是在Struts和WebWork的技术基础上进行了合并的全新的Struts2框架。其全新的Struts2的体系结构与Struts的体系结构差别巨大。Struts2以WebWork为核心，采用拦截器的机制来处理用户的请求，这样的设计也使得业务逻辑控制器能够与ServletAPI完全脱离开，所以Struts2可以理解为WebWork的更新产品。
Spring：简单来说，Spring是一个轻量级的控制反转（IoC）和面向切面（AOP）的容器框架。

Hibernate：它是一个开放源代码的对象关系映射框架，它对JDBC进行了非常轻量级的对象封装，使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 Hibernate可以应用在任何使用JDBC的场合，既可以在Java的客户端程序使用，也可以在Servlet/JSP的Web应用中使用，最具革命意义的是，Hibernate可以在应用EJB的J2EE架构中取代CMP，完成数据持久化的重任。
2. 层次划分JavaEE的体系结构有三层：表示层、业务逻辑层和数据持久层。开发一个SSH项目，要遵循这三层模式。用Hibernate来完成数据的持久层应用，用Spring的Bean来管理组件（主要是DAO、业务逻辑和Struts的Action），而用Struts来完成页面的控制跳转。
开发一个项目，一般要先完成持久层数据连接，然后实现DAO，接着是业务逻辑，最后实现页面及逻辑控制。
3. demo设计3.1. 开发环境Eclipse IDE for Java EE Developers + Mysql + Tomcat
3.2. 数据库设计创建一个Web项目，命名为“ssh”。该项目要实现学生、课程及成绩的增加、删除、修改、查找功能，需要三个表，即XSB表、KCB表、CJB表。其中XSB表中含有该学生所属专业的ID，且作为外键，故还应该有一个ZYB表。在登陆学生成绩管理系统时，如果没有登陆成功，就回到登陆界面，登陆成功后方可进行各种操作，所以还要有个DLB表。数据库名为XSCJ。
3.2.1. XSB1、学生信息表结构



项目名
列名
数据类型
可空
默认值
说明



学号
XH
定长字符串型（char6）
×
无
主键


姓名
XM
不定长字符串型（varchar8）
×
无



性别
XB
位型（bit）
×
无
值约束：1/0。1表示男，0表示女


出生时间
CSSJ
日期时间型（datetime）
√
无



专业Id
ZY_ID
整数型（int）
×
无



总学分
ZXF
整数型（int）
√
0
0&lt;=总学分&lt;160


备注
BZ
不定长字符串型（varchar500）
√
无



照片
ZP
longblob
√
无



2、学生信息表样本数据



学号
姓名
性别
出生时间
专业
总学分
备注



081101
王林
男
1990-2-10
1
50



081102
程明
男
1991-2-01
1
50



081201
王敏
男
1989-6-10
2
42



081202
孙艳
女
1989-12-29
2
40
有一门功课不及格，待补考


081241
罗琳琳
女
1990-1-30
2
50
转专业学习


3.2.2. KCB1、课程信息表结构



项目名
列名
数据类型
可空
默认值
说明



课程号
KCH
定长字符型（char3）
×
无
主键


课程名
KCM
不定长字符串型（varchar12）
√
无



开学学期
KXXQ
整数型（smallint）
√
无
只能为1~8


学时
XS
整数型（int）
√
0



学分
XF
整数型（int）
√
0



2、课程信息表样本数据



课程号
课程名
开学学期
学时
学分



101
计算机基础
1
80
5


102
程序设计语言
2
68
4


206
离散数学
4
68
4


208
数据结构
5
68
4


3.2.3. CJB1、学生成绩表结构



项目名
列名
数据类型
可空
默认值
说明



学号
XH
定长字符型（char6）
×
无
主键


课程号
KCH
定长字符型（char3）
×
无
主键


成绩
CJ
整数型（int）
√
0



学分
XF
整数型（int）
√




2、学生成绩表样本数据



学号
课程号
成绩



081101
101
80


081101
102
78


081101
206
76


3.2.4. ZYB1、专业信息表结构



项目名
列名
数据类型
可空
默认值
说明



Id
ID
整数型（int）
×
增1
主键


专业名
ZYM
定长字符型（char12）
×




人数
RS
整数型（int）
√
0



辅导员
FDY
定长字符型（char8）
×




2、专业信息表样本数据



专业
人数
辅导员



计算机
150
黄日生


通信工程
131
赵红


3.2.5. DLB1、登陆表结构



项目名
列名
数据类型
可空
默认值
说明



标志
ID
整数型（int）
×

主键，是标志


登陆号
XH
定长字符型（char6）
×
无
与XSB表学号关联


口令
KL
定长字符型（char20）
√
无
可以加密，长度为8~20


2、登录表样本数据



标志
登陆号
口令



1
081101
voidking


2
081102
voidking


3.2.6. XS_KCB1、连接表结构



项目名
列名
数据类型
可空
默认值
说明



学号
XH
定长字符型（char6）
×

主键


课程号
KCH
定长字符型（char3）
×

主键










2、连接表样本数据










学号
课程号



081101
101


081101
102


081101
206


3.3. 包设计com.voidking.ssh.action：放置对应的用户自定义的Action类。由Action类调用业务逻辑来处理用户请求，然后控制跳转。
com.voidking.ssh.dao：放置DAO（数据访问对象）的接口，接口中的方法用来和数据库进行交互，这些方法由它们的类来实现。
com.voidking.ssh.dao.imp：放置实现DAO接口的类。
com.voidking.ssh.model：放置表对应的POJO类及映射文件*.hbm.xml。
com.voidking.ssh.service：放置业务逻辑接口。接口中的方法用来处理用户请求，这些方法由实现接口的类来实现。
com.voidking.ssh.service.imp：放置实现业务逻辑接口的类。
com.voidking.ssh.tool：放置公用的工具类，如分页类。
struts.properties：实现Struts2和Spring整合。
struts.xml：配置Action。
3.4. 添加Spring开发3.5. 实现Hibernate持久层3.6. 实现DAO3.7. 实现业务逻辑层3.8. 实现Web层4. 源代码分享5. 改进设计使用SSH自动生成表，或者通过表自动生成POJO类。
6. 参考文档《Java EE基础实用教程》，郑阿奇主编SSH框架总结：http://blog.csdn.net/shan9liang/article/details/8803989Struts1和Struts2：http://blog.csdn.net/bjyfb/article/details/8679523手把手教你搭建SSH：http://www.tuicool.com/articles/f2u6VrAhttp://blog.csdn.net/zhaolijing2012/article/details/39700187eclipse搭建SSH框架详解：http://blog.csdn.net/aaaaaaaa0705/article/details/6288431
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>ssh</tag>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>hibernate</tag>
        <tag>spring</tag>
        <tag>struts2</tag>
      </tags>
  </entry>
  <entry>
    <title>Spring与Hibernate整合应用</title>
    <url>/dev-spring-hibernate-integration/</url>
    <content><![CDATA[1. 创建数据库和表其实我们在之前的例子中已经创建过了，数据库名xscj，表为dlb。详情请见《Hibernate与Struts2整合应用》。
2. 创建项目创建Dynamic Web Project，命名为hibernate_spring。
3. 加载Spring框架右击项目名，Properties，Java Build Path，Libraries，Add Library…，User Library，Next，User Libraries…，New…，输入“spring”，OK，Add External JARs…，选中需要的jar文件，打开，OK。
4. 加载Hibernate框架右击项目名，Properties，Java Build Path，Libraries，Add Library…，User Library，Next，User Libraries…，New…，输入“hibernate”，OK，Add External JARs…，选中需要的jar文件，打开，OK。

5. 生成POJO类和*.hbm.xmlWindow，Open Perspective，Hibernate。
Add Configuration…，选中Project（hibernate_spring），选择Database connection，Setup… Propery file，Setup… Configuration file（需要进行一些配置）。
这时，会提示缺少MySQL驱动，在Classpath选项卡中配置一下就好了。
File，New，Hibernate Reverse Engineering File（reveng.xml），选中hibernate_spring，Next，选择Console configuration，Refresh，
在工具栏中，有三个三角号（绿色圆底），其中一个三角号的右下角有Hibernate的小标志。点击它的下拉菜单，Hibernate Code Generation Configurations…。
New，在Main选项卡中选择Console configuration，选择Output directory，勾选Reverse engineer from JDBC Connection。
在Exporters选项卡中勾选Domain code（.java）和Hibernate XML Mappings（.hbm.xml）。Run。
6. 编写DlDao接口package com.voidking.hibernate_spring.dao;import com.voidking.hibernate_spring.model.Dlb;public interface DlDao &#123;	public void save(Dlb dl);&#125;

7. 编写DlDaoImp实现类package com.voidking.hibernate_spring.dao.imp;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.classic.Session;import com.voidking.hibernate_spring.dao.DlDao;import com.voidking.hibernate_spring.model.Dlb;public class DlDaoImp implements DlDao &#123;	// 依赖注入SessionFactory对象，set方法注入	private SessionFactory sessionFactory;	public void setSessionFactory(SessionFactory sessionFactory) &#123;		this.sessionFactory = sessionFactory;	&#125;	@Override	public void save(Dlb dl) &#123;		try &#123;			Session session = sessionFactory.openSession();			Transaction ts = session.beginTransaction();			session.save(dl);			ts.commit();		&#125; catch (Exception e) &#123;			// TODO: handle exception			e.printStackTrace();		&#125;			&#125;&#125;

8. applicationContext.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!--  - Middle tier application context definition for the image database.  --&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;		xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;		xmlns:context=&quot;http://www.springframework.org/schema/context&quot;		xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;		xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd				http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd&quot;&gt;	&lt;!-- 用Bean定义数据源 --&gt;	&lt;bean id=&quot;datasource&quot; class=&quot;org.apache.commons.dbcp.BasicDataSource&quot;&gt;		&lt;property name=&quot;driverClassName&quot; value=&quot;com.mysql.jdbc.Driver&quot;&gt;&lt;/property&gt;		&lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/xscj&quot;&gt;&lt;/property&gt;		&lt;property name=&quot;username&quot; value=&quot;scott&quot;&gt;&lt;/property&gt;		&lt;property name=&quot;password&quot; value=&quot;tiger&quot;&gt;&lt;/property&gt;	&lt;/bean&gt;		&lt;!-- 定义Hibernate的SessionFactory --&gt;	&lt;bean id=&quot;sessionFactory&quot; class=&quot;org.springframework.orm.hibernate3.LocalSessionFactoryBean&quot;&gt;		&lt;!-- 定义SessionFactory必须注入DataSource --&gt;		&lt;property name=&quot;dataSource&quot;&gt;			&lt;ref bean=&quot;datasource&quot;/&gt;		&lt;/property&gt;				&lt;!-- 定义Hibernate的SessionFactory属性 --&gt;		&lt;property name=&quot;hibernateProperties&quot;&gt;			&lt;props&gt;				&lt;prop key=&quot;hibernate.dialect&quot;&gt;					org.hibernate.dialect.MySQLDialect				&lt;/prop&gt;			&lt;/props&gt;		&lt;/property&gt;				&lt;!-- 定义POJO的映射文件 --&gt;		&lt;property name=&quot;mappingResources&quot;&gt;			&lt;list&gt;				&lt;value&gt;/com/voidking/hibernate_spring/model/Dlb.hbm.xml&lt;/value&gt;			&lt;/list&gt;		&lt;/property&gt;	&lt;/bean&gt;		&lt;!-- 注入dlDao --&gt;	&lt;bean id=&quot;dlDao&quot; class=&quot;com.voidking.hibernate_spring.dao.imp.DlDaoImp&quot;&gt;		&lt;property name=&quot;sessionFactory&quot;&gt;			&lt;ref bean=&quot;sessionFactory&quot;/&gt;		&lt;/property&gt;	&lt;/bean&gt;&lt;/beans&gt;
Spring的Bean很好地管理了以前在hibernate.cfg.xml文件中创建的SessionFactory，使文件更易阅读。
9. 编写测试类package com.voidking.hibernate_spring.test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.FileSystemXmlApplicationContext;import com.voidking.hibernate_spring.dao.DlDao;import com.voidking.hibernate_spring.model.Dlb;public class Test &#123;	public static void main(String[] args) &#123;		Dlb dlb = new Dlb();		dlb.setXh(&quot;081109&quot;);		dlb.setKl(&quot;123456&quot;);		ApplicationContext context = new FileSystemXmlApplicationContext(&quot;src/applicationContext.xml&quot;);		DlDao dlDao = (DlDao)context.getBean(&quot;dlDao&quot;);		dlDao.save(dlb);			&#125;&#125;

10. 运行报错Cannot find class [org.apache.commons.dbcp.BasicDataSource]
缺少commons-dbcp.jar、commons-pool.jar这两个包，在spring-framework-*/lib/jakarta-commons目录下可以找到，添加进路径即可。
上述问题解决之后，运行成功，数据插入成功。但是，并不能运行第二次，因为Duplicate entry &#39;0&#39; for key &#39;PRIMARY&#39;，且不去管它。
11. 源代码分享https://github.com/voidking/hibernate_spring.git
12. 小结这个工程中，并不需要hibernate.cfg.xml、hibernate.properties，因为它们完成的工作，applicationContext帮它们完成了。
13. 参考文档《Java EE基础实用教程》，郑阿奇主编

Cannot find class [org.apache.commons.dbcp.BasicDataSource]http://blog.csdn.net/a105421548/article/details/43016953

]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>hibernate</tag>
        <tag>spring</tag>
      </tags>
  </entry>
  <entry>
    <title>Spring与Struts2整合应用</title>
    <url>/dev-spring-struts2-integration/</url>
    <content><![CDATA[1. 新建项目新建Dynamic Web Project，命名为struts2_spring。
2. 添加struts2框架添加struts2五个核心jar包到lib文件夹。
3. web.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app id=&quot;WebApp_9&quot; version=&quot;2.4&quot; xmlns=&quot;http://java.sun.com/xml/ns/j2ee&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd&quot;&gt;	&lt;welcome-file-list&gt;		&lt;welcome-file&gt;/login.jsp&lt;/welcome-file&gt;	&lt;/welcome-file-list&gt;    &lt;filter&gt;        &lt;filter-name&gt;struts2&lt;/filter-name&gt;        &lt;filter-class&gt;org.apache.struts2.dispatcher.FilterDispatcher&lt;/filter-class&gt;    &lt;/filter&gt;    &lt;filter-mapping&gt;        &lt;filter-name&gt;struts2&lt;/filter-name&gt;        &lt;url-pattern&gt;/*&lt;/url-pattern&gt;    &lt;/filter-mapping&gt;&lt;/web-app&gt;


4. login.jsp&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-tags&quot; prefix=&quot;s&quot; %&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;登录界面&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;s:form action=&quot;login.action&quot; method=&quot;post&quot;&gt;		&lt;s:textfield name=&quot;xh&quot; label=&quot;学号&quot;&gt;&lt;/s:textfield&gt;		&lt;s:password name=&quot;kl&quot; label=&quot;口令&quot;&gt;&lt;/s:password&gt;	&lt;/s:form&gt;&lt;/body&gt;&lt;/html&gt;

5. LoginAction.javapackage com.voidking.struts2_spring.action;import com.opensymphony.xwork2.ActionSupport;public class LoginAction extends ActionSupport&#123;	private String xh;	private String kl;	public String getXh() &#123;		return xh;	&#125;	public void setXh(String xh) &#123;		this.xh = xh;	&#125;	public String getKl() &#123;		return kl;	&#125;	public void setKl(String kl) &#123;		this.kl = kl;	&#125;		public String execute() throws Exception&#123;		return SUCCESS;	&#125;&#125;

6. struts.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;	&lt;include file=&quot;struts-default.xml&quot;&gt;&lt;/include&gt;	&lt;package name=&quot;default&quot; extends=&quot;struts-default&quot;&gt;		&lt;action name=&quot;login&quot; class=&quot;com.voidking.struts2_spring.action.LoginAction&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/login_success.jsp&lt;/result&gt;		&lt;/action&gt;	&lt;/package&gt;&lt;/struts&gt;

7. login_success.jsp&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-tags&quot; prefix=&quot;s&quot; %&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;登录成功&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;h2&gt;您好！&lt;s:property value=&quot;xh&quot;/&gt; 欢迎您登录成功&lt;/h2&gt;&lt;/body&gt;&lt;/html&gt;

8. 部署运行在登录框和密码框输入任意输入，单击登录按钮，转向登录成功界面，并输出登录名。
9. 添加Spring框架拷贝Spring的核心jar包到lib文件夹。
10. 添加Spring支持包拷贝struts2-spring-plugin.jar到lib文件夹，该包位于Struts2的lib目录下。
11. 修改web.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app id=&quot;WebApp_9&quot; version=&quot;2.4&quot; xmlns=&quot;http://java.sun.com/xml/ns/j2ee&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd&quot;&gt;	&lt;welcome-file-list&gt;		&lt;welcome-file&gt;/login.jsp&lt;/welcome-file&gt;	&lt;/welcome-file-list&gt;    &lt;filter&gt;        &lt;filter-name&gt;struts2&lt;/filter-name&gt;        &lt;filter-class&gt;org.apache.struts2.dispatcher.FilterDispatcher&lt;/filter-class&gt;    &lt;/filter&gt;    &lt;filter-mapping&gt;        &lt;filter-name&gt;struts2&lt;/filter-name&gt;        &lt;url-pattern&gt;/*&lt;/url-pattern&gt;    &lt;/filter-mapping&gt;	&lt;listener&gt;		&lt;listener-class&gt;			org.springframework.web.context.ContextLoaderListener		&lt;/listener-class&gt;	&lt;/listener&gt;	&lt;context-param&gt;		&lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;		&lt;param-value&gt;			/WEB-INF/classes/applicationContext.xml		&lt;/param-value&gt;	&lt;/context-param&gt;&lt;/web-app&gt;

12. struts.propertiesstruts.objectFactory=spring
该文件位于src文件夹下，运行过程中会被Struts2框架加载，是的Struts2的类对象的生成交给Spring完成。
13. applicationContext.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!--  - Middle tier application context definition for the image database.  --&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;		xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;		xmlns:context=&quot;http://www.springframework.org/schema/context&quot;		xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;		xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd				http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd&quot;&gt;	&lt;bean id=&quot;loginAction&quot; class=&quot;com.voidking.struts2_spring.action.LoginAction&quot;&gt;&lt;/bean&gt;&lt;/beans&gt;

14. 修改struts.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;	&lt;include file=&quot;struts-default.xml&quot;&gt;&lt;/include&gt;	&lt;package name=&quot;default&quot; extends=&quot;struts-default&quot;&gt;		&lt;!-- 使用Spring生成的类对象 --&gt;		&lt;action name=&quot;login&quot; class=&quot;loginAction&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/login_success.jsp&lt;/result&gt;		&lt;/action&gt;	&lt;/package&gt;&lt;/struts&gt;

15. 再次部署运行我们可以得到和第一次部署运行同样的结果。
16. 源代码分享https://github.com/voidking/struts2_spring.git
17. 参考文档《Java EE基础实用教程》，郑阿奇主编
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>spring</tag>
        <tag>struts2</tag>
      </tags>
  </entry>
  <entry>
    <title>Spring事务支持</title>
    <url>/dev-spring-affair/</url>
    <content><![CDATA[1. 概述Spring的事务管理不需要与任何特定的事务API耦合。Spring同时支持编程式事务策略和声明式事务策略，大部分时候都采用声明式事务策略。声明式事务管理的优势非常明显：代码中无需关注事务逻辑，让Spring声明式事务管理负责事务逻辑，声明式事务管理无需与具体的事务逻辑耦合，可以方便地在不同事务逻辑之间切换。
声明式事务逻辑的配置方式，通常有以下4中：1、使用TransactionProxyFactoryBean为目标Bean生成事务代理的配置。此方式是最传统、配置文件最臃肿、最难以阅读的方式。2、采用Bean继承的事务代理配置方式，比较简洁，但仍然是增量式配置。3、采用BeanNameAutoProxyCreator，根据Bean Name自动生成事务代理的方式。这是直接利用Spring的AOP框架配置事务代理的方式，需要对Spring的AOP框架有所理解。但这种方式避免了增量式方式，效果非常不错。4、采用DefaultAdvisorAutoProxyCreator，直接利用Spring的AOP框架配置事务代理的方式，效果非常不错，只是这种配置方式的可读性不如第3种方式。

2. 参考文档《Java EE基础实用教程》，郑阿奇主编
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>spring</tag>
      </tags>
  </entry>
  <entry>
    <title>Spring AOP</title>
    <url>/dev-spring-aop/</url>
    <content><![CDATA[1. 从代理机制初探AOPpackage com.voidking.aop;import java.util.logging.*;public class HelloSpeaker &#123;	private Logger logger = Logger.getLogger(this.getClass().getName());	public void hello(String name)	&#123;		logger.log(Level.INFO, &quot;hello method starts...&quot;);		System.out.println(&quot;hello,&quot;+name);		logger.log(Level.INFO, &quot;hello method ends...&quot;);	&#125;&#125;
在HelloSpeaker类中，当执行hello()方法时，程序员希望该方法执行开始与执行完毕时都留下日志。最简单的做法是上面的程序设计，在方法执行前后加上日志动作。
然而对于HelloSpeaker类来说，日志的这种动作并不属于HelloSpeaker的逻辑，这使得HelloSpeaker增加了额外的职责。

如果程序中这种日志动作到处都有，以上的写法势必造成程序员必须到处撰写这些日志动作的代码。这将使得维护日志代码的困难加大。如果需要的服务不只是日志动作，有一些非类本身职责的相关动作也混入到类中，如权限检查、事务管理等，会使得类的负担加重，甚至混淆类本身的职责。
另一方面，使用以上的写法，如果有一天不再需要日志（或权限检查、交易管理等）服务，将需要修改所有留下日志动作的程序，无法简单地将这些相关服务从现有的程序中移除。
可以使用代理（Proxy）机制来解决这个问题，有两种代理方式：静态代理（static proxy）和动态代理（dynamic proxy）。
在静态代理的实现中，代理类与被代理的类必须实现同一个接口。在代理类中可以实现记录等相关服务，并在需要的时候再呼叫被代理类。这样被代理类就可以仅仅保留业务相关的职责了。
举个简单的例子，首先定义一个IHello接口：
package com.voidking.aop;public interface IHello &#123;	public void hello(String name);&#125;

然后让实现业务逻辑的HelloSpeaker2类实现IHello接口：
package com.voidking.aop;public class HelloSpeaker2 implements IHello &#123;		@Override	public void hello(String name) &#123;		// TODO Auto-generated method stub		System.out.println(&quot;hello,&quot;+name);	&#125;&#125;

代理类HelloProxy同样要实现IHello接口：
package com.voidking.aop;import java.util.logging.Level;import java.util.logging.Logger;public class HelloProxy implements IHello &#123;	private Logger logger = Logger.getLogger(this.getClass().getName());	private IHello helloObject;	public  HelloProxy(IHello helloObject) &#123;		// TODO Auto-generated constructor stub		this.helloObject = helloObject;	&#125;			@Override	public void hello(String name) &#123;		// TODO Auto-generated method stub		log(&quot;hello method starts...&quot;);		helloObject.hello(name);		log(&quot;hello method ends...&quot;);	&#125;		private void log(String msg)	&#123;		logger.log(Level.INFO, msg);	&#125;&#125;
写一个测试程序来看看效果：
package com.voidking.aop;public class ProxyDemo &#123;	public static void main(String[] args) &#123;		IHello proxy = new HelloProxy(new HelloSpeaker2());		proxy.hello(&quot;Justin&quot;);	&#125;&#125;

这是静态代理的基本示例，但是可以看到，代理类的一个接口只能服务于一种类型的类，而且如果要代理的方法很多，势必要为每个方法进行代理。静态代理在程序规模稍大时必定无法胜任。
2. 动态代理JDK1.3之后加入了可协助开发动态代理功能的API等相关类别，不需要为特定类和方法编写特定代理类，使用动态代理。使用动态代理可以使一个处理者（Handler）为各个类服务。
IHello.java、HelloSpeaker.java、LogHandler.java、ProxyDemo.java代码分别如下：
package com.voidking.aop2;public interface IHello &#123;	public void hello(String name);&#125;

package com.voidking.aop2;public class HelloSpeaker implements IHello &#123;	@Override	public void hello(String name) &#123;		// TODO Auto-generated method stub		System.out.println(&quot;Hello,&quot;+name);	&#125;&#125;

package com.voidking.aop2;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class LogHandler implements InvocationHandler &#123;	private Object sub;	public LogHandler() &#123;		// TODO Auto-generated constructor stub	&#125;	public LogHandler(Object obj)&#123;		sub=obj;	&#125;	@Override	public Object invoke(Object proxy, Method method, Object[] args)			throws Throwable &#123;		// TODO Auto-generated method stub		System.out.println(&quot;before you do thing&quot;);		method.invoke(sub, args);		System.out.println(&quot;after you do thing&quot;);		return null;	&#125;&#125;

package com.voidking.aop2;import java.lang.reflect.Proxy;public class ProxyDemo &#123;	public static void main(String[] args) &#123;		// TODO Auto-generated method stub		HelloSpeaker helloSpeaker = new HelloSpeaker();		LogHandler logHandler = new LogHandler(helloSpeaker);		Class cls = helloSpeaker.getClass();		IHello iHello = (IHello)Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), logHandler);		iHello.hello(&quot;Justin&quot;);	&#125;&#125;
HelloSpeaker本身的职责是显示文字，却必须插入日志动作，这是的HelloSpeaker的职责加重。日志的程序代码横切（cross-cutting）到HelloSpeaker的程序执行流程中，日志这样的动作在AOP术语中被称为横切关注点（cross-cutting concerns）。
使用代理类将记录与业务逻辑无关的动作提取出来，设计为一个服务类，如同前面的范例HelloProxy或者LogHandler，这样的类称为切面（Aspect）。
将日志等动作（cross-cutting concerns）设计为通用，不介入特定业务类的一个职责清楚的Aspect类，这就是所谓的Aspect-Oriented Programming，AOP。
3. 通知AdviceSpring提供了5种通知（Advice）类型：

Interception Around Advice：在目标对象的方法执行前后被调用。
Before Advice：在目标对象的方法执行前被调用。
After Returning Advice：在目标对象的方法执行后被调用。
Throw Advice：在目标对象的方法抛出异常时被调用。
Introduction Advice：一种特殊类型的拦截通知，只有在目标对象的方法调用完毕后执行。

4. 切入点PointcutPointcut定义了Advice应用的时机。在Spring中，使用PointcutAdvisor把Pointcut与Advice结合为一个对象。Spring中大部分内建的Pointcut都有对应的PointAdvisor。静态切入点只限于给定的方法和目标类，而不考虑方法的参数。动态切入点与静态切入点的区别是，动态切入点不仅限定于给定的方法和类，还可以指定方法的参数。大多数切入点，可以使用静态切入点，很少有机会创建动态切入点。
5. 源代码分享https://github.com/voidking/aop.git
6. 小结Advice和Pointcut到底干嘛用的？书上根本没有讲清楚哇！不去查资料了，需要的时候再去深入学习。
7. 参考文档《Java EE基础实用教程》，郑阿奇主编
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>spring</tag>
      </tags>
  </entry>
  <entry>
    <title>Spring概述</title>
    <url>/dev-spring-start/</url>
    <content><![CDATA[1. 名词解释简单来说，Spring是一个轻量级的控制反转（IoC）和面向切面（AOP）的容器框架。
Spring框架是Rod Johnson开发的，2003年发布了Spring框架的第一个版本。Spring是一个从实际开发中抽取出来的框架，因此它完成了大量开发中的通用步骤，从而大大提高了企业应用的开发效率。
Spring为企业应用的开发提供了一个轻量级的解决方案。其中依赖注入、基于AOP的声明式事务管理、多种持久层的整合与优秀的Web MVC框架等最为人们关注。Spring可以贯穿程序的各层之间，但它并不是要取代那些已有的框架，而是以高度的开发性与它们紧密地整合，这也是Spring被广泛应用的原因之一。
Spring使用最基本的JavaBean来完成以前只可能由EJB完成的事情。然而，Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合性的角度而言，任何Java应用都可以从Spring中受益。
2. 官网http://spring.io

3. 下载3.1. 方法一http://repo.spring.io搜索spring-framework，选择下载即可。
3.2. 方法二1)打开eclipse-help-Install New Software…2)Add…，弹出Add Repository。4)对话框里Name随意，Location输入：http://springide.org/updatesite/ ，点击OK。5)选择Core/Spring IDE，Next。
4. 导入包Spring4的包，官方没有给出下载，只给出了Maven的引用方法，个人感觉非常好。Spring3及以前的包，可以使用Maven引用，也可以下载后导入jar包。
spring-framework-**.zip解压后,将spring-framework-**文件夹的dist目录下的jar包导入工程中。
5. 分层架构Spring框架的主要优势之一是其分层架构，分层架构允许选择使用任何一个组件，同时为J2EE应用程序开发提供集成的框架。Spring框架的功能可以用在任何J2EE服务器中，大多数功能也适用于不受管理的环境。Spring的核心要点是：支持不绑定到特定J2EE服务的可重用业务和数据访问对象。这样的对象可以在不同J2EE环境（Web或EJB）、独立应用程序、测试环境之间重用。
组成Spring 框架的每个模块（或组件）都可以单独存在，或者与其他一个或多个模块联合实现。每个模块的功能如下：

核心容器：核心容器提供Spring 框架的基本功能。核心容器的主要组件是BeanFactory，它是工厂模式的实现。BeanFactory使用控制反转（IOC）模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。

Spring 上下文：Spring 上下文是一个配置文件，向Spring 框架提供上下文信息。Spring 上下文包括企业服务，例如JNDI 、EJB、电子邮件、国际化、校验和调度功能。

Spring AOP ：通过配置管理特性，Spring AOP 模块直接将面向方面的编程功 能集成到了Spring 框架中。所以，可以很容易地使Spring 框架管理的任何对象支持AOP 。Spring AOP 模块为基于Spring 的应用程序中的对象提供了事务管理服务。通过使用Spring AOP ，不用依赖EJB 组件，就可以将声明性事务管理集成到应用程序中。

Spring DAO ：JDBC DAO抽象层提供了有意义的异常层次结构，可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理，并且极大地降低了需要编写的异常代码数量（例如打开和关闭连接）。Spring DAO 的面向JDBC 的异常遵从通用的DAO 异常层次结构。

Spring ORM ：Spring 框架插入了若干个ORM 框架，从而提供了ORM 的对象关系工具，其中包括JDO 、Hibernate 和iBatisSQLMap 。所有这些都遵从Spring 的通用事务和DAO异常层次结构。

Spring Web：为基于Web的应用程序提供上下文。它建立在应用程序上下文模块之上，简化了处理多份请求及将请求参数绑定到域对象的工作。Spring框架支持Jakarta Struts的集成。

Spring Web MVC：是一个全功能Web应用程序的MVC实现。通过策略接口实现高度可配置，MVC容纳了大量视图技术，其中包括JSP、Velocity、Tiles、iText和POI。


6. 依赖注入Spring的核心机制是依赖注入（Dependency Inversion），也称为控制反转。
6.1. 工厂模式6.1.1. Human.javapackage com.voidking.factory;public interface Human &#123;	void eat();	void walk();&#125;

6.1.2. Chinese.javapackage com.voidking.factory;public class Chinese implements Human &#123;	@Override	public void eat() &#123;		// TODO Auto-generated method stub		System.out.println(&quot;中国人很会吃！&quot;);	&#125;	@Override	public void walk() &#123;		// TODO Auto-generated method stub		System.out.println(&quot;中国人健步如飞！&quot;);	&#125;&#125;
6.1.3. American.javapackage com.voidking.factory;public class American implements Human &#123;	@Override	public void eat() &#123;		// TODO Auto-generated method stub		System.out.println(&quot;美国人吃西餐！&quot;);	&#125;	@Override	public void walk() &#123;		// TODO Auto-generated method stub		System.out.println(&quot;美国人经常坐车！&quot;);	&#125;&#125;

6.1.4. Factory.javapackage com.voidking.factory;public class Factory &#123;	public Human getHuman(String name)	&#123;		if(name.equals(&quot;Chinese&quot;))		&#123;			return new Chinese();		&#125;else if (name.equals(&quot;American&quot;)) &#123;			return new American();		&#125;else &#123;			throw new IllegalArgumentException(&quot;参数不正确&quot;);		&#125;	&#125;&#125;

6.1.5. Test.javapackage com.voidking.factory;public class Test &#123;	public static void main(String[] args) &#123;		Human human=null;		human= new Factory().getHuman(&quot;Chinese&quot;);		human.eat();		human.walk();				human= new Factory().getHuman(&quot;American&quot;);		human.eat();		human.walk();	&#125;&#125;

6.2. 依赖注入应用上面工厂模式中，甲组件需要乙组件的对象的时候，无需直接创建其实例，而是通过工厂获得，只要创建一个工厂即可。而Spring容器则提供了更好的办法，开发人员不用创建工厂，可以直接使用Spring提供的依赖注入方式。可以把上例修改为使用Spring容器来创建对象。
6.2.1. 引入Spring的jar包spring.jar、spring-source.jar、commons-logging.jar。
6.2.2. applicationContext.xml在src文件夹下，新建文件applicationContext.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!--  - Middle tier application context definition for the image database.  --&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;		xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;		xmlns:context=&quot;http://www.springframework.org/schema/context&quot;		xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;		xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd				http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd&quot;&gt;	&lt;bean id=&quot;chinese&quot; class=&quot;com.voidking.factory.Chinese&quot;&gt;&lt;/bean&gt;	&lt;bean id=&quot;american&quot; class=&quot;com.voidking.factory.American&quot;&gt;&lt;/bean&gt;	&lt;/beans&gt;

6.2.3. 修改Test.javapackage com.voidking.factory;import org.springframework.context.ApplicationContext;import org.springframework.context.support.FileSystemXmlApplicationContext;public class Test &#123;	public static void main(String[] args) &#123;		/*		Human human=null;		human= new Factory().getHuman(&quot;Chinese&quot;);		human.eat();		human.walk();				human= new Factory().getHuman(&quot;American&quot;);		human.eat();		human.walk();		*/				ApplicationContext ctx = new FileSystemXmlApplicationContext(&quot;src/applicationContext.xml&quot;);		Human human = null;		human =(Human)ctx.getBean(&quot;chinese&quot;);		human.eat();		human.walk();		human = (Human)ctx.getBean(&quot;american&quot;);		human.eat();		human.walk();	&#125;&#125;
所谓依赖注入，就是在运行的过程中，如果需要调用另一个对象协助时，无需在代码中创建被调用者，而是依赖于外部的注入。Spring的依赖注入对调用者和被调用者几乎没有任何要求，完全支持POJO之间依赖关系的管理。依赖注入通常有两种：
6.3. 设置注入设置注入是通过set方法注入被调用者的实例。这种方法简单、直观，很容易理解，因而Spring的依赖注入被大量使用。
6.3.1. Human.javapackage com.voidking.dependencyinversion;public interface Human &#123;	void speak();&#125;

6.3.2. Languige.javapackage com.voidking.dependencyinversion;public interface Languige &#123;	public String kind();&#125;

6.3.3. Chinese.javapackage com.voidking.dependencyinversion;public class Chinese implements Human &#123;		private Languige lan;	@Override	public void speak() &#123;		// TODO Auto-generated method stub		System.out.println(lan.kind());	&#125;	public void setLan(Languige lan) &#123;		this.lan = lan;	&#125;&#125;

6.3.4. English.javapackage com.voidking.dependencyinversion;public class English implements Languige &#123;	@Override	public String kind() &#123;		// TODO Auto-generated method stub		return &quot;中国人也会说英语！&quot;;	&#125;&#125;

6.3.5. applicationContext.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!--  - Middle tier application context definition for the image database.  --&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;		xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;		xmlns:context=&quot;http://www.springframework.org/schema/context&quot;		xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;		xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd				http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd&quot;&gt;	&lt;bean id=&quot;chinese&quot; class=&quot;com.voidking.dependencyinversion.Chinese&quot;&gt;		&lt;property name=&quot;lan&quot; ref=&quot;english&quot;&gt;&lt;/property&gt;	&lt;/bean&gt;		&lt;bean id=&quot;english&quot; class=&quot;com.voidking.dependencyinversion.English&quot;&gt;&lt;/bean&gt;&lt;/beans&gt;
各个Bean之间的依赖关系放在配置文件中完成，而不是用代码体现。通过配置文件，Spring能精确地为每个Bean注入属性。注意，配置文件的Bean的class属性值，不能是接口，必须是真正的实现类。
Spring会自动接管每个Bean定义里的property元素定义。Spring会在执行无参数的构造器并创建默认的Bean实例后，调用对应的set方法为程序注入属性值。
每个Bean的id属性是该Bean的唯一标识，程序通过id属性访问Bean。而且各个Bean之间的依赖关系也通过id属性关联。
6.3.6. Test.javapackage com.voidking.dependencyinversion;import org.springframework.context.ApplicationContext;import org.springframework.context.support.FileSystemXmlApplicationContext;public class Test &#123;	public static void main(String[] args) &#123;		ApplicationContext ctx = new FileSystemXmlApplicationContext(&quot;src/applicationContext.xml&quot;);		Human human = null;		human =(Human)ctx.getBean(&quot;chinese&quot;);		human.speak();	&#125;&#125;


6.4. 构造注入在构造实例时，已经为其完成了属性的初始化，利用构造函数来设置依赖注入的方法，成为构造注入。在前面dependencyinversion工程的基础上修改。
6.4.1. 修改Chinese.javapackage com.voidking.dependencyinversion2;public class Chinese implements Human &#123;		private Languige lan;		public Chinese() &#123;		super();		// TODO Auto-generated constructor stub	&#125;	public Chinese(Languige lan) &#123;		super();		this.lan = lan;	&#125;	@Override	public void speak() &#123;		// TODO Auto-generated method stub		System.out.println(lan.kind());	&#125;&#125;

6.4.2. 修改applicationContext.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!--  - Middle tier application context definition for the image database.  --&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;		xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;		xmlns:context=&quot;http://www.springframework.org/schema/context&quot;		xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;		xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd				http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd&quot;&gt;	&lt;bean id=&quot;chinese&quot; class=&quot;com.voidking.dependencyinversion2.Chinese&quot;&gt;		&lt;constructor-arg ref=&quot;english&quot;&gt;&lt;/constructor-arg&gt;	&lt;/bean&gt;		&lt;bean id=&quot;english&quot; class=&quot;com.voidking.dependencyinversion2.English&quot;&gt;&lt;/bean&gt;&lt;/beans&gt;
设置注入和构造注入，区别在于创建Human实例中的Languige属性的时间不同。（呃，前面命名的时候搞错了，Language写成了Languige，领会精神。。。）设置注入是先创建一个默认的Bean实例，然后调用对应的set方法注入依赖关系；而构造注入则在创建Bean实例时，已经完成了依赖关系的注入。
7. Spring核心接口7.1. BeanFactoryBean工厂，由org.springframework.beans.factory.BeanFactory接口定义。
BeanFactory采用工厂设计模式。这个接口负责创建和分发Bean，但与其他工厂模式的实现不同，它们只分发一种类型的对象。BeanFactory是一个通用的工厂，可以创建和分发各种类型的Bean。
在Spring中有几种BeanFactory的实现，其中最常用的是org.springframework.bean.factory.xml.XmlBeanFactory。它根据XML文件中的定义装载Bean。
要创建XMLBeanFactory，需要传递一个java.io.InputStream对象给构造函数。InputStream对象提供XML文件给工厂。例如，下面的例子使用一个java.io.FileInputStream对象把Bean XML定义文件给XMLBeanFactory：
BeanFactory factory = new XmlBeanFactory(new FileInputStream(&quot;applicationContext.xml&quot;));
这行代码告诉BeanFactory从XML文件中读取Bean的定义信息，但是现在BeanFactory没有实例化Bean，Bean被延迟加载到BeanFactory中，就是说BeanFactory会立即把Bean定义信息加载进来，但是Bean只有在需要的时候才被实例化。
为了从BeanFactory得到Bean，只要简单地调用getBean()方法，把需要的Bean的名字当做参数传递进去就行了。由于得到的是Object类型，所以要进行强制类型转化。
MyBean myBean = (MyBean)factory.getBean(&quot;myBean&quot;);
当getBean()方法被调用时，工厂就会实例化Bean，并使用依赖注入开始设置Bean的属性。这样就在Spring容器中开始了Bean的生命周期。
7.2. ApplicationContext应用上下文，由org.springframework.context.ApplicationContext接口定义，是BeanFactory的子接口。
BeanFactory对简单应用来说已经很好了，但是为了获得Spring框架的强大功能，需要使用Spring更高级的容器——ApplicationContext。
表面上，ApplicationContext和BeanFactory差不多。两者都是载入Bean定义信息，装配Bean，根据需要分发Bean。但是ApplicationContext提供了更多功能：1、应用上下文提供了文本解析工具，包括对国际化的支持。2、应用上下文提供了载入文本资源的通用方法，如载入图片。3、应用上下文可以向注册为监听器的Bean发送事件。
由于它提供了附加功能，几乎所有的应用系统都选择ApplicationContext，而不是BeanFactory。
在ApplicationContext的诸多实现中有三个常用的实现：
ApplicationContext context = new FileSystemXmlApplicationContext(&quot;c:/foo.xml&quot;);ApplicationContext context = new FileSystemXmlApplicationContext(&quot;foo.xml&quot;);ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext());
FileSystemXmlApplicationContext只能在指定的路径中寻找foo.xml文件，而FileSystemXmlApplicationContext可以在整个类路径中寻找foo.xml。
ApplicationContext与BeanFactory的另一个重要区别是单实例Bean如何被加载。BeanFactory延迟加载所有Bean，直到getBean()被调用时，Bean才被创建。ApplicationContext则聪明一点，它会在上下文启动后预载入所有的单实例Bean。通过预载入单实例Bean，确保需要的时候它们已经准备好了，应用程序不需要等待它们被创建。
8. Spring基本配置在Spring容器中拼接Bean叫做装配。装配Bean实际上是告诉容器需要哪些Bean，以及容器如何使用依赖注入，将它们配合起来。
8.1. 使用XML装配理论上，Bean装配可以从任何配置资源获得。但实际上，XML是最常见的Spring应用系统配置源。
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans...&gt;	&lt;bean id=&quot;chinese&quot; class=&quot;com.voidking.factory.Chinese&quot;&gt;&lt;/bean&gt;	&lt;bean id=&quot;american&quot; class=&quot;com.voidking.factory.American&quot;&gt;&lt;/bean&gt;	&lt;/beans&gt;
在XML文件定义Bean，上下文定义文件的根元素是。有多个子元素，每个元素定义了一个Bean（任何一个Java对象）如何被装配到Spring容器中。
8.2. 添加一个Bean在Spring中对一个Bean的最基本配置包括Bean的id和它的全称类名。向Spring容器中添加一个Bean只需要向XML文件中添加一个元素。
当通过Spring容器创建一个Bean时，不仅可以完成Bean实例的实例化，还可以为Bean指定特定的作用域。
1、原型模式与单实例模式：Spring中的Bean默认情况下是单实例模式。在容器分配Bean的时候，它总是返回同一个实例。但是，如果每次向ApplicationContext请求一个Bean的时候需要得到一个不同的实例，需要将Bean定义为原型模式。
&lt;bean id=&quot;chinese&quot; class=&quot;com.voidking.factory.Chinese&quot; singleton=&quot;false&quot;&gt;&lt;/bean&gt; //原型模式Bean
2、request或session：对于每次HTTP请求或HttpSession，使用request或session定义的Bean都将产生一个新实例，即每次HTTP请求或HttpSession将会产生不同的Bean实例。只有在Web应用中使用Spring时，该作用域有效。
3、golbal session：每个全局的HttpSession对应一个Bean实例。典型情况下，仅在使用portlet context的时候有效。只有在Web应用中使用Spring时，该作用域才有效。
当一个Bean实例化的时候，有事需要做一些初始化的工作，然后才能使用。同样，当Bean不再需要，从容器中删除时，需要按顺序做一些清理工作。因此，Spring可以在创建和拆卸Bean的时候调用Bean的两个生命周期方法。
在Bean的定义中设置自己的init-method，这个方法在Bean被实例化时马上被调用。同样，也可以设置自己的destroy-method，这个方法在Bean从容器中删除之前调用。
一个典型的例子是连接池Bean：
public class MyConnectionPool&#123;	...	public void initailize()&#123;&#125;	public void close()&#123;&#125;	....&#125;
Bean的定义如下：
&lt;bean id=&quot;connectionPool&quot; class=&quot;com.voidking.test.MyConnectionPool&quot; init-method=&quot;initialize&quot; destroy-method=&quot;close&quot;&gt;&lt;/bean&gt;
MyConnectionPool被实例化后，initialize方法马上被调用，给Bean初始化的机会。在Bean从容器中删除前，close方法将释放数据库连接。
9. 源代码分享https://github.com/voidking/factory.git
https://github.com/voidking/dependencyinversion.git
https://github.com/voidking/dependencyinversion2.git
10. 参考文档《Java EE基础实用教程》，郑阿奇主编

J2EE 领域的一些技术框架结构图http://www.oschina.net/question/28_47106

Caused by:java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactoryhttp://blog.csdn.net/liuxilil/article/details/5734079

]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>spring</tag>
      </tags>
  </entry>
  <entry>
    <title>Hibernate与Struts2整合应用</title>
    <url>/dev-hibernate-struts2-integration/</url>
    <content><![CDATA[应用实例：学生选课系统。要求：学生登录系统后，可以查看、修改个人信息，查看个人选课情况，选定课程及退选课程。
1. 建立数据库和表郝同学仍然使用MySQL数据库，scott用户。在xscj数据库中建立登录表、学生表、专业表、课程表，以及学生课程表即连接表。
create table dlb(id int not null primary key,xh char(6) not null ,kl varchar(20));create table xsb(xh char(6) not null primary key,xm varchar(8) not null,xb bit not null check(xb=0 or xb=1),cssj datetime ,zy_id int,zxf int default 0 check(0&lt;=zxf&lt;160),bz varchar(500),zp longblob);create table zyb(id int not null auto_increment primary key,zym varchar(12) not null,rs int default 0,fdy varchar(8));create table kcb(kch char(3) not null primary key,kcm varchar(12),kxxq smallint check(kxxq&gt;=1 and kxxq&lt;=8),xs int default 0,xf int default 0);create table xs_kcb(xh char(6),kch char(6),primary key(xh,kch));

2. 创建Web项目新建Dynamic Web Project，命名为struts2_hibernate。
3. 添加Hibernate开发能力右击项目名，Properties，Java Build Path，Libraries，Add Library…，User Library，Next，User Libraries…，New…，输入“hibernate”，OK，Add External JARs…，选中需要的jar文件，打开，OK。
4. 自动生成POJO类和映射文件4.1. 新建JPA工程新建JPA Project，输入一个工程名，Next，Next，走到一个界面，提示At least one user library must be selected.。勾选User Library中hibernate之后，显示The class &#39;javax.persistence.Convert&#39; is required to be in the selected libraries.莫非Hibernate的某些包没包含？研究了所有的包，发现，确实没有含有Convert这个类的。估计又是版本问题，真麻烦！
Back，Back，在JPA version中选择v1.0。Next，Next，勾选hibernate，Finish。
右击工程名，JPA Tools，Generate Entities from Tables…。选择Connection，激活连接，勾选Tables，Finish（或者一路Next，Finish）。
查看src文件夹，发现需要的POJO类已经生成，拷贝到需要的工程下即可。
但是，问题来了：POJO类有了，但是没有*.hbm.xml文件！
聪明的小伙伴肯定想到了，使用myeclipse生成POJO和*.hbm.xml，然后全部拷贝到eclipse的工程下。我只想说，同时安装eclipse和myeclipse，还换着用，这是有多无聊！
自己写？当然可以！但是人家myeclipse的POJO类和*.hbm.xml都可以自动生成，eclipse凭什么不可以？下面我们就研究一下方法。
4.2. Hibernate Tools插件4.2.1. 第一招Help，Eclipse Marketplace…，在find中输入“Hibernate”，搜索到的选项中有Hibernate Tools（Indigo）3.4.X，单击Install。安装过程中，报错中断：
&#x27;Installing Software &#x27; has encountered a problem.An error occurred while collecting items to be installed
本人的Eclipse是Luna版本（当前最新版），怀疑是兼容问题。重来，搜索到的选项中有JBoss Tools（Luna），单击Install。安装过程中选择Hibernate Tools安装，最终，依然报错，还能不能愉快地玩耍了！
4.2.2. 第二招我们来到JBoss Tools官网，然后找到下载界面。可以看到，官方给出了三种下载方式：Eclipse Marketplace，Update Site和Artifacts。
刚才我们无意中使出第一招，失败了，让我们试试第二招。
Help ，Install New Software… ，Work with：http://download.jboss.org/jbosstools/updates/stable/luna/ ，然后选择Hibernate Tools，Next。
慢，非常慢，完全受不了这个速度。。。
4.2.3. 第三招第三招是我喜欢的方式：离线安装。感觉很靠谱，来试试看。首先下载所需的zip文件，这里郝同学选择的是Update site (including sources) bundle of all Hibernate Tools。下载下来文件名为jbosstools-4.2.3.Final_2015-03-26_23-05-30-B264-updatesite-hibernatetools.zip。
Help ， Install New Software… ， Add… ， Archive… 。选中下载的文件，选择需要安装的插件，Next。啊勒，我靠，为什么还是这么慢！仔细观察一下进度条的提示，在Fetching什么玩意，貌似明白了什么。点击停止，然后把Contact all update sites during install to find requred software前面的勾去掉，再次Next。果然，进度马上就跑起来了！但是，跑着跑着就停了！进度条提示Connot perform operation.Computing alternate solutions,may take a while:，这个a while有点长啊！-_-|||，还能怎么办？放大招吧！下载Update site (including sources) bundle of all JBoss Core Tools，然后全部安装。我就不信了，全部的工具都在这，你还缺少依赖？
我去，这下更不得了，需要下载好多好多依赖的包，彻底醉了。。。
只勾选Hibernate Tools，试一试，成功！不容易啊！T_T
4.2.4. 使用官方教程：http://tools.jboss.org/features/hibernate.html
这篇文章写得更详细：Step by step auto code generation for POJO domain java classes and hbm using Eclipse Hibernate plugin
这篇文章写得也不错：How to generate Hibernate mapping files &amp; annotation with Hibernate Tools
Window，Open Perspective，Hibernate。
Add Configuration…，选中Project，Database connection ，Setup… Propery file，Setup… Configuration file（并且进行一些配置）。然后，然后，然后就出现了错误！！！[Classpath]: Could not parse configuration:/hibernate.cfg.xml，试了各种方法，都没法解决这个问题，这是要闹哪样啊！
多么奇葩的问题，本以为还要花费一番功夫。半小时后，莫名其妙的好了！好了。。。
重新来一遍，完整的：
Window，Open Perspective，Hibernate。
Add Configuration…，选中Project（struts2_hibernate），选择Database connection，Setup… Propery file，Setup… Configuration file（需要进行一些配置）。
这时，会提示缺少MySQL驱动，在Classpath选项卡中配置一下就好了。
File，New，Hibernate Reverse Engineering File（reveng.xml），选中struts2_hibernate，Next，选择Console configuration，Refresh，
在工具栏中，有三个三角号（绿色圆底），其中一个三角号的右下角有Hibernate的小标志。点击它的下拉菜单，Hibernate Code Generation Configurations…。New，在Main选项卡中选择Console configuration，选择Output directory，勾选Reverse engineer from JDBC Connection。在Exporters选项卡中勾选Domain code（.java）和Hibernate XML Mappings（.hbm.xml）。Run。
观察struts2_hibernate工程下的文件，可以发现，我们需要的POJO类和*.hbm.xml文件都有了！
4.2.5. 断网配置本地dtd文件刚才完整操作的时候，断网了，无法解析hibernate.cfg.xml文件，看来需要配置个本地dtd文件才可以。这里需要配置hibernate-configuration-3.0.dtd，最好也配置一下hibernate-mapping-3.0.dtd，这两个文件在hibernate3.jar中可以找到。配置好之后，问题完美解决。选中一些表，Include…，Finish。
下面是Struts2验证框架出现同样问题时的解决办法，从《Struts2概述》摘录，方便查阅：
1、解压xwork-core-*.jar包，找到xwork-validator-1.0.dtd。
2、eclipse，Window，Preferences，XML，XML Catelog，Add，File System…，选中刚才解压的xwork-validator-1.0.dtd，打开，
3、Location已经选好D:\jar\struts2\xwork-validator-1.0.dtd，Key type选择Public ID，Key填-//OpenSymphony Group//XWork Validator Config 1.0//EN，Alternative web address填http://struts.apache.org/dtds/xwork-validator-1.0.dtd。（本地dtd不存在时回去web上去找dtd）
4、StrutsAction-validation.xml需要修改如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!DOCTYPE validators PUBLIC        &quot;-//OpenSymphony Group//XWork Validator Config 1.0//EN&quot;        &quot;D:\jar\struts2\xwork-validator-1.0.dtd&quot;&gt;&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;name&quot;&gt;		&lt;field-validator type=&quot;requiredstring&quot;&gt;			&lt;!--去空格--&gt;			&lt;param name=&quot;trim&quot;&gt;true&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;姓名是必须的&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

5. *.hbm.xml文件修改5.1. Xsb.hbm.xml&lt;?xml version=&quot;1.0&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd&quot;&gt;&lt;!-- Generated 2015-5-23 0:27:57 by Hibernate Tools 3.4.0.CR1 --&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;Xsb&quot; table=&quot;xsb&quot; catalog=&quot;xscj&quot;&gt;        &lt;id name=&quot;xh&quot; type=&quot;string&quot;&gt;            &lt;column name=&quot;xh&quot; length=&quot;6&quot; /&gt;            &lt;generator class=&quot;assigned&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;xm&quot; type=&quot;string&quot;&gt;            &lt;column name=&quot;xm&quot; length=&quot;8&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;xb&quot; type=&quot;boolean&quot;&gt;            &lt;column name=&quot;xb&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;cssj&quot; type=&quot;timestamp&quot;&gt;            &lt;column name=&quot;cssj&quot; length=&quot;19&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;zyId&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;zy_id&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;zxf&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;zxf&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;bz&quot; type=&quot;string&quot;&gt;            &lt;column name=&quot;bz&quot; length=&quot;500&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;zp&quot; type=&quot;binary&quot;&gt;            &lt;column name=&quot;zp&quot; /&gt;        &lt;/property&gt;                &lt;many-to-one name=&quot;zyb&quot; class=&quot;com.voidking.struts2_hibernate.model.Zyb&quot; fetch=&quot;select&quot; cascade=&quot;all&quot; lazy=&quot;false&quot;&gt;        	&lt;column name=&quot;zy_id&quot;&gt;&lt;/column&gt;        &lt;/many-to-one&gt;                &lt;set name=&quot;kcs&quot; table=&quot;xs_kcb&quot; lazy=&quot;false&quot; cascade=&quot;save-update&quot;&gt;        	&lt;key column=&quot;xh&quot;&gt;&lt;/key&gt;        	&lt;many-to-many class=&quot;com.voidking.struts2_hibernate.model.Kcb&quot; column=&quot;kch&quot;&gt;&lt;/many-to-many&gt;        &lt;/set&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;
5.2. Kcb.hbm.xml&lt;?xml version=&quot;1.0&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd&quot;&gt;&lt;!-- Generated 2015-5-23 0:27:57 by Hibernate Tools 3.4.0.CR1 --&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;Kcb&quot; table=&quot;kcb&quot; catalog=&quot;xscj&quot;&gt;        &lt;id name=&quot;kch&quot; type=&quot;string&quot;&gt;            &lt;column name=&quot;kch&quot; length=&quot;3&quot; /&gt;            &lt;generator class=&quot;assigned&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;kcm&quot; type=&quot;string&quot;&gt;            &lt;column name=&quot;kcm&quot; length=&quot;12&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;kxxq&quot; type=&quot;java.lang.Short&quot;&gt;            &lt;column name=&quot;kxxq&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;xs&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;xs&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;xf&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;xf&quot; /&gt;        &lt;/property&gt;        &lt;set name=&quot;xss&quot; table=&quot;xs_kcb&quot; lazy=&quot;true&quot; inverse=&quot;true&quot;&gt;        	&lt;key column=&quot;kch&quot;&gt;&lt;/key&gt;        	&lt;many-to-many class=&quot;com.voidking.struts2_hibernate.model.Xsb&quot;&gt;&lt;/many-to-many&gt;        &lt;/set&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

6. Dao层组件实现6.1. DlDao.javapackage com.voidking.struts2_hibernate.dao;import com.voidking.struts2_hibernate.model.Dlb;public interface DlDao &#123;	public Dlb validate(String xh,String kl);&#125;

6.2. DlDaoImp.javapackage com.voidking.struts2_hibernate.dao.imp;import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.struts2_hibernate.dao.DlDao;import com.voidking.struts2_hibernate.model.Dlb;public class DlDaoImp implements DlDao &#123;	@Override	public Dlb validate(String xh, String kl) &#123;		// TODO Auto-generated method stub		try &#123;			Configuration cf = new Configuration();			SessionFactory sf = cf.buildSessionFactory();			Session session = sf.openSession();						Transaction ts = session.beginTransaction();			Query query = session.createQuery(&quot;from Dlb where xh=? and kl=?&quot;);			query.setParameter(0, xh);			query.setParameter(1, kl);			query.setMaxResults(1);			Dlb dlb =(Dlb)query.uniqueResult();						ts.commit();			session.close();			sf.close();						if(dlb!=null)&#123;				return dlb;			&#125;else&#123;				return null;			&#125;		&#125; catch (Exception e) &#123;			// TODO: handle exception			e.printStackTrace();		&#125;		return null;	&#125;&#125;

6.3. XsDao.javapackage com.voidking.struts2_hibernate.dao;import com.voidking.struts2_hibernate.model.Xsb;public interface XsDao &#123;	public Xsb getOneXs(String xh);		public void update(Xsb xs);&#125;

6.4. XsDaoImp.javapackage com.voidking.struts2_hibernate.dao.imp;import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.struts2_hibernate.dao.XsDao;import com.voidking.struts2_hibernate.model.Xsb;public class XsDaoImp implements XsDao &#123;	@Override	public Xsb getOneXs(String xh) &#123;		// TODO Auto-generated method stub		try &#123;			Configuration cf = new Configuration();			SessionFactory sf = cf.buildSessionFactory();			Session session = sf.openSession();			Transaction ts = session.beginTransaction();			Query query = session.createQuery(&quot;from xsb where xh=?&quot;);			query.setParameter(0, xh);			query.setMaxResults(1);			Xsb xs = (Xsb)query.uniqueResult();			ts.commit();			session.close();			sf.close();					&#125; catch (Exception e) &#123;			// TODO: handle exception			e.printStackTrace();		&#125;		return null;	&#125;	@Override	public void update(Xsb xs) &#123;		// TODO Auto-generated method stub		try &#123;			Configuration cf = new Configuration();			SessionFactory sf = cf.buildSessionFactory();			Session session = sf.openSession();			Transaction ts = session.beginTransaction();						session.update(xs);						ts.commit();			session.close();			sf.close();		&#125; catch (Exception e) &#123;			// TODO: handle exception			e.printStackTrace();		&#125;	&#125;&#125;
写完这个文件，我突然发现，写一个HibernateSessionFactory类当做工具真的太必要了！不然的话，每次我想用Session的时候，都要
Configuration cf = new Configuration();SessionFactory sf = cf.buildSessionFactory();Session session = sf.openSession();
每次都重新new一个Configuration，建立一个SessionFactory，一定加大了系统的开销。而且，一个数据库对应一个SessionFactory就够了！
myeclipse自动生成的HibernateSessionFactory类，感觉有点复杂，自己写一个吧！
6.5. MySessionFactory.javapackage com.voidking.struts2_hibernate.util;import org.hibernate.SessionFactory;import org.hibernate.cfg.Configuration; //在使用hibernate开发项目，请一定保证只有一个SessionFactory//一个数据库对应一个SessionFactory 对象.final public class MySessionFactory &#123;     private static SessionFactory sessionFactory=null;         private MySessionFactory()&#123;             &#125;         static&#123;                 sessionFactory =new Configuration().configure(&quot;/hibernate.cfg.xml&quot;).buildSessionFactory();     &#125;         public static SessionFactory getSessionFactory()&#123;        return sessionFactory;    &#125;     &#125;

6.6. 重写DlDaoImp.javapackage com.voidking.struts2_hibernate.dao.imp;import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.struts2_hibernate.dao.DlDao;import com.voidking.struts2_hibernate.model.Dlb;import com.voidking.struts2_hibernate.util.MySessionFactory;public class DlDaoImp implements DlDao &#123;	@Override	public Dlb validate(String xh, String kl) &#123;		// TODO Auto-generated method stub		try &#123;			Session session = MySessionFactory.getSessionFactory().openSession();						Transaction ts = session.beginTransaction();			Query query = session.createQuery(&quot;from Dlb where xh=? and kl=?&quot;);			query.setParameter(0, xh);			query.setParameter(1, kl);			query.setMaxResults(1);			Dlb dlb =(Dlb)query.uniqueResult();						ts.commit();			session.close();						if(dlb!=null)&#123;				return dlb;			&#125;else&#123;				return null;			&#125;		&#125; catch (Exception e) &#123;			// TODO: handle exception			e.printStackTrace();		&#125;		return null;	&#125;&#125;

6.7. 重写XsDaoImp.javapackage com.voidking.struts2_hibernate.dao.imp;import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.struts2_hibernate.dao.XsDao;import com.voidking.struts2_hibernate.model.Xsb;import com.voidking.struts2_hibernate.util.MySessionFactory;public class XsDaoImp implements XsDao &#123;	@Override	public Xsb getOneXs(String xh) &#123;		// TODO Auto-generated method stub		try &#123;			Session session = MySessionFactory.getSessionFactory().openSession();			Transaction ts = session.beginTransaction();			Query query = session.createQuery(&quot;from xsb where xh=?&quot;);			query.setParameter(0, xh);			query.setMaxResults(1);			Xsb xs = (Xsb)query.uniqueResult();			ts.commit();			session.close();					&#125; catch (Exception e) &#123;			// TODO: handle exception			e.printStackTrace();		&#125;		return null;	&#125;	@Override	public void update(Xsb xs) &#123;		// TODO Auto-generated method stub		try &#123;			Session session = MySessionFactory.getSessionFactory().openSession();			Transaction ts = session.beginTransaction();						session.update(xs);						ts.commit();			session.close();		&#125; catch (Exception e) &#123;			// TODO: handle exception			e.printStackTrace();		&#125;	&#125;&#125;
6.8. ZyDao.javapackage com.voidking.struts2_hibernate.dao;import java.util.List;import com.voidking.struts2_hibernate.model.Zyb;public interface ZyDao &#123;	public Zyb getOneZy(Integer zyId);	public List getAll();&#125;

6.9. ZyDaoImp.javapackage com.voidking.struts2_hibernate.dao.imp;import java.util.List;import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.Transaction;import com.voidking.struts2_hibernate.dao.ZyDao;import com.voidking.struts2_hibernate.model.Zyb;import com.voidking.struts2_hibernate.util.MySessionFactory;public class ZyDaoImp implements ZyDao &#123;	@Override	public Zyb getOneZy(Integer zyId) &#123;		try &#123;			Session session = MySessionFactory.getSessionFactory().openSession();			Transaction ts = session.beginTransaction();						Query query = session.createQuery(&quot;from zyb where id=?&quot;);			query.setParameter(0, zyId);			query.setMaxResults(1);			Zyb zy = (Zyb)query.uniqueResult();						ts.commit();			session.close();						return zy;		&#125; catch (Exception e) &#123;			// TODO: handle exception			e.printStackTrace();		&#125;		return null;	&#125;	@Override	public List getAll() &#123;		// TODO Auto-generated method stub		try &#123;			Session session = MySessionFactory.getSessionFactory().openSession();			Transaction ts = session.beginTransaction();						List list = session.createQuery(&quot;from zyb&quot;).list();						ts.commit();			session.close();			return list;					&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;		return null;	&#125;&#125;

6.10. KcDao.javapackage com.voidking.struts2_hibernate.dao;import java.util.List;import com.voidking.struts2_hibernate.model.Kcb;public interface KcDao &#123;	public Kcb getOneKc(String kch);	public List getAll();&#125;

6.11. KcDaoImp.javapackage com.voidking.struts2_hibernate.dao.imp;import java.util.List;import org.hibernate.HibernateException;import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.Transaction;import com.voidking.struts2_hibernate.dao.KcDao;import com.voidking.struts2_hibernate.model.Kcb;import com.voidking.struts2_hibernate.util.MySessionFactory;public class KcDaoImp implements KcDao &#123;	@Override	public Kcb getOneKc(String kch) &#123;		// TODO Auto-generated method stub		try &#123;			Session session = MySessionFactory.getSessionFactory().openSession();			Transaction ts = session.beginTransaction();						Query query = session.createQuery(&quot;from kcb where kch=?&quot;);			query.setParameter(0, kch);			query.setMaxResults(1);			Kcb kc = (Kcb)query.uniqueResult();						ts.commit();			session.close();			return kc;					&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;		return null;	&#125;	@Override	public List getAll() &#123;		// TODO Auto-generated method stub		try &#123;			Session session = MySessionFactory.getSessionFactory().openSession();			Transaction ts = session.beginTransaction();			List list = session.createQuery(&quot;from kcb order by kch&quot;).list();			ts.commit();			session.close();			return list;		&#125; catch (HibernateException e) &#123;			// TODO Auto-generated catch block			e.printStackTrace();		&#125;				return null;	&#125;&#125;

7. struts.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;	&lt;constant name=&quot;struts.multipart.saveDir&quot; value=&quot;/tmp&quot;/&gt;    &lt;package name=&quot;default&quot;  extends=&quot;struts-default&quot;&gt;    	&lt;action name=&quot;login&quot; class=&quot;com.voidking.struts2_hibernate.action.LoginAction&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/main.jsp&lt;/result&gt;			&lt;result name=&quot;error&quot;&gt;/login.jsp&lt;/result&gt;		&lt;/action&gt;		&lt;action name=&quot;xsInfo&quot; class=&quot;org.action.XsAction&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/xsInfo.jsp&lt;/result&gt;		&lt;/action&gt;		&lt;action name=&quot;getImage&quot; class=&quot;com.voidking.struts2_hibernate.action.XsAction&quot; method=&quot;getImage&quot;&gt;&lt;/action&gt;		&lt;action name=&quot;updateXsInfo&quot; class=&quot;com.voidking.struts2_hibernate.action.XsAction&quot; method=&quot;updateXsInfo&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/updateXsInfo.jsp&lt;/result&gt;		&lt;/action&gt;		&lt;action name=&quot;updateXs&quot; class=&quot;com.voidking.struts2_hibernate.action.XsAction&quot; method=&quot;updateXs&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/updateXs_success.jsp&lt;/result&gt;		&lt;/action&gt;		&lt;action name=&quot;getXsKcs&quot; class=&quot;com.voidking.struts2_hibernate.action.XsAction&quot; method=&quot;getXsKcs&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/xsKcs.jsp&lt;/result&gt;		&lt;/action&gt;		&lt;action name=&quot;deleteKc&quot; class=&quot;com.voidking.struts2_hibernate.action.XsAction&quot; method=&quot;deleteKc&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/deleteKc_success.jsp&lt;/result&gt;		&lt;/action&gt;		&lt;action name=&quot;getAllKc&quot; class=&quot;com.voidking.struts2_hibernate.action.KcAction&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/allKc.jsp&lt;/result&gt;		&lt;/action&gt;		&lt;action name=&quot;selectKc&quot; class=&quot;com.voidking.struts2_hibernate.action.XsAction&quot; method=&quot;selectKc&quot;&gt;			&lt;result name=&quot;success&quot;&gt;/selectKc_success.jsp&lt;/result&gt;			&lt;result name=&quot;error&quot;&gt;/selectKc_fail.jsp&lt;/result&gt;		&lt;/action&gt;    &lt;/package&gt;&lt;/struts&gt;

8. web.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app id=&quot;WebApp_9&quot; version=&quot;2.4&quot; xmlns=&quot;http://java.sun.com/xml/ns/j2ee&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd&quot;&gt;	&lt;welcome-file-list&gt;		&lt;welcome-file&gt;/login.jsp&lt;/welcome-file&gt;	&lt;/welcome-file-list&gt;    &lt;filter&gt;        &lt;filter-name&gt;struts2&lt;/filter-name&gt;        &lt;filter-class&gt;org.apache.struts2.dispatcher.FilterDispatcher&lt;/filter-class&gt;    &lt;/filter&gt;    &lt;filter-mapping&gt;        &lt;filter-name&gt;struts2&lt;/filter-name&gt;        &lt;url-pattern&gt;/*&lt;/url-pattern&gt;    &lt;/filter-mapping&gt;&lt;/web-app&gt;

9. 界面交互Action类和jsp界面，在此省略。详情请见源代码。
10. 严重错误部署，运行，404错误。进入Tomcat的Manager App界面，start项目，报错如下：
SEVERE:Exception starting filter struts2java.lang.ClassNotFoundException: org.apache.struts2.dispatcher.FilterDispatcher
我勒个去，怎么就找不到过滤器了！换成org.apache.struts2.dispatcher.ng.filter.StrutsPrepareFilter试试，报同样的错：
SEVERE:Exception starting filter struts2java.lang.ClassNotFoundException: org.apache.struts2.dispatcher.ng.filter.StrutsPrepareFilter
这个工程，和之前写的struts2工程有什么差别？struts2中，我直接把jar包放到了WebContent/WEB-INF/lib里面，而在这个项目中，我把jar包放到了WebContent/WEB-INF/lib/struts2里。删除一层目录试试，问题完美解决。
11. 很多错误输入学号，密码，点击登录，报错如下：
HTTP Status 500 - type Exception reportmessage description The server encountered an internal error that prevented it from fulfilling this request.exception java.lang.reflect.InvocationTargetException	sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)	sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)	……	root cause 	……
root caose下面的内容是重点，要么是lib中缺少jar文件，要么某个类写错了，要么配置文件错误……总之各种错误，根据提示定位修改即可。
12. 源代码分享https://github.com/voidking/struts2_hibernate.git
13. 小结太复杂了，根本没法愉快的玩耍！改了几十个错误！！！尤其是Hibernate中POJO类和*.hbm.xml对应的部分，太难搞了！记得JPA似乎简单一点，等到复习到那部分再深入研究。最终，程序跑起来了，但是很多功能都没法用，各种not mapped，实在搞不下去了，能力有限啊。。。
发现：在JavaSE中，可以通过引入jar包的方式使用外部类。但是在JavaEE中，也就是Web工程中，需要拷贝jar包到lib文件夹中才可以使用外部类。分析：发布到Tomcat中的工程，根本没有记录引用外部类的文件，也就是说，找不到外部jar包。而lib文件夹中的jar包，应该是默认寻找外部类的地方。
14. 参考文档《Java EE基础实用教程》，郑阿奇主编

在Eclipse中从数据库表自动生成hibernate的java实体类http://www.shangxueba.com/jingyan/2453455.htmlhttp://blog.csdn.net/zheng2008hua/article/details/6274659

Hibernate初始化时的Could not parse configurationhttp://blog.csdn.net/mydeman/article/details/6134820

如何理解Hibernate中的HibernateSessionFactory类http://www.cnblogs.com/dyllove98/archive/3243769.html

org.hibernate.MappingException:An association from the table * refers to an unmapped class:http://blog.csdn.net/jayqean/article/details/5608330

org.hibernate.HibernateException: Unable to instantiate default tuplizerhttp://www.cnblogs.com/chuyuhuashi/archive/2423235.html

]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>hibernate</tag>
        <tag>struts2</tag>
      </tags>
  </entry>
  <entry>
    <title>Hibernate高级应用</title>
    <url>/dev-hibernate-senior/</url>
    <content><![CDATA[1. Hibernate批量处理1.1. 批量插入1.1.1. Hibernate直接处理首先在hibernate.cfg.xml中设置批量尺寸属性hibernate.jdbc.batch_size，最好关闭Hibernate的二级缓存以提高效率。
&lt;hibernate-configuration&gt;	&lt;session-factory&gt;		&lt;property name=&quot;hibernate.jdbc.batch_size&quot;&gt;50&lt;/property&gt;		&lt;property name=&quot;hibernate.jdbc.use_second_level_cache&quot;&gt;false&lt;/property&gt;	&lt;/session-factory&gt;&lt;/hibernate-configuration&gt;
下面批量插入500个课程到数据库表中：
//KcbBatch.javapackage com.voidking.hibernate.test;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Kcb;public class KcbBatch &#123;	public static void main(String[] args)&#123;		//创建Session对象		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				//创建事务对象		Transaction ts=session.beginTransaction();				for (int i = 0; i &lt; 500; i++) &#123;			Kcb kcb = new Kcb();			kcb.setKch(i+&quot;&quot;);			session.save(kcb);			if(i%50==0)			&#123;				session.flush();				session.clear();			&#125;		&#125;				ts.commit();				session.close();		sf.close();			&#125;&#125;



1.1.2. 调用JDBC由于Hibernate只是对JDBC进行了轻量级的封装，因此完全可以绕过Hibernate直接用JDBC进行批量插入。因此上面的代码可以改成：
//KcbBatch.javapackage com.voidking.hibernate.test;import java.sql.Connection;import java.sql.PreparedStatement;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Kcb;public class KcbBatch2 &#123;	public static void main(String[] args)&#123;		//创建Session对象		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				//创建事务对象		Transaction ts=session.beginTransaction();				Connection conn = session.connection();				try &#123;			PreparedStatement stmt = conn.prepareStatement(&quot;insert into kcb(kch) values(?)&quot;);			for(int i=0;i&lt;500;i++)			&#123;				stmt.setString(1, i+&quot;&quot;);				stmt.addBatch();			&#125;		&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;				ts.commit();				session.close();		sf.close();			&#125;&#125;

1.1.3. 对比在用Hibernate进行操作的时候操作的是表对应的类，而在用JDBC进行操作时操作的是数据库中的表。
1.2. 批量更新1.2.1. Hibernate直接处理为了使Hibernate的HQL直接支持update/delete的批量更新语法，首先要hibernate.cfg.xml中设置HQL/SQL查询翻译器属性hibernate.query.factory_class。
&lt;property name=&quot;hibernate.query.factory_class&quot;&gt;	org.hibernate.hql.ast.ASTQueryTranslatorFactory&lt;/property&gt;
为了防止类名输入错误，可以按住Ctrl键，同时单击这个类名。如果可以跳转到.class文件，说明输入正确，否则输入错误。下面使用HQL批量更新把课程表中XS修改为30。
Query query = session.createQuery(&quot;update kcb set xs=30&quot;);query.executeUpdate();


1.2.2. 调用JDBCtry&#123;	Statement stmt = conn.createStatement();	stmt.executeUpdate(&quot;update kcb set xs=30&quot;);&#125;catch(Exception)&#123;	e.printStackTrace();&#125;

1.3. 批量删除1.3.1. Hibernate直接处理删除课程表中课程号大于200的课程。
Query query = session.createQuery(&quot;delete kcb where kch &gt; 200&quot;);query.executeUpdate();
1.3.2. 调用JDBCtry&#123;	Statement stmt = conn.createStatement();	stmt.executeUpdate(&quot;delete from kcb where kch&gt;200&quot;);&#125;catch(Exception)&#123;	e.printStackTrace();&#125;

2. 实体对象生命周期实体对象，特指Hibernate O/R映射关系中的域对象（即O/R中的O）。
2.1. transient（瞬时态）瞬时态，即实体对象在内存中的存在，与数据库中的记录无关。
Student stu = new Student();stu.setSnumber(&quot;081101&quot;);stu.setSname(&quot;李方方&quot;);stu.setSage(21);
这里的stu对象，与数据库中的记录没有任何关联。
2.2. persisent（持久态）Student stu = new Student();stu.setSnumber(&quot;081101&quot;);stu.setSname(&quot;李方方&quot;);stu.setSage(21);Student stu1 = new Student();stu1.setSnumber(&quot;081102&quot;);stu1.setSname(&quot;程明&quot;);stu1.setSage(22);Transaction ts = session.beginTransaction();session.save(stu);//stu对象转换为持久态，由Hibernate纳入实体管理器；stu1仍然处于瞬时态ts.commit();Transaction ts2 = session.beginTransaction();stu.setSname(&quot;李方&quot;);//虽然这个事务没有显式调用session.sava()保存stu对象，但是由于stu为持久态，将自动被固化到数据库stu1.setSname(&quot;程明明&quot;);//stu1仍是一个普通Java对象，对数据库未产生任何影响ts2.commit();
处于瞬时态的对象，可以通过Session的save()方法转换成持久状态。同样，如果一个实体对象由Hibernate加载，那么，它也处于持久状态。
Student stu = (Student)session.load(Student.class,net Integer(1));
持久对象对应着数据库中的一条记录，可以看做是数据库记录的对象化操作接口，其状态的变更将对数据库中的记录产生影响。
2.3. Detached（脱管状态）处于持久化的对象，其对应的Session实例关闭之后，此对象就处于脱管状态。Session实例可以看做是持久对象的宿主，一旦此宿主失效，其从属的持久对象进入脱管状态。
Student stu = new Student();//stu对象为瞬时态stu.setSnumber(&quot;081101&quot;);stu.setSname(&quot;李方方&quot;);stu.setSage(21);Transaction ts = session.beginTransaction();session.save(stu);//stu对象由Hibernate纳入管理容器，处于持久状态ts.commit();session.close();//stu对象为脱管状态，因为与其关联的session已经关闭
托管态和瞬时态有什么区别？瞬时状态的stu对象与库表中的数据缺乏对应关系，而脱管状态的stu对象，却在库表中存在相对应的记录，只不过由于脱管对象脱离Session这个数据操作平台，其状态的改变无法更新到库表中对应的记录。
有时候为了方便，将处于瞬时和脱管状态的对象统称为值对象（Value Object，VO），将处于持久态的对象称为持久对象（Persistent Object，PO）。也就是说，对Hibernate实体管理容器而言，非管理的实体对象统称为VO，被管理的对象称为PO。
3. Hibernate事务管理事务是数据库并发控制不可分割的基本工作单位，具有原子性、一致性、隔离性和持久性的特点。
事务（Transaction）是工作中的基本逻辑单元，可以用于确保数据库能够被正确修改，避免数据只修改一部分而导致数据不完整，或者在修改时受到用户干扰。
3.1. 基于JDBC的事务管理Hibernate是JDBC的轻量级封装，本身并不具备事务管理能力。在事务管理层，Hibernate将其委托给底层的JDBC或JTA，以实现事务管理和调度功能。
在JDBC中，事务默认是自动提交。也就是说，一条对数据库的更新表达式代表一项事务操作。操作成功后，系统将自动调用commit提交。否则，将调用rollback回滚。
在JDBC中，可以通过调用setAutoCommit(false)禁止自动提交。之后就可以把多个数据库操作的表达式作为一个事务，在操作完成后调用commit进行整体提交。
将事务管理委托给JDBC进行处理是最简单的实现方式，Hibernate对于JDBC事务的封装也比较简单。如下面的代码：
//创建Session对象Configuration cfg = new Configuration();SessionFactory sf = cfg.configure().buildSessionFactory();Session session = sf.openSession();//创建事务对象Transaction ts=session.beginTransaction();seesion.save(stu);ts.commit();session.close();sf.close();
从JDBC层面而言，上面的代码实际对应着：
Connection cn = getConnection;cn.setAutoCommit(false);//JDBC调用相关的SQL语句cn.commit();

在sf.openSession()语句中，Hibernate会初始化数据库连接。与此同时，将其AutoCommit设为关闭状态（false）。即一开始获得的session，其自动提交属性已经被关闭。下面的代码不会对数据库产生任何效果：
//创建Session对象Configuration cfg = new Configuration();SessionFactory sf = cfg.configure().buildSessionFactory();Session session = sf.openSession();seesion.save(stu);session.close();sf.close();
这实际上相当于JDBC Connection的AutoCommit属性被设为false，执行了若干JDBC操作之后，没有调用commit操作。
3.2. 基于JTA的事务管理JTA（Java Transaction API）是由Java EE Transaction Manager去管理的事务。其最大的特点是调用UserTransaction接口的begin、commit和rollback方法来完成事务范围的界定、事务的提交和回滚。JTA可以实现统一事务对应不同的数据库。
JTA主要用于分布式的多个数据源的两阶段提交的事务，而JDBC的Connection提供单个数据源的事务。后者因为只涉及一个数据源，所以其事务可以由数据库自己单独实现。而JTA事务因为其分布式和多数据库的特性，不可能由任何一个数据源实现事务。因此，JTA中的事务是由“事务管理器”实现的。它会在多个数据源之间统筹事务，具体使用的技术就是所谓的“两阶段提交”。
JTA提供了跨Session的事务管理能力。这一点是与JDBC Transaction最大的差异。JDBC事务由Connection管理，即事务管理实际上是在JDBC Connection中实现。事务周期限于Connection的生命周期之内。同样，对于基于JDBC Transaction的Hibernate事务管理机制而言，事务管理在Session所依托的JDBC Connection中实现，事务周期限于Session的生命周期。
JTA事务管理则由JTA容器实现。JTA对当前加入事务的众多Connection进行调度，实现事务性要求。JTA的事务周期可以横跨多个JDBC Connection生命周期。同样，对于基于JTA事务的Hibernate而言，JTA事务横跨多个Session。
3.3. 锁Hibernate支持两种锁机制，悲观锁（Pessimistic Locking）和乐观锁（Optimistic Locking）。悲观锁是指对数据被外界修改保持保守态度。假定任何时刻存取数据时，都可能有一个客户也正在存取同一数据。为了保持数据被操作的移植性，于是对数据采取了数据库层次的锁定状态，依靠数据库提供的锁机制来实现。
乐观锁则乐观地认为数据很少发生同时存取的问题，因此不做数据库层次上的锁定。为了维护正确的数据，乐观锁采用应用程序上的逻辑实现版本控制的方法。
4. 源代码分享Hibernate概述、Hibernate关系映射、Hibernate高级应用，三篇博客中使用的代码，都在下面这个工程。https://github.com/voidking/hibernate.git
5. 参考文档《Java EE基础实用教程》，郑阿奇主编
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>hibernate</tag>
      </tags>
  </entry>
  <entry>
    <title>Hibernate关系映射</title>
    <url>/dev-hibernate-orm/</url>
    <content><![CDATA[1. 前言Hibernate关系映射的主要任务是实现数据库关系表与持久化类之间的映射。
2. 一对一关联（共享主键方式）在注册某个论坛会员的时候，往往不但要填写登录账户和密码，还要填写其他的详细信息，这两部分信息通常会放在不同的表中。共享主键方式就是限制两个数据表的主键使用相同的值，通过主键形成一对一映射关系。
2.1. 建立表命令建立login表和detail表：
create table login(id int4 primary key not null,username varchar(20) not null,password varchar(20) not null);create table detail(id int4 primary key auto_increment not null,truename varchar(8),email varchar(50));

登陆表和详细信息表属于典型的一对一关联关系，可按共享主键方式进行。在Hibernate概述中实例的基础上，接着开发。

2.2. Login.java在包com.voidking.hibernate.model中，新建类Login，代码如下：
package com.voidking.hibernate.model;public class Login implements java.io.Serializable &#123;	// Fields	private Integer id;	private String username;	private String password;	private Detail detail;			// Constructors	/** default constructor */	public Login() &#123;	&#125;	/** full constructor */	public Login(String username, String password, Detail detail) &#123;		this.username = username;		this.password = password;		this.detail = detail;		&#125;	// Property accessors	public Integer getId() &#123;		return this.id;	&#125;	public void setId(Integer id) &#123;		this.id = id;	&#125;	public String getUsername() &#123;		return this.username;	&#125;	public void setUsername(String username) &#123;		this.username = username;	&#125;	public String getPassword() &#123;		return this.password;	&#125;	public void setPassword(String password) &#123;		this.password = password;	&#125;		public Detail getDetail()&#123;		return this.detail;	&#125;	public void setDetail(Detail detail)&#123;		this.detail = detail;	&#125;&#125;
2.3. Login.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Login&quot; table=&quot;login&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;ID&quot; /&gt;            &lt;generator class=&quot;foreign&quot;&gt;            	&lt;param name=&quot;property&quot;&gt;detail&lt;/param&gt;            &lt;/generator&gt;        &lt;/id&gt;        &lt;property name=&quot;username&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;USERNAME&quot; length=&quot;20&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;password&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;PASSWORD&quot; length=&quot;20&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;		&lt;!--constrained=&quot;true&quot;表明当前的主键上存在一个外键约束--&gt;        &lt;one-to-one name=&quot;detail&quot; class=&quot;com.voidking.hibernate.model.Detail&quot; constrained=&quot;true&quot;&gt;&lt;/one-to-one&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

2.4. Detail.javapackage com.voidking.hibernate.model;public class Detail implements java.io.Serializable &#123;	// Fields	private Integer id;	private String truename;	private String email;	private Login login;			// Constructors	/** default constructor */	public Detail() &#123;	&#125;	/** full constructor */	public Detail(String truename, String email, Login login) &#123;		this.truename = truename;		this.email = email;		this.login = login;			&#125;	// Property accessors	public Integer getId() &#123;		return this.id;	&#125;	public void setId(Integer id) &#123;		this.id = id;	&#125;	public String getTruename() &#123;		return this.truename;	&#125;	public void setTruename(String truename) &#123;		this.truename = truename;	&#125;	public String getEmail() &#123;		return this.email;	&#125;	public void setEmail(String email) &#123;		this.email = email;	&#125;		public Login getLogin()&#123;		return this.login;	&#125;	public void setLogin(Login login)&#123;		this.login = login;	&#125;&#125;

2.5. Detail.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Detail&quot; table=&quot;detail&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;ID&quot; /&gt;            &lt;generator class=&quot;identity&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;truename&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;TRUENAME&quot; length=&quot;8&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;email&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;EMAIL&quot; length=&quot;50&quot; /&gt;        &lt;/property&gt;		&lt;!--cascade=all，表示主控类的所有操作，对关联类也执行同样操作--&gt;        &lt;one-to-one name=&quot;login&quot; class=&quot;com.voidking.hibernate.model.Login&quot; cascade=&quot;all&quot; lazy=&quot;false&quot;&gt;&lt;/one-to-one&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

2.6. hibernate.cfg.xml&lt;?xml version=&#x27;1.0&#x27; encoding=&#x27;utf-8&#x27;?&gt;&lt;!DOCTYPE hibernate-configuration PUBLIC        &quot;-//Hibernate/Hibernate Configuration DTD 3.0//EN&quot;        &quot;http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd&quot;&gt;     &lt;hibernate-configuration&gt;    &lt;session-factory&gt;         &lt;!-- Database connection settings --&gt;    &lt;property name=&quot;connection.driver_class&quot;&gt;com.mysql.jdbc.Driver&lt;/property&gt;    &lt;property name=&quot;connection.url&quot;&gt;jdbc:mysql://localhost/xscj&lt;/property&gt;    &lt;property name=&quot;connection.username&quot;&gt;scott&lt;/property&gt;    &lt;property name=&quot;connection.password&quot;&gt;tiger&lt;/property&gt;             &lt;!-- JDBC connection pool (use the built-in) --&gt;    &lt;!-- &lt;property name=&quot;connection.pool_size&quot;&gt;1&lt;/property&gt; --&gt;             &lt;!-- SQL dialect --&gt;    &lt;property name=&quot;dialect&quot;&gt;org.hibernate.dialect.MySQLDialect&lt;/property&gt;             &lt;!-- Echo all executed SQL to stdout --&gt;    &lt;property name=&quot;show_sql&quot;&gt;true&lt;/property&gt;             &lt;!-- Enable Hibernate&#x27;s automatic session context management --&gt;    &lt;!--&lt;property name=&quot;current_session_context_class&quot;&gt;thread&lt;/property&gt;--&gt;             &lt;!-- Drop and re-create the database schema on startup --&gt;    &lt;property name=&quot;hbm2ddl.auto&quot;&gt;create&lt;/property&gt;             &lt;!-- Disable the second-level cache --&gt;    &lt;property name=&quot;cache.provider_class&quot;&gt;org.hibernate.cache.NoCacheProvider&lt;/property&gt;         &lt;mapping resource=&quot;com/voidking/hibernate/model/Kcb.hbm.xml&quot;/&gt;    &lt;mapping resource=&quot;com/voidking/hibernate/model/Login.hbm.xml&quot;/&gt;    &lt;mapping resource=&quot;com/voidking/hibernate/model/Detail.hbm.xml&quot;/&gt;       &lt;/session-factory&gt;&lt;/hibernate-configuration&gt;

2.7. SharePrimaryKey.javapackage com.voidking.hibernate.test;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Detail;import com.voidking.hibernate.model.Login;public class SharePrimaryKey&#123;	public static void main(String[] args) &#123;		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				// 创建事务对象		Transaction ts=session.beginTransaction();		//共享主键方式		Detail detail=new Detail();		Login login=new Login();		login.setUsername(&quot;yanhong&quot;);		login.setPassword(&quot;123&quot;);		detail.setTruename(&quot;严红&quot;);		detail.setEmail(&quot;yanhong@126.com&quot;);		//相互设置关联		login.setDetail(detail);		detail.setLogin(login);		//这样完成后就可以通过Session对象调用session.save(detail)来持久化该对象		session.save(detail);				ts.commit();				session.close();		sf.close();	&#125;&#125;

2.8. 运行以Java Application运行，查看数据库，可以发现，数据成功插入。
3. 唯一外键方式唯一外键方式就是一个表的外键和另一个表的唯一主键形成一对一的映射关系，这种一对一的关系其实就是多对一关联关系的一种特殊情况而已。
唯一外键的情况很多，比如，每个人对应一个房间。很多情况下，可以是几个人住在同一个房间里面，就是多对一的关系。但是如果让一个人住一个房间，就变成了一对一的关系了，这就是前面说的一对一关系其实是多对一关系的一种特殊情况。
3.1. 创建表命令创建表person和room：
create table person(id int primary key auto_increment not null ,name varchar(20) not null,room_id int(4) );create table room(id int(4) primary key auto_increment not null,address varchar(100) not null);

3.2. Person.javapackage com.voidking.hibernate.model;public class Person implements java.io.Serializable &#123;	// Fields	private Integer id;	private String name;	private Room room;				// Constructors	/** default constructor */	public Person() &#123;	&#125;	/** minimal constructor */	public Person(String name) &#123;		this.name = name;	&#125;	/** full constructor */	public Person(String name, Room room) &#123;		this.name = name;		//this.roomId = roomId;		this.room = room;			&#125;	// Property accessors	public Integer getId() &#123;		return this.id;	&#125;	public void setId(Integer id) &#123;		this.id = id;	&#125;	public String getName() &#123;		return this.name;	&#125;	public void setName(String name) &#123;		this.name = name;	&#125;		public Room getRoom()&#123;		return this.room;	&#125;	public void setRoom(Room room)&#123;		this.room = room;	&#125;&#125;

3.3. Person.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Person&quot; table=&quot;Person&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;id&quot; /&gt;            &lt;generator class=&quot;native&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;name&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;name&quot; length=&quot;20&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;many-to-one name=&quot;room&quot; class=&quot;com.voidking.hibernate.model.Room&quot;  column=&quot;room_id&quot; cascade=&quot;all&quot; unique=&quot;true&quot;&gt;&lt;/many-to-one&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

3.4. Room.javapackage com.voidking.hibernate.model;public class Room implements java.io.Serializable &#123;	// Fields	private Integer id;	private String address;	private Person person;	// Constructors	/** default constructor */	public Room() &#123;	&#125;	/** full constructor */	public Room(String address) &#123;		this.address = address;		//this.person = person;	&#125;	// Property accessors	public Integer getId() &#123;		return this.id;	&#125;	public void setId(Integer id) &#123;		this.id = id;	&#125;	public String getAddress() &#123;		return this.address;	&#125;	public void setAddress(String address) &#123;		this.address = address;	&#125;		public Person getPerson()&#123;		return this.person;	&#125;	public void setPerson(Person person)&#123;		this.person = person;	&#125;&#125;

3.5. Room.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Room&quot; table=&quot;Room&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;id&quot; /&gt;            &lt;generator class=&quot;native&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;address&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;address&quot; length=&quot;100&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;one-to-one name=&quot;person&quot; class=&quot;com.voidking.hibernate.model.Person&quot; property-ref=&quot;room&quot;/&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

3.6. hibernate.cfg.xml&lt;mapping resource=&quot;com/voidking/hibernate/model/Person.hbm.xml&quot;/&gt; &lt;mapping resource=&quot;com/voidking/hibernate/model/Room.hbm.xml&quot;/&gt;  

3.6.1. OneForeign.javapackage com.voidking.hibernate.test;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Person;import com.voidking.hibernate.model.Room;public class OneForeign&#123;	public static void main(String[] args) &#123;		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				// 创建事务对象		Transaction ts=session.beginTransaction();		//唯一外键方式		Person person=new Person();		person.setName(&quot;liumin&quot;);		Room room=new Room();		room.setAddress(&quot;NJ-S1-328&quot;);		person.setRoom(room);		session.save(person);				ts.commit();				session.close();		sf.close();	&#125;&#125;

3.7. 运行以Java Application运行，查看数据库，可以发现，数据成功插入。
3.8. 关于外键在创建表person时，我们并没有设置room_id为外键，但是，当我们运行这个项目的之后。使用desc person;命令可以发现，room_id变成了外键。可见，Hibernate不仅可以改变数据库中表的数据，也可以改变数据库中表的属性。理论上，也可以创建删除表。曾经开发时，使用JPA直接生成表，根本不用操作数据库，很方便。
4. 多对一单向关联只要把上例中的一对一的唯一外键关联实例稍微修改，就可以变成多对一。
4.1. Person.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Person&quot; table=&quot;Person&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;id&quot; /&gt;            &lt;generator class=&quot;native&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;name&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;name&quot; length=&quot;20&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;many-to-one name=&quot;room&quot; class=&quot;com.voidking.hibernate.model.Room&quot;  column=&quot;room_id&quot; cascade=&quot;all&quot;&gt;&lt;/many-to-one&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

4.2. Room.javapackage com.voidking.hibernate.model;public class Room implements java.io.Serializable &#123;	private Integer id;	private String address;	//setter和getter方法&#125;

4.3. Room.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Room&quot; table=&quot;Room&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;id&quot; /&gt;            &lt;generator class=&quot;native&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;address&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;address&quot; length=&quot;100&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

4.4. ManyToOne.javapackage com.voidking.hibernate.test;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Person;import com.voidking.hibernate.model.Room;public class ManyToOne&#123;	public static void main(String[] args) &#123;		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				// 创建事务对象		Transaction ts=session.beginTransaction();		Room room=new Room();		room.setAddress(&quot;NJ-S1-328&quot;);		Person person=new Person();		person.setName(&quot;liumin&quot;);				person.setRoom(room);		session.save(person);				ts.commit();				session.close();		sf.close();	&#125;&#125;

5. 一对多双向关联上面的例子中，多对一单向关联是从多控制一，也就是说，从多可以知道一，但从一不知道多。如果一也只知道多，那么就变成了双向一对多（或多对一）关联。
5.1. Room.javapackage com.voidking.hibernate.model;import java.util.HashSet;import java.util.Set;public class Room implements java.io.Serializable &#123;	// Fields	private Integer id;	private String address;		private Set person = new HashSet();	// Constructors	/** default constructor */	public Room() &#123;	&#125;	/** full constructor */	public Room(String address) &#123;		this.address = address;		//this.person = person;	&#125;	// Property accessors	public Integer getId() &#123;		return this.id;	&#125;	public void setId(Integer id) &#123;		this.id = id;	&#125;	public String getAddress() &#123;		return this.address;	&#125;	public void setAddress(String address) &#123;		this.address = address;	&#125;	public Set getPerson() &#123;		return person;	&#125;	public void setPerson(Set person) &#123;		this.person = person;	&#125;&#125;

5.2. Room.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Room&quot; table=&quot;Room&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;id&quot; /&gt;            &lt;generator class=&quot;native&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;address&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;address&quot; length=&quot;100&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;		&lt;set name=&quot;person&quot; inverse=&quot;false&quot; cascade=&quot;all&quot;&gt;			&lt;key column=&quot;room_id&quot;&gt;&lt;/key&gt;			&lt;one-to-many class=&quot;com.voidking.hibernate.model.Person&quot;/&gt;		&lt;/set&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

5.3. cascade取值
all：表示所有操作在关联层级上进行连锁操作。
save-update：表示只有save和update操作进行连锁操作。
delete：表示只有delete操作进行连锁操作。
all-delete-orphan：在删除当前持久化对象时，它相当于delete；在保存或更新当前持久化对象时，它相当于save-update。另外它还可以删除与当前持久化对象断开关联关系的其他持久化对象。

5.4. OneToMany.javapackage com.voidking.hibernate.test;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Person;import com.voidking.hibernate.model.Room;public class OneToMany&#123;	public static void main(String[] args) &#123;		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				// 创建事务对象		Transaction ts=session.beginTransaction();		Person person1=new Person(); 		Person person2=new Person();		Room room=new Room();		room.setAddress(&quot;NJ-S1-328&quot;);		person1.setName(&quot;李方方&quot;);		person2.setName(&quot;王艳&quot;);		person1.setRoom(room);		person2.setRoom(room);		//这样完成后就可以通过Session对象		//调用session.save(person1)和session.save(person)		//会自动保存room		session.save(person1);		session.save(person2);				ts.commit();				session.close();		sf.close();	&#125;&#125;

6. 多对多单向关联学生和课程就是多对多的关系，一个学生可以选择多门课程，而一门课程又可以被多个学生选择。多对多关系在关系数据库中不能直接表现，还必须依赖一张连接表。由于是单向的，也就是说一方可以知道另一方，反之不行。这里以从学生知道选择了哪些课程为例实现多对多单向关联。
6.1. 创建表命令create table student(id int primary key auto_increment not null ,snumber varchar(10) not null,sname varchar(10),sage int);create table course(id int primary key auto_increment not null,cnumber varchar(10) not null,cname varchar(20));create table stu_cour(sid int not null,cid int not null,primary key (sid,cid));

6.2. Student.javapackage com.voidking.hibernate.model;import java.util.HashSet;import java.util.Set;public class Student implements java.io.Serializable &#123;	// Fields	private Integer id;	private String snumber;	private String sname;	private Integer sage;	private Set courses = new HashSet();	// Constructors	/** default constructor */	public Student() &#123;	&#125;	/** minimal constructor */	public Student(String snumber) &#123;		this.snumber = snumber;	&#125;	/** full constructor */	public Student(String snumber, String sname, Integer sage) &#123;		this.snumber = snumber;		this.sname = sname;		this.sage = sage;	&#125;	// Property accessors	public Integer getId() &#123;		return this.id;	&#125;	public void setId(Integer id) &#123;		this.id = id;	&#125;	public String getSnumber() &#123;		return this.snumber;	&#125;	public void setSnumber(String snumber) &#123;		this.snumber = snumber;	&#125;	public String getSname() &#123;		return this.sname;	&#125;	public void setSname(String sname) &#123;		this.sname = sname;	&#125;	public Integer getSage() &#123;		return this.sage;	&#125;	public void setSage(Integer sage) &#123;		this.sage = sage;	&#125;		public Set getCourses()&#123;		return courses;	&#125;	public void setCourses(Set courses)&#123;		this.courses = courses;	&#125;&#125;

6.3. Student.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Student&quot; table=&quot;student&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;ID&quot; /&gt;            &lt;generator class=&quot;identity&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;snumber&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;SNUMBER&quot; length=&quot;10&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;sname&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;SNAME&quot; length=&quot;10&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;sage&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;SAGE&quot; /&gt;        &lt;/property&gt;        &lt;set name=&quot;courses&quot; table=&quot;stu_cour&quot; lazy=&quot;true&quot; cascade=&quot;all&quot;&gt;        	&lt;key column=&quot;SID&quot;&gt;&lt;/key&gt;        	&lt;many-to-many class=&quot;com.voidking.hibernate.model.Course&quot; column=&quot;CID&quot;/&gt;        &lt;/set&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

6.4. Course.javapackage com.voidking.hibernate.model;public class Course implements java.io.Serializable &#123;	// Fields	private Integer id;	private String cnumber;	private String cname;	// Constructors	/** default constructor */	public Course() &#123;	&#125;	/** minimal constructor */	public Course(String cnumber) &#123;		this.cnumber = cnumber;	&#125;	/** full constructor */	public Course(String cnumber, String cname) &#123;		this.cnumber = cnumber;		this.cname = cname;	&#125;	// Property accessors	public Integer getId() &#123;		return this.id;	&#125;	public void setId(Integer id) &#123;		this.id = id;	&#125;	public String getCnumber() &#123;		return this.cnumber;	&#125;	public void setCnumber(String cnumber) &#123;		this.cnumber = cnumber;	&#125;	public String getCname() &#123;		return this.cname;	&#125;	public void setCname(String cname) &#123;		this.cname = cname;	&#125;&#125;
6.5. Course.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Course&quot; table=&quot;course&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;ID&quot; /&gt;            &lt;generator class=&quot;identity&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;cnumber&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;CNUMBER&quot; length=&quot;10&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;cname&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;CNAME&quot; length=&quot;20&quot; /&gt;        &lt;/property&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

6.6. hibernate.cfg.xml&lt;mapping resource=&quot;com/voidking/hibernate/model/Student.hbm.xml&quot;/&gt; &lt;mapping resource=&quot;com/voidking/hibernate/model/Course.hbm.xml&quot;/&gt;   

6.7. ManyToManySingle.javapackage com.voidking.hibernate.test;import java.util.HashSet;import java.util.Set;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Course;import com.voidking.hibernate.model.Student;public class ManyToManySingle&#123;	public static void main(String[] args) &#123;		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				// 创建事务对象		Transaction ts=session.beginTransaction();		//多对多单向关联		Course cour1=new Course();		Course cour2=new Course();		Course cour3=new Course();		cour1.setCnumber(&quot;101&quot;);		cour1.setCname(&quot;计算机基础&quot;);		cour2.setCnumber(&quot;102&quot;);		cour2.setCname(&quot;数据库原理&quot;);		cour3.setCnumber(&quot;103&quot;);		cour3.setCname(&quot;计算机原理&quot;);		Set courses=new HashSet();		courses.add(cour1);		courses.add(cour2);		courses.add(cour3);		Student stu=new Student();		stu.setSnumber(&quot;081101&quot;);		stu.setSname(&quot;李方方&quot;);		stu.setSage(21);		stu.setCourses(courses);		session.save(stu);		ts.commit();				session.close();		sf.close();	&#125;&#125;

7. 多对多双向关联学会多对多单向关联后，只要同时实现两个互逆的多对多单向关联便可轻而易举地实现多对多双向关联。在上面的例子中只要修改课程的代码就可以了。
7.1. Course.javapackage com.voidking.hibernate.model;import java.util.HashSet;import java.util.Set;public class Course implements java.io.Serializable &#123;	// Fields	private Integer id;	private String cnumber;	private String cname;	private Set stus = new HashSet();	// Constructors	/** default constructor */	public Course() &#123;	&#125;	/** minimal constructor */	public Course(String cnumber) &#123;		this.cnumber = cnumber;	&#125;	/** full constructor */	public Course(String cnumber, String cname) &#123;		this.cnumber = cnumber;		this.cname = cname;	&#125;	// Property accessors	public Integer getId() &#123;		return this.id;	&#125;	public void setId(Integer id) &#123;		this.id = id;	&#125;	public String getCnumber() &#123;		return this.cnumber;	&#125;	public void setCnumber(String cnumber) &#123;		this.cnumber = cnumber;	&#125;	public String getCname() &#123;		return this.cname;	&#125;	public void setCname(String cname) &#123;		this.cname = cname;	&#125;	public Set getStus() &#123;		return stus;	&#125;	public void setStus(Set stus) &#123;		this.stus = stus;	&#125;&#125;

7.2. Course.hbm.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;&quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt;&lt;hibernate-mapping&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Course&quot; table=&quot;course&quot;&gt;        &lt;id name=&quot;id&quot; type=&quot;java.lang.Integer&quot;&gt;            &lt;column name=&quot;ID&quot; /&gt;            &lt;generator class=&quot;identity&quot; /&gt;        &lt;/id&gt;        &lt;property name=&quot;cnumber&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;CNUMBER&quot; length=&quot;10&quot; not-null=&quot;true&quot; /&gt;        &lt;/property&gt;        &lt;property name=&quot;cname&quot; type=&quot;java.lang.String&quot;&gt;            &lt;column name=&quot;CNAME&quot; length=&quot;20&quot; /&gt;        &lt;/property&gt;        &lt;set name=&quot;stus&quot; table=&quot;stu_cours&quot; lazy=&quot;true&quot; cascade=&quot;all&quot;&gt;        	&lt;key column=&quot;cid&quot;&gt;&lt;/key&gt;        	&lt;many-to-many class=&quot;com.voidking.hibernate.model.Student&quot; column=&quot;sid&quot;&gt;&lt;/many-to-many&gt;        &lt;/set&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

7.3. ManyToManyDouble.javapackage com.voidking.hibernate.test;import java.util.HashSet;import java.util.Set;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Course;import com.voidking.hibernate.model.Student;public class ManyToManyDouble&#123;	public static void main(String[] args) &#123;		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				// 创建事务对象		Transaction ts=session.beginTransaction();		//多对多单向关联		Course cour1=new Course();		Course cour2=new Course();		Course cour3=new Course();		cour1.setCnumber(&quot;101&quot;);		cour1.setCname(&quot;计算机基础&quot;);		cour2.setCnumber(&quot;102&quot;);		cour2.setCname(&quot;数据库原理&quot;);		cour3.setCnumber(&quot;103&quot;);		cour3.setCname(&quot;计算机原理&quot;);		Set courses=new HashSet();		courses.add(cour1);		courses.add(cour2);		courses.add(cour3);		Student stu=new Student();		stu.setSnumber(&quot;081101&quot;);		stu.setSname(&quot;李方方&quot;);		stu.setSage(21);		stu.setCourses(courses);		session.save(stu);		ts.commit();				session.close();		sf.close();	&#125;&#125;


8. 小结我靠，Hibernate关系映射好复杂！根本没有明显规律，能不能愉快地玩耍了！
9. 参考文档《Java EE基础实用教程》，郑阿奇主编
初识Hibernate——关系映射：http://blog.csdn.net/laner0515/article/details/12905711
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>hibernate</tag>
      </tags>
  </entry>
  <entry>
    <title>Hibernate概述</title>
    <url>/dev-hibernate-start/</url>
    <content><![CDATA[1. 名词解释Hibernate是一个开放源代码的对象关系映射框架，它对JDBC进行了非常轻量级的对象封装（未完全封装），使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 
Hibernate可以应用在任何使用JDBC的场合，既可以在Java的客户端程序使用，也可以在Servlet/JSP的Web应用中使用，最具革命意义的是，Hibernate可以在应用EJB的J2EE架构中取代CMP，完成数据持久化的重任。
对目前的JavaEE信息化系统而言，通常采用面向对象分析和面向对象设计的过程。系统从需求分析到系统设计都是按面向对象方式进行。但是到详细设计阶段，由于数据持久化需要保存到关系数据库，不得不自底向上修改设计方案，又回到了按照过程进行编程的老路上来，这是非常令人沮丧的。
但人们的智慧是无穷的，遇到问题总会想办法解决，而不是与之妥协或绕道而走。Hibernate的问世解决了这个问题，Hibernate是一个面向Java环境的对象/关系映射工具，它可将对象模型表示的对象映射到基于SQL的关系数据模型中。这样就不用再为怎样用面向对象的方法进行数据持久化而大伤脑筋了。

2. 官网http://hibernate.org
3. 下载http://sourceforge.net/projects/hibernate/files/hibernate3/
4. 导入包解压缩hibernate-distribution-*.zip。导入hibernate-distribution-*GA/lib/required目录中的jar包。hibernate3.jar       核心类库antlr-2.7.6.jar      代码扫描器,用来翻译HQL语句commons-collections-3.1.jar    Apache Commons包中的一个，包含了一些Apache开发的集合类，功能比java.util.*强大dom4j-1.6.1.jar      一个Java的XML API，类似于jdom，用来读写XML文件的javassist-3.4.GA.jar    Javassist 字节码解释器jta-1.1.jar     标准的JTA API。slf4j-api-1.5.2.jarslf4j-nop-1.5.2.jar 
5. ORM对象/关系映射ORM（Object-Relation Mapping）是用于将对象与对象之间的关系对应到数据库表与表之间的关系一种模式。
简单的说，ORM是通过使用描述对象和数据库之间映射的元数据，将Java程序中的对象自动持久化到关系数据库中。对象和关系数据是业务实现的两种表现形式，业务实体在内存中表现为对象，在数据库中表现为关系数据。内存中的对象之间存在着关联和继承关系。而在数据库中，关系数据无法直接表达多对多关联和继承关系。因此，ORM系统一般以中间件的形式存在，主要实现程序对象到关系数据库数据的映射。
一般的ORM包括四个部分：对持久类对象进行CRUD操作的API、用来规定类和类属性相关查询的语言或API、规定mapping metadata的工具，以及可以让ORM实现同事务对象一起进行dirty checking、lazy association fetching和其他优化操作的技术。
目前，很多厂商和开源社区都提供了持久层框架的实现，但其中Hibernate的轻量级ORM模型逐步确立了在Java ORM架构中的领导地位。
6. Hibernate体系结构Hibernate作为模型层/数据访问层。它通过配置文件（hibernate.cfg.xml或hibernate.properties）和映射文件（*.hbm.xml）把Java对象或持久化对象（Persistent Object，PO）映射到数据库中的数据表，然后通过操作PO，对数据库中的表进行各种操作。实际上，PO由POJO和映射文件生成。
7. POJO、JavaBean和POPOJO = pure old java object or plain ordinary java object or what ever。POJO中文可以翻译成：普通Java类，具有一部分get/set方法的那种类就可以称作POJO。
JavaBean则比POJO复杂很多，JavaBean是一种组件技术，就好像你做了一个扳子，而这个扳子会在很多地方被拿去用，这个扳子也提供多种功能(你可以拿这个扳子扳、锤、撬等等)，而这个扳子就是一个组件。很显然POJO也是JavaBean的一种。一般在web应用程序中建立一个数据库的映射对象时，我们只能称它为POJO。JavaBean必须满足如下规范：1）必须有一个零参数的默认构造函数2）必须有get和set方法，类的字段必须通过get/set方法来访问。 
PO = persisent object 持久对象。实际上，PO必须对应数据库中的entity，所以和POJO有所区别。比如说POJO是由new创建，由GC回收。但是PO是数据库insert创建，由数据库delete删除的。基本上持久对象生命周期和数据库密切相关。另外持久对象往往只能存在一个数据库Connection之中，Connnection关闭以后，持久对象就不存在了，而POJO只要不被GC回收，总是存在的。由于存在诸多差别，因此PO在代码上肯定和POJO不同，起码PO相对于POJO会增加一些用来管理数据库entity状态的属性和方法。而ORM追求的目标就是要PO在使用上尽量和POJO一致，对于程序员来说，他们可以把PO当做POJO来用，而感觉不到PO的存在。所以我们项目中的entity可以看作是POJO，其实Hibernate最后作持久，是把PO持久化的，主要的机制是：1、编写POJO2、编译POJO3、直接运行，在运行期，由Hibernate的CGLIB动态把POJO转换为PO。
8. 应用实例Hibernate的配置文件是实体对象与数据库关系表之间相互转换的重要依据。一般而言，一个映射配置文件对应着数据库中一个关系表，关系表之间的关系也在映射文件中进行配置。
8.1. 建立数据库和表使用MySQL，scott用户。在xscj数据库中建立kcb，表结构如下：



项目名
列名
数据类型
可空
默认值
说明



课程号
KCH
定长字符型（char3）
×
无
主键


课程名
KCM
不定长字符串型（varchar12）
√
无



开学学期
KXXQ
整数型（smallint）
√
无
只能为1~8


学时
XS
整数型（int）
√
0



学分
XF
整数型（int）
√
0



create table kcb(kch char(3) not null primary key,kcm varchar(12),kxxq smallint check(kxxq&gt;=1 and kxxq&lt;=8),xs int default 0,xf int default 0);

8.2. 新建项目新建Dynamic Web Project，命名为hibernate。
8.3. 添加hibernate开发能力右击项目名，Properties，Java Build Path，Libraries，Add Library…，User Library，Next，User Libraries…，New…，输入“hibernate”，OK，Add External JARs…，选中需要的jar文件，打开，OK。
8.4. POJO对象和映射文件新建包com.voidking.hibernate.model，在包中新建POJO，命名为Kcb，代码如下：
package com.voidking.hibernate.model;public class Kcb &#123;	private String kch;	private String kcm;	private short kxxq;	private Integer xs;	private Integer xf;	public Kcb() &#123;	&#125;	public String getKch() &#123;		return kch;	&#125;	public void setKch(String kch) &#123;		this.kch = kch;	&#125;	public String getKcm() &#123;		return kcm;	&#125;	public void setKcm(String kcm) &#123;		this.kcm = kcm;	&#125;	public short getKxxq() &#123;		return kxxq;	&#125;	public void setKxxq(short kxxq) &#123;		this.kxxq = kxxq;	&#125;	public Integer getXs() &#123;		return xs;	&#125;	public void setXs(Integer xs) &#123;		this.xs = xs;	&#125;	public Integer getXf() &#123;		return xf;	&#125;	public void setXf(Integer xf) &#123;		this.xf = xf;	&#125;		&#125;
在包中新建文件Kcb.hbm.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!DOCTYPE hibernate-mapping PUBLIC    &quot;-//Hibernate/Hibernate Mapping DTD 3.0//EN&quot;    &quot;http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd&quot;&gt; &lt;hibernate-mapping&gt;&lt;!-- name指定POJO类，table指定对应数据库的表 --&gt;    &lt;class name=&quot;com.voidking.hibernate.model.Kcb&quot; table=&quot;kcb&quot;&gt;    	&lt;!-- name指定主键，type主键类型 --&gt;        &lt;id name=&quot;kch&quot; type=&quot;java.lang.String&quot;&gt;        	&lt;column name=&quot;kch&quot; length=&quot;3&quot;&gt;&lt;/column&gt;        	&lt;!-- 主键生成策略 --&gt;        	&lt;generator class=&quot;assigned&quot;&gt;&lt;/generator&gt;        &lt;/id&gt;                &lt;!-- POJO属性及表中字段对应的属性 --&gt;        &lt;property name=&quot;kcm&quot; type=&quot;java.lang.String&quot;&gt;        	&lt;column name=&quot;kcm&quot; length=&quot;12&quot;&gt;&lt;/column&gt;        &lt;/property&gt;                &lt;property name=&quot;kxxq&quot; type=&quot;java.lang.Short&quot;&gt;        	&lt;column name=&quot;kxxq&quot;&gt;&lt;/column&gt;        &lt;/property&gt;                &lt;property name=&quot;xs&quot; type=&quot;java.lang.Integer&quot;&gt;        	&lt;column name=&quot;xs&quot;&gt;&lt;/column&gt;        &lt;/property&gt;                &lt;property name=&quot;xf&quot; type=&quot;java.lang.Integer&quot;&gt;        	&lt;column name=&quot;xf&quot;&gt;&lt;/column&gt;        &lt;/property&gt;    &lt;/class&gt;&lt;/hibernate-mapping&gt;

8.5. hibernate.cfg.xml在src中新建hibernate.cfg.xml或者hibernate.properties，内容如下：
&lt;?xml version=&#x27;1.0&#x27; encoding=&#x27;utf-8&#x27;?&gt;&lt;!DOCTYPE hibernate-configuration PUBLIC        &quot;-//Hibernate/Hibernate Configuration DTD 3.0//EN&quot;        &quot;http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd&quot;&gt;     &lt;hibernate-configuration&gt;    &lt;session-factory&gt;         &lt;!-- Database connection settings --&gt;    &lt;property name=&quot;connection.driver_class&quot;&gt;com.mysql.jdbc.Driver&lt;/property&gt;    &lt;property name=&quot;connection.url&quot;&gt;jdbc:mysql://localhost/xscj&lt;/property&gt;    &lt;property name=&quot;connection.username&quot;&gt;scott&lt;/property&gt;    &lt;property name=&quot;connection.password&quot;&gt;tiger&lt;/property&gt;             &lt;!-- JDBC connection pool (use the built-in) --&gt;    &lt;!-- &lt;property name=&quot;connection.pool_size&quot;&gt;1&lt;/property&gt; --&gt;             &lt;!-- SQL dialect --&gt;    &lt;property name=&quot;dialect&quot;&gt;org.hibernate.dialect.MySQLDialect&lt;/property&gt;             &lt;!-- Echo all executed SQL to stdout --&gt;    &lt;property name=&quot;show_sql&quot;&gt;true&lt;/property&gt;             &lt;!-- Enable Hibernate&#x27;s automatic session context management --&gt;    &lt;!--&lt;property name=&quot;current_session_context_class&quot;&gt;thread&lt;/property&gt;--&gt;             &lt;!-- Drop and re-create the database schema on startup --&gt;    &lt;!-- &lt;property name=&quot;hbm2ddl.auto&quot;&gt;create&lt;/property&gt; --&gt;             &lt;!-- Disable the second-level cache --&gt;    &lt;property name=&quot;cache.provider_class&quot;&gt;org.hibernate.cache.NoCacheProvider&lt;/property&gt;         &lt;mapping resource=&quot;com/voidking/hibernate/model/Kcb.hbm.xml&quot;/&gt;             &lt;/session-factory&gt;&lt;/hibernate-configuration&gt;

8.6. 创建测试类新建包com.voidking.hibernate.test，新建类Test，代码如下：
package com.voidking.hibernate.test;import java.util.List;import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.hibernate.cfg.Configuration;import com.voidking.hibernate.model.Kcb;public class Test &#123;	public static void main(String[] args)&#123;		//创建Session对象		Configuration cfg = new Configuration();        SessionFactory sf = cfg.configure().buildSessionFactory();		Session session = sf.openSession();				//创建事务对象		Transaction ts=session.beginTransaction();		Kcb kc = new Kcb();		kc.setKch(&quot;198&quot;);		kc.setKcm(&quot;机电&quot;);		kc.setKxxq(new Short((short) 5));		kc.setXf(new Integer(5));		kc.setXs(new Integer(59));				//保存对象		session.save(kc);		ts.commit();		Query query = session.createQuery(&quot;from Kcb where kch=198&quot;);		List list = query.list();		Kcb kcl=(Kcb)list.get(0);		System.out.println(kcl.getKcm());		session.close();		sf.close();			&#125;&#125;

8.7. 导入数据库驱动导入mysql-connector-java-*-bin.jar。
8.8. 运行错误运行Test.java为Java Application，出现错误。Exception in thread &quot;main&quot; java.lang.NoClassDefFoundError: javax/persistence/EntityListeners原因是缺少hibernate-jpa-*-api-*.Final.jar，导入该包，问题解决。
8.9. 提示运行错误解决了，但是控制台仍然有提示信息。
SLF4J: Failed to load class &quot;org.slf4j.impl.StaticLoggerBinder&quot;.SLF4J: Defaulting to no-operation (NOP) logger implementationSLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
经过搜索，解决办法如下：
Placing one (and only one) of slf4j-nop.jar, slf4j-simple.jar, slf4j-log4j12.jar, slf4j-jdk14.jar or logback-classic.jar on the class path should solve the problem.
之前下载的hibernate压缩包里没有slf4j-nop.jar，就没有导入。单独下载该jar文件，导入，问题解决。
8.10. 再次运行错误其实，报错是应该的，因为两次插入的数据完全相同，而主键要求唯一。这时，应该怎么配置，才能在不改变代码的情况下不断运行呢？经测试，只要在hibernate.cfg.xml配置文件中，把注释中的这一句释放出来，就可以了。
&lt;property name=&quot;hbm2ddl.auto&quot;&gt;create&lt;/property&gt;

9. 各文件的作用9.1. POJO和映射配置文件POJO中的属性和表中的字段是一一对应的。那么通过什么方法把它们一一映射起来呢？就是*.hbm.xml映射文件。该配置文件大致分为3个部分：1、类、表映射配置
&lt;class name=&quot;com.voidking.hibernate.model.Kcb&quot; table=&quot;kcb&quot;&gt;
2、id映射配置
&lt;id name=&quot;kch&quot; type=&quot;java.lang.String&quot;&gt;	&lt;column name=&quot;kch&quot; length=&quot;3&quot;&gt;&lt;/column&gt;	&lt;generator class=&quot;assigned&quot;&gt;&lt;/generator&gt;&lt;/id&gt;
Hibernate的主键生成策略分为三大类：Hibernate对主键id赋值、应用程序自身对id赋值、由数据库对id赋值。

assigned：应用程序自身对id赋值。
native：由数据库对id赋值。
hilo：通过hi/lo算法实现的主键生成机制，需要额外的数据库表保存主键生成历史状态。
seqhilo：与hi/lo类似，通过hi/lo算法实现的主键生成机制，只是主键历史状态保存在sequence中，适用于支持sequence的数据库，比如Oracle。
increment：主键按数值顺序递增。
identity：采用数据库提供的主键生成机制，如SQL Server、MySQL中的自增主键生成机制。
sequence：采用数据库提供的sequence机制生成主键，如Oracle sequence。
uuid.hex：由Hibernate基于128位唯一值产生算法，根据当前设备IP、时间、JVM启动时间、内部自增量等4个参数生成十六进制数值（编码长度为32位的字符串表示）作为主键。即使是在多实例并发运行的情况下，这种算法在最大程度上保证了产生id的唯一性。
uuid.string：与uuid.hex类似，只是对生成的主键进行编码（长度16位）。在某些数据库中可能出现问题。
foreign：使用外部表的字段作为主键。
select：Hibernate3引入的主键生成机制，主要针对遗留系统的改造工程。

由于常用的数据库，如SQL Sever、MySQL等，都提供了易用的主键生成机制（如auto-increase字段）。可以在数据库提供的主键生成机制上，采用generator class=”native”的主键生成方式。
3、属性、字段映射配置
&lt;property name=&quot;kcm&quot; type=&quot;java.lang.String&quot;&gt;	&lt;column name=&quot;kcm&quot; length=&quot;12&quot;&gt;&lt;/column&gt;&lt;/property&gt;

4、关系配置表与表之间的关系，会被映射成类与类之间的关系，它们的关系的具体体现也会在该配置文件中配置。
9.2. hibernate.cfg.xmlHibernate配置文件主要用于配置数据库连接和Hibernate运行时所需要的各种属性，配置文件一般默认为hibernate.cfg.xml，Hibernate初始化期间会自动在classpath中寻找这个文件，并读取其中的配置信息，为后期数据库操作做好准备。
9.3. HibernateSessionFactorypackage com.voidking.hibernate.util;import org.hibernate.HibernateException;import org.hibernate.Session;import org.hibernate.cfg.Configuration;import org.hibernate.service.ServiceRegistry;import org.hibernate.service.ServiceRegistryBuilder;/** * Configures and provides access to Hibernate sessions, tied to the * current thread of execution.  Follows the Thread Local Session * pattern, see &#123;@link http://hibernate.org/42.html &#125;. */public class HibernateSessionFactory &#123;    /**      * Location of hibernate.cfg.xml file.     * Location should be on the classpath as Hibernate uses       * #resourceAsStream style lookup for its configuration file.      * The default classpath location of the hibernate config file is      * in the default package. Use #setConfigFile() to update      * the location of the configuration file for the current session.        */	private static final ThreadLocal&lt;Session&gt; threadLocal = new ThreadLocal&lt;Session&gt;();    private static org.hibernate.SessionFactory sessionFactory;	    private static Configuration configuration = new Configuration();    private static ServiceRegistry serviceRegistry; 	static &#123;    	try &#123;			configuration.configure();			serviceRegistry = new ServiceRegistryBuilder().applySettings(configuration.getProperties()).buildServiceRegistry();			sessionFactory = configuration.buildSessionFactory(serviceRegistry);		&#125; catch (Exception e) &#123;			System.err.println(&quot;%%%% Error Creating SessionFactory %%%%&quot;);			e.printStackTrace();		&#125;    &#125;    private HibernateSessionFactory() &#123;    &#125;		/**     * Returns the ThreadLocal Session instance.  Lazy initialize     * the &lt;code&gt;SessionFactory&lt;/code&gt; if needed.     *     *  @return Session     *  @throws HibernateException     */    public static Session getSession() throws HibernateException &#123;        Session session = (Session) threadLocal.get();		if (session == null || !session.isOpen()) &#123;			if (sessionFactory == null) &#123;				rebuildSessionFactory();			&#125;			session = (sessionFactory != null) ? sessionFactory.openSession()					: null;			threadLocal.set(session);		&#125;        return session;    &#125;	/**     *  Rebuild hibernate session factory     *     */	public static void rebuildSessionFactory() &#123;		try &#123;			configuration.configure();			serviceRegistry = new ServiceRegistryBuilder().applySettings(configuration.getProperties()).buildServiceRegistry();			sessionFactory = configuration.buildSessionFactory(serviceRegistry);		&#125; catch (Exception e) &#123;			System.err.println(&quot;%%%% Error Creating SessionFactory %%%%&quot;);			e.printStackTrace();		&#125;	&#125;	/**     *  Close the single hibernate session instance.     *     *  @throws HibernateException     */    public static void closeSession() throws HibernateException &#123;        Session session = (Session) threadLocal.get();        threadLocal.set(null);        if (session != null) &#123;            session.close();        &#125;    &#125;	/**     *  return session factory     *     */	public static org.hibernate.SessionFactory getSessionFactory() &#123;		return sessionFactory;	&#125;	/**     *  return hibernate configuration     *     */	public static Configuration getConfiguration() &#123;		return configuration;	&#125;&#125;
上面的实例中，我并没有使用这个类，如果使用它的话，Test.java文件可以修改如下：
package com.voidking.hibernate.test;import java.util.List;import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.Transaction;import org.model.Kcb;import com.voidking.hibernate.util.HibernateSessionFactory;public class Test &#123;   public static void main(String[] args) &#123;	// 调用HibernateSessionFactory的getSession方法创建Session对象	Session session=HibernateSessionFactory.getSession();	// 创建事务对象	Transaction ts=session.beginTransaction();	Kcb kc=new Kcb();                       	// 创建POJO类对象	kc.setKch(&quot;198&quot;);                        	// 设置课程号	kc.setKcm(&quot;机电&quot;);                       	// 设置课程名	kc.setKxxq(new Short((short) 5));			 	// 设置开学学期	kc.setXf(new Integer(5));				 	// 设置学分	kc.setXs(new Integer(59));				 	// 设置学时	// 保存对象	session.save(kc);	ts.commit();                             	// 提交事务	Query query=session.createQuery(&quot;from Kcb where kch=198&quot;);	List list=query.list();	Kcb kc1=(Kcb) list.get(0);	System.out.println(kc1.getKcm());	HibernateSessionFactory.closeSession();		 	// 关闭Session   &#125;&#125;
HibernateSessionFactory类是自定义的SessionFactory，名字可以根据自己的喜好来决定。在Hibernate中，Session负责完成对象持久化操作。该文件负责创建Session对象，以及关闭Session对象。从HibernateSessionFactory文件可以看出，Session对象的创建大致需要以下3个步骤：1、初始化Hibernate配置管理类Configuration。2、通过Configuration类实例创建Session的工厂类SessionFactory。3、通过SessionFactory得到Session实例。
10. Hibernate核心接口Hibernate核心接口一共有5个：Configuration、SessionFactory、Session、Transaction和Query。这5个接口在任何开发中都会用到。通过这些接口，不仅可以对持久化对象进行存取，还能够进行事务控制。
10.1. ConfigurationConfiguration接口负责Hibernate的配置信息。Hibernate运行时需要一些底层实现的基本信息。这些信息包括：数据库URL、数据库用户名、数据库用户密码、数据库JDBC驱动类、数据库dialect。用于对特定数据库提供支持，其中包含了针对特定数据库特性的实现，如Hibernate数据库类型到特定数据库类型的映射等。使用Hibernate必须首先提供这些基础信息以完成初始化工作，为后续操作做好准备。这些属性在Hibernate配置文件hibernate.cfg.xml中加以设定，当调用：
Configuration config = new Configuration().configure();
Hibernate会自动在目录下搜索hibernate.cfg.xml文件，并将其自动读取到内存中作为后续操作的基础配置。
10.2. SessionFactorySessionFactory负责创建Session实例，可以通过Configuration实例构建SessionFactory。
Configuration config = new Configuration().configure();SessionFactory sessionFactory = config.buildSessionFactory();
Configuration实例config会根据当前数据库的配置信息，构造SessionFactory实例并返回。SessionFactory一旦构造完毕，即被赋予特定的配置信息。也就是说，以后config的任何变更将不会影响到已经创建的SessionFactory实例sessionFactory。如果应用中需要访问多个数据库，针对每个数据库，应分别对其创建对应的SessionFactory实例。SessionFactory保存了对应当前数据库配置的所有映射关系，同时也负责维护当前的二级数据缓存和Statement Pool。由此可见，SessionFactory的创建过程非常复杂、代价高昂。由于SessionFactory采用了线程安全的设计，可由多个线程并发调用。大多数情况下，应用中针对一个数据库共享一个SessionFactory实例即可。
10.3. SessionSession是Hibernate持久化操作的基础，提供了众多持久化方法，如save、update、delete等。通过这些方法，透明地完成对象的增加、删除、修改、查找等操作。同时，值得注意的是，Hibernate Session的设计是非线程安全的，即一个Session实例同时只可由一个线程使用。同一个Session实例的多线程并发调用将导致难以预知的错误。Session实例由SessionFactory构建：
Configuration config = new Configuration().configure();SessionFactory sessionFactory = config.buildSessionFactory();Session session = sessionFactory.openSession();
之后可以调用Session提供的save、get、delete等方法完成持久层操作。
10.4. TransactionTransaction是Hibernate中进行事务操作的接口，Transaction接口是对实际事务实现的一个抽象，这些实现包括JDBC的事务、JTA中的UserTransaction，甚至可以是CORBA事务。之所以这样设计是可以让开发者能够使用一个统一的操作界面，使得自己的项目可以在不同的环境和容器之间方便地移植。事务对象通过Session创建。
Transaction ts = session.beginTransaction();



10.5. Query在Hibernate 2.X中，find()方法用于执行HQL语句。Hibernate 3.X废除了find()方法，取而代之的是Query接口，它们都用于执行HQL语句。Query和HQL是分不开的。
Query query = session.createQuery(&quot;from Kcb where kch=198&quot;);

Query query = session.createQuery(&quot;from Kcb where kch=?&quot;);query.setString(0,&quot;要设置的值&quot;);

Query query = session.createQuery(&quot;from Kcb where kch=:kchValue&quot;);query.setString(&quot;kchValue&quot;,&quot;要设置的值&quot;);
由于上例中的kch为String类型，所以设置的时候用setString，如果是int类型就要用setInt。还有一种通用的设置方法，就是setParameter方法，不管什么类型的参数都可以应用。
query.setParameter(0,&quot;要设置的值&quot;);query.setParameter(&quot;kchValue&quot;,&quot;要设置的值&quot;);

Query还有一个list()方法，用于取得一个List集合的示例，此示例中包括可能是一个Object集合，也可能是Object数组集合。
Query query = session.createQuery(&quot;form Kcb where kch=198&quot;);List list = query.list();

11. HQL查询HQL是Hibernate Query Language的缩写。HQL的语法很像SQL，但HQL是一种面向对象查询语言。SQL的操作对象是数据表和列等数据对象，而HQL的操作对象是类、实例、属性等。HQL的查询依赖于Query类，每个Query实例对应一个查询对象。
11.1. 基本查询1、查询所有课程信息
Session session=HibernateSessionFactory.getSession();Transaction ts=session.beginTransaction();Query query=session.createQuery(&quot;from Kcb&quot;);List list=query.list();ts.commit(); HibernateSessionFactory.closeSession();		 	
2、查询某门课程信息
Session session=HibernateSessionFactory.getSession();Transaction ts=session.beginTransaction();Query query=session.createQuery(&quot;from Kcb order by xs desc&quot;);query.setMaxResults(1);//设置最大检索数目为1Kcb kc = (Kcb)query.uniqueResult();ts.commit(); HibernateSessionFactory.closeSession();		 	
3、查询满足条件的课程信息
Session session=HibernateSessionFactory.getSession();Transaction ts=session.beginTransaction();Query query=session.createQuery(&quot;from Kcb where kch=001&quot;);List list=query.list();ts.commit(); HibernateSessionFactory.closeSession();		 	

11.2. 条件查询1、按指定参数查询
Session session=HibernateSessionFactory.getSession();Transaction ts=session.beginTransaction();Query query=session.createQuery(&quot;from Kcb where kcm=?&quot;);query.setParameter(0,&quot;计算机基础&quot;);List list=query.list();ts.commit(); HibernateSessionFactory.closeSession();		 	

2、使用范围运算查询
Session session=HibernateSessionFactory.getSession();Transaction ts=session.beginTransaction();Query query=session.createQuery(&quot;from Kcb where (xs between 40 and 60) and kcm in(&#x27;计算机基础&#x27;,&#x27;数据结构&#x27;)&quot;);List list=query.list();ts.commit(); HibernateSessionFactory.closeSession();		 	

3、使用比较运算符查询
Session session=HibernateSessionFactory.getSession();Transaction ts=session.beginTransaction();Query query=session.createQuery(&quot;from Kcb where xs &gt; 50 and kcm is not null&quot;);List list=query.list();ts.commit(); HibernateSessionFactory.closeSession();		 	

4、使用字符串匹配运算查询
Session session=HibernateSessionFactory.getSession();Transaction ts=session.beginTransaction();Query query=session.createQuery(&quot;from Kcb where kch like &#x27;%001%&#x27; and kcm like &#x27;计算机%&#x27;&quot;);List list=query.list();ts.commit(); HibernateSessionFactory.closeSession();		 	

11.3. 分页查询在页面上显示数据结果时，如果数据太多，一个页面无法全部显示，这是务必要对查询结果进行分页显示。为了满足分页查询的需要，Hibernate的Query实例提供了两个有用的方法：setFirstResult(int firstResult)和setMaxResult(int maxResult)。
Session session=HibernateSessionFactory.getSession();Transaction ts=session.beginTransaction();Query query=session.createQuery(&quot;from Kcb&quot;);int pageNow=1;int pageSize=5;query.setFirstResult((pageNow-1)*pageSize);query.setMaxResult(pageSize);List list=query.list();ts.commit(); HibernateSessionFactory.closeSession();		 	
通常情况下，pageNow会作为一个参数传进来，这样就可以得到想要显示的页数的结果集了。
12. 参考文档《Java EE基础实用教程》，郑阿奇主编关于对POJO PO 的理解：http://fluagen.blog.51cto.com/146595/36396/
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>hibernate</tag>
      </tags>
  </entry>
  <entry>
    <title>Struts2综合应用实例</title>
    <url>/dev-struts2-comprehensive-example/</url>
    <content><![CDATA[1. 前言本实例实现了一个简单的功能：添加学生信息。我们仍然在Struts2概述中struts2项目的基础上进行。
2. 建立数据库使用MySQL，scott用户，建立数据库，名为XSCJ，其中有一张表XSB，结构如下：



项目名
列名
数据类型
可空
默认值
说明



学号
XH
定长字符串型（char6）
×
无
主键


姓名
XM
不定长字符串型（varchar8）
×
无



性别
XB
位型（bit）
×
无
值约束：1/0。1表示男，0表示女


出生时间
CSSJ
日期时间型（datetime）
√
无



专业Id
ZY_ID
整数型（int）
×
无



总学分
ZXF
整数型（int）
√
0
0&lt;=总学分&lt;160


备注
BZ
不定长字符串型（varchar500）
√
无



照片
ZP
longblob
√
无



create database xscj;use xscj;create table xsb(xh char(6) not null primary key,xm varchar(8) not null,xb bit not null check(xb=0 or xb=1),cssj datetime ,zy_id int,zxf int default 0 check(0&lt;=zxf&lt;160),bz varchar(500),zp longblob);

3. 建立stu.jsp&lt;%@ page language=&quot;java&quot; pageEncoding=&quot;utf-8&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-tags&quot; prefix=&quot;s&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-dojo-tags&quot; prefix=&quot;sx&quot; %&gt;&lt;html&gt;&lt;head&gt;	&lt;s:head /&gt;	&lt;sx:head/&gt;&lt;/head&gt;&lt;body&gt;	&lt;h3&gt;添加学生信息&lt;/h3&gt;	&lt;s:form action=&quot;save&quot; namespace=&quot;/&quot; method=&quot;post&quot; theme=&quot;simple&quot;&gt;		&lt;table&gt;			&lt;tr&gt;				&lt;td&gt;学号（六位数）：&lt;/td&gt;				&lt;td&gt;&lt;s:textfield name=&quot;xs.xh&quot;&gt;&lt;/s:textfield&gt;&lt;/td&gt;			&lt;/tr&gt;			&lt;tr&gt;				&lt;td&gt;姓名：&lt;/td&gt;				&lt;td&gt;&lt;s:textfield name=&quot;xs.xm&quot; &gt;&lt;/s:textfield&gt;&lt;/td&gt;			&lt;/tr&gt;			&lt;tr&gt;				&lt;td&gt;性别：&lt;/td&gt;				&lt;td&gt;&lt;s:radio name=&quot;xs.xb&quot; list=&quot;#&#123;true:&#x27;男&#x27;,false:&#x27;女&#x27;&#125;&quot; value=&quot;true&quot;&gt;&lt;/s:radio&gt;&lt;/td&gt;			&lt;/tr&gt;			&lt;tr&gt;				&lt;td width=&quot;70&quot;&gt;出生时间:&lt;/td&gt;				&lt;td&gt;&lt;sx:datetimepicker name=&quot;xs.cssj&quot; id=&quot;cssj&quot;	displayFormat=&quot;yyyy-MM-dd&quot;&gt;&lt;/sx:datetimepicker&gt;&lt;/td&gt;			&lt;/tr&gt;			&lt;tr&gt;				&lt;td&gt;专业ID：&lt;/td&gt;				&lt;td&gt;&lt;s:textfield name=&quot;xs.zy_id&quot; label=&quot;专业&quot;&gt;&lt;/s:textfield&gt;&lt;/td&gt;			&lt;/tr&gt;			&lt;tr&gt;				&lt;td&gt;备注：&lt;/td&gt;				&lt;td&gt;&lt;s:textarea name=&quot;xs.bz&quot; label=&quot;备注&quot;&gt;&lt;/s:textarea&gt;&lt;/td&gt;			&lt;/tr&gt;			&lt;tr&gt;				&lt;td&gt;&lt;s:submit value=&quot;添加&quot;&gt;&lt;/s:submit&gt;&lt;/td&gt;				&lt;td&gt;&lt;s:reset value=&quot;重置&quot;&gt;&lt;/s:reset&gt;&lt;/td&gt;			&lt;/tr&gt;		&lt;/table&gt;	&lt;/s:form&gt;&lt;/body&gt;&lt;/html&gt;
注意，上面的代码使用了/struts-dojo-tags标签，需要导入包struts2-dojo-plugin-*.jar。Struts2的标签具有自动排版的功能，如果想要自己排版，可以在form标签中加入theme=”simple”，但加入该元素后，标签中的label属性就没用了。
4. 建立addsuccess.jsp&lt;%@ page language=&quot;java&quot; pageEncoding=&quot;utf-8&quot;%&gt;&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;    	恭喜你，添加成功！&lt;/body&gt;&lt;/html&gt;

5. 建立JavaBeanpackage com.voidking.struts2.model;import java.sql.Date;public class Xsb &#123;   	private String xh;	private String xm;	private boolean xb;	private Date cssj;	private int zy_id;	private int zxf;	private String bz;	private byte[] zp;	public String getXh() &#123;		return xh;	&#125;	public void setXh(String xh) &#123;		this.xh = xh;	&#125;	public String getXm() &#123;		return xm;	&#125;	public void setXm(String xm) &#123;		this.xm = xm;	&#125;		public boolean isXb() &#123;		return xb;	&#125;	public void setXb(boolean xb) &#123;		this.xb = xb;	&#125;	public Date getCssj() &#123;		return cssj;	&#125;	public void setCssj(Date cssj) &#123;		this.cssj = cssj;	&#125;	public int getZy_id() &#123;		return zy_id;	&#125;	public void setZy_id(int zy_id) &#123;		this.zy_id = zy_id;	&#125;	public int getZxf() &#123;		return zxf;	&#125;	public void setZxf(int zxf) &#123;		this.zxf = zxf;	&#125;	public String getBz() &#123;		return bz;	&#125;	public void setBz(String bz) &#123;		this.bz = bz;	&#125;	public byte[] getZp() &#123;		return zp;	&#125;	public void setZp(byte[] zp) &#123;		this.zp = zp;	&#125;		&#125;

6. 建立DBConnpackage com.voidking.struts2.db;import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import com.voidking.struts2.model.Xsb;public class DBConn &#123;	Connection conn;	PreparedStatement pstmt;	public DBConn()&#123;		try&#123;			Class.forName(&quot;com.mysql.jdbc.Driver&quot;);			conn=DriverManager.getConnection(&quot;jdbc:mysql://localhost/xscj&quot;,&quot;scott&quot;,&quot;tiger&quot;);		&#125;catch(Exception e)&#123;			e.printStackTrace();		&#125;	&#125;	// 添加学生	public boolean save(Xsb xs)&#123;		try&#123;			pstmt=conn.prepareStatement(&quot;insert into XSB(xh,xm,xb,cssj,zy_id,bz) values(?,?,?,?,?,?)&quot;);			pstmt.setString(1, xs.getXh());			pstmt.setString(2, xs.getXm());			pstmt.setBoolean(3, xs.isXb());			pstmt.setDate(4, xs.getCssj());			pstmt.setInt(5, xs.getZy_id());			pstmt.setString(6, xs.getBz());			pstmt.executeUpdate();			return true;		&#125;catch(Exception e)&#123;			e.printStackTrace();			return false;		&#125;	&#125;&#125;
注意，别忘记导入包mysql-connector-java-*-bin.jar。
7. 建立SaveActionpackage com.voidking.struts2.action;import com.opensymphony.xwork2.ActionSupport;import com.voidking.struts2.db.DBConn;import com.voidking.struts2.model.Xsb;public class SaveAction extends ActionSupport&#123;	private Xsb xs;	public Xsb getXs() &#123;		return xs;	&#125;	public void setXs(Xsb xs) &#123;		this.xs=xs;	&#125;	public String execute() throws Exception &#123;		DBConn db=new DBConn();		Xsb stu=new Xsb();		stu.setXh(xs.getXh());		stu.setXm(xs.getXm());		stu.setXb(xs.isXb());		stu.setZy_id(xs.getZy_id());		stu.setCssj(xs.getCssj());		stu.setBz(xs.getBz());		if(db.save(stu))&#123;			return SUCCESS;		&#125;else			return ERROR;	&#125;&#125;

8. struts.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;	&lt;constant name=&quot;struts.multipart.saveDir&quot; value=&quot;/tmp&quot;/&gt;    &lt;package name=&quot;default&quot;  extends=&quot;struts-default&quot;&gt;        	&lt;interceptors&gt;    		&lt;interceptor name=&quot;myInterceptor&quot; class=&quot;com.voidking.struts2.tool.MyInterceptor&quot;&gt;&lt;/interceptor&gt;    	&lt;/interceptors&gt;    	&lt;default-interceptor-ref name=&quot;&quot;&gt;&lt;/default-interceptor-ref&gt;    	        &lt;default-action-ref name=&quot;index&quot; /&gt;        &lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;            &lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;            &lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;            &lt;result name=&quot;input&quot;&gt;/hello.jsp&lt;/result&gt;                        &lt;!-- 拦截配置在result后面 --&gt;            &lt;!-- 使用系统默认拦截器栈 --&gt;            &lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;            &lt;!-- 配置拦截器 --&gt;            &lt;interceptor-ref name=&quot;myInterceptor&quot;&gt;&lt;/interceptor-ref&gt;                    &lt;/action&gt;                &lt;action name=&quot;upload&quot; class=&quot;com.voidking.struts2.action.UploadAction&quot;&gt;        	&lt;result name=&quot;success&quot;&gt;/uploadsuccess.jsp&lt;/result&gt;        	&lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;        &lt;/action&gt;                &lt;action name=&quot;upload2&quot; class=&quot;com.voidking.struts2.action.Upload2Action&quot;&gt;        	&lt;result name=&quot;success&quot;&gt;/uploadsuccess.jsp&lt;/result&gt;        	&lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;        &lt;/action&gt;                &lt;action name=&quot;save&quot; class=&quot;com.voidking.struts2.action.SaveAction&quot;&gt;        	&lt;result name=&quot;success&quot;&gt;/addsuccess.jsp&lt;/result&gt;        	&lt;result name=&quot;error&quot;&gt;/stu.jsp&lt;/result&gt;        	&lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;        &lt;/action&gt;    &lt;/package&gt;&lt;/struts&gt;
9. 源代码分享https://github.com/voidking/struts2.git包括Struts2概述、Struts2国际化应用、Struts2文件上传、Struts2综合应用实例。
10. 小结性别保存为bit类型数据，搞起来感觉有点麻烦，换成char(2)类型，限定值为“男”或“女”，也许更方便一些。
11. 参考文档《Java EE基础实用教程》，郑阿奇主编
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>struts2</tag>
      </tags>
  </entry>
  <entry>
    <title>Struts2文件上传</title>
    <url>/dev-struts2-upload/</url>
    <content><![CDATA[1. Struts2文件上传1.1. 上传单个文件Struts2中，提供了一个很容易操作的文件上传组件。用Struts2上传单个文件的功能非常容易实现，只要使用普通的Action即可。但为了获得一些文件上传的信息，如上传文件名等，需要按照一定规则来为Action类增加一些getter和setter方法。Struts2的文件上传默认使用的是Jakarta的Common-FileUpload文件上传框架。因此需要在Web应用中增加两个Jar包，即common-io-*.jar和common-fileupload-*.jar。
我们接着在struts2概述中的struts2项目中开发文件上传实例。
1.1.1. 指定文件夹在D盘下新建upload文件夹，上传的文件放到这个目录下。

1.1.2. upload.jsp在WebContent中新建upload.jsp，代码如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-tags&quot; prefix=&quot;s&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;文件上传&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;s:form action=&quot;upload.action&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;		&lt;s:file name=&quot;upload&quot; label=&quot;上传的文件&quot;&gt;&lt;/s:file&gt;		&lt;s:submit value=&quot;上传&quot;&gt;&lt;/s:submit&gt;	&lt;/s:form&gt;&lt;/body&gt;&lt;/html&gt;
1.1.3. UploadActionpackage com.voidking.struts2.action;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import com.opensymphony.xwork2.ActionSupport;public class UploadAction extends ActionSupport&#123;	private File upload;	private String uploadFileName;		public String getUploadFileName() &#123;		return uploadFileName;	&#125;	public void setUploadFileName(String uploadFileName) &#123;		this.uploadFileName = uploadFileName;	&#125;	public File getUpload()	&#123;		return upload;	&#125;		public void setUpload(File upload)	&#123;		this.upload=upload;	&#125;		public String execute() throws Exception&#123;		InputStream is=new FileInputStream(getUpload());		OutputStream os = new FileOutputStream(&quot;d:\\upload\\&quot;+uploadFileName);		byte buffer[] = new byte[1024];		int count= 0;		while((count=is.read(buffer))&gt;0)		&#123;			os.write(buffer,0,count);		&#125;		os.close();		is.close();		return SUCCESS;	&#125;	&#125;
上传的文件经过Action处理后，会被写到指定的路径下。其实也可以把上传的文件写入数据库中，之后的例子会介绍如何把照片写入到数据库。需要注意的是，Struts2上传文件的默认大小限制为2MB，如果需要修改默认大小，只需要在Struts2的struts.properties文件中修改struts.multipart.maxSize。如struts.mulitipart.maxSize=1024表示上传文件的总大小不能超过1KB。
1.1.4. struts.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;    &lt;package name=&quot;default&quot; namespace=&quot;/&quot; extends=&quot;struts-default&quot;&gt;        	&lt;interceptors&gt;    		&lt;interceptor name=&quot;myInterceptor&quot; class=&quot;com.voidking.struts2.tool.MyInterceptor&quot;&gt;&lt;/interceptor&gt;    	&lt;/interceptors&gt;    	&lt;default-interceptor-ref name=&quot;&quot;&gt;&lt;/default-interceptor-ref&gt;    	        &lt;default-action-ref name=&quot;index&quot; /&gt;        &lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;            &lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;            &lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;            &lt;result name=&quot;input&quot;&gt;/hello.jsp&lt;/result&gt;                        &lt;!-- 拦截配置在result后面 --&gt;            &lt;!-- 使用系统默认拦截器栈 --&gt;            &lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;            &lt;!-- 配置拦截器 --&gt;            &lt;interceptor-ref name=&quot;myInterceptor&quot;&gt;&lt;/interceptor-ref&gt;                    &lt;/action&gt;                &lt;action name=&quot;upload&quot; class=&quot;com.voidking.struts2.action.UploadAction&quot;&gt;        	&lt;result name=&quot;success&quot;&gt;/uploadsuccess.jsp&lt;/result&gt;        &lt;/action&gt;    &lt;/package&gt;&lt;/struts&gt;

1.1.5. uploadsuccess.jsp&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;上传成功&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	恭喜你，上传成功！&lt;/body&gt;&lt;/html&gt;

1.1.6. 404错误部署项目，启动Tomcat，访问：http://localhost:8080/struts2/upload.jsp ，提示 HTTP Status 404 - /struts2/upload.jsp。访问主页：http://localhost:8080/struts2/ ，同样提示404错误！
啊勒，这是肿么回事？想来想去，引起错误的原因，可能是昨天关闭eclipse的时候，没有先关闭Tomcat服务器，引起了一些配置错误。
查看tomcat/conf文件夹下的配置文件，server.xml和tomcat-users.xml，感觉没啥问题。
访问：http://localhost:8080 ，使用manager-gui账户登录tomcat，Manager App，可以看到，struts2项目的Running属性的值为false。奥~这就是问题所在了！点击start按钮，啊勒，无效！
删除webapps下的struts2工程，重新发布，运行tomcat，还是404错误！
再次点击start按钮，同时观察Console下面的信息：
SEVERE: Exception starting filter struts2Unable to load configuration. - package - file:/D:/Server/tomcat/webapps/struts2/WEB-INF/classes/struts.xml:7:67......SEVERE: Error filterStart

经过查找资料，确定了是struts.xml的配置问题。首先修改struts.xm如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;    &lt;package name=&quot;default&quot; namespace=&quot;/&quot; extends=&quot;struts-default&quot;&gt;        	&lt;interceptors&gt;    		&lt;interceptor name=&quot;myInterceptor&quot; class=&quot;com.voidking.struts2.tool.MyInterceptor&quot;&gt;&lt;/interceptor&gt;    	&lt;/interceptors&gt;    	&lt;default-interceptor-ref name=&quot;&quot;&gt;&lt;/default-interceptor-ref&gt;    	        &lt;default-action-ref name=&quot;index&quot; /&gt;        &lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;            &lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;            &lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;            &lt;result name=&quot;input&quot;&gt;/hello.jsp&lt;/result&gt;                        &lt;!-- 拦截配置在result后面 --&gt;            &lt;!-- 使用系统默认拦截器栈 --&gt;            &lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;            &lt;!-- 配置拦截器 --&gt;            &lt;interceptor-ref name=&quot;myInterceptor&quot;&gt;&lt;/interceptor-ref&gt;                    &lt;/action&gt;    &lt;/package&gt;&lt;/struts&gt;
启动Tomcat，项目果然可以访问了！那么，为什么在package中多加了一个action就会出错呢？很多教程中，明明可以多个action写在一个package里啊！再看这个package，有何特殊之处？配置了拦截器！那么，接下来的action是否必须配置拦截器？修改试试。
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;    &lt;package name=&quot;default&quot; namespace=&quot;/&quot; extends=&quot;struts-default&quot;&gt;        	&lt;interceptors&gt;    		&lt;interceptor name=&quot;myInterceptor&quot; class=&quot;com.voidking.struts2.tool.MyInterceptor&quot;&gt;&lt;/interceptor&gt;    	&lt;/interceptors&gt;    	&lt;default-interceptor-ref name=&quot;&quot;&gt;&lt;/default-interceptor-ref&gt;    	        &lt;default-action-ref name=&quot;index&quot; /&gt;        &lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;            &lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;            &lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;            &lt;result name=&quot;input&quot;&gt;/hello.jsp&lt;/result&gt;                        &lt;!-- 拦截配置在result后面 --&gt;            &lt;!-- 使用系统默认拦截器栈 --&gt;            &lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;            &lt;!-- 配置拦截器 --&gt;            &lt;interceptor-ref name=&quot;myInterceptor&quot;&gt;&lt;/interceptor-ref&gt;                    &lt;/action&gt;                &lt;action name=&quot;upload&quot; class=&quot;com.voidking.struts2.action.UploadAction&quot;&gt;        	&lt;result name=&quot;success&quot;&gt;/uploadsuccess.jsp&lt;/result&gt;        	&lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;        &lt;/action&gt;    &lt;/package&gt;&lt;/struts&gt;
启动Tomcat，项目可以正常访问。综上，可以得出一个结论：package包中如果定义了拦截器，那么，接下来的action中必须配置拦截器，否则会引起项目无法启动！
1.1.7. 警告我们的项目已经可以正常运行，上传功能也正常。但是，当我们访问：http://localhost:8080/struts2/upload.jsp ，会报出如下警告：
&#x27;upload.action&#x27; in namespace: &#x27;&#x27;. Form action defaulting to &#x27;action&#x27; attribute&#x27;s literal value.
经过查找资料，原来这是因为没有正确使用tag，upload.jsp修改如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-tags&quot; prefix=&quot;s&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;文件上传&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;s:form action=&quot;upload&quot; namespace=&quot;/&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;		&lt;s:file name=&quot;upload&quot; label=&quot;上传的文件&quot;&gt;&lt;/s:file&gt;		&lt;s:submit value=&quot;上传&quot;&gt;&lt;/s:submit&gt;	&lt;/s:form&gt;&lt;/body&gt;&lt;/html&gt;
再次访问，已经没有警告了。但是还是有提示信息：
At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
对于有洁癖的同学可以参考下面的参考文档去除这个信息，郝同学这里不再赘述。
1.2. 多文件上传多文件上传和单文件上传类似，只需要改动几个地方即可。
1.2.1. upload.jsp&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-tags&quot; prefix=&quot;s&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;文件上传&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;s:form action=&quot;upload&quot; namespace=&quot;/&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;		&lt;s:file name=&quot;upload&quot; label=&quot;上传的文件&quot;&gt;&lt;/s:file&gt;		&lt;s:submit value=&quot;上传&quot;&gt;&lt;/s:submit&gt;	&lt;/s:form&gt;	&lt;hr/&gt;	&lt;s:form action=&quot;upload2&quot; namespace=&quot;/&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;		&lt;s:file name=&quot;upload2&quot; label=&quot;上传的文件&quot;&gt;&lt;/s:file&gt;		&lt;s:file name=&quot;upload2&quot; label=&quot;上传的文件&quot;&gt;&lt;/s:file&gt;		&lt;s:file name=&quot;upload2&quot; label=&quot;上传的文件&quot;&gt;&lt;/s:file&gt;		&lt;s:submit value=&quot;批量上传&quot;&gt;&lt;/s:submit&gt;	&lt;/s:form&gt;&lt;/body&gt;&lt;/html&gt;

1.2.2. Upload2Actionpackage com.voidking.struts2.action;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import java.util.List;import com.opensymphony.xwork2.ActionSupport;public class Upload2Action extends ActionSupport&#123;	private List&lt;File&gt; upload2;	private List&lt;String&gt; upload2FileName;				public List&lt;File&gt; getUpload2() &#123;		return upload2;	&#125;	public void setUpload2(List&lt;File&gt; upload2) &#123;		this.upload2 = upload2;	&#125;	public List&lt;String&gt; getUpload2FileName() &#123;		return upload2FileName;	&#125;	public void setUpload2FileName(List&lt;String&gt; upload2FileName) &#123;		this.upload2FileName = upload2FileName;	&#125;	public String execute() throws Exception&#123;				if(upload2!=null || upload2FileName!=null)		&#123;			for(int i=0;i&lt;upload2.size();i++)			&#123;				InputStream is = new FileInputStream(upload2.get(i));				OutputStream os=new FileOutputStream(&quot;d:\\upload\\&quot;+getUpload2FileName().get(i));				byte buffer[] = new byte[1024];				int count=0;				while((count=is.read(buffer))&gt;0)				&#123;					os.write(buffer,0,count);				&#125;				os.close();				is.close();								return SUCCESS;			&#125;		&#125;				return ERROR;	&#125;	&#125;
这里需要注意的是属性命名问题，如果private List&lt;File&gt; upload2;，那么必须private List&lt;String&gt; upload2FileName;，否则无法获取到文件名。
1.2.3. struts.xml&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;	&lt;constant name=&quot;struts.multipart.saveDir&quot; value=&quot;/tmp&quot;/&gt;    &lt;package name=&quot;default&quot;  extends=&quot;struts-default&quot;&gt;        	&lt;interceptors&gt;    		&lt;interceptor name=&quot;myInterceptor&quot; class=&quot;com.voidking.struts2.tool.MyInterceptor&quot;&gt;&lt;/interceptor&gt;    	&lt;/interceptors&gt;    	&lt;default-interceptor-ref name=&quot;&quot;&gt;&lt;/default-interceptor-ref&gt;    	        &lt;default-action-ref name=&quot;index&quot; /&gt;        &lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;            &lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;            &lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;            &lt;result name=&quot;input&quot;&gt;/hello.jsp&lt;/result&gt;                        &lt;!-- 拦截配置在result后面 --&gt;            &lt;!-- 使用系统默认拦截器栈 --&gt;            &lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;            &lt;!-- 配置拦截器 --&gt;            &lt;interceptor-ref name=&quot;myInterceptor&quot;&gt;&lt;/interceptor-ref&gt;                    &lt;/action&gt;                &lt;action name=&quot;upload&quot; class=&quot;com.voidking.struts2.action.UploadAction&quot;&gt;        	&lt;result name=&quot;success&quot;&gt;/uploadsuccess.jsp&lt;/result&gt;        	&lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;        &lt;/action&gt;                &lt;action name=&quot;upload2&quot; class=&quot;com.voidking.struts2.action.Upload2Action&quot;&gt;        	&lt;result name=&quot;success&quot;&gt;/uploadsuccess.jsp&lt;/result&gt;        	&lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;        &lt;/action&gt;    &lt;/package&gt;&lt;/struts&gt;
其中，&lt;constant name=&quot;struts.multipart.saveDir&quot; value=&quot;/tmp&quot;/&gt;最好保留，不然会有提示信息:INFO: Unable to find &#39;struts.multipart.saveDir&#39; property setting.。另一个解决办法是在struts.properties加入：struts.multipart.saveDir = /tmp。
1.3. 参考文档《Java EE基础实用教程》，郑阿奇主编

Form action defaulting to ‘action’ attribute’s literal value：http://www.blogjava.net/parable-myth/archive/336387.html

解决Tomcat7“At least one JAR was scanned for TLDs yet contained no TLDs”问题：http://mov-webhobo.iteye.com/blog/1939655http://love-love-l.blog.163.com/blog/static/2107830420131159580327/

]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>struts2</tag>
      </tags>
  </entry>
  <entry>
    <title>Struts2国际化应用</title>
    <url>/dev-struts2-international/</url>
    <content><![CDATA[1. 前言有的时候，一个项目不仅要求只支持一种语言。如用中文开发的项目，只有懂中文的用户能用，而别的国家由于不使用中文将难以使用。若再重新开发一套功能相同但只是语言不同的项目，显然是不可取的。所以对于一个项目，国际化的应用是必要的。
2. 建立资源文件在Struts2概述中的实例的基础上，在src文件夹下新建一个文件struts.properties，内容如下：
struts.custom.resources=messageResource
或者直接在struts.xml中添加：
&lt;constant name=&quot;struts.custom.i18n.resources&quot; value=&quot;messageResource&quot;&gt;&lt;/constant&gt;

在src文件夹下新建两个文件，messageResource_en_US.properties和messageResource_zh_CN.properties，内容分别如下：
username=DLMpassword=KLlogin=login


username=登录名password=口令login=登录
PS：在输入中文时，eclipse会帮助我们自动转成ASCII字符，所以，输入完成后，我们看到的结果如下：
username=\u767B\u5F55\u540Dpassword=\u53E3\u4EE4login=\u767B\u5F55

3. 建立login.jsp文件为了让程序可以显示国际化信息，则需要在JSP页面输出key，而不是直接输出字符常量。Struts2访问国际化消息主要有以下三种方式：1、在JSP页面输出国际化消息，可以使用Struts2的&lt;s:text…/&gt;标签，该标签可以指定name属性，该属性指定国际化资源文件中的key。2、在Action中访问国际化消息，可以使用ActionSupport类的getText()方法，该方法可以接收一个参数，该参数指定了国际化资源文件中key。3、在表单元素的label属性里输出国际化信息，可以为该表单标签指定一个key属性，该属性指定了国际化资源文件中的key。
下面是login.jsp文件代码：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-tags&quot; prefix=&quot;s&quot; %&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;登录&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;s:i18n name=&quot;messageResource&quot;&gt;		&lt;s:form action=&quot;login.action&quot; method=&quot;post&quot;&gt;			&lt;s:textfield name=&quot;XH&quot; key=&quot;username&quot; size=&quot;20&quot;&gt;&lt;/s:textfield&gt;			&lt;s:textfield name=&quot;KL&quot; key=&quot;password&quot; size=&quot;21&quot;&gt;&lt;/s:textfield&gt;			&lt;s:submit value=&quot;%&#123;getText(&#x27;login&#x27;)&#125;&quot;&gt;&lt;/s:submit&gt;		&lt;/s:form&gt;	&lt;/s:i18n&gt;&lt;/body&gt;&lt;/html&gt;

4. 部署运行部署项目，启动Tomcat，使用IE访问：http://localhost:8080/struts2/login.jsp ，可以看到中文界面。工具，Internet选项，常规，语言，添加，英语（美国）[en-US]，上移。刷新页面，就可以看到英文界面了。
5. eclipse下显示classes文件夹eclipse，Window，Show View，Navigator。
6. 参考文档《Java EE基础实用教程》，郑阿奇主编
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>struts2</tag>
      </tags>
  </entry>
  <entry>
    <title>Struts2概述</title>
    <url>/dev-struts2-start/</url>
    <content><![CDATA[1. 名词解释Struts：它通过采用 Java Servlet/JSP 技术，实现了基于JavaEE Web应用的MVC设计模式的应用框架，是MVC经典设计模式中的一个经典产品。
Struts2：它是Struts的下一代产品，是在Struts和WebWork的技术基础上进行了合并的全新的Struts2框架。其全新的Struts2的体系结构与Struts的体系结构差别巨大。Struts2以WebWork为核心，采用拦截器的机制来处理用户的请求，这样的设计也使得业务逻辑控制器能够与ServletAPI完全脱离开，所以Struts2可以理解为WebWork的更新产品。
MVC模式的提出改变了程序设计的思路，但代码的规范性还是很差，而Struts框架则具有组件的模块化、灵活性和重用性的优点，同时也简化了基于MVC的Web应用程序的开发，从应用的角度来说，Struts有三大块：Struts核心类、Struts配置文件及Struts标签库。
Struts本身就实现了MVC模式，就Struts的发展来说，从以前的Struts1到现在的Struts2，其目的是为了给程序员一个好的框架来开发应用软件。

2. 官网http://struts.apache.org/index.html
3. 下载http://struts.apache.org/download
下载的下载 struts-*-all.zip 解压后，有四个文件夹：1、apps。包含基于Struts2的示例应用，是学习Struts2非常有用的资料。2、docs。包含Struts2的相关文档，如Struts2快速入门、Struts2文档、API文档等资料。3、lib。包含Struts2框架的核心类库，以及Struts2的第三方插件类库。其中有5个是必须的：struts2-core-*.jar、xwork-*.jar、 ognl-*.jar、commons-logging-*.jar、freemarker-*.jar。还有3个包也要注意导入，不导入运行Tomcat时候可能会出现异常。分别是：commons-io-*.jar、commons-fileupload-*.jar和javassist-*.ga.jar。整合Spring与Struts时，在Struts的lib目录中找到struts2-spring-plugin-*.jar，引入到工程中。4、src。包含Struts2框架的全部源代码。
4. MVC简介MVC包含三个基础部分：Model、View和Controller，这三个部分以最小的耦合协同工作，以增加程序的可扩展性和可维护性。在JSP demo设计中，首先JSP页面作为View，Servlet作为Controller，而JavaBean作为Model。具体来说，MVC具有以下优点：1、多个视图可以对应一个模型。按MVC设计模式，一个模型对应多个视图，可以减少代码的复制及代码的维护量，一旦模型发生改变，也易于维护。
2、模型返回的数据与现实逻辑分离。模型数据可以应用任何显示技术，例如，使用JSP页面、Velocity模板或者直接产生Excel文档等。
3、应用被分为三层，降低了各层之间的耦合，提供了应用的可扩展性。
4、控制层的概念也很有效，由于它把不同的模型和不同的视图组合在一起，完成不同的请求，因此控制层可以说是包含了用户请求权限的概念。
5、MVC更符合软件工程化管理的精神。不同的层各司其职，每一层的组件具有相同的特征，有利于通过工程化和工具化产生管理程序代码。
5. Struts2体系结构Struts2的基本流程如下：1、Web浏览器请求一个资源。2、过滤器Dispatcher查找请求，确定适当的Action。3、拦截器自动对请求应用通用功能，如验证和文件上传等操作。4、Action的execute方法通常用来存储和重新获得信息（通过数据库）。5、结果被返回到浏览器。可能是HTML、图片、PDF或其他。
其实，Struts2框架的应用着重在控制上。简单的流程是：页面-&gt;控制器-&gt;页面。最重要的控制器的取数据与处理后传数据的问题。
6. Struts2实例开发6.1. 建立一个Web项目新建Dynamic Web Project，命名为“struts2”。
6.2. 加载Struts2基本类库右击项目名，Properties，Java Build Path，Libraries，Add Library…，User Library，Next，User Libraries…，New…，输入“struts2”，OK，Add External JARs…，选中5个必须的jar文件，打开，OK。
右击项目名，Properties，Java Build Path，Libraries，Add Library…，User Library，Next，选中“struts2”，Finish。
6.3. 配置web.xml文件在WebContent/WEB-INF中新建文件web.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app id=&quot;WebApp_9&quot; version=&quot;2.4&quot; xmlns=&quot;http://java.sun.com/xml/ns/j2ee&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd&quot;&gt;	&lt;welcome-file-list&gt;		&lt;welcome-file&gt;/hello.jsp&lt;/welcome-file&gt;	&lt;/welcome-file-list&gt;    &lt;filter&gt;        &lt;filter-name&gt;struts2&lt;/filter-name&gt;        &lt;filter-class&gt;org.apache.struts2.dispatcher.FilterDispatcher&lt;/filter-class&gt;    &lt;/filter&gt;    &lt;filter-mapping&gt;        &lt;filter-name&gt;struts2&lt;/filter-name&gt;        &lt;url-pattern&gt;/*&lt;/url-pattern&gt;    &lt;/filter-mapping&gt;&lt;/web-app&gt;
6.4. 创建hello.jsp在WebContent中新建hello.jsp，内容如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;Struts2应用&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;form action=&quot;struts.action&quot; method=&quot;post&quot;&gt;		请输入姓名：&lt;input type=&quot;text&quot; name=&quot;name&quot; /&gt;&lt;br/&gt;		&lt;input type=&quot;submit&quot; value=&quot;提交&quot;/&gt;	&lt;/form&gt;&lt;/body&gt;&lt;/html&gt;
6.5. Action实现类新建包com.voidking.struts2.action，新建类StrutsAction，代码如下：
package com.voidking.struts2.action;import java.util.Map;import com.opensymphony.xwork2.ActionContext;import com.opensymphony.xwork2.ActionSupport;public class StrutsAction extends ActionSupport&#123;	private String name;		public String getName()	&#123;		return name;			&#125;		public void setName(String name)	&#123;		this.name=name;	&#125;		public String execute() throws Exception	&#123;		if(!name.equals(&quot;HelloWorld&quot;))		&#123;			Map request = (Map)ActionContext.getContext().get(&quot;request&quot;);			request.put(&quot;name&quot;, getName());			return &quot;success&quot;;		&#125;else &#123;			return &quot;error&quot;;		&#125;	&#125;&#125;
在写代码的时候，郝同学发现，当输入类ActionSupport时，不会出现代码提示。于是，郝同学直接把刚才的Struts的5个主要类库拷贝到了WebContent/lib下，果然出现了代码提示。估计是因为eclipse在编辑时不会扫描通过Java Build Path添加的类库，而会扫描lib中的类库。
6.6. 创建并配置struts.xml文件在src中新建文件struts.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;    &lt;package name=&quot;default&quot; namespace=&quot;/&quot; extends=&quot;struts-default&quot;&gt;        &lt;default-action-ref name=&quot;index&quot; /&gt;        &lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;            &lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;            &lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;        &lt;/action&gt;    &lt;/package&gt;&lt;/struts&gt;
6.7. 创建welcome.jsp在WebContent中新建文件welcome.jsp，代码如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@ taglib uri=&quot;/struts-tags&quot; prefix=&quot;s&quot; %&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;欢迎&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	hello&lt;s:property value=&quot;#request.name&quot;/&gt;!&lt;/body&gt;&lt;/html&gt;
6.8. 部署和运行发布，启动Tomcat，访问http://localhost:8080/struts2/ 或者 http://localhost:8080/struts2/hello.jsp 
PS：在调试过程中，如果修改了项目中的.java文件或者配置文件，就必须重新启动Tomcat服务器，而修改了JSP文件则只需要刷新页面即可。
7. Struts2实例分析7.1. 工作流程用户发送一个请求后，web.xml中配置的FilterDispatcher就会过滤该请求。如果请求是以.action结尾，该请求就会被转入Struts2框架处理。Struts2框架接收到*.action请求后，将根据*.action请求前面的“*”来决定调用哪个业务。
Struts2框架中的配置文件struts.xml会起映射作用，它会根据“*”来决定调用用户定义的哪个Action类。例如在上面项目中，请求为struts.action，所以，在struts.xml中有个Action类的name为“struts”，这表示该请求与这个Action来匹配，就会调用该Action中class属性指定的Action类。
但是，在Struts2中，用户定义的Action类并不是业务拦截器，而是Action代理，其并没有和Servlet耦合。所以Struts2框架提供了一系列拦截器，它负责将HttpServletRequest请求中的请求参数解析出来，传入到用户定义的Action类中。然后再调用其execute()方法处理用户请求，处理结束后，会返回一个值，这时，Struts2框架的struts.xml文件又起到映射作用，会根据其返回的值来决定跳转到哪一个页面。如在上面项目中，如果返回的是SUCCESS，就会跳到welcome.jsp页面；如果是ERROR，就会回到原页面。
7.2. 各文件详解7.2.1. web.xmlweb.xml中定义了一个过滤器，那么，过滤器是什么？
过滤器是用户请求和处理程序之间的一层处理程序。它可以对用户请求和处理程序响应的内容进行处理，通常用于权限控制、编码转换等场合。它先于与之相关的servlet或JSP页面运行在服务器上。过滤器可附加到一个或多个servlet或JSP页面上，并且可以检查进入这些资源的请求信息。在这之后，过滤器可以作如下的选择：1、以常规的方式调用资源（即，调用servlet或JSP页面）。2、利用修改过的请求信息调用资源。3、调用资源，但在发送响应到客户机前对其进行修改。4、阻止该资源调用，代之以转到其他的资源，返回一个特定的状态代码或生成替换输出。
在Servlet作为过滤器使用时，它可以对客户的请求进行处理。处理完成后，它会交给下一个过滤器处理，这样，客户的请求在过滤链里逐个处理，直到请求发送到目标为止。例如，某网站里有提交“修改的注册信息”的网页，当用户填写完修改信息并提交后，服务器在进行处理时需要做两项工作：判断客户端的会话是否有效；对提交的数据进行统一编码。这两项工作可以在由两个过滤器组成的过滤链里进行处理。当过滤器处理成功后，把提交的数据发送到最终目标；如果过滤器处理不成功，将把视图派发到指定的错误页面。
Servlet过滤器是在Java Servlet规范中定义的，它能够对过滤器关联的URL请求和响应进行相关检查和修改。Servlet过滤器能够在Servlet被调用之后检查response对象，修改response的Header对象和response内容。Servlet过滤器过滤的URL资源可以是Servlet、JSP、HTML文件，或者是整个路径下的任何资源。多个过滤器可以构成一个过滤器链，当请求过滤器关联的URL时，过滤器就会逐个发生作用。
所有过滤器必须实现java.Servlet.Filter接口，这个接口中含有3个过滤器类必须实现的方法：1、init(FilterConfig)：Servlet过滤器的初始化方法，Servlet容器创建Servlet过滤器实例后将调用这个方法。2、doFilter(ServletRequest,ServletResponse,FilterChain)：完成实际的过滤操作，当用户请求与过滤器关联的URL时，Servlet容器将先调用过滤器doFilter方法，返回响应之前也会调用此方法。FilterChain参数用于访问过滤器链上的下一个过滤器。3、destroy()：Servlet容器在销毁过滤器实例前调用该方法，这个方法可以释放Servlet过滤器占用的资源。
过滤器类编写完成后，必须要在web.xml中进行配置，格式如下：
&lt;filter&gt;	&lt;!--自定义的名称--&gt;	&lt;filter-name&gt;过滤器名&lt;/filter-name&gt;	&lt;!--自定义的过滤器类，注意，如果写在包下，要加包名--&gt;	&lt;filter-class&gt;过滤器对应类&lt;/filter-class&gt;	&lt;init-param&gt;		&lt;!--类中参数名称--&gt;		&lt;param-name&gt;参数名称&lt;/param-name&gt;		&lt;!--对应参数的值--&gt;		&lt;param-value&gt;参数值&lt;/param-value&gt;	&lt;/init-param&gt;&lt;/filter&gt;
过滤器必须和特定的URL关联才能发挥作用，过滤器的关联方式有3种：与一个URL关联、与一个URL目录下的所有资源关联、与一个Servelet关联。
&lt;filter-mapping&gt;	&lt;!--这里与上面配置的名称要相同--&gt;	&lt;filter-name&gt;过滤器名&lt;/filter-name&gt;	&lt;!--与一个URL资源关联--&gt;	&lt;url-pattern&gt;xxx.jsp&lt;/url-pattern&gt;&lt;/filter-mapping&gt;
&lt;filter-mapping&gt;	&lt;!--这里与上面配置的名称要相同--&gt;	&lt;filter-name&gt;过滤器名&lt;/filter-name&gt;	&lt;!--与一个URL目录下所有资源关联--&gt;	&lt;url-pattern&gt;/*&lt;/url-pattern&gt;&lt;/filter-mapping&gt;
&lt;filter-mapping&gt;	&lt;!--这里与上面配置的名称要相同--&gt;	&lt;filter-name&gt;过滤器名&lt;/filter-name&gt;	&lt;!--与一个Servlet关联--&gt;	&lt;url-pattern&gt;Servlet名称&lt;/url-pattern&gt;&lt;/filter-mapping&gt;

7.2.2. struts.xmlstruts.xml是Struts2框架的核心配置文件，主要用于配置开发人员编写的action。struts.xml文件通常放在Web应用程序的WEB-INF/classes目录下（src目录下也可以，src目录下编译生成的文件默认放在WEB-INF/classes目录下），该目录下的struts.xml将被Struts2框架自动加载。
struts.xml是一个XML文件，文件前面是XML的头文件，然后是标签，位于Struts2配置的最外层，其他标签都是包含在它里面的。
7.2.3. package元素Struts2的包类似于Java中的包，将action、result、result类型、拦截器和拦截器栈组织为一个逻辑单元，从而简化了维护工作，提高了重用性。
与Java中的包不同的是，Struts2中的包可以扩展另外的包，从而“继承”原有包的所有定义，并可以添加自己包的特有配置，以及修改原有包的部分配置。从这一点上看，Struts2中的包更像Java中的类。
package有以下几个常用属性：1、name：该属性是必选的，指定包的名字，这个名字将作为引用该包的键。注意，包的名字必须是唯一的，在一个struts.xml文件中不能出现两个同名的包。2、extends：该属性是可选的，允许一个包继承一个或多个先前定义的包。3、abstract：该属性是可选的，将其设置为true，可以把一个包定义为抽象的。抽象包不能有action定义，它只能作为“父”包，被其他包所继承。注意，以为Struts2的配置文件是从上到下处理的，所以父包应该在子包前面定义。4、namespace：该属性是可选的，将保存的action配置为不同的名称空间。如果接收到一个请求为/space/main.action，框架将首先查找/space名称空间，如果找到了，则执行main.action；如果没有找到，则到默认的名称空间（name=”default”）中继续查找。
如果接收到一个请求为/main.action，框架将首先查找“/”名称空间，如果找到了，则执行main.action；如果没有找到，则到默认的名称空间（name=”default”）中继续查找。
7.2.4. Action元素Struts2的核心功能是Action。对于开发人员来说，使用Struts2框架，主要的编码工作就是编写Action类。而开发好Action类后，就需要配置Action映射，以告诉Struts2框架，针对某个URL的请求应该交由哪个Action进行处理。
当一个请求匹配到某个Action名字时，框架就使用这个映射来确定如何处理请求。
&lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;	&lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;	&lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;&lt;/action&gt;
在上面代码中，如果一个请求映射到struts时，就会执行该Action配置的class属性对应的Action类。然后根据Action类的返回值决定跳转的方向。其实一个Action类中不一定只能有execute()方法。如果一个请求要调用Action类中的其他方法，就需要在Action配置中加以配置。例如，如果在com.voidking.struts2.action.StrutsAction中有另外一个方法为：
public String find() throws Exception&#123;return SUCCESS;&#125;
如果想要调用这个方法，就必须在Action中配置method属性，其配置方法为：
&lt;action name=&quot;find&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot; method=&quot;find&quot;&gt;	&lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;	&lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;&lt;/action&gt;


7.2.5. result元素一个result元素代表一个可能的输出。当Action类中的方法执行完成时，返回一个字符串型的结果代码，框架根据这个结果代码选择对应的result，向用户输出。
&lt;result name=&quot;逻辑视图名&quot; type=&quot;视图结果类型&quot; &gt;	&lt;param name=&quot;参数名&quot;&gt;参数值&lt;/param&gt;&lt;/result&gt;
param中的name属性有两个值：1、location：指定逻辑视图。2、parse：是否允许在实际视图名中使用OGNL表达式，默认参数为true。实际上通常不需要写这个param标签，而是直接在中指定物理视图位置。
result中的name属性有如下值：1、success：表示请求处理成功，该值也是默认值。2、error：表示请求处理失败。3、none：表示请求处理完成后不跳转到任何界面。4、input：表示输入时如果验证失败应该跳转到什么地方。5、login：表示登录失败后跳转的目标。
type（非默认类型）属性支持的结果类型有以下几种：1、chain：用来处理Action链。2、chart：用来整合JFreeChart的结果类型。3、dispatcher：用来转向页面，通常处理JSP，该类型也为默认类型。4、freemarker：处理FreMarker模板。5、httpheader：控制特殊HTTP行为的结果类型。6、jsf：JSF整合的结果类型。7、redirect：重定向到一个URL。8、redirect-action：重定向到一个action。9、stream：想浏览器发送InputStream对象，通常用来处理文件下载，还可用于返回Ajax数据。10、tiles：与Tiles整合的结果类型。11、velocity：处理Velocity模板。12、xslt：处理XML/XSML模板。13、plaintext：显示原始文本文件，如文件源代码。
7.2.6. ActionSupport类在Struts2中，Action与容器已经做到完全解耦，不再继承某个类或实现某个接口，也就是说，上面的实例中，StrutsAction完全可以不继承ActionSupport类。但是，在特殊情况下，为了降低编程的工作难度，充分利用Struts2提供的功能，定义Action时会继承ActionSupport类，该类位于xwork2提供的包com.opensymphony.xwork2中。下面是ActionSupport实现的接口：
public class ActionSupport implements Action,Validateable,ValidationAware,TextProvider,LocaleProvider,Serializable&#123;&#125;
Action接口同样位于com.opensymphony.xwork2包，定义了一些常量和一个execute()方法。
public interface Action&#123;	public static final String SUCCESS = &quot;success&quot;;	public static final String NONE = &quot;none&quot;;	public static final String ERROR = &quot;error&quot;;	public static final String INPUT = &quot;input&quot;;	public static final String LOGIN = &quot;login&quot;;	public String execute() throws Exception;&#125;
接口ValidationAware的实现类ValidationAwareSupport定义了三个集合成员，这些集合用户存储运行时的错误或者消息。ValidationAware的众多方法主要完成对这些成员的存储操作和判断集合中是否有元素的操作，ActionSupport仅仅实现对这些方法的简单调用。
8. Struts2数据验证及验证框架8.1. 数据验证上面的实例中，即使用户输入空的name，服务器也会处理用户请求。但是，如果是注册时，用户注册了空的用户名和密码，并且保存到数据库中，如果后面要根据用户输入的用户名和密码来查询数据，这些空的输入就可能会引起异常。（PS：可以使用JavaScript在客户端验证）Action类继承了ActionSupport类，该类实现的接口中有Validateable，定义了validate()方法。所以只要在用户自定义的Action类中重写该方法就可以实现验证功能，修改ActionSupport如下：
package com.voidking.struts2.action;import java.util.Map;import com.opensymphony.xwork2.ActionContext;import com.opensymphony.xwork2.ActionSupport;public class StrutsAction extends ActionSupport&#123;	private String name;		public String getName()	&#123;		return name;			&#125;		public void setName(String name)	&#123;		this.name=name;	&#125;		public String execute() throws Exception	&#123;		if(!name.equals(&quot;HelloWorld&quot;))		&#123;			Map request = (Map)ActionContext.getContext().get(&quot;request&quot;);			request.put(&quot;name&quot;, getName());			return SUCCESS;		&#125;else &#123;			return ERROR;		&#125;	&#125;		public void validate()	&#123;		//如果姓名为空，则把错误信息添加到Action类的fieldErrors		if(this.getName()==null || this.getName().trim().equals(&quot;&quot;))		&#123;			addFieldError(&quot;name&quot;, &quot;姓名是必须的！&quot;);		&#125;			&#125;&#125;
validate()方法会在execute()方法之前执行，执行该方法之后，Action类的fieldErrors中已经包含了数据校验错误信息，将把请求转发到input逻辑视图出，所以要在配置中加入以下代码：
&lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;	&lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;	&lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;	&lt;result name=&quot;input&quot;&gt;/hello.jsp&lt;/result&gt;&lt;/action&gt;

经过测试，我们发现，当输入为空时，界面上并不会有任何提示！废话，根本就没有在界面上设置显示错误的区域好不来！修改hello.jsp如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@taglib prefix=&quot;s&quot; uri=&quot;/struts-tags&quot; %&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;Struts2应用&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;form action=&quot;struts.action&quot; method=&quot;post&quot;&gt;		请输入姓名：&lt;input type=&quot;text&quot; name=&quot;name&quot; /&gt;&lt;br/&gt;		&lt;input type=&quot;submit&quot; value=&quot;提交&quot;/&gt;		&lt;s:fielderror fieldName=&quot;name&quot;/&gt;	&lt;/form&gt;&lt;/body&gt;&lt;/html&gt;


8.2. 验证框架上面的校验通过重写validate方法实现，这种方法虽然可以达到预期效果，但是如果不是一个输入框，而是2、3个甚至更多，那就要在validate方法中做出很多判断，而且这些判断的语句基本相同。所以Struts2提供了校验框架，只需要增加一个校验配置文件，就可以完成对数据的校验。Struts2提供了大量的数据校验框架，包括表单域校验器和非表单域校验器两种。
8.2.1. 必填字符串校验器在包com.voidking.struts2.action中，新建文件StrutsAction-validation.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!DOCTYPE validators PUBLIC        &quot;-//OpenSymphony Group//XWork Validator Config 1.0//EN&quot;        &quot;http://www.opensymphony.com/xwork/xwork-validator-config-1.0.dtd&quot;&gt;&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;name&quot;&gt;		&lt;field-validator type=&quot;requiredstring&quot;&gt;			&lt;!--去空格--&gt;			&lt;param name=&quot;trim&quot;&gt;true&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;姓名是必须的&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;
如果想对StrutsAction中find()方法进行验证，命名应该为StrutsAction-find-validation.xml。
经过测试，完全没有效果，而且会报出警告！java.io.FileNotFoundException: http://www.opensymphony.com/xwork/xwork-validator-1.0.dtd
经过查找资料，原来是这个url已经过时了，opensymphony这个组织貌似已经停止运营了，但其主要的开源项目，也都基本找到了新东家，比如struts交由Apache来运营了，改成下面这个写法就没问题：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!DOCTYPE validators PUBLIC        &quot;-//OpenSymphony Group//XWork Validator Config 1.0//EN&quot;        &quot;http://struts.apache.org/dtds/xwork-validator-1.0.dtd&quot;&gt;&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;name&quot;&gt;		&lt;field-validator type=&quot;requiredstring&quot;&gt;			&lt;!--去空格--&gt;			&lt;param name=&quot;trim&quot;&gt;true&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;姓名是必须的&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;
关闭网络连接，再次测试，果然又不可以了！晕，还非得联网？难道不可以使用本地的文件吗！1、解压xwork-core-*.jar包，找到xwork-validator-1.0.dtd。
2、eclipse，Window，Preferences，XML，XML Catelog，Add，File System…，选中刚才解压的xwork-validator-1.0.dtd，打开，
3、Location已经选好D:\jar\struts2\xwork-validator-1.0.dtd，Key type选择Public ID，Key填-//OpenSymphony Group//XWork Validator Config 1.0//EN，Alternative web address填http://struts.apache.org/dtds/xwork-validator-1.0.dtd。（本地dtd不存在时回去web上去找dtd）
4、StrutsAction-validation.xml需要修改如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!DOCTYPE validators PUBLIC        &quot;-//OpenSymphony Group//XWork Validator Config 1.0//EN&quot;        &quot;D:\jar\struts2\xwork-validator-1.0.dtd&quot;&gt;&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;name&quot;&gt;		&lt;field-validator type=&quot;requiredstring&quot;&gt;			&lt;!--去空格--&gt;			&lt;param name=&quot;trim&quot;&gt;true&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;姓名是必须的&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

8.2.2. 必填校验器&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;name&quot;&gt;		&lt;field-validator type=&quot;required&quot;&gt;			&lt;!--去空格--&gt;			&lt;param name=&quot;trim&quot;&gt;true&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;姓名是必须的&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

8.2.3. 整数校验器&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;age&quot;&gt;		&lt;field-validator type=&quot;int&quot;&gt;			&lt;param name=&quot;min&quot;&gt;18&lt;/param&gt;			&lt;param name=&quot;max&quot;&gt;100&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;年龄必须在18至100之间&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

8.2.4. 日期校验器&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;date&quot;&gt;		&lt;field-validator type=&quot;date&quot;&gt;			&lt;param name=&quot;min&quot;&gt;1980-01-01&lt;/param&gt;			&lt;param name=&quot;max&quot;&gt;2016-01-01&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;日期必须在1980-01-01到2016-01-01之间&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

8.2.5. 邮件地址校验器&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;email&quot;&gt;		&lt;field-validator type=&quot;email&quot;&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;必须输入有效的电子邮件地址&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

8.2.6. 网址校验器&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;url&quot;&gt;		&lt;field-validator type=&quot;url&quot;&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;必须输入有效的网址&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

8.2.7. 字符串长度校验器&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;password&quot;&gt;		&lt;field-validator type=&quot;stringlength&quot;&gt;			&lt;param name=&quot;minLength&quot;&gt;8&lt;/param&gt;			&lt;param name=&quot;maxLength&quot;&gt;16&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;密码长度必须在8到16位之间&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

8.2.8. 正则表达式校验器&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;xh&quot;&gt;		&lt;field-validator type=&quot;regex&quot;&gt;			&lt;param name=&quot;expression&quot;&gt;&lt;![CDATA[(\d&#123;6&#125;)]]&gt;&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;学号必须是6位数字&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

9. Struts2标签库Struts2标签库，大大简化了JSP页面输出逻辑的实现。借助Struts2标签库，完全可以避免在JSP页面中使用Java脚本代码。虽然Struts2把所有的标签都定义在URI为/struts-tags的命名空间下，但依然可以对Struts2标签进行简单的分类。1、UI标签。用于生成HTML元素，分为表单标签和非表单标签。2、非UI标签。用于数据访问和逻辑控制。3、Ajax标签。用于Ajax支持。
9.1. OGNL表达式OGNL是Object Graphic Navigation Language（对象图导航语言）的缩写，是一个开源项目。OGNL是一种功能强大的EL（Expression Language，表达式语言），可以通过简单的表达式来访问Java对象中的属性。
标准的OGNL会设定一个根对象（root对象）。假设使用标准OGNL表达式来求值（不是Struts2 OGNL），如果OGNL上下文有两个对象foo对象和bar对象，同时foo对象被设置为根对象，则利用下面的OGNL表达式求值。
#foo.blah	//返回foo.getBlah()#bar.blah	//返回bar.getBlah()blah		//返回foo.getBlah()，因为foo是根对象

在Struts2框架中，值栈（Value Stack）就是OGNL的根对象。假设值栈中存在两个对象实例Man和Animal，这两个对象实例都有一个name属性，Animal有一个species属性，Man有一个salary属性。假设Animal在值栈的顶部，Man在Animal后面，下面的代码片段能够更好地理解OGNL表达式。
species		//调用animal.getSpecies()salary		//调用man.getSalary()name		//调用animal.getName()
如果要获得Man的name值，需要如下代码：
man.name
Struts2允许在值栈中使用索引，实例代码如下：
[0].name	//调用animal.getName()[1].name	//调用man.getName()
Struts2中的OGNL Context是ActionContext，Context Map包括：application、session、值栈（root）、request、parameters、attr。
由于值栈是Struts2中ONGL的根对象。如果用户需要访问值栈中的对象，则可以通过如下代码访问值栈中的属性：
$&#123;foo&#125;	//获得值栈中foo的属性
如果访问其他Context中的对象，由于不是根对象，在访问时需要加#前缀。1、application对象：用来访问ServletContext，如#application.userName或者#application[“userName”]，相当于调用Servlet的getAttribute(“userName”)。2、session对象：用来访问HttpSession，如#session.userName或者#session[“userName”]，相当于调用session.getAttribute(“userName”)。3、request对象：用来访问HttpServletRequest属性的Map，如#request.userName或者#request[“userName”]，相当于调用request.getAttribute(“userName”)。StrutsAction中有如下代码：
Map request = (Map)ActionContext.getContext().get(&quot;request&quot;);request.put(&quot;name&quot;,getName);
这就是先得到的request对象，然后把值放进去，该例的welcome.jsp中有：
&lt;s:property value=&quot;#request.name&quot;/&gt;
其中#request.name相当于调用了request.getAttribute(“name”)。
如果需要一个集合元素时，可以使用OGNL中集合相关的表达式。使用如下代码生成一个List对象：
&#123;e1,e2,e3,...&#125;
使用如下代码生成一个Map对象：
#&#123;key:value1,key2:value2,...&#125;
对于集合类型，OGNL表达式可以使用in和not in两个元素符号。其中，in表达式用来判断某个元素是否在指定的集合对象中；not in判断某个元素是否不在指定的集合对象中，代码如下所示：
&lt;s:if test=&quot;&#x27;foo&#x27; in &#123;&#x27;foo&#x27;,&#x27;bar&#x27;&#125;&quot;&gt;	...&lt;s:/if&gt;
或
&lt;s: if test=&quot;&#x27;foo&#x27; not in &#123;&#x27;foo&#x27;,&#x27;bar&#x27;&#125;&quot;&gt;	...&lt;s:/if&gt;
除了in和not in之外，OGNL还允许使用某个规则获得集合对象的自己，常用的有以下3个相关操作符。

?:获得所有符合逻辑的元素。
^:获得符合逻辑的第一个元素。
$:获得符合逻辑的最后一个元素。Person.relatives.&#123;?#this.gender==&#x27;male&#x27;&#125;
该代码可以获得Person的所有性别为male的relatives集合。

9.2. 数据标签数据标签属于非UI标签，主要用于提供各种数据访问相关的功能。

property：用于输出某个值。
set：用于设置一个新变量。
param：用于设置参数，通常用于bean标签和action标签的子标签。
bean：用于创建一个JavaBean实例。如果指定id属性，则可以将创建的JavaBean实例放入Stack Context中。
action：用于JSP页面直接调用一个Action。
date：用于格式化输出一个日期。
debug：用于在页面上生成一个调试链接，当单击该链接时，可以看到当前值栈和Stack Context中的内容。
il8n：用于指定国际化资源文件的baseName。
include：用于在JSP页面中包含其他的JSP或Servlet资源。
push：用于将某个值放入值栈的栈顶。
text：用于输出国际化。
url：用于生成一个URL地址。

9.3. 控制标签控制标签也属于非UI标签，主要用于完成流程的控制，以及对值栈的控制。

if、elseif、else：用于控制选择输出的标签。
append：用于将多个集合拼接成一个新的集合。
generator：用于将一个字符串按指定的分隔符分隔成多个字符串，临时生成的多个字符串可以使用iterator标签来迭代输出。
iterator：用于将ihelloworld迭代输出。
merge：用于将多个集合拼接成一个新的集合，但与append的拼接方式不同。
sort：用于对集合进行排序。
subset：用于截取集合的部分元素，形成新的子集合。

9.4. 表单标签大部分表单标签和HTML表单元素是一一对应的关系，表单元素中的name属性值会映射到程序员定义的Action类中。
在上面的实例中，属性name的值“name”在StrutsAction的成员变量中就有setName()和getName()的定义，这样Struts2框架就可以把它们关联起来。实际上，表单元素的名字封装着一个请求参数，而请求参数被封装到Action类中，根据其set方法赋值，再根据其get方法取值。
如果有这样一个JavaBean类，类名为User，该类有两个属性：一个是username，一个是password。并分别生成它们的getter和setter方法，在JSP页面的表单中可以这样为表单元素命名：
&lt;s:textfield name=&quot;user.username&quot; label=&quot;用户名&quot;/&gt;&lt;s:textfield name=&quot;user.password&quot; label=&quot;密码&quot;&gt;
这时可以在Action类中直接定义user对象user属性，并生成其getter和setter方法，这样就可以用user.getUsername()和user.getPassword()方法访问表单提交的username和password的值。
下面介绍和HTML表单元素不是一一对应的几个重要的表单标签：

checkboxlist：可以一次创建多个复选框，相当于HTML标签的多个&lt;input type=”checkbox” …/&gt;，它根据list属性指定的集合来申请多个复选框。因此，该标签需要指定一个list属性。

combobox：生成一个单行文本框和下拉列表框的组合。两个表单元素只能对应一个请求参数，只有单行文本框里的值才会才包含请求参数，下拉列表框只是用于辅助输入，并没有name属性，故不会产生请求参数。

datetimepicker：用于生成一个日期、时间下拉列表框。当使用该日期、时间列表框选择某个日期、时间时，系统会自动将选中日期、时间输出指定文本框中。在使用该标签时，要在HTML的head部分加入&lt;s:head/&gt;，因为datetimepicker标签中有一个日历小控件，其中包含JavaScript代码。

select：用于生成一个下拉列表框，通过为该元素指定list属性的值，来生成下拉列表框的选项。

radio：用法与checkboxlist用法很相似，唯一的区别就是checkboxlist生成的是复选框，而radio生成的是单选框。

head：用于生成HTML页面的head部分。如果需要在页面中使用Ajax组件，就需要在head标签中加入theme=”ajax”属性。这样就可以将标准Ajax的头信息包含到页面中。


9.5. 非表单标签非表单标签主要用于在界面中生成一些非表单的可视化元素。这些标签不经常用到：

a：生成超链接。
actionerror：输出Action实例的getActionMessage()方法返回的消息。
component：生成一个自定义组件。
div：生成一个div片段。
fielderror：输出表单域的类型转化错误、校验错误提示。
tablePanel：生成HTML页面的Tab页。
tree：生成一个树形结构。
treenode：生成树形结构的节点。

10. Struts2拦截器Struts2框架的绝大部分功能是通过拦截器来完成的。当FilterDispatcher拦截到用户请求后，大量拦截器将会对用户请求进行处理，然后才调用用户自定义的Action类中的方法来处理请求。可见，拦截器是Struts2的核心所在。当需要扩展Struts2功能时，只需要提供相应的拦截器，并将它配置在Struts2容器即可。反之，如果不需要某个功能，也只需要取消该拦截器即可。
Struts2内建的大量拦截器都是以name-class对的形式配置在struts-default.xml文件中，其中name是拦截器的名称，class指定该拦截器的实现类。从前面的实例中可以看出，配置struts.xm时，继承了strut-default包，这样就可以应用里面定义的拦截器。否则，就必须自己定义这些拦截器。
10.1. 拦截器配置拦截器在struts.xml中配置，格式为：
&lt;interceptor name=&quot;拦截器名&quot; class=&quot;拦截器实现类&quot;&gt;&lt;/interceptor&gt;
有些时候，在拦截器实现类中会定义一些参数，那么在配置拦截器时就需要为其传入拦截器参数。
&lt;interceptor name=&quot;myInterceptor&quot; class=&quot;com.voidking.struts2.tool.MyInterceptor&quot;&gt;	&lt;param name=&quot;参数名&quot;&gt;参数值&lt;/param&gt;&lt;/interceptor&gt;
通常情况下，往往多个拦截器一起使用来进行过滤，这时就会把需要的拦截器组成一个拦截器栈。
&lt;interceptor-stack name=&quot;拦截器栈名&quot;&gt;	&lt;interceptor-ref name=&quot;拦截器一&quot;&gt;&lt;/interceptor-ref&gt;	&lt;interceptor-ref name=&quot;拦截器二&quot;&gt;&lt;/interceptor-ref&gt;	&lt;interceptor-ref name=&quot;拦截器三&quot;&gt;&lt;/interceptor-ref&gt;&lt;/interceptor-stack&gt;
在配置拦截器栈时，用到的拦截器必须是已经存在的拦截器，即已经配置好的拦截器。拦截器栈也可以引用拦截器栈，实质上就是把引用的拦截器栈中的拦截器包含到了该拦截器栈中。
当在struts.xml中配置一个包时，可以为其指定默认的拦截器，如果为包指定了某个拦截器，则该拦截器会对每个Action起作用，但是如果显式地为某个Action配置了拦截器，则默认的拦截器将不会起作用。默认拦截器用元素来定义。
&lt;package name=&quot;包名&quot;&gt;	&lt;interceptors&gt;		&lt;interceptor name=&quot;拦截器一&quot; class=&quot;拦截器实现类&quot;&gt;&lt;/interceptor&gt;		&lt;interceptor name=&quot;拦截器二&quot; class=&quot;拦截器实现类&quot;&gt;&lt;/interceptor&gt;		&lt;interceptor-stack name=&quot;拦截器栈名&quot;&gt;			&lt;interceptor-ref name=&quot;拦截器一&quot;&gt;&lt;/interceptor-ref&gt;			&lt;interceptor-ref name=&quot;拦截器二&quot;&gt;&lt;/interceptor-ref&gt;		&lt;/interceptor-stack&gt;	&lt;/interceptors&gt;	&lt;default-interceptor-ref name=&quot;拦截器名或拦截器栈名&quot;&gt;&lt;/default-interceptor-ref&gt;&lt;/package&gt;

10.2. 拦截器实现类Struts2框架提供了很多拦截器，但总有一些功能需要程序员自定义拦截器来完成，比如权限控制等。Struts2提供了一些接口或类供程序员自定义拦截器。如Struts2提供了com.opensymphony.xwork2.interceptor.Interceptor接口，程序员只要实现该接口就可以完成拦截器实现类。该接口代码如下：
import java.io.Serializable;import com.opensymphony.xwork2.ActionInvocation;public interface Interceptor extends Serializable&#123;	void init();	String intercept(ActionInvocation invocation) throws Exception;	void destroy();&#125;

init()：该方法在拦截器被实例化之后、拦截器被执行之前调用。该方法只被执行一次，主要用于初始化资源。
intercept(ActionInvocation invocation)：该方法用于实现拦截的动作。该方法有个参数，该参数调用其invoke方法，将控制权交给下一拦截器，或者交给Action类的方法。
destroy()：该方法与init()方法对应，拦截器实例被销毁之前调用，用于销毁在init方法中打开的资源。

除了Intercept接口外，Struts2还提供了AbstractInterceptor类，该类提供了init方法和destroy方法的空实现。在一般的拦截器中，都会继承该类，因为一般实现的拦截器是不需要打开资源的，故无需实现这两个方法，继承该类会更简洁。
10.3. 实例应用在上面的实例中，添加功能：在输入框输入“hello”，不能通过，返回当前页面。新建包com.voidking.struts2.tool，新建类MyInterceptor，代码如下：
package com.voidking.struts2.tool;import com.opensymphony.xwork2.Action;import com.opensymphony.xwork2.ActionInvocation;import com.opensymphony.xwork2.interceptor.AbstractInterceptor;import com.voidking.struts2.action.StrutsAction;public class MyInterceptor extends AbstractInterceptor&#123;	@Override	public String intercept(ActionInvocation arg0) throws Exception &#123;		// 得到StrutsAction类对象		StrutsAction action=(StrutsAction)arg0.getAction();		// 如果Action类的name属性值为“hello”，则返回错误页面		if(action.getName().equals(&quot;hello&quot;))		&#123;			return Action.ERROR;		&#125;		return arg0.invoke();	&#125;&#125;
struts.xml修改如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE struts PUBLIC    &quot;-//Apache Software Foundation//DTD Struts Configuration 2.0//EN&quot;    &quot;http://struts.apache.org/dtds/struts-2.0.dtd&quot;&gt;&lt;struts&gt;    &lt;package name=&quot;default&quot; namespace=&quot;/&quot; extends=&quot;struts-default&quot;&gt;        	&lt;interceptors&gt;    		&lt;interceptor name=&quot;myInterceptor&quot; class=&quot;com.voidking.struts2.tool.MyInterceptor&quot;&gt;&lt;/interceptor&gt;    	&lt;/interceptors&gt;    	&lt;default-interceptor-ref name=&quot;&quot;&gt;&lt;/default-interceptor-ref&gt;    	        &lt;default-action-ref name=&quot;index&quot; /&gt;        &lt;action name=&quot;struts&quot; class=&quot;com.voidking.struts2.action.StrutsAction&quot;&gt;            &lt;result name=&quot;success&quot;&gt;/welcome.jsp&lt;/result&gt;            &lt;result name=&quot;error&quot;&gt;/hello.jsp&lt;/result&gt;            &lt;result name=&quot;input&quot;&gt;/hello.jsp&lt;/result&gt;                        &lt;!-- 拦截配置在result后面 --&gt;            &lt;!-- 使用系统默认拦截器栈 --&gt;            &lt;interceptor-ref name=&quot;defaultStack&quot;&gt;&lt;/interceptor-ref&gt;            &lt;!-- 配置拦截器 --&gt;            &lt;interceptor-ref name=&quot;myInterceptor&quot;&gt;&lt;/interceptor-ref&gt;                    &lt;/action&gt;    &lt;/package&gt;&lt;/struts&gt;
Struts框架提供了
11. XML代码提示eclipse，Window，Preferences，XML，XML Files，Editor，Content Assist。Auto activation delay(ms):，设置为200。Prompt when these characters are inserted:，添加&quot;-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ。
12. 小结校验错误有提示fielderror，那么校验成功的提示该怎么搞？
13. 参考文档《Java EE基础实用教程》，郑阿奇主编Servlet过滤器介绍之原理分析：http://zhangjunhd.blog.51cto.com/113473/20629/struts2和servlet区别：http://blog.csdn.net/qiluluwawa/article/details/8619568
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>struts2</tag>
      </tags>
  </entry>
  <entry>
    <title>JSP demo设计</title>
    <url>/dev-jsp-demo/</url>
    <content><![CDATA[1. 需求分析既然是留言系统，肯定要有用户登录，所有需要一个用户表（user）。字段包括：id、username和password。其中id设为自动增长的int型，并设为主键。username和password都设为varchar型。登录成功后要有个主界面，显示别人和自己的留言信息，那就应该有个留言表（message）。字段包括：id、userid、date、title、content。其中id设为自动增长的int型，并设为主键。userid是user表中的id，表明该条留言是该用户留的。date表示发布留言的时间，datetime型。title表示发布留言的标题，varchar型。content表示发布的内容，varchar型。
2. 创建数据库和表使用MySQL，scott用户。创建数据库“jsp”，创建表user、message。

2.1. user表结构


字段名称
数据类型
主键
自增
允许为空
描述



id
int
是
增1

ID号


username
varchar(20)



用户名


password
varchar(20)



密码


2.2. message表结构


字段名称
数据类型
主键
自增
允许为空
描述



id
int
是
增1

ID号


userid
int



用户ID号


date
datetime



发布时间


title
varchar(20)



标题


content
varchar(500)



留言内容


2.3. MySQL命令记录root用户登录
mysql -u root -pgrant all privileges on *.* to scott@&#x27;localhost&#x27;;flush privileges;exit
scott用户登录
create database jsp;show databases;use jsp;create table user(id int,username varchar(20),password varchar(20));create table message(id int,userid int ,date datetime,title varchar(20),content varchar(500));alter table user modify int id primary key auto_increment;alter table message modify id int primary key auto_increment;alter table message add constraint fk_id foreign key(userid) references user(id);desc user;desc message;

3. 创建项目使用eclipse，新建Dynamic Web Project，命名为“jsp”。
4. 创建表对应的JavaBean新建包com.voidking.jsp.model，然后建立表对应的标准JavaBean：User和Message。
package com.voidking.jsp.model;public class User &#123;	//Fields	private Integer id;	private String username;	private String password;		//Property accessors	//属性 id 的 get/set 方法	public Integer getId()&#123;		return this.id;	&#125;	public void setId(Integer id)&#123;		this.id=id;	&#125;	//属性 username 的 get/set 方法	public String getUsername()&#123;		return this.username;	&#125;	public void setUsername(String username)&#123;		this.username=username;	&#125;	//属性 password 的 get/set 方法	public String getPassword()&#123;		return this.password;	&#125;	public void setPassword(String password)&#123;		this.password=password;	&#125;&#125;

package com.voidking.jsp.model;import java.sql.Date;public class Message &#123;	//Fields	private Integer id;	private Integer userid;	private Date date;	private String title;	private String content;		//Property accessors	//属性 id 的 get/set 方法	public Integer getId()&#123;		return this.id;			&#125;	public void setId(Integer id)&#123;		this.id=id;	&#125;	//属性 userId 的 get/set 方法	public Integer getUserid()&#123;		return this.userid;	&#125;	public void setUserid(Integer userid)&#123;		this.userid=userid;	&#125;	//属性 date 的 get/set 方法	public Date getDate()&#123;		return this.date;	&#125;	public void setDate(Date date)&#123;		this.date=date;	&#125;	//属性 title 的 get/set 方法	public String getTitle()&#123;		return this.title;	&#125;	public void setTitle(String title)&#123;		this.title=title;	&#125;	//属性 content 的 get/set 方法	public String getContent()&#123;		return this.content;	&#125;	public void setContent(String content)&#123;		this.content=content;	&#125;&#125;
5. 创建登录页面在WebContent中新建login.jsp，代码如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;简易留言板&lt;/title&gt;&lt;/head&gt;&lt;body bgcolor=&quot;#E3E3E3&quot;&gt;&lt;form action=&quot;mainServlet&quot; method=&quot;post&quot;&gt;&lt;table&gt;	&lt;caption&gt;用户登录&lt;/caption&gt;	&lt;tr&gt;		&lt;td&gt;			用户名：&lt;input type=&quot;text&quot; name=&quot;username&quot; size=&quot;20&quot;/&gt;		&lt;/td&gt;	&lt;/tr&gt;	&lt;tr&gt;		&lt;td&gt;			密&amp;nbsp;&amp;nbsp;码：&lt;input type=&quot;password&quot; name=&quot;password&quot; size=&quot;21&quot;/&gt;		&lt;/td&gt;	&lt;/tr&gt;	&lt;tr&gt;		&lt;td&gt;			&lt;input type=&quot;submit&quot; value=&quot;登录&quot;/&gt;			&lt;input type=&quot;reset&quot; value=&quot;重置&quot;/&gt;		&lt;/td&gt;	&lt;/tr&gt;&lt;/table&gt;&lt;/form&gt;如果没注册单击&lt;a href=&quot;register.jsp&quot;&gt;这里&lt;/a&gt;注册！&lt;/body&gt;&lt;/html&gt;
6. 创建DB类新建包com.voidking.jsp.db，新建类DB，代码如下：
package com.voidking.jsp.db;import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.util.ArrayList;import com.voidking.jsp.model.Message;import com.voidking.jsp.model.User;public class DB &#123;	Connection ct;	PreparedStatement pstmt;		public DB()	&#123;		try &#123;			Class.forName(&quot;com.mysql.jdbc.Driver&quot;);			ct=DriverManager.getConnection(&quot;jdbc:mysql://localhost/jsp&quot;,&quot;scott&quot;,&quot;tiger&quot;);		&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;			&#125;		public User checkUser(String username,String password)	&#123;		try &#123;			pstmt = ct.prepareStatement(&quot;select * from user where username=? and password=?&quot;);			pstmt.setString(1, username);			pstmt.setString(2, password);						ResultSet rs=pstmt.executeQuery();			User user = new User();			while (rs.next()) &#123;				user.setId(rs.getInt(1));				user.setUsername(rs.getString(2));				user.setPassword(rs.getString(3));				return user;							&#125;		&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;		return null;		&#125;		public ArrayList findMessage()	&#123;				try &#123;			ArrayList al = new ArrayList();			pstmt= ct.prepareStatement(&quot;select * from message&quot;);			ResultSet rs = pstmt.executeQuery();			while(rs.next())			&#123;				Message message = new Message();				message.setId(rs.getInt(1));				message.setUserid(rs.getInt(2));				message.setDate(rs.getDate(3));				message.setTitle(rs.getString(4));				message.setContent(rs.getString(5));				al.add(message);							&#125;			return al;		&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;		return null;				&#125;		public String getUsername(int id)	&#123;		String username = null;		try &#123;			pstmt = ct.prepareStatement(&quot;select username from user where id=?&quot;);			pstmt.setInt(1,id);			ResultSet rs = pstmt.executeQuery();			while(rs.next())			&#123;				username = rs.getString(1);										&#125;			return username;		&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;		return username;			&#125;		public boolean addMessage(Message message)	&#123;		try &#123;			pstmt= ct.prepareStatement(&quot;insert into message(userid,date,title,content) values(?,?,?,?)&quot;);			pstmt.setInt(1, message.getUserid());			pstmt.setDate(2,message.getDate());			pstmt.setString(3, message.getTitle());			pstmt.setString(4, message.getContent());						pstmt.executeUpdate();			return true;		&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;				return false;				&#125;			public boolean insertUser(String username,String password)	&#123;		try &#123;			pstmt = ct.prepareStatement(&quot;insert into user(username,password) values(?,?)&quot;);			pstmt.setString(1, username);			pstmt.setString(2, password);			pstmt.executeUpdate();			return true;		&#125; catch (Exception e) &#123;			e.printStackTrace();		&#125;		return false;					&#125;&#125;
7. 创建MainServlet类新建包com.voidking.jsp.servlet，新建类MainServlet，代码如下：
package com.voidking.jsp.servlet;import java.io.IOException;import java.util.ArrayList;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import com.voidking.jsp.db.DB;import com.voidking.jsp.model.Message;import com.voidking.jsp.model.User;public class MainServlet extends HttpServlet &#123;	public void doGet(HttpServletRequest request, HttpServletResponse response)			throws ServletException, IOException &#123;		request.setCharacterEncoding(&quot;utf8&quot;);		response.setContentType(&quot;utf8&quot;);		String username = request.getParameter(&quot;username&quot;);		String password = request.getParameter(&quot;password&quot;);		DB db = new DB();		HttpSession session = request.getSession();		User user = (User) session.getAttribute(&quot;user&quot;);		if (user == null) &#123;			user = db.checkUser(username, password);		&#125;		session.setAttribute(&quot;user&quot;, user);		if (user != null) &#123;			ArrayList al = db.findMessage();			session.setAttribute(&quot;al&quot;, al);			response.sendRedirect(&quot;main.jsp&quot;);		&#125; else &#123;			response.sendRedirect(&quot;login.jsp&quot;);		&#125;	&#125;	public void doPost(HttpServletRequest request, HttpServletResponse response)			throws ServletException, IOException &#123;		doGet(request, response);	&#125;&#125;

8. 创建mian.jsp在WebContent中新建main.jsp，代码如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;%@ page import=&quot;com.voidking.jsp.model.Message&quot; %&gt;&lt;%@ page import=&quot;com.voidking.jsp.model.User&quot; %&gt;&lt;%@ page import=&quot;com.voidking.jsp.db.DB&quot;%&gt;&lt;%@page import=&quot;java.util.Iterator&quot;%&gt;&lt;%@page import=&quot;java.util.ArrayList&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;留言板信息&lt;/title&gt;&lt;/head&gt;&lt;body bgcolor=&quot;#E3E3E3&quot;&gt;&lt;% User user = (User)session.getAttribute(&quot;user&quot;);	%&gt;	当前用户为：&lt;%=user.getUsername() %&gt;	&lt;form action=&quot;liuyan.jsp&quot; method=&quot;post&quot;&gt;		&lt;table border=&quot;1&quot;&gt;			&lt;caption&gt;所有留言信息&lt;/caption&gt;			&lt;tr&gt;				&lt;th&gt;留言人姓名&lt;/th&gt;&lt;th&gt;留言时间&lt;/th&gt;&lt;th&gt;留言标题&lt;/th&gt;&lt;th&gt;留言内容&lt;/th&gt;			&lt;/tr&gt;		&lt;%			ArrayList al = (ArrayList)session.getAttribute(&quot;al&quot;);				if(al != null)			&#123;				Iterator iter = al.iterator();				while(iter.hasNext())				&#123;					Message message=(Message)iter.next();		%&gt;									&lt;tr&gt;				&lt;td&gt;&lt;%=new DB().getUsername(message.getUserid())%&gt;&lt;/td&gt;				&lt;td&gt;&lt;%=message.getDate().toString()%&gt;&lt;/td&gt;				&lt;td&gt;&lt;%=message.getTitle()%&gt;&lt;/td&gt;				&lt;td&gt;&lt;%=message.getContent()%&gt;&lt;/td&gt;			&lt;/tr&gt;		&lt;%				&#125;			&#125;		%&gt;		&lt;/table&gt;		&lt;input type=&quot;submit&quot; value=&quot;留言&quot;/&gt;	&lt;/form&gt;&lt;/body&gt;&lt;/html&gt;
9. 创建liuyan.jsp在WebContent中新建liuyan.jsp，内容如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;留言板&lt;/title&gt;&lt;/head&gt;&lt;body bgcolor=&quot;#E3E3E3&quot;&gt;	&lt;center&gt;		&lt;form action = &quot;addServlet&quot; method=&quot;post&quot;&gt;			&lt;table border=&quot;1&quot;&gt;				&lt;caption&gt;填写留言信息&lt;/caption&gt;				&lt;tr&gt;					&lt;td&gt;留言标题&lt;/td&gt;					&lt;td&gt;&lt;input type=&quot;text&quot; name=&quot;title&quot;/&gt;&lt;/td&gt;				&lt;/tr&gt;				&lt;tr&gt;					&lt;td&gt;留言内容&lt;/td&gt;					&lt;td&gt;&lt;textarea rows=&quot;5&quot; cols=&quot;35&quot; name=&quot;content&quot;&gt;&lt;/textarea&gt;&lt;/td&gt;				&lt;/tr&gt;			&lt;/table&gt;			&lt;input type=&quot;submit&quot; value=&quot;提交&quot;/&gt;			&lt;input type=&quot;reset&quot; value=&quot;重置&quot; /&gt;		&lt;/form&gt;		&lt;/center&gt;&lt;/body&gt;&lt;/html&gt;
10. 创建AddServlet类在包com.voidking.jsp.servlet中，新建类AddServlet，内容如下：
package com.voidking.jsp.servlet;import java.io.IOException;import java.sql.Date;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import com.voidking.jsp.db.DB;import com.voidking.jsp.model.Message;import com.voidking.jsp.model.User;public class AddServlet extends HttpServlet &#123;	public void doGet(HttpServletRequest request, HttpServletResponse response)			throws ServletException, IOException &#123;		request.setCharacterEncoding(&quot;utf8&quot;);		response.setCharacterEncoding(&quot;utf8&quot;);		String title = request.getParameter(&quot;title&quot;);		String content = request.getParameter(&quot;content&quot;);		User user = (User) request.getSession().getAttribute(&quot;user&quot;);		Message message = new Message();		message.setUserid(user.getId());		message.setDate(new Date(System.currentTimeMillis()));		message.setTitle(title);		message.setContent(content);		if (new DB().addMessage(message)) &#123;			response.sendRedirect(&quot;success.jsp&quot;);		&#125;	&#125;	public void doPost(HttpServletRequest request, HttpServletResponse response)			throws ServletException, IOException &#123;		doGet(request, response);	&#125;&#125;
11. 创建成功页面在WebContent中新建success.jsp，内容如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;留言成功&lt;/title&gt;&lt;/head&gt;&lt;body bgcolor=&quot;#E3E3E3&quot;&gt;	留言成功，单击&lt;a href=&quot;mainServlet&quot;&gt;这里&lt;/a&gt;返回主界面。&lt;/body&gt;&lt;/html&gt;
12. 配置web.xml在WebContent中新建web.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app version=&quot;2.5&quot; 	xmlns=&quot;http://java.sun.com/xml/ns/javaee&quot; 	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; 	xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/javaee 	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd&quot;&gt;  &lt;display-name&gt;&lt;/display-name&gt;	  &lt;welcome-file-list&gt;    &lt;welcome-file&gt;login.jsp&lt;/welcome-file&gt;  &lt;/welcome-file-list&gt;    &lt;servlet&gt;  	&lt;servlet-name&gt;mainServlet&lt;/servlet-name&gt;  	&lt;servlet-class&gt;com.voidking.jsp.servlet.MainServlet&lt;/servlet-class&gt;  &lt;/servlet&gt;    &lt;servlet-mapping&gt;  	&lt;servlet-name&gt;mainServlet&lt;/servlet-name&gt;  	&lt;url-pattern&gt;/mainServlet&lt;/url-pattern&gt;  &lt;/servlet-mapping&gt;    &lt;servlet&gt;  	&lt;servlet-name&gt;addServlet&lt;/servlet-name&gt;  	&lt;servlet-class&gt;com.voidking.jsp.servlet.AddServlet&lt;/servlet-class&gt;  &lt;/servlet&gt;    &lt;servlet-mapping&gt;  	&lt;servlet-name&gt;addServlet&lt;/servlet-name&gt;  	&lt;url-pattern&gt;/addServlet&lt;/url-pattern&gt;  &lt;/servlet-mapping&gt;    &lt;servlet&gt;  	&lt;servlet-name&gt;registerServlet&lt;/servlet-name&gt;  	&lt;servlet-class&gt;com.voidking.jsp.servlet.RegisterServlet&lt;/servlet-class&gt;  &lt;/servlet&gt;    &lt;servlet-mapping&gt;  	&lt;servlet-name&gt;registerServlet&lt;/servlet-name&gt;  	&lt;url-pattern&gt;/registerServlet&lt;/url-pattern&gt;  &lt;/servlet-mapping&gt;&lt;/web-app&gt;

13. 创建注册页面在WebContent中新建register.jsp，代码如下：
&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;    pageEncoding=&quot;UTF-8&quot;%&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;&lt;html&gt;&lt;head&gt;&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;&lt;title&gt;用户注册&lt;/title&gt;&lt;/head&gt;&lt;body bgcolor=&quot;#E3E3E3&quot;&gt;	&lt;form action=&quot;registerServlet&quot; method=&quot;post&quot;&gt;		&lt;table&gt;			&lt;caption&gt;用户注册&lt;/caption&gt;			&lt;tr&gt;				&lt;td&gt;登录名&lt;/td&gt;				&lt;td&gt;&lt;input type=&quot;text&quot; name=&quot;username&quot;/&gt;&lt;/td&gt;			&lt;/tr&gt;			&lt;tr&gt;				&lt;td&gt;密码：&lt;/td&gt;				&lt;td&gt;&lt;input type=&quot;password&quot; name=&quot;password&quot;/&gt;&lt;/td&gt;			&lt;/tr&gt;		&lt;/table&gt;		&lt;input type=&quot;submit&quot; value=&quot;注册&quot; /&gt;		&lt;input type=&quot;reset&quot; value=&quot;重置&quot;/&gt;	&lt;/form&gt;&lt;/body&gt;&lt;/html&gt;
14. 创建RegisterServlet类在包com.voidking.jsp.servlet下，新建类RegisterServlet，内容如下：
package com.voidking.jsp.servlet;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import com.voidking.jsp.db.DB;public class RegisterServlet extends HttpServlet &#123;	public void doGet(HttpServletRequest request, HttpServletResponse response)			throws ServletException, IOException &#123;		request.setCharacterEncoding(&quot;utf8&quot;);		response.setCharacterEncoding(&quot;utf8&quot;);		String username = request.getParameter(&quot;username&quot;);		String password = request.getParameter(&quot;password&quot;);		if (new DB().insertUser(username, password)) &#123;			response.sendRedirect(&quot;login.jsp&quot;);		&#125;	&#125;	public void doPost(HttpServletRequest request, HttpServletResponse response)			throws ServletException, IOException &#123;		doGet(request, response);	&#125;&#125;
15. 部署和运行拷贝mysql-connector-java-*-bin.jar到WebContent/WEB-INF/lib。发布工程，启动Tomcat服务器，访问地址：http://localhost:8080/jsp/login.jsp
16. 编码问题经过调试修改，登录、注册、留言，功能正常。但是，查看留言时，中文留言在界面上显示问号。在数据库中查询，也显示问号，那么问题出在哪里呢？
16.1. 查看MySQL默认编码status;show variables like &#x27;character%&#x27;;
发现结果如下：



Variable_name
Value



character_set_client
gbk


character_set_connection
gbk


character_set_database
latin1


character_set_filesystem
binary


character_set_results
gbk


character_set_server
latin1


character_set_system
utf8


character_sets_dir
d:\server\xampp\mysql\share\charsets\


推测是数据库编码问题，全部修改成utf8应该就可以了。



16.2. 修改无效1、由于我的MySQL是XAMPP的一部分，比较精简，所以在bin文件夹下没有MySQLInstanceConfig.exe。没有办法利用这个工具重新配置。
2、修改配置文件。在my.ini中添加：
[mysqld]default-character-set=utf8character_set_server=utf8init_connect=&#x27;SET NAMES utf8&#x27;
重启MySQL，完全没有效果，靠！
3、通过命令配置
//create database jsp character set utf8;alter database jsp character set utf8;//在没有table的情况下，此命令才有效。set names utf8;  
经过测试，还是显示乱码。查看编码如下：



Variable_name
Value



character_set_client
utf8


character_set_connection
utf8


character_set_database
utf8


character_set_filesystem
binary


character_set_results
utf8


character_set_server
latin1


character_set_system
utf8


character_sets_dir
d:\server\xampp\mysql\share\charsets\


看来character_set_server的值是重点啊！



16.3. 重装备份数据库。复制D:\Server\xampp\mysql\data路径下的数据库文件夹以及ibdata1文件，待会放到新的MySQL的data目录下。
卸载MySQL，使用绿色版安装，提示服务已存在，无法安装。最终，使用了windows安装版。注意，配置数据库的时候，一定要选择utf8编码。安装成功，拷贝数据库文件到新的data下，结果，MySQL无法启动。推测是因为新安装的32位，而原数据库是64位。
没办法，重新建立用户，重新建立数据库和表。经过测试，可以正常显示中文！重装治百病，很有道理！
17. 源代码分享https://github.com/voidking/jsp.git
18. 小结书上给的代码有几处错误，修改了之后，才最终跑起来。书本，确实只是用来参考就够了。编程能力是写出来的！在一行行敲代码的过程中，会遇到很多问题，而解决这些问题的过程，培养出来的，就是编程能力！
19. 参考文档《Java EE基础实用教程》，郑阿奇主编mysql添加外键：http://www.cnblogs.com/xiangxiaodong/archive/3061049.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>jsp</tag>
      </tags>
  </entry>
  <entry>
    <title>JSP和servlet概述</title>
    <url>/dev-jsp-servlet-start/</url>
    <content><![CDATA[1. JSP概述1.1. 名词解释JSP全名为Java Server Pages，中文名叫java服务器页面，其根本是一个简化的Servlet设计。
JSP文件类似于HTML文件，但又不完全相同，其实JSP是由HTML、Java片段和JSP标记组成的。
1.2. ServletJava的运行方式是通过Java虚拟机把*.java的文件编译成*.class文件，但JSP文件却是后缀名为.jsp的文件，怎么执行呢？当*.jsp文件被送到服务器后，先由服务器翻译成Servlet文件，而Servlet文件就是*.java文件，然后*.java文件又被编译成*.class文件，再由Java虚拟机解释执行。

Servlet和Java Applet一样，它们都不是独立的应用程序，都没有main()方法，而是生存在容器中，由容器来管理。编写一个Servlet文件，需要实现javax.servlet.Servlet接口。
1.3. Servlet demo设计新建Dynamic Web Project，命名为servlet。新建包com.voidking.servlet，新建类HelloWorld，实现Servlet接口，内容如下：
package com.voidking.servlet;import java.io.IOException;import java.io.PrintWriter;import javax.servlet.Servlet;import javax.servlet.ServletConfig;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;public class HelloWorld implements Servlet &#123;	@Override	public void destroy() &#123;		// TODO Auto-generated method stub	&#125;	@Override	public ServletConfig getServletConfig() &#123;		// TODO Auto-generated method stub		return null;	&#125;	@Override	public String getServletInfo() &#123;		// TODO Auto-generated method stub		return null;	&#125;	@Override	public void init(ServletConfig arg0) throws ServletException &#123;		// TODO Auto-generated method stub	&#125;	@Override	public void service(ServletRequest req, ServletResponse res)			throws ServletException, IOException &#123;		// TODO Auto-generated method stub		PrintWriter pw = res.getWriter();		pw.println(&quot;HelloWorld&quot;);	&#125;&#125;

在WebContent/WEB-INF下新建web.xml，内容如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app version=&quot;2.5&quot; 	xmlns=&quot;http://java.sun.com/xml/ns/javaee&quot; 	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; 	xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/javaee 	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd&quot;&gt;  &lt;display-name&gt;&lt;/display-name&gt;	  &lt;servlet&gt;  	&lt;servlet-name&gt;HelloWorld&lt;/servlet-name&gt;  	&lt;servlet-class&gt;com.voidking.servlet.HelloWorld&lt;/servlet-class&gt;  &lt;/servlet&gt;    &lt;servlet-mapping&gt;  	&lt;servlet-name&gt;HelloWorld&lt;/servlet-name&gt;  	&lt;url-pattern&gt;/helloWorld&lt;/url-pattern&gt;  &lt;/servlet-mapping&gt;&lt;/web-app&gt;

发布工程，访问http://localhost:8080/servlet/helloWorld ，即可在界面上看到“HelloWorld”。
1.4. 原理分析init()：在Servlet实例化之后，Servlet容器会调用init()方法，来初始化该对象。该方法主要是为了让Servlet对象在处理客户请求前可以完成一些初始化工作，例如，建立数据库的连接，获取配置信息等。对于每个Servlet实例，init()方法只能被调用一次。init()方法有一个类型为ServletConfig的参数，Servlet容器通过这个参数向Servlet传递配置信息。Servlet使用该对象从Web应用程序的配置信息中获取初始化参数。另外，在Servlet中，还可以通过ServletConfig对象获取描述Servlet运行环境的ServletContext对象，使用该对象，Servlet可以和它的Servlet容器进行通信。
service()：容器调用service()方法来处理客户端的请求。在init()方法正确完成后，service()方法被容器调用。在service()方法中有用于接受客户端请求信息的请求对象（类型为ServletRequest）和用于对客户端进行响应的响应对象（类型为ServletResponse）的参数。Servlet对象通过ServletRequest对象得到客户端的相关信息和请求信息，在对请求进行处理后，调用ServletResponse对象的方法设置响应信息。
destroy()：当容器检测到一个Servlet对象应该从服务中被移除时，容器会调用该对象的destroy()方法，来释放Servlet对象所使用的资源，保存数据到持久存储设备中。例如，将内存中的数据保存到数据库中，关闭数据库连接等。在Servlet容器调用destroy()方法前，如果还有其他线程正在service()方法中执行，容器会等待这些线程执行完毕或等待服务器设定的最大时间到达。一旦Servlet对象的destroy()方法被调用，容器不会再把其他的请求发送给该对象。如果需要该Servlet对象再次为客户端服务，容器将会重新生成一个Servlet对象来处理客户端的请求。在destroy()方法调用之后，容器会释放这个Servlet对象，在随后的时间内，该对象会被Java的垃圾收集器回收。
getServletConfig()：放回容器调用init()方法时传递给Servlet对象的ServletConfig对象，ServletConfig对象包含了Servlet的初始化参数。
getServletInfo()：放回一个String类型的字符串，其中包括关于Servlet的信息，例如，作者、版本和版权。
web.xml文件中包含配置信息。web.xml文件的第一行是对xml文件的生命，接着就是xml的根元素，其属性中声明了版本等信息。和之间配置的是和，其中的值“HelloWorld”是为Servlet起的一个名字，这个可以随便起名，只要符合Java的命名规则就行；而的值则是自己写的Servlet类的类名，这个必须配置正确，如果有包，还要在前面加上包名。这个类名不带.java，也不带.class。
与之间配置的是和，其中的值就是上面刚刚配置的的值；而的值也可以随便起名，但其前面必须加“/”，如上面例子中的“/helloWorld”。
访问地址：http://localhost:8080/servlet/helloWorld  ，其中，http://localhost:8080 是服务器URL，而后面的servlet是项目名，最后面的helloWorld是在web.xml中配置的的值。
1.5. 优化1.5.1. GenericServlet类上面的例子中，采用实现Servlet接口的方法，需要实现5个方法。为了简化Servlet的编写，在javax.servlet包中提供了一个抽象的类GenericServlet。它给出了出service()外其他4个方法的简单实现。GenericServlet实现了Servlet接口和ServletConfig接口。HelloWorld.java可以改写为：
package com.voidking.servlet;import java.io.IOException;import java.io.PrintWriter;import javax.servlet.GenericServlet;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;public class HelloWorld extends GenericServlet &#123;	@Override	public void service(ServletRequest arg0, ServletResponse arg1)			throws ServletException, IOException &#123;		// TODO Auto-generated method stub		PrintWriter pw = arg1.getWriter();		pw.println(&quot;HelloWorld&quot;);	&#125;&#125;

1.5.2. HttpServlet类在大部分网络中，都是客户端通过HTTP协议来访问服务端的资源。为了快速开发应用于HTTP协议的Servlet类，在javax.servlet.http包中提供了一个抽象HttpServlet。它继承了GenericServlet类。
当容器接到一个针对HttpServlet对象请求时，该对象就会调用public的service()方法，首先将参数类型转换为HttpServletRequest和HttpServletResponse，然后调用protected的service()方法将参数传进去。
接着调用HttpServletRequest对象的getMethod()方法获取请求方法名来调用相应的doXxx()方法。所以一个Servlet类在继承HttpServlet时，不用覆盖它的service()方法，只需要覆盖相应的doXxx()方法就行了。通常情况下，都是覆盖其doGet()和doPost()方法。然后在其中的一个方法中调用另一个方法，这样就可以做到合二为一。
HelloWorld.java可以改写为：
package com.voidking.servlet;import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class HelloWorld extends HttpServlet &#123;	protected void doGet(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException	&#123;		PrintWriter pw = res.getWriter();		pw.println(&quot;HelloWorld&quot;);	&#125;		protected void doPost(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException	&#123;		doGet(req, res);	&#125;&#125;

1.6. Servlet进阶在一个HTML文件中建立一个表单，里面有一个输入框，当客户输入内容后，提交到一个Servlet类，这个Servlet类取出客户输入的信息，并在HTML页面上显示该内容。
右击servlet项目下的WebContent，New，HTML File，命名为index.html，内容如下：
&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;UTF-8&quot;&gt;&lt;title&gt;Servlet进阶&lt;/title&gt;&lt;/head&gt;&lt;body&gt;	&lt;form action=&quot;inputServlet&quot; method=&quot;post&quot;&gt;		请输入你想显示的内容：&lt;input type=&quot;text&quot; name=&quot;input&quot;/&gt;&lt;/br&gt;		&lt;input type=&quot;submit&quot; value=&quot;提交&quot; /&gt;		&lt;input type=&quot;reset&quot; value=&quot;重置&quot; /&gt;	&lt;/form&gt;&lt;/body&gt;&lt;/html&gt;

在com.voidking.servlet包下新建InputServlet类，继承HttpServlet，内容如下：
package com.voidking.servlet;import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class InputServlet extends HttpServlet &#123;	private static final long serialVersionUID = 1L;           public InputServlet() &#123;        super();    &#125;	protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException &#123;		// TODO Auto-generated method stub		res.setCharacterEncoding(&quot;utf8&quot;);		req.setCharacterEncoding(&quot;utf8&quot;);				String input = req.getParameter(&quot;input&quot;);				PrintWriter pw = res.getWriter();		pw.println(&quot;&lt;html&gt;&lt;head&gt;&lt;meta charset=\&quot;UTF-8\&quot;&gt;&lt;title&gt;&quot;);		pw.println(&quot;显示输入内容&quot;);		pw.println(&quot;&lt;/title&gt;&lt;body&gt;&quot;);		pw.println(input);		pw.println(&quot;&lt;/body&gt;&lt;/html&gt;&quot;);	&#125;	protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException &#123;		// TODO Auto-generated method stub		doGet(req, res);	&#125;&#125;

web.xml修改如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app version=&quot;2.5&quot; xmlns=&quot;http://java.sun.com/xml/ns/javaee&quot;	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;	xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/javaee 	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd&quot;&gt;	&lt;display-name&gt;&lt;/display-name&gt;	&lt;servlet&gt;		&lt;servlet-name&gt;HelloWorld&lt;/servlet-name&gt;		&lt;servlet-class&gt;com.voidking.servlet.HelloWorld&lt;/servlet-class&gt;	&lt;/servlet&gt;	&lt;servlet-mapping&gt;		&lt;servlet-name&gt;HelloWorld&lt;/servlet-name&gt;		&lt;url-pattern&gt;/helloWorld&lt;/url-pattern&gt;	&lt;/servlet-mapping&gt;	&lt;servlet&gt;		&lt;servlet-name&gt;InputServlet&lt;/servlet-name&gt;		&lt;servlet-class&gt;com.voidking.servlet.InputServlet&lt;/servlet-class&gt;	&lt;/servlet&gt;	&lt;servlet-mapping&gt;		&lt;servlet-name&gt;InputServlet&lt;/servlet-name&gt;		&lt;url-pattern&gt;/inputServlet&lt;/url-pattern&gt;	&lt;/servlet-mapping&gt;&lt;/web-app&gt;

发布工程，访问：http://localhost:8080/servlet/
1.7. 源码分享https://github.com/voidking/servlet.git
1.8. JSP语法1.8.1. 数据定义&lt;%! 变量声明 %&gt;，相当于Servlet类里面的变量声明。
1.8.2. 程序块&lt;% 程序块 %&gt;，相当于在Servlet类里面service()函数里。
1.8.3. 表达式&lt;%=Java表达式 %&gt;，相当于Servlet类里面service()函数里，PrintWriter调用println()函数。
1.8.4. 注释&lt;%-- 注释内容[&lt;%=表达式%&gt;]--&gt;，输出注释。&lt;%-- 注释内容--%&gt;，隐藏注释。
1.8.5. 指令JSP指令用来提供整个JSP页面的相关信息和设定JSP页面的相关属性，如设定网页的编码方式、脚本语言及导入需要用到的包等。语法格式如下：&lt;%@ 指令名 属性名=&quot;属性值&quot;%&gt;
1、page指令2、include指令3、taglib指令
1.8.6. 动作1、&lt;jsp:param&gt;2、&lt;jsp:include&gt;3、&lt;jsp:useBean&gt;4、&lt;jsp:setProperty&gt;5、&lt;jsp:getProperty&gt;6、&lt;jsp:forward&gt;7、&lt;jsp:plugin&gt;
1.8.7. 内置对象1、page2、config3、out4、response5、request6、session7、application8、pageContext9、exception
1.9. 参考文档《Java EE基础实用教程》，郑阿奇主编
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>jsp</tag>
        <tag>servlet</tag>
      </tags>
  </entry>
  <entry>
    <title>JDBC概述</title>
    <url>/dev-jdbc-start/</url>
    <content><![CDATA[1. 名词解释JDBC（Java Data Base Connectivity,java数据库连接）是一种用于执行SQL语句的Java API，可以为多种关系数据库提供统一访问，它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准，据此可以构建更高级的工具和接口，使数据库开发人员能够编写数据库应用程序。
2. 下载jar包MySQL：MySQL Connectors
Oracle：JDBC、SQLJ、Oracle JPublisher 和通用连接池 (UCP)JDBC and Universal Connection Pool (UCP)
SQL Server：Microsoft JDBC Drivers 4.1 and 4.0 for SQL Server


3. MySQL用户管理创建新用户
mysql -u root -pcreate user &#x27;scott&#x27;@&#x27;localhost&#x27; identified by &#x27;tiger&#x27;;//创建本地用户create user &#x27;scott&#x27;@&#x27;%&#x27; identified by &#x27;tiger&#x27;;//创建远程用户，可选命令create database test;grant all prvivileges on test.* to scott;flush privileges;select host,user,password from mysql.user;//查看系统有哪些用户exit

新用户登录
mysql -u scott -pshow databases;use test;show tables;exit

删除新用户
mysql -u root -pdrop user &#x27;scott&#x27;@&#x27;localhost&#x27;;drop user &#x27;scott&#x27;;//相当于drop user &#x27;scott&#x27;@&#x27;%&#x27;;

4. 测试连通性以测试eclipse和MySQL连接为例。
4.1. 设置驱动打开eclipse，Window，Open Perspective，Other…，Database Development，OK。在左侧Data Source Explorer中，右击Database Connections文件夹，New…，选中MySQL，Name随意，Description随意，Next，New Driver Definition，Name/Type中选中一个System Version，然后在JAR List中选中mysql-connector-java-*-bin.jar，Edit JAR/Zip…，然后选中刚才下载解压的jar包，OK。
4.2. 设置连接在Properties的General选项卡中，输入Database、URL、User name、Password，Save password前打钩。
4.3. 测试连接设置好连接后，点击Test Connection，即可测试连通性。会提示ping succeeded！或者ping failed！
5. MySQL demo设计使用scott登录MySQL
use test;create table userbase(id int,username varchar(16),passwd varchar(16));insert into userbase values(1,&#x27;voidking&#x27;,&#x27;voidking&#x27;);insert into userbase values(2,&#x27;voidking2&#x27;,&#x27;voidking2&#x27;);insert into userbase values(3,&#x27;voidking3&#x27;,&#x27;voidking3&#x27;);

创建jdbc工程，创建包com.voidking.jdbc，新建JdbcMySQL类，内容如下。
package com.voidking.jdbc;import java.sql.*;public class JdbcMySQL &#123;	// JDBC driver name and database URL	static final String JDBC_DRIVER = &quot;com.mysql.jdbc.Driver&quot;;	static final String DB_URL = &quot;jdbc:mysql://localhost/test&quot;;	// Database credentials	static final String USER = &quot;scott&quot;;	static final String PASS = &quot;tiger&quot;;	public static void main(String[] args) &#123;		Connection conn = null;		Statement stmt = null;		try &#123;			// STEP 2: Register JDBC driver			Class.forName(JDBC_DRIVER);			// STEP 3: Open a connection			System.out.println(&quot;Connecting to database...&quot;);			conn = DriverManager.getConnection(DB_URL, USER, PASS);			// STEP 4: Execute a query			System.out.println(&quot;Creating statement...&quot;);			stmt = conn.createStatement();			String sql;			sql = &quot;select id,username,passwd from userbase&quot;;			ResultSet rs = stmt.executeQuery(sql);			// STEP 5: Extract data from result set			while (rs.next()) &#123;				// Retrieve by column name				int id = rs.getInt(&quot;id&quot;);				String username = rs.getString(&quot;username&quot;);				String passwd = rs.getString(&quot;passwd&quot;);				// Display values				System.out.print(&quot;ID: &quot; + id);				System.out.print(&quot;, username: &quot; + username);				System.out.println(&quot;, passwd: &quot; + passwd);			&#125;			// STEP 6: Clean-up environment			rs.close();			stmt.close();			conn.close();		&#125; catch (SQLException se) &#123;			// Handle errors for JDBC			se.printStackTrace();		&#125; catch (Exception e) &#123;			// Handle errors for Class.forName			e.printStackTrace();		&#125; finally &#123;			// finally block used to close resources			try &#123;				if (stmt != null)					stmt.close();			&#125; catch (SQLException se2) &#123;			&#125;// nothing we can do			try &#123;				if (conn != null)					conn.close();			&#125; catch (SQLException se) &#123;				se.printStackTrace();			&#125;// end finally try		&#125;// end try		System.out.println(&quot;Goodbye!&quot;);	&#125;// end main&#125;// end JdbcMySQL

右击JRE System Library，Build Path，Configure Build Path…，Add External JARs…，选中下载解压好的mysql-connector-java-*-bin.jar。
运行项目，即可在控制台看到输出。
6. SQL Server demo设计使用sa登录SQL Server
create database test;//切换到test数据库create table userbase(id int,username varchar(16),passwd varchar(16));insert into userbase values(1,&#x27;voidking&#x27;,&#x27;voidking&#x27;);insert into userbase values(2,&#x27;voidking2&#x27;,&#x27;voidking2&#x27;);insert into userbase values(3,&#x27;voidking3&#x27;,&#x27;voidking3&#x27;);

新建JdbcSQLServer类，内容如下：
package com.voidking.jdbc;import java.sql.*;public class JdbcSQLServer &#123;	// JDBC driver name and database URL	static final String JDBC_DRIVER = &quot;com.microsoft.sqlserver.jdbc.SQLServerDriver&quot;;	static final String DB_URL = &quot;jdbc:sqlserver://127.0.0.1:1433;databaseName=test&quot;;	// Database credentials	static final String USER = &quot;sa&quot;;	static final String PASS = &quot;123&quot;;	public static void main(String[] args) &#123;		Connection conn = null;		Statement stmt = null;		try &#123;			// STEP 2: Register JDBC driver			Class.forName(JDBC_DRIVER);			// STEP 3: Open a connection			System.out.println(&quot;Connecting to database...&quot;);			conn = DriverManager.getConnection(DB_URL, USER, PASS);			// STEP 4: Execute a query			System.out.println(&quot;Creating statement...&quot;);			stmt = conn.createStatement();			String sql;			sql = &quot;select id,username,passwd from userbase&quot;;			ResultSet rs = stmt.executeQuery(sql);			// STEP 5: Extract data from result set			while (rs.next()) &#123;				// Retrieve by column name				int id = rs.getInt(&quot;id&quot;);				String username = rs.getString(&quot;username&quot;);				String passwd = rs.getString(&quot;passwd&quot;);				// Display values				System.out.print(&quot;ID: &quot; + id);				System.out.print(&quot;, username: &quot; + username);				System.out.println(&quot;, passwd: &quot; + passwd);			&#125;			// STEP 6: Clean-up environment			rs.close();			stmt.close();			conn.close();		&#125; catch (SQLException se) &#123;			// Handle errors for JDBC			se.printStackTrace();		&#125; catch (Exception e) &#123;			// Handle errors for Class.forName			e.printStackTrace();		&#125; finally &#123;			// finally block used to close resources			try &#123;				if (stmt != null)					stmt.close();			&#125; catch (SQLException se2) &#123;			&#125;// nothing we can do			try &#123;				if (conn != null)					conn.close();			&#125; catch (SQLException se) &#123;				se.printStackTrace();			&#125;// end finally try		&#125;// end try		System.out.println(&quot;Goodbye!&quot;);	&#125;// end main&#125;

7. Oracle demo设计使用scott用户登录
create table userbase(id int,username varchar(16),passwd varchar(16));insert into userbase values(1,&#x27;voidking&#x27;,&#x27;voidking&#x27;);insert into userbase values(2,&#x27;voidking2&#x27;,&#x27;voidking2&#x27;);insert into userbase values(3,&#x27;voidking3&#x27;,&#x27;voidking3&#x27;);

新建JdbcOracle类，内容如下：
package com.voidking.jdbc;import java.sql.*;public class JdbcOracle &#123;	// JDBC driver name and database URL	static final String JDBC_DRIVER = &quot;oracle.jdbc.OracleDriver&quot;;	static final String DB_URL = &quot;jdbc:oracle:thin:@localhost:1521:orcl&quot;;	// Database credentials	static final String USER = &quot;scott&quot;;	static final String PASS = &quot;tiger&quot;;	public static void main(String[] args) &#123;		Connection conn = null;		Statement stmt = null;		try &#123;			// STEP 2: Register JDBC driver			Class.forName(JDBC_DRIVER);			// STEP 3: Open a connection			System.out.println(&quot;Connecting to database...&quot;);			conn = DriverManager.getConnection(DB_URL, USER, PASS);			// STEP 4: Execute a query			System.out.println(&quot;Creating statement...&quot;);			stmt = conn.createStatement();			String sql;			sql = &quot;select id,username,passwd from userbase&quot;;			ResultSet rs = stmt.executeQuery(sql);			// STEP 5: Extract data from result set			while (rs.next()) &#123;				// Retrieve by column name				int id = rs.getInt(&quot;id&quot;);				String username = rs.getString(&quot;username&quot;);				String passwd = rs.getString(&quot;passwd&quot;);				// Display values				System.out.print(&quot;ID: &quot; + id);				System.out.print(&quot;, username: &quot; + username);				System.out.println(&quot;, passwd: &quot; + passwd);			&#125;			// STEP 6: Clean-up environment			rs.close();			stmt.close();			conn.close();		&#125; catch (SQLException se) &#123;			// Handle errors for JDBC			se.printStackTrace();		&#125; catch (Exception e) &#123;			// Handle errors for Class.forName			e.printStackTrace();		&#125; finally &#123;			// finally block used to close resources			try &#123;				if (stmt != null)					stmt.close();			&#125; catch (SQLException se2) &#123;			&#125;// nothing we can do			try &#123;				if (conn != null)					conn.close();			&#125; catch (SQLException se) &#123;				se.printStackTrace();			&#125;// end finally try		&#125;// end try		System.out.println(&quot;Goodbye!&quot;);	&#125;// end main&#125;// end JdbcOracle

8. 源代码分享https://github.com/voidking/jdbc.git
9. 小结通过上面三个连接不同数据库的例子，我们发现，代码的差别，仅仅在于驱动包名、数据库的地址、用户名、密码。那么，这四个信息在哪里获得呢？除了自己记忆之外，郝同学提供一个查询测试的方法。Database Development，右击连接，Properties，Driver Properties。这时，已经可以看到驱动、用户名、密码。至于驱动，请接着点开Edit Driver Definition，Properties，Driver Class后面的就是驱动包。至此，四项信息都有了。
10. 参考文档JDBC快速入门教程：http://www.yiibai.com/jdbc/jdbc_quick_guide.html
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
        <tag>jdbc</tag>
      </tags>
  </entry>
  <entry>
    <title>JPA与Hibernate的关系</title>
    <url>/dev-jpa-and-hibernate/</url>
    <content><![CDATA[1. 名词解释JPA：Java Persistence API，是Java EE 5的标准ORM接口，也是ejb3规范的一部分。JPA通过JDK5.0注解或XML描述对象-关系表的映射关系，并将运行期实体对象持久化到数据库中去。
ORM：Object-Relational Mapping，对象关系映射，即实体对象和数据库表的映射。
Hibernate：是一个开放源代码的对象关系映射框架，它对JDBC进行了非常轻量级的对象封装，使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。
2. JPA与Hibernate的关系JPA和Hibernate之间的关系，可以简单的理解为JPA是标准接口，Hibernate是实现。

Hibernate主要通过hibernate-annotation、hibernate-core和hibernate-entitymanager三个组件来实现与JPA的关系。
hibernate-annotation是Hibernate支持annotation方式配置的基础，它包括了标准的JPA annotation以及Hibernate自身特殊功能的annotation。
hibernate-core是Hibernate的核心实现，提供了Hibernate所有的核心功能。
hibernate-entitymanager实现了标准的JPA，可以把它看成hibernate-core和JPA之间的适配器，它并不直接提供ORM的功能，而是对hibernate-core进行封装，使得Hibernate符合JPA的规范。
3. 参考文档http://blog.163.com/hero_213/blog/static/398912142010312024809/
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>hibernate</tag>
        <tag>jpa</tag>
      </tags>
  </entry>
  <entry>
    <title>eclipse实用设置</title>
    <url>/dev-eclipse-practical-setting/</url>
    <content><![CDATA[1. 设置项目默认编码在eclipse中，Window，Preference，General，Workspace，Text file encoding，Other选择UTF-8。
2. 设置JSP页面默认编码在eclipse中，Window，Preference，Web，JSP Files，Encoding选择ISO 10646/Uincode(UTF-8)。
3. 设置代码提示在eclipse中，Window，Preference，Java，Editor，Content Assist，Auto Activation，Auto activation triggers for Java中输入“.”和52个英文字符。
4. 设置快捷键在eclipse中，Window，Preference，General，Keys。


5. 恢复默认布局Window，Perspective，Reset Perspective
6. eclipse查看JDK源代码eclipse查看JDK中的类，出现The jar file rt.jar has no source attachment。
解决办法：1、Window，Preferences，Java，Installed JREs，选中安装的JRE，Edit。2、JRE system libraries中展开rt.jar。3、Source Attachment…，选中JDK安装目录中的src.zip。
7. eclipse查看tomcat源代码eclipse源码查看tomcat中的类，出现Source not found。解决办法：1、下载相应版本tomcat源码，类似于apache-tomcat-8.0.44-src.zip。找不到相应版本的话，在页面上找到Quick Navigation，Archives，即可找到历史版本。2、单击Change Attached Source…，选中External location。3、Path选中apache-tomcat-8.0.44-src.zip，OK。
8. 带源文件的jar包下载http://www.java2s.com/Code/Jar/CatalogJar.htm
9. 反编译class文件1、下载相应版本的jadClipse，这里下载net.sf.jadclipse_3.3.0.jar。2、下载相应版本的jad，这里下载Jad 1.5.8g for Windows 9x/NT/2000 on Intel platform。3、将net.sf.jadclipse_3.3.0.jar移动到eclipse的plugins目录下。4、将jad.exe移动到JDK安装目录下的bin目录下（方便使用）。5、移动eclipse的configuration目录下的org.eclipse.update文件夹到其他位置（或者直接删除）。6、重启eclipse，Window-&gt;Preferences-&gt;Java，此时发现多了一个JadClipse。7、Path to decompiler填入jad.exe的路径。8、Window-&gt;Preferences-&gt;General-&gt;Editors-&gt;File Associations，查看.class文件的默认打开方式，已经变成了JadClipse。查看.class without sources文件的默认打开方式，修改为JadClipse。9、选择需要查看的类，点击F3查看源码。如果不能查看，请关闭tab然后重新查看（反编译可能需要一点时间）。
10. 代码提示上屏设置例如：要新建一个String类型的变量value，则当输入到value的时候，eclipse会在候选列表中列出valueString，如果此时再输入空格的话，就会选中候选列表中的valueString，则新建的变量将会变成valueString。
解决办法：打开eclipse，Window，Show View，Plug-ins，找到org.eclipse.jface.text右击，import as，Source Project，导入完成后，在你的workspace就可以看到这个project了。
PS：修改插件，需要下载相同Version的Eclipse RCP（该版本修改源代码比较方便，能自动导入源代码），因为开发版、企业版等导出Plug-ins项目后无法编辑。
然后，在导入工程下的src文件夹下，找到包org.eclipse.jface.text.contentassist，类CompletionProposalPopup，函数verifyKey()。
if (contains(triggers, key)) &#123;	e.doit= false;	hide();	insertProposal(p, key, e.stateMask, fContentAssistSubjectControlAdapter.getSelectedRange().x);&#125;
修改为
if (key != &#x27;=&#x27; &amp;&amp; key != 0x20 &amp;&amp; key != &#x27;;&#x27; &amp;&amp; contains(triggers, key)) &#123;	e.doit= false;	hide();	insertProposal(p, key, e.stateMask, fContentAssistSubjectControlAdapter.getSelectedRange().x);&#125;
实现效果：等号键、空格键、分号键不上屏。
case &#x27;\t&#x27;:	e.doit= false;	fProposalShell.setFocus();	return false;
修改为
case &#x27;\t&#x27;:	e.doit= false;	insertSelectedProposalWithMask(e.stateMask);	return false;
实现效果：Tab键上屏。
修改完成后，项目右击，Export，Deployable plug-ins and fragments，勾选Available Plug-ins and Fragments，指定Destination的Directory，Finish。
使用导出的jar包，替换原eclipse/plugins下的jar包，即可实现插件的修改。
注意：最新版的eclipse，Neon.1a Release (4.6.1)，插件的所在目录变更如下：C:\Users\Administrator\.p2\pool\plugins\，使用导出的jar包，替换该目录下的jar包，即可实现插件的修改。
如果再次变更插件所在目录，请依次点击Window，Show View，Plug-ins。然后单击任意插件，在eclipse底部即可看到插件路径。
11. 配置本地dtd文件我们以配置xwork-validator-1.0.dtd文件为例：
1、解压xwork-core-*.jar包，找到xwork-validator-1.0.dtd。
2、eclipse，Window，Preferences，XML，XML Catelog，Add，File System…，选中刚才解压的xwork-validator-1.0.dtd，打开，
3、Location已经选好D:\jar\struts2\xwork-validator-1.0.dtd，Key type选择Public ID，Key填-//OpenSymphony Group//XWork Validator Config 1.0//EN，Alternative web address填http://struts.apache.org/dtds/xwork-validator-1.0.dtd。（本地dtd不存在时回去web上去找dtd）
4、StrutsAction-validation.xml需要修改如下：
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!DOCTYPE validators PUBLIC        &quot;-//OpenSymphony Group//XWork Validator Config 1.0//EN&quot;        &quot;D:\jar\struts2\xwork-validator-1.0.dtd&quot;&gt;&lt;validators&gt;&lt;!--需要校验的字段的字段名--&gt;	&lt;field name=&quot;name&quot;&gt;		&lt;field-validator type=&quot;requiredstring&quot;&gt;			&lt;!--去空格--&gt;			&lt;param name=&quot;trim&quot;&gt;true&lt;/param&gt;			&lt;!--错误提示信息--&gt;			&lt;message&gt;姓名是必须的&lt;/message&gt;		&lt;/field-validator&gt;	&lt;/field&gt;&lt;/validators&gt;

12. 参考文档修改空格键”=”键不上屏比较全面的Eclipse配置详解
]]></content>
      <categories>
        <category>engineering</category>
        <category>java</category>
      </categories>
      <tags>
        <tag>java</tag>
        <tag>eclipse</tag>
      </tags>
  </entry>
  <entry>
    <title>单词缩写</title>
    <url>/hobby-word-abbreviation/</url>
    <content><![CDATA[缩写    全称POSIX    Portable Operating System Interface of Unix（可移植操作系统接口）LSB           Linux Standard Base (Linux标准基础)FHS        File system Hierarchy Standard(文件系统层次标准)GNU        GNU’s Not UnixFSF        Free Software FoundationBSD        Berkeley Software DistributionLFS        Linux From ScratchBLFS    Beyond Linux From ScratchCLFS    Cross Linux From ScratchHLFS    Hardened Linux From ScratchMBR        Master Boot RecorderGNOME    The GNU Network Object Model Environment

SELinux    Security Enhanced LinuxLPI        Linux Professional Instituteext3    Third Extended File System(第三文件扩展系统)VFS        Virtual Filesystem Switch(虚拟文件交换系统)KDE        Kool Desktop EnvironmentIDE        Integrated Development Environment(集成开发环境)CLI        command-line interface(命令行界面)GUI        Graphical User Interface(图形用户界面)GPT        全局唯一标识分区表(GUID Partition Table)VPS        Virtual Private Server （虚拟专用服务器）DNSR    domain name system resolutionSSH        Secure ShellPAM     Pluggable Authentication Modules, 嵌入式模块SEO        Search Engine Optimizationrss        Really Simple Syndication | RDF Site Summary | Rich Site SummaryRDF        Resource Description FrameworkVPN        Virtual Private NetworkRE        Regular Expression(正则表达式)RHCA    Red Hat Certified Architect(红帽认证架构师)RHCT    红帽认证技师RHCE    红帽认证工程师RHEL    Red Hat Enterprise LinuxOSI(OSI/RM)        Open System Interconnection Reference ModelMAC        Media Access ControlLLC        Logic Link ContorlWLAN    Wireless Local Area NetworkNOS        Network Operation SystemTCP        Transmission Control ProtocolIP        Internet ProtocolUDP        User Datagram protocolARP        Address Resolution ProtocolRARP    Reverse Address Resolution ProtocolDHCP    Dynamic Host Configuration Protocoltty        teletypewriterGnome    GNU’s network object model environmentipconfig        Internet Protocol Configuring（IP地址配置）ifconfig        Network Interfaces Configuring（网络接口配置）css        Cascading Style Sheets
]]></content>
      <categories>
        <category>computer</category>
      </categories>
  </entry>
  <entry>
    <title>好好学Linux：Linux中安装配置FTP服务</title>
    <url>/dev-linux-ftp/</url>
    <content><![CDATA[1. 前言FTP服务是最常见的服务之一，本文中我们学习在Linux中安装配置FTP服务。
参考文档：

File Transfer Protocol
CentOS7 安装 vsftpd 服务



2. 安装vsftpd服务2.1. vsftpd简介vsftpd是在Linux发行版中最推崇的一种FTP服务器程序，vsftpd的特点：小巧轻快、安全易用等。 
2.2. centos7安装vsftpd1、安装 vsftpd 和 ftp
yum install vsftpd ftp -y

2、启动vsftpd服务
systemctl enable vsftpdsystemctl start vsftpdsystemctl status vsftpd

vsftpd启动后，默认端口号21，允许匿名访问。
3、访问测试
ftp 127.0.0.1ftp 192.168.56.101

Name 输入 anonymous ， Password 为空直接回车。登录后，当前目录为根目录，默认有个pub目录，里面为空。
2.3. 配置文件说明
/etc/vsftpd/vsftpd.conf 主配置文件
/etc/vsftpd/ftpusers 不允许访问的用户列表
/etc/vsftpd/user_list 允许或者不允许访问的用户列表
/var/log/xferlog 上传下载日志

2.4. 配置允许系统用户访问FTP1、编辑配置文件
vim /etc/vsftpd/vsftpd.conf

修改如下：
anonymous_enable=NOlocal_enable=YESwrite_enable=YESlocal_umask=022chroot_local_user=YESuserlist_enable=YESuserlist_deny=NOuserlist_file=/etc/vsftpd/user_listpam_service_name=vsftpdtcp_wrappers=YES

2、重启vsftpd
systemctl restart vsftpd

3、创建ftp用户
useradd ftpuser -d /data/ftpuserpasswd ftpuserchmod a-w /data/ftpuserecho ftpuser &gt; /etc/vsftpd/user_list

4、限制用户只能ftp登录，不能ssh登录
usermod -s /sbin/nologin ftpuserecho &quot;/sbin/nologin&quot; &gt;&gt; /etc/shells

5、访问测试
ftp 127.0.0.1ftp 192.168.56.101

Name 输入 ftpuser ， Password 输入该用户的密码。登录后，当前目录为/data/ftpuser。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>ftp</tag>
      </tags>
  </entry>
  <entry>
    <title>Win7不用软件开wifi热点</title>
    <url>/hobby-win7-no-software-open-wifi/</url>
    <content><![CDATA[1. 标准设置流程首先win+R，输入cmd，打开命令符提示界面，然后用以下命令来完成设置
netsh wlan set hostednetwork mode=allownetsh wlan set hostednetwork ssid=&quot;ihelloworld&quot;netsh wlan set hostednetwork key=&quot;helloworld&quot;netsh wlan refresh hostednetwork keynetsh wlan start hostednetwork 
如果有小伙伴不会做，请直接下载文件wifi.bat，然后双击即可。然后必须执行的一步是：
打开网络和共享中心——更改适配器设置——右击你正在使用的网络连接——属性——共享——然后把两个勾都勾上
至此，理论上你已经成功开启了名为“ihelloworld”，密码为“helloworld”的wifi。如果有什么问题，欢迎留言或直接联系我。


2. 常用命令集锦有时候通过以上命令无法成功启动无线，就需要下面的命令来调试。
显示支持的承载网络

netsh wlan show drivers

显示你的无线承载网络的信息

netsh wlan show hostednetwork

启动无线承载网络

netsh wlan start hostednetwork

停止你的无线承载网络

netsh wlan stop hostednetwork

更改网络名称

netsh wlan set hostednetwork ssid=”你的名称”

更改密码

netsh wlan set hostednetwork key=”你的密码”

如果想密码立即生效可以用

netsh wlan refresh hostednetwork key

最后就是你真的不用这个无线网络了，你停止之后，也不想看到多出来的那块无线网卡，那么就执行

netsh wlan set hostednetwork mode=

就再也看不到那块网卡了，当然你想重新开启的话就执行

netsh wlan set hostednetwork mode=allow

PS：如何设置才能让它开机自动启动？在win7系统下可以这样设置，打开记事本，输入netsh wlan start hostednetwork，保存（文件名为”启动wifi.bat”，保存类型选所有文件）。打开开始-所有程序-附件-系统工具-任务计划程序-创建基本任务-填写名称（自己命名）-下一步-选择计算机启动时-下一步-浏览-找你建的那个文件选定-下一步-完成。OK了,下次在你开机的时候就会自己启动了。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
        <tag>wifi</tag>
      </tags>
  </entry>
  <entry>
    <title>网页内容无法复制解决办法</title>
    <url>/hobby-page-content-copy/</url>
    <content><![CDATA[很多网站内容不能复制，但是由于某些原因，我们需要复制一些内容。这时，不妨试试void给出的两个方法。
方法一：1、打开http://www.360doc.com/content/15/0410/21/22116372_462274865.shtml ，我们发现，在转载之前内容无法复制。

2、Ctrl+S，保存为网页仅html。
3、双击打开刚才保存的文件，我们发现网页内容可以复制了！

方法二：我们发现，同样的方法，对起点这种神级网站无效！没关系，我们还有进阶教程！1、打开http://read.qidian.com/BookReader/2502372,42712100.aspx，无法复制。
2、同样的，Ctrl+S，保存为网页仅html，双击打开保存的文件，我们发现还是无法复制
3、选择保存的文件，用记事本打开
4、Ctrl+F，搜索关键词“全屏阅读”（关键词不唯一），“查找下一个”，直到找不到那个关键字（目的是为了定位到正文部分）
5、点击“确定”，关闭查找框，我们把“全屏阅读”那一行往上的内容全部删除
6、Ctrl+S，保存后关闭记事本。再次打开保存的html文件，发现已经可以复制了
]]></content>
      <categories>
        <category>computer</category>
      </categories>
  </entry>
  <entry>
    <title>Windows快捷键</title>
    <url>/hobby-windows-shortcut-key/</url>
    <content><![CDATA[1. f系列F1 帮助F2 改名F3 搜索F4 地址F5 刷新F6 切换F10 菜单


2. ctrl系列ctrl+A 全选ctrl+C 复制ctrl+X 剪切ctrl+V 粘贴ctrl+Z 撤消ctrl+O 打开ctrl+P 打印ctrl+S 保存ctrl+F4 关闭tab页ctrl+esc 开始菜单Ctrl+1,2,3…　切换到从左边数起第1,2,3…个标签 
3. shift系列shift+delete 永久删除shift+右键 可以选择打开powershell
4. alt系列alt+F4 关闭软件alt+enter 属性alt+tab 切换窗口alt+空格 窗口菜单 
5. win系列win 开始菜单win+F1 帮助win+D 显示桌面win+R 运行win+E 打开我的电脑win+F 搜索文件或文件夹win+U 打开设置win+tab 切换窗口win+P 多显示器设置
6. 三键组合系列ctrl+alt+delete 切换用户、注销、更改密码、任务管理器ctrl+shift+esc 任务管理器win+shift+左右键 移动窗口到另一个屏幕
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>Windows运行命令大全</title>
    <url>/hobby-windows-cmd-all/</url>
    <content><![CDATA[
gpedit.msc—–组策略 

sndrec32——-录音机

Nslookup——-IP地址侦测器

explorer——-打开资源管理器

logoff———注销命令

tsshutdn——-60秒倒计时关机命令

lusrmgr.msc—-本机用户和组

services.msc—本地服务设置

oobe/msoobe /a—-检查XP是否激活

notepad——–打开记事本

cleanmgr——-垃圾整理

net start messenger—-开始信使服务

compmgmt.msc—计算机管理

net stop messenger—–停止信使服务

conf———–启动netmeeting

dvdplay——–DVD播放器

charmap——–启动字符映射表

diskmgmt.msc—磁盘管理实用程序

calc———–启动计算器

dfrg.msc——-磁盘碎片整理程序

chkdsk.exe—–Chkdsk磁盘检查

devmgmt.msc— 设备管理器

regsvr32 /u *.dll—-停止dll文件运行

drwtsn32—— 系统医生

rononce -p —-15秒关机

dxdiag———检查DirectX信息

regedt32——-注册表编辑器

Msconfig.exe—系统配置实用程序

rsop.msc——-组策略结果集

mem.exe——–显示内存使用情况

regedit.exe—-注册表

winchat——–XP自带局域网聊天

progman——–程序管理器

winmsd———系统信息

perfmon.msc—-计算机性能监测程序

winver———检查Windows版本 

sfc /scannow—–扫描错误并复原

taskmgr—–任务管理器（2000／xp／2003

winver———检查Windows版本 

wmimgmt.msc—-打开windows管理体系结构(WMI) 

wupdmgr——–windows更新程序 

wscript——–windows脚本宿主设置 

write———-写字板 

winmsd———系统信息 

wiaacmgr——-扫描仪和照相机向导 

winchat——–XP自带局域网聊天

mem.exe——–显示内存使用情况 

Msconfig.exe—系统配置实用程序 

mplayer2——-简易widnows media player 

mspaint——–画图板 

mstsc———-远程桌面连接 

mplayer2——-媒体播放机 

magnify——–放大镜实用程序 

mmc————打开控制台 

mobsync——–同步命令

dxdiag———检查DirectX信息 

drwtsn32—— 系统医生 

devmgmt.msc— 设备管理器 

dfrg.msc——-磁盘碎片整理程序 

diskmgmt.msc—磁盘管理实用程序 

dcomcnfg——-打开系统组件服务 

ddeshare——-打开DDE共享设置 

dvdplay——–DVD播放器

net stop messenger—–停止信使服务 

net start messenger—-开始信使服务 

notepad——–打开记事本 

nslookup——-网络管理的工具向导 

ntbackup——-系统备份和还原 

narrator——-屏幕“讲述人” 

ntmsmgr.msc—-移动存储管理器 

ntmsoprq.msc—移动存储管理员操作请求 

netstat -an—-(TC)命令检查接口

syncapp——–创建一个公文包 

sysedit——–系统配置编辑器 

sigverif——-文件签名验证程序 

sndrec32——-录音机 

shrpubw——–创建共享文件夹 

secpol.msc—–本地安全策略 

syskey———系统加密，一旦加密就不能解开，保护windows xp系统的双重密码 

services.msc—本地服务设置 

Sndvol32——-音量控制程序 

sfc.exe——–系统文件检查器 

sfc /scannow—windows文件保护

tsshutdn——-60秒倒计时关机命令 

tourstart——xp简介（安装完成后出现的漫游xp程序） 

taskmgr——–任务管理器

eventvwr——-事件查看器 

eudcedit——-造字程序 

explorer——-打开资源管理器

packager——-对象包装程序 

perfmon.msc—-计算机性能监测程序 

progman——–程序管理器

regedit.exe—-注册表 

rsop.msc——-组策略结果集 

regedt32——-注册表编辑器 

rononce -p —-15秒关机 

regsvr32 /u *.dll—-停止dll文件运行 

regsvr32 /u zipfldr.dll——取消ZIP支持

cmd.exe——–CMD命令提示符 

chkdsk.exe—–Chkdsk磁盘检查 

certmgr.msc—-证书管理实用程序 

calc———–启动计算器 

charmap——–启动字符映射表 

cliconfg——-SQL SERVER 客户端网络实用程序 

Clipbrd——–剪贴板查看器 

conf———–启动netmeeting 

compmgmt.msc—计算机管理 

cleanmgr——-垃圾整理 

ciadv.msc——索引服务程序

osk————打开屏幕键盘 

odbcad32——-ODBC数据源管理器 

oobe/msoobe /a—-检查XP是否激活 

lusrmgr.msc—-本机用户和组 

logoff———注销命令

iexpress——-木马捆绑工具，系统自带

Nslookup——-IP地址侦测器

fsmgmt.msc—–共享文件夹管理器

utilman——–辅助工具管理器

gpedit.msc—–组策略


]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>Dell的BIOS设置</title>
    <url>/hobby-dell-bois-setting/</url>
    <content><![CDATA[对于戴尔预装了 Microsoft Windows 8 的笔记本电脑，BIOS 中的 Boot （启动）选项，在默认情况下的设置为



选项
值



Secure Boot
[Enabled]


Load Legacy Option Rom
[Disabled]


Boot List Option
[UEFI]


如下图所示：









在这种设置下，如果使用者希望从光驱或其它USB设备启动，有时候会发生无法引导的情况。此时，需要对 BIOS 的选项做如下修改。



选项
值



Secure Boot
[Disabled]


Load Legacy Option Rom
[Enabled]


Boot List Option
[Legacy]


修改后，能看到 Boot 选项中列出了 USB 存储与光驱等设备。
BIOS 修改好后保存退出，重启电脑的时候按键盘的 F12，重新进入引导菜单，此时就能够从连接的 USB 设备或光驱进行引导。另外请注意：对于支持 UEFI 启动的机型，如果开机无法进入系统，提示 “internal hard disk drive not found” 错误，如下图所示。
请进入 BIOS 检查硬盘在 BIOS 中是否能够被正常识别。如果可以，进入 Boot 页面，务必将 Secure Boot 改为[Disable]。此情况经常发生在用户删除了原厂预装的 Windows 8 并装为 Windows 7 或其它系统之后。因为非 Windows 8 操作系统，不能支持 Secure Boot。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>操作系统</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：CentOS小技巧</title>
    <url>/dev-centos-skill/</url>
    <content><![CDATA[1. 前言本文中，记录使用CentOS过程中的一些常用操作，备忘。
参考文档：

如何开启Centos6.4系统的SSH服务
通过SSH连接VirtualBox中的CentOS
CentOS 7 修改时区



2. 添加网络命令CentOS7 默认没有 netstat 和 ifconfig 命令，需要自己安装
yum install net-tools

3. 启用网卡1、CentOS7 查看IP
ifconfig -aip add

如果没有获取到DHCP分配的动态IP地址，那么需要修改一下网卡配置。
2、修改网卡配置
vi /etc/sysconfig/network-scripts/ifcfg-enp0s3

ONBOOT=no 修改为 ONBOOT=yes
3、重启网络服务，再次查看IP 
service network restartip add

4. 修改网卡名有些系统中，网卡名是 enp0s3 这种形式，有时候想要变成 eth0 这种形式，该怎么操作？
1、编辑grub文件
vim /etc/default/grub

找到：
GRUB_CMDLINE_LINUX=&quot;xxx net.ifnames=x biosdevname=x&quot;

其中的 net.ifnames 和 biosdevname 修改 0，其他配置不变，例如：
GRUB_CMDLINE_LINUX=&quot;xxx net.ifnames=0 biosdevname=0&quot;

2、重新生成grub配置
grub-mkconfig -o /boot/grub/grub.cfg# orgrub2-mkconfig -o /boot/grub2/grub.cfg

3、修改网卡配置，使用新的网卡名称比如enp0s3为eth0，enp0s8修改为eth1，配置方法参考文档《好好学Linux：Linux系统配置静态IP》
4、重启主机
reboot

PS：本方法在Ubuntu系统中也是适用的。
5. 修改时区CentOS7修改时区
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime# orln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

6. 修改主机名1、修改当前主机名
hostname host1

2、永久修改主机名
vim /etc/hostname

修改为想要的主机名，比如 host1
3、添加本地主机名解析（可选）
vim /etc/hosts

添加：
127.0.0.1  host1

7. 修改root密码修改root密码，passwd root
8. SSH设置1、启动CentOS，打开终端，切换到root用户。2、检查是否安装了ssh：rpm -qa | grep ssh如果没有安装ssh，则执行命令：yum install openssh-server
3、启动ssh：service sshd start如果提示:redirecting to /bin/systemctl start，那么改用命令：/bin/systemctl start sshd.service
4、检查是否已经成功启动：netstat -antp | grep sshd 如果看到如下信息，则表示启动成功。
5、设置开机自启动：chkconfig sshd on
9. 客户端连接CentOS9.1. xshell连接CentOS假设CentOS主机IP为：192.168.56.101
1、文件，新建，主机填入192.168.56.101。2、选择用户身份验证，输入用户名和密码。3、点击确定，建立连接。
如果连接失败，请检查centos的防火墙是否关闭。关闭防火墙命令为：systemctl stop firewalld.service
9.2. xftp连接CentOS假设CentOS主机IP为：192.168.56.101
1、文件，新建，主机填入192.168.56.101。2、协议选择SFTP。3、输入用户名和密码。4、点击确定，建立连接。
10. 安装中文输入法1、安装中文支持包yum install &quot;@Chinese Support&quot;
2、回到桌面，system-&gt;preferences-&gt;input method
3、如果没有，先注销一下。
4、到里面add输入法到列表里。
5、最后再注销、登录。
在centOS 6.4下面测试无误。
11. 挂载NTFS有时候做大数据量迁移时，为了快速迁移大数据，有可能在Linux服务器上临时挂载NTFS格式的移动硬盘， 一般情况下，Linux是识别不了NTFS格式移动硬盘的（需要重编译Linux核心才能，加挂NTFS分区），这时候为了能让Linux服务器能够识别NTFS的移动硬盘，就必须安装ntfs-3g（Third Generation Read/Write NTFS Driver）的包。
官方网址：http://www.tuxera.com/文档手册：http://www.tuxera.com/community/ntfs-3g-manual/下载地址：http://www.tuxera.com/community/ntfs-3g-download/
1、解压安装NTFS-3G。
tar -xvzf ntfs-3g_ntfsprogs-2012.1.15.tgzcd ntfs-3g_ntfsprogs-2012.1.15./configure make make install 

之后系统会提示安装成功，下面就可以用ntfs-3g来实现对NTFS分区的读写了
2、配置挂载NTFS格式的移动硬盘（1）首先得到NTFS分区的信息 sudo fdisk -l | grep NTFS
（2）设置挂载点，用如下命令实现挂载 mount -t ntfs-3g &lt;NTFS Partition&gt; &lt;Mount Point&gt;
例如得到的NTFS分区信息为/dev/sdc1，挂载点设置在/mnt/usb下，可以用 mount -t ntfs-3g /dev/sdc1 /mnt/usb或者直接用 ntfs-3g ntfs-3g /dev/sdc1 /mnt/usb 
3、如果想实现开机自动挂载，可以在/etc/fstab里面添加如下格式语句
&lt;NTFS Partition&gt; &lt;Mount Point&gt; ntfs-3g silent,umask=0,locale=zh_CN.utf8 0 0 

这样可以实现NTFS分区里中文文件名的显示。 
4、卸载分区可以用umount实现umount &lt;NTFS Partition&gt;或者 　　umount &lt;Mount Point&gt;
12. U盘安装系统最近要给服务器重装系统，由于使用dvd安装刻盘比较麻烦，所以决定采用U盘安装，U盘下安装CentOS/Red Hat比较麻烦，相对于Ubuntu来说要麻烦很多（Ubuntu只需要使用Universal USB Installer即可，非常简单），此方法经过本人亲测，真实有效。
这里选择在windows下使用UltraISO进行安装，建议使用bin DVD版的CentOS，不用live版本的，同时我们需要一个8GB及以上容量的U盘，因为完整版的安装包需要4G，加上启动引导部分，4G的U盘是不够的。这里简单介绍一下原理，首先使用UltraISO将DVD版的iso镜像写入U盘来制作启动盘，然后将完整的CentOS的DVD版安装iso复制到U盘根目录，待安装程序启动之后有一步会提示寻找安装镜像，选择U盘对应的目录就能找到。
具体步骤：1、下载UltraISO，选择“文件”-&gt;“打开”，找到CentOS安装文件，我的为CentOS-6.3-x86_64-bin-DVD1.iso（iso格式的）
2、选择“启动”-&gt;“写入硬盘映像…”，“硬盘驱动器”选择要安装的U盘，“写入方式”选择“USB-HDD+”，在这之前最好将U盘格式化，默认的为FAT32，最后选择“写入”，再选择“是”即可，具体过程见下图。
3、在U盘写入完毕之后，打开U盘，将Packages目录全部删除，其实那4G的安装镜像大部分都是相关系统软件以及服务的安装包，这里我们只是制作U盘引导系统安装，所以Packages目录不需要，删除之后再将CentOS-6.3-x86_64-bin-DVD1.iso复制到U盘的根目录即可。
将U盘插上电脑选择U盘启动之后就可以参考网上大量的安装步骤，不过这里有一点要注意的是：从U盘启动之后U盘被系统识别为/dev/sda，而电脑的SATA硬盘被识别为/dev/sdb。因此到了有一步让你选择安装镜像(install image)的时候请选择sda（即U盘），后面要求选择安装引导程序的时候请安装在SATA硬盘对应的设备名称上，否则如果选择安装在U盘上了之后拔掉U盘就不能启动系统了。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>ipvs入门篇</title>
    <url>/dev-ipvs-start/</url>
    <content><![CDATA[1. ipvs简介
Linux Virtual Server (LVS) is load balancing software for Linux kernel–based operating systems.


LVS is a free and open-source project started by Wensong Zhang in May 1998, subject to the requirements of the GNU General Public License (GPL), version 2. The mission of the project is to build a high-performance and highly available server for Linux using clustering technology, which provides good scalability, reliability and serviceability.


The major work of the LVS project is now to develop advanced IP load balancing software (IPVS), application-level load balancing software (KTCPVS), and cluster management components.


IPVS (IP Virtual Server) implements transport-layer load balancing, usually called Layer 4 LAN switching, as part of the Linux kernel. It’s configured via the user-space utility ipvsadm(8) tool.


IPVS is incorporated into the Linux Virtual Server (LVS), where it runs on a host and acts as a load balancer in front of a cluster of real servers. IPVS can direct requests for TCP- and UDP-based services to the real servers, and make services of the real servers appear as virtual services on a single IP address. IPVS is built on top of the Netfilter.

简而言之，lvs是一个四层负载均衡软件（项目），ipvs是lvs的一部分。ipvs内置在linux内核空间中，在用户空间使用ipvsadm对ipvs进行管理。
参考文档：

Wikipedia - Linux Virtual Server
Wikipedia - IP Virtual Server
The Linux Virtual Server Project



2. ipvs工作原理参考文档：

LVS原理详解
LVS原理详解以及部署
IPVS从入门到精通kube-proxy实现原理
什么是隧道？| 网络中的隧道
IPVS在k8s中的使用

2.1. ipvs术语
DS：Director Server。指的是前端负载均衡器节点。
RS：Real Server。后端真实的工作服务器。
VIP：Virtual IP，向外部直接面向用户请求，作为用户请求的目标的IP地址。
DIP：Director Server IP，主要用于和内部主机通讯的IP地址。
RIP：Real Server IP，后端服务器的IP地址。
CIP：Client IP，访问客户端的IP地址。

2.2. ipvs基本原理1、当用户向负载均衡调度器（Director Server）发起请求，调度器将请求发往至内核空间。2、PREROUTING链首先会接收到用户请求，判断目标IP确定是本机IP，将数据包发往INPUT链。3、IPVS是工作在INPUT链上的，当用户请求到达INPUT时，IPVS会将用户请求和自己已定义好的集群服务进行比对，如果用户请求的就是定义的集群服务，那么此时IPVS会强行修改数据包里的目标IP地址及端口，并将新的数据包发往POSTROUTING链。4、POSTROUTING链接收数据包后发现目标IP地址刚好是自己的后端服务器，那么此时通过选路，将数据包最终发送给后端的服务器。
2.3. ipvs工作模式
nat模式：通过网络地址转换，调度器重写请求报文的目标地址，根据预设的调度算法，将请求分派给后端的真实服务器；真实服务器的响应报文通过调度器时，报文的源地址被重写，再返回给客户，完成整个负载调度过程。
tunnel模式：改写请求的IP地址，将请求和响应分开处理，即在负载调度器中只负责调度请求而响应直接返回给客户，将极大地提高整个集群系统的吞吐量。
dr模式：改写请求报文的MAC地址，将请求发送到真实服务器，而真实服务器将响应直接返回给客户。这种方法没有IP隧道的开销，对集群中的真实服务器也没有必须支持IP隧道协议的要求，但是要求调度器与真实服务器都有一块网卡连在同一物理网段上。

2.4. ipvs调度算法固定调度算法：即调度器不会去判断后端服务器的繁忙与否，一如既往得将请求派发下去。具体包括rr、wrr、dh、sh动态调度算法：调度器会去判断后端服务器的繁忙程度，然后依据调度算法动态得派发请求。具体包括wlc、lc、lblc、lblcr
2.4.1. rrrr：轮询（round robin）这种算法是最简单的，就是按依次循环的方式将请求调度到不同的服务器上，该算法最大的特点就是简单。轮询算法假设所有的服务器处理请求的能力都是一样的，调度器会将所有的请求平均分配给每个真实服务器，不管后端 RS 配置和处理能力，非常均衡地分发下去。这个调度的缺点是，不管后端服务器的繁忙程度是怎样的，调度器都会讲请求依次发下去。如果A服务器上的请求很快请求完了，而B服务器的请求一直持续着，将会导致B服务器一直很忙，而A很闲，这样便没起到均衡的左右。
2.4.2. wrrwrr：加权轮询（weight round robin）这种算法比 rr 的算法多了一个权重的概念，可以给 RS 设置权重，权重越高，那么分发的请求数越多，权重的取值范围 0 – 100。主要是对rr算法的一种优化和补充， LVS 会考虑每台服务器的性能，并给每台服务器添加要给权值，如果服务器A的权值为1，服务器B的权值为2，则调度到服务器B的请求会是服务器A的2倍。权值越高的服务器，处理的请求越多。
2.4.3. dhdh：目标地址散列调度算法 （destination hash）简单的说，即将同一类型的请求分配给同一个后端服务器，例如将以 .jgp、.png等结尾的请求转发到同一个节点。这种算法其实不是为了真正意义的负载均衡，而是为了资源的分类管理。这种调度算法主要应用在使用了缓存节点的系统中，提高缓存的命中率。
2.4.4. shsh：源地址散列调度算法（source hash）即将来自同一个ip的请求发给后端的同一个服务器，如果后端服务器工作正常没有超负荷的话。这可以解决session共享的问题，但是这里有个问题，很多企业、社区、学校都是共用的一个IP，这将导致请求分配的不均衡。
2.4.5. lclc：最少连接数（least-connection）这个算法会根据后端 RS 的连接数来决定把请求分发给谁，比如 RS1 连接数比 RS2 连接数少，那么请求就优先发给 RS1。这里问题是无法做到会话保持，即session共享。
2.4.6. wlcwlc：加权最少连接数（weight least-connection）这个比最少连接数多了一个加权的概念，即在最少连接数的基础上加一个权重值，当连接数相近，权重值越大，越优先被分派请求。
2.4.7. lblclblc：基于局部性的最少连接调度算法（locality-based least-connection）将来自同一目的地址的请求分配给同一台RS如果这台服务器尚未满负荷，否则分配给连接数最小的RS，并以它为下一次分配的首先考虑。
2.4.8. lblcrlblcr：基于地址的带重复最小连接数调度 (Locality-Based Least-Connection with Replication)
3. 安装ipvsadm3.1. ubuntuapt-get install ipvsadm

3.2. centosyum install ipvsadm#systemctl start ipvsadm#systemctl enable ipvsadm

4. 使用ipvsadm4.1. ipvsadm命令帮助ipvsadm v1.27 2008/5/15 (compiled with popt and IPVS v1.2.1)Usage:  ipvsadm -A|E -t|u|f service-address [-s scheduler] [-p [timeout]] [-M netmask] [--pe persistence_engine] [-b sched-flags]  ipvsadm -D -t|u|f service-address  ipvsadm -C  ipvsadm -R  ipvsadm -S [-n]  ipvsadm -a|e -t|u|f service-address -r server-address [options]  ipvsadm -d -t|u|f service-address -r server-address  ipvsadm -L|l [options]  ipvsadm -Z [-t|u|f service-address]  ipvsadm --set tcp tcpfin udp  ipvsadm --start-daemon state [--mcast-interface interface] [--syncid sid]  ipvsadm --stop-daemon state  ipvsadm -hCommands:Either long or short options are allowed.  --add-service     -A        add virtual service with options  --edit-service    -E        edit virtual service with options  --delete-service  -D        delete virtual service  --clear           -C        clear the whole table  --restore         -R        restore rules from stdin  --save            -S        save rules to stdout  --add-server      -a        add real server with options  --edit-server     -e        edit real server with options  --delete-server   -d        delete real server  --list            -L|-l     list the table  --zero            -Z        zero counters in a service or all services  --set tcp tcpfin udp        set connection timeout values  --start-daemon              start connection sync daemon  --stop-daemon               stop connection sync daemon  --help            -h        display this help messageOptions:  --tcp-service  -t service-address   service-address is host[:port]  --udp-service  -u service-address   service-address is host[:port]  --fwmark-service  -f fwmark         fwmark is an integer greater than zero  --ipv6         -6                   fwmark entry uses IPv6  --scheduler    -s scheduler         one of rr|wrr|lc|wlc|lblc|lblcr|dh|sh|sed|nq,                                      the default scheduler is wlc.  --pe            engine              alternate persistence engine may be sip,                                      not set by default.  --persistent   -p [timeout]         persistent service  --netmask      -M netmask           persistent granularity mask  --real-server  -r server-address    server-address is host (and port)  --gatewaying   -g                   gatewaying (direct routing) (default)  --ipip         -i                   ipip encapsulation (tunneling)  --masquerading -m                   masquerading (NAT)  --weight       -w weight            capacity of real server  --u-threshold  -x uthreshold        upper threshold of connections  --l-threshold  -y lthreshold        lower threshold of connections  --mcast-interface interface         multicast interface for connection sync  --syncid sid                        syncid for connection sync (default=255)  --connection   -c                   output of current IPVS connections  --timeout                           output of timeout (tcp tcpfin udp)  --daemon                            output of daemon information  --stats                             output of statistics information  --rate                              output of rate information  --exact                             expand numbers (display exact values)  --thresholds                        output of thresholds information  --persistent-conn                   output of persistent connection info  --nosort                            disable sorting output of service/server entries  --sort                              does nothing, for backwards compatibility  --ops          -o                   one-packet scheduling  --numeric      -n                   numeric output of addresses and ports  --sched-flags  -b flags             scheduler flags (comma-separated)

参考文档：

ipvsadm(8)
ipvsadm命令 – 管理Linux虚拟服务器
LVS策略管理工具：ipvsadm命令详解

4.2. 环境描述192.168.56.254作为DS，创建虚拟服务时，虚拟服务的VIP要使用这个IP。192.168.56.102-104作为RS，上面部署nginx服务在80端口。
4.3. 查看虚拟服务和真实服务器ipvsadm -Ln

4.4. 查看ipvs模块中的链接信息ipvsadm -lnc

4.5. 查看ipvs模块中的转发情况信息ipvsadm -Ln --stats | --rate

4.6. 添加虚拟服务ipvsadm -A -t 192.168.56.254:80 -s rr

4.7. 修改虚拟服务算法ipvsadm -E -t 192.168.56.254:80 -s wlc

4.8. 删除虚拟服务ipvsadm -D -t 192.168.56.254:80

4.9. 添加真实服务器NAT模式：
ipvsadm -a -t 192.168.56.254:80 -r 192.168.56.102:80 -mipvsadm -a -t 192.168.56.254:80 -r 192.168.56.103:80 -m

NAT模式加权轮询：
ipvsadm -a -t 192.168.56.254:80 -r 192.168.56.104:80 -m -w 1

访问测试：
curl 192.168.56.254

4.10. 修改真实服务器ipvsadm -a -t 192.168.56.254:80 -r 192.168.56.102:80 -m -w 2

4.11. 删除真实服务器ipvsadm -d -t 192.168.56.254:80 -r 192.168.56.103:80

4.12. 清空转发数据计数器ipvsadm -Z

4.13. 删除所有ipvs规则ipvsadm -C

4.14. 保存ipvs规则ipvsadm-save &gt; /path/to/ipvsadm

4.15. 加载ipvs规则ipvsadm-restore &lt; /path/to/ipvsadm

4.16. VIP自定义上面的例子中，我们使用的VIP是宿主机的IP，如果想要自己再定义一个IP作为VIP，该怎么搞？答：创建一个VIP，绑定给宿主机的网卡。
例如，创建一个VIP为10.0.0.10/24，后端RIP为192.168.56.102/24
ip addr add 10.0.0.10/24 dev enp0s3:0ipvsadm -A -t 10.0.0.10:80 -s rripvsadm -a -t 10.0.0.10:80 -r 192.168.56.102:80 -mcurl 10.0.0.10







]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>ipvs</tag>
      </tags>
  </entry>
  <entry>
    <title>VM9下ubuntu安装vmtools</title>
    <url>/dev-vm9-ubuntu-vmtools/</url>
    <content><![CDATA[1.右击“ubuntu”，选择安装Vmware tools
2.设备（CD-ROM）中会出现Vmware tools
3.选中里面的压缩文件，复制到“主文件夹”（为了下面的操作方便），解压
4.ctrl+ait+t，打开终端
5.输入“cd”，回车（为了确保进入了自己的家目录）

6.输入“su”，回车，输入root用户密码，回车（这步是为了切换到root用户）
7.输入“ls -l”，回车，会看到文件列表中出现了vmware-tools-distrib这个文件夹 
8.输入“cd vmware-tools-distrib”，回车
9.输入“ls -l”，回车，会看到vmware-install.pl这个文件
10.输入“./vmware-install.pl”，回车
11.接下来一路回车
12.等到再也没有提示“yes”或“no”时，关闭窗口即可。
13.再次右击“ubuntu”，你会发现“安装vmware tools”的提示变成了“重新安装vmware tools”，至此，大功告成！
14.检验一下：重启虚拟机，从主机复制一个文件然后粘贴到虚拟机
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
      </tags>
  </entry>
  <entry>
    <title>iptables入门篇</title>
    <url>/dev-iptables-start/</url>
    <content><![CDATA[1. iptables历史防火墙，其实说白了讲，就是用于实现Linux下访问控制的功能的，它分为硬件的或者软件的防火墙两种。无论是在哪个网络中，防火墙工作的地方一定是在网络的边缘。而我们的任务就是需要去定义到底防火墙如何工作，这就是防火墙的策略，规则，以达到让它对出入网络的IP、数据进行检测。
iptables的前身叫ipfirewall （内核1.x时代），这是一个作者从freeBSD上移植过来的，能够工作在内核当中的，对数据包进行检测的一款简易访问控制工具。但是ipfirewall工作功能极其有限（它需要将所有的规则都放进内核当中，这样规则才能够运行起来，而放进内核，这个做法一般是极其困难的）。当内核发展到2.x系列的时候，软件更名为ipchains，它可以定义多条规则，将他们串起来，共同发挥作用，而现在，它叫做iptables，可以将规则组成一个列表，实现绝对详细的访问控制功能。
参考文档：

Basic iptables howto
iptables详解
ipatebles详解（1）：iptales概念
iptables命令
25个iptables常用示例
Linux防火墙iptables学习笔记（三）iptables命令详解和举例 



2. 规则链iptables工作在用户空间中，是定义规则的工具，本身并不算是防火墙。它定义的规则，可以让内核空间中的netfilter来读取，并且实现让防火墙工作。netfilter的架构就是在整个网络流程的若干位置设置一些关卡（HOOK），而在每个关卡上登记了一些规则，所以这些关卡的术语叫做规则链。
在内核空间中，netfilter选择了5个位置，来作为控制的关卡，专业点叫做规则链。朱双印大神的博客中给出了直观的图像：
根据上图，我们能够想象出某些常用场景中，报文的流向：到本机某进程的报文：PREROUTING –&gt; INPUT由本机转发的报文：PREROUTING –&gt; FORWARD –&gt; POSTROUTING由本机的某进程发出报文（通常为响应报文）：OUTPUT –&gt; POSTROUTING
五个规则链的作用如下：

INPUT链：处理输入数据包。
OUTPUT链：处理输出数据包。
PORWARD链：处理转发数据包。
PREROUTING链：用于目标地址转换（DNAT）。
POSTOUTING链：用于源地址转换（SNAT）。

3. 表的概念我们再想想另外一个问题，我们对每个”链”上都放置了一串规则，但是这些规则有些很相似，比如，A类规则都是对IP或者端口的过滤，B类规则是修改报文，那么这个时候，我们是不是能把实现相同功能的规则放在一起呢，必须能的。
我们把具有相同功能的规则的集合叫做”表”，所以说，不同功能的规则，我们可以放置在不同的表中进行管理，而iptables已经为我们定义了4种表，每种表对应了不同的功能，而我们定义的规则也都逃脱不了这4种功能的范围，所以，学习iptables之前，我们必须先搞明白每种表的作用。iptables为我们提供了如下规则的分类，或者说，iptables为我们提供了如下”表”：

filter表：负责过滤功能，防火墙；内核模块：iptables_filter
nat表：network address translation，网络地址转换功能；内核模块：iptable_nat
mangle表：拆解报文，做出修改，并重新封装的功能；iptable_mangle
raw表：关闭nat表上启用的连接追踪机制；iptable_raw

也就是说，我们自定义的所有规则，都是这四种分类中的规则，或者说，所有规则都存在于这4张”表”中。
4. 数据包流向可以将数据包通过防火墙的流程总结为下图：我们在写iptables规则的时候，要时刻牢记这张路由次序图，灵活配置规则。
5. 规则规则：根据指定的匹配条件来尝试匹配每个流经此处的报文，一旦匹配成功，则由规则后面指定的处理动作进行处理。规则由匹配条件和处理动作组成。
匹配条件分为基本匹配条件与扩展匹配条件：基本匹配条件包括源地址Source IP和目标地址 Destination IP。
扩展匹配条件包括源端口Source Port，目标端口Destination Port等等。
动作也可以分为基本动作和扩展动作，此处列出一些常用动作：

ACCEPT：允许数据包通过。
DROP：直接丢弃数据包，不给任何回应信息，这时候客户端会感觉自己的请求泥牛入海了，过了超时时间才会有反应。
REJECT：拒绝数据包通过，必要时会给数据发送端一个响应的信息，客户端刚请求就会收到拒绝的信息。
SNAT：源地址转换，解决内网用户用同一个公网地址上网的问题。
MASQUERADE：是SNAT的一种特殊形式，适用于动态的、临时会变的ip上。
DNAT：目标地址转换，解决外网用户访问内网的问题。
REDIRECT：在本机做端口映射。
LOG：在/var/log/messages文件中记录日志信息，然后将数据包传递给下一条规则，也就是说除了记录以外不对数据包做任何其他操作，仍然让下一条规则去匹配。

6. 安装iptablesubuntu
apt-get install iptables

centos
yum install iptables

7. iptables命令详解命令格式：
iptables [选项] [参数]

选项包括：
-t：指定要操纵的表；-A：向规则链中添加条目；-D：从规则链中删除条目；-I：向规则链中插入条目；-R：替换规则链中的条目；-L：显示规则链中已有的条目；-F：清楚规则链中已有的条目；-Z：清空规则链中的数据包计算器和字节计数器；-N：创建新的用户自定义规则链；-P：定义规则链中的默认目标；-h：显示帮助信息；-p：指定要匹配的数据包协议类型；-s：指定要匹配的数据包源ip地址；-d：指定要匹配的数据包目标ip地址；-j：指定要跳转的目标；-i：指定数据包进入本机的网络接口（网卡）；-o：指定数据包离开本机的网络接口（网卡）；--sport：匹配来源端口号；--dport：匹配目标端口号。

命令选项输入顺序：
iptables -t 表名 &lt;-A/I/D/R&gt; 规则链名 [规则号] &lt;-i/o 网卡名&gt; -p 协议名 &lt;-s 源IP/源子网&gt; --sport 源端口 &lt;-d 目标IP/目标子网&gt; --dport 目标端口 -j 动作

8. iptables示例8.1. 清除已有iptables规则iptables -Fiptables -Xiptables -Z

8.2. 开放指定的端口iptables -A INPUT -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT               #允许本地回环接口(即允许本机访问本机)iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT    #允许已建立的或相关连的通行iptables -A OUTPUT -j ACCEPT         #允许所有本机向外的访问iptables -A INPUT -p tcp --dport 22 -j ACCEPT    #允许访问22端口iptables -A INPUT -p tcp --dport 80 -j ACCEPT    #允许访问80端口iptables -A INPUT -p tcp --dport 21 -j ACCEPT    #允许ftp服务的21端口iptables -A INPUT -p tcp --dport 20 -j ACCEPT    #允许FTP服务的20端口iptables -A INPUT -j REJECT       #禁止其他未允许的规则访问iptables -A FORWARD -j REJECT     #禁止其他未允许的规则访问

8.3. 屏蔽IPiptables -I INPUT -s 123.45.6.7 -j DROP       #屏蔽单个IP的命令iptables -I INPUT -s 123.0.0.0/8 -j DROP      #封整个段即从123.0.0.1到123.255.255.254的命令iptables -I INPUT -s 124.45.0.0/16 -j DROP    #封IP段即从123.45.0.1到123.45.255.254的命令iptables -I INPUT -s 123.45.6.0/24 -j DROP    #封IP段即从123.45.6.1到123.45.6.254的命令是

8.4. 查看已添加的iptables规则iptables -L -n -viptables -t nat -L -viptables -t mangle -L -v

8.5. 删除已添加的iptables规则将所有iptables以序号标记显示，执行：
iptables -L -n --line-numbers

比如要删除INPUT里序号为8的规则，执行：
iptables -D INPUT 8

8.6. 备份规则文件iptables-save -t filter &gt; iptables.bak

8.7. 持久化iptables规则持久化iptables规则，重启依然生效
apt-get install iptables-persistentservice iptables-persistent save




]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>network</category>
        <category>shell</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>网络</tag>
        <tag>iptables</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux基础概念</title>
    <url>/dev-linux-basic-concepts/</url>
    <content><![CDATA[1. 粘贴位要删除一个文件，你不一定要有这个文件的写权限，但你一定要有这个文件的上级目录的写权限。也就是说，你即使没有一个文件的写权限，但你有这个文件的上级目录的写权限，你也可以把这个文件给删除，而如果没有一个目录的写权限，也就不能在这个目录下创建文件。
如何才能使一个目录既可以让任何用户写入文件，又不让用户删除这个目录下他人的文件，sticky就是能起到这个作用。stciky一般只用在目录上，用在文件上起不到什么作用。在一个目录上设了sticky位后，（如/home，权限为1777)所有的用户都可以在这个目录下创建文件，但只能删除自己创建的文件(root除外)，这就对所有用户能写的目录下的用户文件启到了保护的作用。chmod 777 abc + chmod +t abc ，等价于 chmod 1777 abc 。


2. 块大小对于单一文件，若块大小=1024，最大容量为16GB；若块大小=4096，最大容量为2TB。（根据inode的格式来看(12个直接块指针,1个间接块指针,1个二次间接块指针,1个三次间接块指针),假设磁盘块尺寸为n的话,单个文件尺寸应该是:12*n + (1/4)*n^2 + (1/16)*n^3 + (1/64)*n^4
3. 任务前后台
CTRL+Z停止进程并放入后台     
jobs显示当前暂停的进程     
bg %N使第N个任务在后台运行(%前有空格)     
fg %N使第N个任务在前台运行     
默认bg,fg不带%N时表示对最后一个进程操作

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux挂载文件系统</title>
    <url>/dev-linux-mount/</url>
    <content><![CDATA[1. mount/umount简介
mount - mount a filesystemumount - unmount file systemsAll files accessible in a Unix system are arranged in one big tree, the file hierarchy, rooted at /.  These files can be spread out over several devices. The mount command serves to attach the filesystem found on some device to the big file tree. Conversely, the umount command will detach it again.

参考文档：

Linux mount命令详解：挂载Linux系统外的文件
mount



2. 磁盘分区并格式化2.1. 使用fdisk磁盘分区1、查看磁盘
fdisk -l

2、磁盘分区
fdisk /dev/sdb

按照提示，依次输入p，n，回车（Partition number），回车（First sector），回车（Last sector），w。这种方法分区后，整块磁盘划分成了一个分区 /dev/sdb1
2.2. 使用parted磁盘分区fdisk只支持MBR分区，MBR分区表最大支撑2T的磁盘，所以无法划分大于2T的分区。而parted工具可以划分单个分区大于2T的GPT格式的分区，也可以划分普通的MBR分区。参考文档Linux parted 分区命令详解
1、查看磁盘
fdisk -l

2、磁盘分区（可选）
parted /dev/sdb

按照提示，依次输入mklabel，mkpart，print，quit。
交互示例：
GNU Parted 3.3使用 /dev/sdb欢迎使用 GNU Parted！输入 &#x27;help&#x27; 来查看命令列表。(parted) help  align-check 类型 N                         检查分区 N 是否为 (最小=min|最佳=opt) 对齐类型  help [COMMAND]                           打印通用求助信息，或 COMMAND 的帮助  mklabel,mktable LABEL-TYPE               创建新的磁盘卷标 (分区表)  mkpart 分区类型 [文件系统类型] 起始点 结束点 创建一个分区  name 编号 名称                           将指定“编号”的分区命名为“名称”  print [devices|free|list,all|数字]        显示分区表、可用设备、剩余空间、所有分区或特殊分区  quit                                     退出程序  rescue 起始点 终止点                      挽救临近“起始点”、“终止点”的遗失的分区  resizepart NUMBER END                    改变 NUMBER 的大小  rm NUMBER                                删除编号为 NUMBER 的分区  select 设备                              选择要编辑的设备  disk_set 旗标 状态                       变更已选设备上的旗标  disk_toggle [旗标]                       切换已选设备上的旗标状态  set 编号 旗标 状态                       改变指定“编号”分区的旗标  toggle [编号 [旗标]]                     切换“编号”分区上的“旗标”状态  unit 单位                                设置缺省的“单位”  version                                  显示目前 GNU Parted 的版本与版权信息(parted) mklabel新的磁盘卷标类型？ gpt(parted) mkpart分区名称？  []? sdb1文件系统类型？  [ext2]? ext4起始点？ 0%结束点？ 100%警告: 所产生的分区没有适当为获得最佳性能而对齐：34s % 2048s != 0s忽略/Ignore/放弃/Cancel? Ignore(parted) print型号：AVAGO INSPUR (scsi)磁盘 /dev/sdb: 20.0TB扇区大小 (逻辑/物理)：512B/512B分区表：gpt磁盘标志：编号  起始点  结束点  大小    文件系统  名称  标志 1    17.4kB  20.0TB  20.0TB  ext4      sdb1(parted) quit

这种方法分区后，整块磁盘划分成了一个分区 /dev/sdb1
2.3. 磁盘重新分区fdisk -l如果出现提示：分区 1 未起始于物理扇区边界。
或者mkfs.ext4 /dev/sdb1出现提示：/dev/sdb1 未对齐，偏移了 244736 个字节。这可能导致性能明显下降，建议重新进行分区。
此时请重新进行分区，否则磁盘性能会很差。fdisk指令：
命令(输入 m 获取帮助)： d

parted指令：
(parted) rm 1

2.4. 使用mkfs磁盘格式化格式化整块磁盘为ext4格式（这种情况下不需要分区）：
mkfs -t ext4 /dev/sdb

格式化某个分区为 ext4 格式：
mkfs.ext4 /dev/sdb1

格式化某个分区为 xfs 格式：
mkfs.xfs /dev/sdb1

查看分区ID：
blkid /dev/sdb1

3. 挂载磁盘3.1. 临时挂载磁盘1、创建挂载目录
mkdir /sdb

2、挂载磁盘
mount /dev/sdb1 /sdb

3.2. 开机自动挂载磁盘1、创建挂载目录
mkdir /sdb

2、添加自动挂载，编辑 /etc/fstab ，添加
# /dev/sdb1 /sdb ext4 defaults 0 0&lt;blkid&gt; /sdb ext4 defaults 0 0

3、执行挂载（根据/etc/fstab的配置）
mount -adf -h

4. 卸载磁盘umount /sdb

5. 挂载NFS在Linux中挂载NFS格式的磁盘需要使用NFS客户端。
1、安装NFS客户端
apt install nfs-common # Debian/Ubuntuyum install nfs-utils # CentOS/RedHat/Fedorapacman -S nfs-utils # Arch Linux

2、查看nfs-server的挂载点
showmount -e 192.168.56.200

3、创建挂载点，执行挂载
mkdir -p /data/nfsmount -t nfs 192.168.56.200:/data/nfs /data/nfs

如果mount卡住，大概率是因为nfs-server的问题，建议检查一下挂载点是否正常。
4、/etc/fstab中配置开机自动挂载（可选）
192.168.56.200:/data/nfs /data/nfs nfs defaults 0 0

6. 挂载NTFS在Linux中挂载NTFS格式的磁盘通常使用ntfs-3g驱动程序。
1、安装 ntfs-3g
apt-get install ntfs-3g # Debian/Ubuntuyum install ntfs-3g # CentOS/RedHat/Fedorapacman -S ntfs-3g # Arch Linux

2、找出磁盘分区
fdisk -llsblk

看到类似 /dev/sda1 或 /dev/sdb2 的分区表示法。
4、创建挂载点，执行挂载假设NTFS分区是 /dev/sdb1，挂载点为 /data/samba/wd
mkdir -p /data/samba/wdmount -t ntfs-3g /dev/sdb1 /data/samba/wd

如果想要挂载时有特定的读写权限，可以使用如下命令：
mount -t ntfs-3g -o uid=1000,gid=1000 /dev/sdb1 /data/samba/wd
其中uid和gid应该替换成你希望给予访问权限的用户和组的ID。
5、/etc/fstab中配置开机自动挂载（可选）
/dev/sdb1 /data/samba/wd ntfs-3g defaults,umask=022 0 0

7. 目录挂载到另外一个目录Linux中允许将一个目录挂载到另一个目录，这被称为”绑定挂载”(bind mount)。这种技术允许我们在文件系统的不同位置访问相同的内容。
基本用法：
# 普通挂载mount --bind /path/to/source /path/to/target# 递归挂载，适用于source中本身就存在挂载的情况mount --rbind /path/to/source /path/to/target# 只读挂载mount --bind -o ro /path/to/source /path/to/target# 取消挂载umount /path/to/target

示例：
mount --bind /data/files /mnt/files_backupmount | grep files_backupdf -h

永久挂载：
echo &#x27;/path/to/source /path/to/target none bind 0 0&#x27; &gt;&gt; /etc/fstabmount -a

8. df -h卡住问题8.1. 问题描述df -h执行后，卡住了
8.2. 解决办法一般是由于 mount 目录所在nfs服务器关机了，或者目录不存在了，造成 df -h 卡住一直不动
排查办法：
# 查看具体有哪些挂载mount# 查看具体卡在哪里了strace df -h# 查看有哪些进程使用这个挂载fuser -cu /data/nfs

解决办法：
# 卸载无效挂载umount -l /data/nfsumount -lf /data/nfs







]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>storage</category>
        <category>linux</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>问题排查</tag>
        <tag>shell</tag>
        <tag>nfs</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux学习资源</title>
    <url>/dev-linux-resources/</url>
    <content><![CDATA[1. 软件推荐linux下的图片处理软件，号称linux下的photoshop：gimp
linux下可用的qq：pidgin-lwqq
linux下的翻译软件：stardict
linux下查看chm文件：chmsee
安装windows软件：wine


2. 软件资源
linux内核下载
linux软件包下载

3. 学习资源
手把手教你如何建立自己的Linux系统
CROSS LFS
Linux From Scratch
LFS linux的安装过程
Linuxsir
Gentoo Linux文档
Unix技术网
IT技术认证考试
自动生成Makefile官方教程

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>shell</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux系统中解压命令大全</title>
    <url>/dev-linux-decompress-command-all/</url>
    <content><![CDATA[1. .tar解包：tar xvf FileName.tar打包：tar cvf FileName.tar DirName（注：tar是打包，不是压缩！）


2. .gz解压1：gunzip FileName.gz解压2：gzip -d FileName.gz压缩：gzip FileName.tar.gz 和 .tgz解压：tar zxvf FileName.tar.gz压缩：tar zcvf FileName.tar.gz DirName
例：下载了一个eclipse，想把它解压到/usr目录tar -zxvf eclipse-jee-indigo-SR2-linux-gtk-x86_64.tar.gz /usr，提示错误：
tar: /usr: Not found in archivetar: Exiting with failure status due to previous errors
原因是因为压缩文件使用的相对路径 在当前目录下找不到 /usr目录，通过使用-C指定解压目录可解决此问题tar -zxvf eclipse-jee-indigo-SR2-linux-gtk-x86_64.tar.gz -C /usr
3. .bz2解压1：bzip2 -d FileName.bz2解压2：bunzip2 FileName.bz2压缩： bzip2 -z FileName
4. .tar.bz2解压：tar jxvf FileName.tar.bz2压缩：tar jcvf FileName.tar.bz2 DirName
5. .bz解压1：bzip2 -d FileName.bz解压2：bunzip2 FileName.bz压缩：未知
6. .tar.bz解压：tar jxvf FileName.tar.bz压缩：未知
7. .Z解压：uncompress FileName.Z压缩：compress FileName
8. .tar.Z解压：tar Zxvf FileName.tar.Z压缩：tar Zcvf FileName.tar.Z DirName
9. .zip解压：unzip FileName.zip压缩：zip FileName.zip DirName
10. .rar解压：rar x FileName.rar压缩：rar a FileName.rar DirName
11. .lha解压：lha -e FileName.lha压缩：lha -a FileName.lha FileName
12. .rpm解包：rpm2cpio FileName.rpm | cpio -div
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>Fedora的一些技巧</title>
    <url>/dev-fedora-skill/</url>
    <content><![CDATA[1. 播放音乐1、audacious:
yum install Audaciousyum install audacious-plugins-*



2、RPM Fusion
因为专利许可证的原因，Fedora软件仓库不包含MP3,DVD和视频播放及解码库。正因为如此，你必须从第三方的软件仓库安装那些软件，请不要担心，这是非常容易的 :)
现在我们开始安装 RPM Fusion 软件仓库，RPM Fusion 是 Fedora 和 Red Hat企业版的软件仓库，是由Dribble, Freshrpms 和 RPM Fusion合并而来的。各种各样的应用程序包含在这个软件仓库中，比如MP3、未加密的 DVD 、Mplayer, VLX, Xine 等多媒体应用程序使用的解码库，以及闭源的 Nvidia 和 ATI 显卡驱动，RPM Fusion包含以下两个主要的软件仓库：一个被命名为“免费”，为开源软件提供（开源软件的含义通过Fedora授权指引定义），但因为美国专利保护法案又不能包含在 Fedora 中。另一个被命名为“非免费”，为非自由软件提供，就是其它所有那些不能被免费提供的，包括公开源代码的软件，但是有“非商业使用”之类的限制。
在这部分指南的最后，我保证你安装并启用了 RPM Fusion 软件仓库，所以，打开一个终端吧，输入：
rpm -ivh http://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-stable.noarch.rpmrpm -ivh http://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-stable.noarch.rpmyum update    

输入上面的指令之后，只要选择一个mp3格式文件的双击播放，就能成功的找到mp3插件，选择安装，就好了，然后选择一个wma格式的文件双击 ，就会自动的去下载wma的插件。
2. 删除图标以前在Fedora15中用wine安装了一个CS和迅雷7，但运行不太流畅，所以想要删掉面板上相关的图标。具体命令如下。
1、cd   ~/.local/share/applications/wine/Programs/2、ls 查看目录3、rm -r *递归删除所有的项目。
3. 常用命令yum install gccyum install gcc-c++yum install vimrpm -ivh adobe-release-i386-1.0-1.noarch.rpm yum install flash-pluginyum install lib* --setopt=protect_multilib=false --skip broken
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
      </tags>
  </entry>
  <entry>
    <title>使用Word2010发布WordPress博客文章</title>
    <url>/dev-word2010-deploy-wordpress-article/</url>
    <content><![CDATA[用word2010编辑发布wordpress博客文章1、打开word，选择新建博客文章


2、如果之前没有用word发布过文章，会提示你注册博客账户
3、选择立即注册，然后选择博客提供商，在此我们选择wordpress
4、把下面这张表格填写完整，就像我给出的例子那样
5、点击确定，等几秒钟会提示“账户注册成功”。
6、然后我们就可以用word来编辑文章，编辑好之后，然后点击左上角的“发布”，就会出现提示：“此文章已于某时间发布到某网站上了”。
7、查看自己的网站，会发现果然发布成功，一切如此简单！发布后的文章属于Uncategorized分类，我们可以自己在网站上修改分类，这样就大功告成了！
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>wordpress</tag>
        <tag>word</tag>
      </tags>
  </entry>
  <entry>
    <title>WordPress首页播放音乐</title>
    <url>/dev-wordpress-music/</url>
    <content><![CDATA[Wordpress首页自动播放音乐最简单方法1、登陆flash网页mp3播放器代码生成器网站，比如http://www.51119.com/play/

2、选择一种播放器格式
3、寻找MP3外链，可以到这个网站http://www.111ttt.com/up/去寻找，找到的外链像这种http://sc.111ttt.com/up/mp3/5060/973D42EA0271C8E4A1E9421D9AD2890E.mp34、粘贴到“MP3地址”那一栏中，简单设置后提交，结果如下
5、复制html代码，接着，到WordPress控制台下，在“外观（Appearance）”选项里找到“小工具（Widgets）”，点击进入，然后拖放一个“文本（Text）”小工具放到边栏（Sidebar）里，在文本框输入区域粘贴代码即可。
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>wordpress</tag>
      </tags>
  </entry>
  <entry>
    <title>WordPress使用说明</title>
    <url>/dev-wordpress-illustration/</url>
    <content><![CDATA[1. WordPress使用说明1.1. 前言WordPress是一个免费的开源项目，在GNU通用公共许可证下授权发布。是一个注重美学、易用性和网络标准的个人信息发布平台。简体中文官网：http://cn.wordpress.org
当年，刚开始接触建站，和WordPress留下了一段难忘的时光。
1.2. 本地环境由于WordPresss是使用PHP语言和MySQL数据库开发的。所以，本地搭建测试环境，需要PHP和MySQL。

这里推荐两个套件：1、XAMPPhttps://www.apachefriends.org/index.html
2、ServKit（曾经叫做PHPnow）http://servkit.org/
1.3. 本地问题解决1、Attempting to start Tomcat service…双击tomcat7.exe，提示不是有效的32位程序。但是在命令行敲startup可以正常启动。自我感觉最好的解决办法是把可以正常使用的tomcat文件夹替换掉xampp的tomcat文件夹。
2、安装xampp后apache不能启动解决方法http://blog.csdn.net/kunlong0909/article/details/7716715
3、MySQL Service detected with wrong path首先需要修改一下环境变量。应该是在安装xampp之前电脑上装过mysql，然后默认启动的是以前的mysql。修改注册表[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MySQL]的ImagePath修改成新的xampp中位置\mysql\bin\mysqld MySQL重启explorer.exe进程，使注册表生效再次点击 mysql 后边的start，OK！mysql服务正常启动！ 
4、安装wordpress过程中，访问wp-admin/install.php出错：数据库连接错误。您在wp-config.php文件中提供的数据库用户名和密码可能不正确crifan的博客
1.4. 优化1、在wordpress功能版块有登入\登出，管理，Feed以外，还有一个wordpress，如何删除它们呢，看起来使博客的所有链接实际有用！找到网站wordpress安装目录，在wp_includes文件夹下面有一个default-widgets.php的文件，在此文件里搜索查找标签里带有rss2_url，comments_rss2_url，wordpress.org的三段代码全部删除掉。
2、WordPress文章置顶1、写好文章并发布。2、点击博客后台文章菜单下的“编辑”选项，进入文章列表。3、把鼠标移到需要置顶的文章上，点击“快速编辑”选项。4、在快速编辑下“保持这篇文章置顶”前面的小框打勾，然后点击更新文章。5、更新文章后，打开博客首页就会发现文章的置顶状态了。Tips：除了使用这个方法，也可以使用WordPree的一些文章置顶插件，相对来说可能会方便许多，在这里不做推荐。
3、WordPress评论框下方“您可以使用这些 HTML 标签和属性”文字去除找到wp-includes/comment-template.php 查找：echo $args[‘comment_notes_after’];注释掉它（也可以直接删除）。
1.5. 参考文档PHP 手册http://php.net/manual/zh/

WordPress中文文档 | WordPress啦http://www.wordpress.la/codex.html

WordPress官方中文文档http://codex.wordpress.org/zh-cn:Main_Page

WordPress连接微博:个人博客开放化策略微博QQ百度账号登录http://www.freehao123.com/wordpress-weibo-qq/

怎样在wordpress网站添加小工具http://www.zhidao91.com/wordpress-add-widget-area/

如何轻松的在你的wordpress网站添加附加小工具或者导航区域http://www.ylsay029.com/how-to-add-widget-or-navigation-areas-wordpress-website.htmlhttp://www.douban.com/note/285551052/

新版京东云擎JAE云空间申请使用和安装运行WordPress博客http://www.freehao123.com/jae-wordpress/

增强Wordpress编辑器功能最简便的方法http://jingyan.baidu.com/article/7c6fb42841891b80652c906b.html

两步搞定WordPress多区域widgethttp://www.wordpress.la/widgetize-wordpress-theme.htmlhttp://www.veryhuo.com/a/view/15116.html

WordPress 博客添加个性标志favicon.icohttp://www.chinaz.com/web/2010/0928/135480.shtml

中文版标签云插件：wp-cumulushttp://plugins.wopus.org/content/tagplugin/146.html

29个实用的WordPress主题函数使用技巧.pdf、WordPress主题教程.pdf、WordPress_主题模板制作及修改教程.pdfhttp://yunpan.cn/cVbAQzcvut7FS  访问密码 dd25

wordpress代码调用大全更新到_3.0版.pdf、WordPress高级教程.pdf、如何用WORDPRESS改成CMS来开发企业站.pdfhttp://yunpan.cn/cVbAqs5Bd32pS  访问密码 82bc

]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>mysql</tag>
        <tag>php</tag>
        <tag>wordpress</tag>
      </tags>
  </entry>
  <entry>
    <title>OpenShift使用说明</title>
    <url>/dev-openshift-illustration/</url>
    <content><![CDATA[1. 前言郝同学的第一个网站，使用WordPress，搭建在OpenShift上面。OpenShift出身RedHat，稳定性和功能性都很nice。
官网：https://www.openshift.com


2. 常用命令云服务器总是出问题，可以来这里试试openshift的命令来检查测试
gem install rhc安装rhc
gem update rhc更新rhc版本
rhc-chk检测本地环境配置
rhc-user-info显示用户信息
rhc-create-domain：创建个人域
rhc-create-app创建应用
rhc-snapshot应用备份
rhc-tail-files查看应用日志
openshift: rhc setup登录
rhc app create -a  app_name -t php-5.3创建php应用
rhc app create -a wordpress -t php-5.3创建wordpress
wordpress添加mysql支持：rhc app cartridge add -a wordpress -c mysql-5.1创建一个自定义应用：rhc-create-app -a  app_name -t diy-0.1创建命名空间：rhc domain create -n mydomain -l rhlogin创建phpmyadmin：rhc app cartridge add -a  app_name -c phpmyadmin-3.4绑定域名:rhc alias add appname domian删除绑定域名: rhc alias removeappname domian如果你是创建一个wordpress，需要MongoDB支持，可以输入如下命令：rhc app cartridge add -a wordpress -c mongodb-2.2
启动应用程序：ctl_app start停止应用程序：ctl_app stop重启应用程序：ctl_app restart查看应用程序：ctl_app status
启动应用程序：ctl_all start停止应用程序：ctl_all stop重启应用程序：ctl_all restart查看应用程序：ctl_all status
3. 书签Red Hat老用户的OpenShift初体验
OpenShift红帽免费云空间申请、WordPress安装(图文教程)
RedHat Openshift 搭建个人博客(wordpress)指南
OpenShift Redhat免费空间SSH登录管理和使用:下载文件安装程序和应用
OpenShift redhat推出PaaS云计算应用平台支持PHP、Java、MySQL
openshift免费空间绑定顶级域名
友好面对开发者 OpenShift加入更多新元素
OpenShift免费空间安装PhpMyadmin及SQL数据库管理（图文教程）
OpenShift推出收费业务和解决OpenShift空间打不开和SSH无法连接
]]></content>
      <categories>
        <category>engineering</category>
        <category>uncategories</category>
      </categories>
      <tags>
        <tag>wordpress</tag>
      </tags>
  </entry>
  <entry>
    <title>yum命令</title>
    <url>/dev-yum-command/</url>
    <content><![CDATA[1. 通过yum安装和删除RPM包安装rpm包,如dhcpyum install dhcp
删除rpm包,包括与该包有依赖性的包yum remove licq注意:同时会提示删除licq-gnome,licq-qt,licq-text
2. 通过yum工具更新软件包检查可更新的rpm包：yum check-update
更新所有的rpm包：yum update

更新指定的rpm包,如更新kernel和kernel source：yum update kernel kernel-source
大规模的版本升级,与yum update不同的是,陈旧的淘汰的包也会升级：yum upgrade
3. 通过yum查询RPM包信息列出资源库中所有可以安装或更新的rpm包的信息：yum info
列出资源库中所有可以更新的rpm包的信息：yum info updates
列出已经安装的所有的rpm包的信息：yum info installed
列出已经安装的但是不包含在资源库中的rpm包的信息：yum info extras注：也就是通过其它网站下载安装的rpm包的信息。
列出资源库中所有可以安装或更新的rpm包的信息：yum list
列出资源库中特定的可以安装或更新以及已经安装的rpm包的信息：yum list sendmailyum list gcc*注意：可以在rpm包名中使用匹配符， 如上面例子是列出所有以gcc开头的rpm包的信息。
搜索匹配特定字符的rpm包的详细信息：yum search wget注意：可以通过“search”在rpm包名，包描述中进行搜索。
搜索包含特定文件名的rpm包：yum provides realplay
4. 通过yum操作暂存信息（/var/cache/yum）清除暂存的rpm包文件：yum clean packages
清除暂存的rpm头文件：yum clean  headers
 清除暂存中旧的rpm头文件和包文件：yum clean  all
5. Redhat Linux下用yum升级系统yum也可以升级Redhat Linux系统，在Redhat Linux系统安装盘中默认没有yum的安装包，由于Redhat Linux与Centos Linux基本一致，因此可以用同版本同内核的Centos Linux的yum包在Redhat Linux上进行安装。安装过程在上面章节已经讲述，这里不在多说。由于使用的是Centos Linux的yum包在Redhat Linux下进行的安装，因此在Redhat Linux下需要增加资源库，定义yum的非官方库文件，让一些必需的软件包通过yum也能够安装。首先建立dag.repo，定义非官方库：vi /etc/yum.repos.d/dag.repo
[dag]name=Dag RPM Repository for RHEL4baseurl=http://ftp.riken.jp/Linux/dag/redhat/el4/en/$basearch/dag/enabled=1gpgcheck=1
接着导入非官方库的GPG：rpm --import  http://ftp.riken.jp/Linux/caos/centos/RPM-GPG-KEY-centos4注意：此步骤很重要，如果没有导入授权的RPM-GPG-KEY，在使用yum升级安装软件时就会提示软件不合法，结合上下文可以看出，在Centos下进行yum配置的时候，并没有涉及到导入RPM-GPG-KEY，那是因为连接的资源库为Centos官方的库，而升级的系统也是Centos，当然无需授 权，而这里我们升级的系统是Redhat Linux，而用的资源文件是Centos的，所以必须导入Centos的RPM-GPG-KEY，系统才认为升级的包是合法的。 
最后，就可以使用非官方定义的rpm包升级系统：yum update
6. 实践yum install vimyum install gccyum install gcc-c++]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>centos</tag>
      </tags>
  </entry>
  <entry>
    <title>ORA-01033</title>
    <url>/dev-ora-01033/</url>
    <content><![CDATA[转载自百度百科
1、进入CMD，执行set ORACLE_SID=ORCL，确保连接到正确的SID；2、命令窗口运行sqlplus “/as sysdba” 启动窗口之后显示如下信息
SQL*Plus: Release 11.1.0.7.0 - Production on 星期三 3月 6 17:17:53 2013Copyright (c) 1982, 2008, Oracle. All rights reserved.
连接到:Oracle Database 11g Enterprise Edition Release 11.1.0.7.0 - ProductionWith the Partitioning, OLAP, Data Mining and Real Application Testing optionsSQL&gt;3、停止服务 shutdown immediateSQL&gt; shutdown immediateORA-01109: 数据库未打开

已经卸载数据库。ORACLE 例程已经关闭。4、 启动服务 startup 观察启动时有无数据文件加载报错，并记住出错数据文件标号SQL&gt; startupORACLE 例程已经启动。Total System Global Area 535662592 bytesFixed Size 1348508 bytesVariable Size 272632932 bytesDatabase Buffers 255852544 bytesRedo Buffers 5828608 bytes数据库装载完毕。ORA-16038: 日志 2 sequence# 59 无法归档ORA-19809: 超出了恢复文件数的限制ORA-00312: 联机日志 2 线程 1: ‘D:\APP\EN\ORADATA\ORCL\REDO02.LOG’
5、检查出错日志所在的组SQL&gt; select group#,sequence#,archived,status from v$log;GROUP# SEQUENCE# ARC STATUS

1 61 NO CURRENT3 60 NO INACTIVE2 59 NO INACTIVE6、修复出错的组日志信息SQL&gt; alter database clear unarchived logfile group 2;数据库已更改。7、打开数据库SQL&gt; alter database open;数据库已更改。SQL&gt;
]]></content>
      <categories>
        <category>engineering</category>
        <category>database</category>
      </categories>
      <tags>
        <tag>数据库</tag>
        <tag>oracle</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux各版本比较</title>
    <url>/hobby-linux-version-comparison/</url>
    <content><![CDATA[
Linux的核心最早由Linus Benedict Torvalds在1991年开发出来。在这之前，Richard Stallman创建了Free Software Foundation（FSF）组织以及GNU项目，并不断的编写创建GNU程序（此类程序的许可方式均为GPL: General Public License）。在不断的有杰出的程序员和开发者加入到GNU组织中后，便造就了今天我们所看到的Linux，或称GNU/Linux。


Linux的发行版本可以大体分为两类，一类是商业公司维护的发行版本，一类是社区组织维护的发行版本，前者以著名的Redhat（RHEL）为代表，后者以 Debian为代表。下面介绍一下各个发行版本的特点：Redhat，应该称为Redhat系列，包括RHEL(Redhat Enterprise Linux，也就是所谓的Redhat Advance Server，收费版本)、Fedora Core(由原来的Redhat桌面版本发展而来，免费版本)、CentOS(RHEL的社区克隆版本，免费)。Redhat应该说是在国内使用人群最多的Linux版本，甚至有人将Redhat等同于Linux，而有些老鸟更是只用这一个版本的Linux。所以这个版本的特点就是使用人群数量大，资料非常多，言下之意就是如果你有什么不明白的地方，很容易找到人来问，而且网上的一般Linux教程都是以Redhat为例来讲解的。Redhat系列的包管理方式采用的是基于RPM包的YUM包管理方式，包分发方式是编译好的二进制文件。稳定性方面RHEL和CentOS的稳定性非常好，适合于服务器使用，但是Fedora Core的稳定性较差，最好只用于桌面应用。
Debian，或者称Debian系列，包括Debian和Ubuntu等。Debian是社区类Linux的典范，是迄今为止最遵循GNU规范的Linux系统。Debian最早由Ian Murdock于1993年创建，分为三个版本分支（branch）： stable, testing和unstable。其中，unstable为最新的测试版本，其中包括最新的软件包，但是也有相对较多的bug，适合桌面用户。testing的版本都经过unstable中的测试，相对较为稳定，也支持了不少新技术（比如SMP等）。而stable一般只用于服务器，上面的软件包大部分都比较过时，但是稳定和安全性都非常的高。Debian最具特色的是apt-get /dpkg包管理方式，其实Redhat的YUM也是在模仿Debian的APT方式，但在二进制文件发行方式中，APT应该是最好的了。Debian的资料也很丰富，有很多支持的社区，有问题求教也有地方可去:)
Ubuntu严格来说不能算一个独立的发行版本，Ubuntu是基于Debian的unstable版本加强而来，可以这么说，Ubuntu就是一个拥有Debian所有的优点，以及自己所加强的优点的近乎完美的Linux桌面系统。根据选择的桌面系统不同，有三个版本可供选择，基于Gnome的Ubuntu，基于KDE的Kubuntu以及基于Xfc的Xubuntu。特点是界面非常友好，容易上手，对硬件的支持非常全面，是最适合做桌面系统的Linux发行版本。
Gentoo，伟大的Gentoo是Linux世界最年轻的发行版本，正因为年轻，所以能吸取在她之前的所有发行版本的优点，这也是Gentoo被称为最完美的Linux发行版本的原因之一。Gentoo最初由Daniel Robbins（FreeBSD的开发者之一）创建，首个稳定版本发布于2002年。由于开发者对FreeBSD的熟识，所以Gentoo拥有媲美FreeBSD的广受美誉的ports系统——Portage包管理系统。不同于APT和YUM等二进制文件分发的包管理系统，Portage是基于源代码分发的，必须编译后才能运行，对于大型软件而言比较慢，不过正因为所有软件都是在本地机器编译的，在经过各种定制的编译参数优化后，能将机器的硬件性能发挥到极致。Gentoo是所有Linux发行版本里安装最复杂的，但是又是安装完成后最便于管理的版本，也是在相同硬件环境下运行最快的版本。
最后，介绍一下FreeBSD，需要强调的是：FreeBSD并不是一个Linux系统！但FreeBSD与 Linux的用户群有相当一部分是重合的，二者支持的硬件环境也比较一致，所采用的软件也比较类似，所以可以将FreeBSD视为一个Linux版本来比较。FreeBSD拥有两个分支：stable和current。顾名思义，stable是稳定版，而current则是添加了新技术的测试版。FreeBSD采用Ports包管理系统，与Gentoo类似，基于源代码分发，必须在本地机器编后后才能运行，但是Ports系统没有Portage系统使用简便，使用起来稍微复杂一些。FreeBSD的最大特点就是稳定和高效，是作为服务器操作系统的最佳选择，但对硬件的支持没有Linux完备，所以并不适合作为桌面系统。
下面给为选择一个Linux发行版本犯愁的朋友一些建议：如果你只是需要一个桌面系统，而且既不想使用盗版，又不想花大量的钱购买商业软件，那么你就需要一款适合桌面使用的Linux发行版本了，如果你不想自己定制任何东西，不想在系统上浪费太多时间，那么很简单，你就根据自己的爱好在ubuntu、kubuntu以及xubuntu中选一款吧，三者的区别仅仅是桌面程序的不一样。
如果你需要一个桌面系统，而且还想非常灵活的定制自己的Linux系统，想让自己的机器跑得更欢，不介意在Linux系统安装方面浪费一点时间，那么你的唯一选择就是Gentoo，尽情享受Gentoo带来的自由快感吧！
如果你需要的是一个服务器系统，而且你已经非常厌烦各种Linux的配置，只是想要一个比较稳定的服务器系统而已，那么你最好的选择就是CentOS了，安装完成后，经过简单的配置就能提供非常稳定的服务了。如果你需要的是一个坚如磐石的非常稳定的服务器系统，那么你的唯一选择就是FreeBSD。
如果你需要一个稳定的服务器系统，而且想深入摸索一下Linux的各个方面的知识，想自己定制许多内容，那么我推荐你使用Gentoo。 对各种操作系统的用户分布方面的概括:　　Mac –&gt; Windows –&gt; Linux–&gt; BSD –&gt; UNIX从左边到右边，分别是”使用该OS的人里精通电脑的用户群最少”到”使用该OS的人里精通电脑的用户群最多”的过渡。我们可以看到，Linux的被放置在了中间，而BSD则更接近于右边。许多人会对此有争论，也有些人可能会感觉被冒犯了。但是，个人认为这是一个对”哪些用户使用哪些系统”相当准确的概括。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux目录结构简析</title>
    <url>/hobby-linux-directory/</url>
    <content><![CDATA[1. 前言Linux继承了unix操作系统结构清晰的特点。在linux下的文件结构非常有条理。但是，上述的优点只有在对linux相当熟悉时，才能体会到。
参考文档：Filesystem Hierarchy Standard


2. /binbin是二进制（binary）英文缩写，这个目录下存放着linux常用的程序和命令。
3. /boot在这个目录下存放的都是系统启动时要用到的程序。我们在使用lilo引导linux的时候，会用到这里的一些信息。
4. /cdrom可以将光驱文件系统挂在这个目录下。例如：mount /dev/cdrom /cdrom
5. /devdev是设备（device）的英文缩写，这个目录中包含了所有linux系统中使用的外部设备。我们可以非常方便地去访问这些外部设备，和访问一个文件，一个目录没有任何区别。例如：cd /dev/cdrom 我们就可以看到光驱中的文件了。同样道理，cd /dev/mouse 就可以看看鼠标的相关文件。在dev下，有一个null设备，如果你向这个设备写入内容，写入的内容就进入了黑洞。
6. /etcetc这个目录是linux系统中最重要的目录之一。在这个目录下存放了系统管理时要用到的各种配置文件和子目录。我们要用到的网络配置文件，文件系统，系统配置文件，设备配置信息，设置用户信息等都在这个目录下。
7. /sbin这个目录用来存放系统管理员的程序和命令。
8. /home如果我们建立一个用户，用户名是”xx”，那么在/home目录下就有一个对应的/home/xx路径，用来存放用户的主目录。
9. /liblib是库（library）英文缩写，这个目录用来存放系统动态连接共享库。几乎所有的应用程序都会用到这个目录下的共享库。

/lib：Libraries essential for the binaries in /bin and /sbin.
/usr/lib：Libraries for the binaries in /usr/bin and /usr/sbin.
/usr/local：Tertiary hierarchy for local data, specific to this host. Typically has further subdirectories (e.g., bin, lib, share).

10. /lost+found这个目录在大多数情况下都是空的。但是如果你正在工作突然停电，或是没有用正常方式关机，在你重新启动机器的时候，有些文件就会找不到应该存放的地方，对于这些文件，系统将他们放在这个目录下，就像为无家可归的人提供一个临时住所。
11. /mnt用于挂载的目录。
12. /proc可以在这个目录下获取系统信息。这些信息是在内存中，由系统自己产生的。
13. /root如果你是以超级用户的身份登录的，这个就是超级用户的主目录。
14. /tmp用来存放不同程序执行时产生的临时文件。
15. /usrunix software resource用户应用程序默认安装的路径，这是linux系统中占用硬盘空间最大的目录。用户安装的应用程序可以安装到/usr/local路径下，/usr/share主要存放一些共享文件，/usr/bin存放普通用户程序所需要用到的命令，/usr/sbin存放系统管理员程序所需要用到的程序。
16. /opt可选的应用程序的安装路径，安装在这个路径下的应用程序，它的源文件，库文件等都是处于同个目录下，这样卸载程序的时候，只需要直接删除了，不会对其他程序造成影响。
17. /varVariable files，可变文件。在系统正常运行期间，其内容预计会不断更改的文件，如日志和缓存。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：使用Ubuntu时遇到的问题</title>
    <url>/dev-ubuntu-problem/</url>
    <content><![CDATA[1. 前言本文记录使用Ubuntu系统时遇到的一些问题和解决办法。


2. 安装Ubuntu系统特别慢2.1. 问题描述ubuntu安装过程“retrieving file 43 of 105”就停住了。
2.2. 解决办法文件系统不对，重新分区，再次安装。
3. Ubuntu22启动报错ACPI BIOS Error3.1. 问题描述Ubuntu22，无论是从U盘启动，还是从硬盘启动，都会报错：
ACPI BIOS Error (bug): Could not resolve symbol[\SHAD._STA.SDSO]AE_NOT_FOUND (20221020/psargs-330).../dev/mapper/vgubuntu-root: clean, 152762/60891136 files, 6004107/243557376 blocks

然后，启动卡死。但是，如果从U盘启动时选择使用 Ubuntu(safe graphics)，是可以正常启动系统的。
3.2. 解决办法原因猜测：Ubuntu内核和主板BIOS存在兼容问题。ACPI（高级配置和电源接口）是一种电源管理技术，用于控制计算机的硬件设备和节能。ACPI+BIOS错误是指在计算机的BIOS配置中存在一些问题，导致ACPI无法正常工作。
解决办法：修改grub配置
1、开机选择Advanced options
2、选择进入recovery mode
3、选择 root    Drop to root shell prompt
4、修改grub
vim /etc/default/grub

GRUB_CMDLINE_LINUX_DEFAULT 部分修改为：
GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet splash nomodeset&quot;

5、更新grub
update-grub

6、重启系统
reboot

修改grub配置后，在启动时依然会出现上面的报错，但是最后会输出一行：
EDAC skx: ECC is disabled on imc 0
然后，就可以正常启动了。
参考文档：ubuntu: acpi bios error (bug) could not resolve symbol解决
4. root账号问题4.1. root账户未激活在安装系统时，root账户并没有被激活来供我们使用，即root帐号被隐藏了，而是通过初始用户与sudo的结合使用来完成一些需要root权限的任务，增强了系统的安全性。
4.2. 解决办法如果我们确实需要使用root用户来完成一些工作的话，可以使用以下命令激活root用户：
法一：在终端中输入：sudo passwd root，然后输入两次root用户的密码，重启后就可以登陆root用户了。若想禁用 root 帐号： sudo passwd -l root
法二：1、重启电脑，选择recovery模式2、找到最下边的root选项3、在recovery模式的root用户下创建一个root用户，输入：passwd root4、两次输入新密码即可
5. vim方向键乱码问题5.1. 问题描述ubuntu下使用vim时方向键变乱码，退格键不能使用。
5.2. 解决方法重新安装vim
sudo apt-get remove vim-commonsudo apt-get install vim

如果安装失败，提示：Package vim is not available, but is referred to by another package.
请先执行：
sudo apt-get update

6. 无法使用root用户登录图形界面6.1. 问题描述Ubuntu12.10，无法使用root用户登录图形界面。
6.2. 解决办法1、先设定一个root的密码
sudo passwd root

2、root 登陆
su root

3、备份一下lightgdm
cp -p /etc/lightdm/lightdm.conf /etc/lightdm/lightdm.conf.bak

4、编辑lightdm.conf
sudo gedit /etc/lightdm/lightdm.conf

修改为：
[SeatDefaults]greeter-session=unity-greeteruser-session=ubuntugreeter-show-manual-login=true

重启登陆即可。已经可以输入root了。
注意：如果root登陆后没声音，是因为pulseaudio没有启动。解决办法：将root加到pulse-access组
sudo usermod -a -G pulse-access root
然后修改配置文件/etc/default/pulseaudio，将PULSEAUDIO_SYSTEM_START设为1。
7. NTFS文件系统权限问题7.1. 问题描述修改 ubuntu NTFS 文件系统中的文件，提示没有执行权限。
7.2. 解决办法由于NTFS本身的特殊性，不能对其分区的文件权限进行修改，无论是suodo还是root都没有用。
安装以下两个插件：
sudo apt-get install ntfs-3gsudo apt-get install ntfs-config

图形界面打开ntfs-config，配置ntfs权限，全开。
再看NTFS目录下的所有文件，权限都有了。不过还是不能用chmod命令来修改。
同时，ntfs-config可以帮助用户自动挂载所有硬盘分区：
sudo ntfs-config，自动挂载所有分区。
实际就是在/etc/fstab中添加相应的挂载信息，如不需要，可以直接删掉，重启后就不再自动挂载了。
PS：查看硬盘分区UUID sudo blkid
8. 回环设备/dev/loop0占用100%8.1. 问题描述Ubuntu20.04，回环设备使用率100%
Filesystem      Size  Used Avail Use% Mounted onudev            3.9G     0  3.9G   0% /devtmpfs           796M  1.9M  794M   1% /run/dev/sda1        20G   18G  1.1G  95% /tmpfs           3.9G     0  3.9G   0% /dev/shmtmpfs           5.0M  4.0K  5.0M   1% /run/locktmpfs           3.9G     0  3.9G   0% /sys/fs/cgroup/dev/loop0       15M   15M     0 100% /snap/gnome-characters/254/dev/loop2       90M   90M     0 100% /snap/core/6673/dev/loop1      3.8M  3.8M     0 100% /snap/gnome-system-monitor/77/dev/loop3      152M  152M     0 100% /snap/gnome-3-28-1804/31/dev/loop4       36M   36M     0 100% /snap/gtk-common-themes/1198/dev/loop7      1.0M  1.0M     0 100% /snap/gnome-logs/61/dev/loop6       54M   54M     0 100% /snap/core18/941/dev/loop5      4.2M  4.2M     0 100% /snap/gnome-calculator/406tmpfs           796M   36K  795M   1% /run/user/1000

8.2. 解决办法清理回环设备挂载
sudo apt autoremove --purge snapd

8.3. 扩展阅读/dev/loopn这些设备在Linux下被称为回环设备。1、查看回环设备
losetup

2、挂载xxx.ios镜像
losetup /dev/loop0 xxx.isomount /dev/loop0 /media

等同于：
mount -o loop xxx.iso /media

9. 很多命令报错：输入/输出错误9.1. 问题描述主机上执行IO相关的命令时，会报错：输入/输出错误例如：
# df -h-bash: /usr/bin/df: 输入/输出错误# tail test.txt-bash: /usr/bin/tail: 输入/输出错误# top-bash: /usr/bin/top: 输入/输出错误

9.2. 问题原因文件系统或者磁盘坏掉了，需要检修。
10. rsa密钥对无法免密登录10.1. 问题描述系统A是Ubuntu22系统，系统B是CentOS7.9系统。在系统A的 $HOME/.ssh/authorized_keys 中添加了系统B的公钥，但是从系统B登录系统A，依然提示需要密码。
10.2. 解决办法参考文档：Ubuntu 22.04 SSH the RSA key isn’t working since upgrading from 20.04
由于各种安全漏洞，RSA SHA-1 哈希算法在操作系统和 SSH 客户端中迅速被弃用，其中许多技术现在完全否认使用此算法。Ubuntu22 中，RSA 公私密钥对被认为不再安全，因此默认不再支持RSA。
解决办法一：使用更安全的加密算法密钥对，比如 ed25519
ssh-keygen -t ed25519

解决办法二：修改Ubuntu22的sshd配置，使之继续支持rsa
1、系统A上修改sshd配置
vim /etc/ssh/sshd_config

添加如下配置：
HostKeyAlgorithms +ssh-rsaPubkeyAcceptedKeyTypes +ssh-rsa

2、系统A上重启sshd
systemctl restart sshd

11. 安装软件报错ubuntu无法修正错误11.1. 问题描述ubuntu20中，执行 apt install nvidia-driver-535 报错：
E: 无法修正错误，因为您要求某些软件包保持现状，就是它们破坏了软件包间的依赖关系。

11.2. 解决办法apt install -y aptitudeaptitude install nvidia-driver-535

参考文档：ubuntu 无法修正错误，因为您要求某些软件包保持现状，就是它们破坏了软件包间的依赖关系
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>troubleshooting</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
        <tag>ubuntu</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux命令大全 RSTUVWXYZ</title>
    <url>/dev-linux-command-all-rstuvwxyz/</url>
    <content><![CDATA[本文摘自《Linux/UNIX指令范例速查手册》。
1. R1.1. raidstart
raidstart: RAID start，启动软件的硬盘阵列

1.2. raidstop
raidstop: RAID stop，关闭软件的硬盘阵列

1.3. rc-status
rc-status: runlevel command status，显示服务器的启动状态

rc-status，列出当前运行等级下的服务状态。
rc-status boot，列出运行等级为boot下的服务状态。
1.4. rc-update
rc-update: runlevel command update，显示与控制服务器的启动状态

1.5. rcp
rcp: remote copy，远程复制文件或目录

1.6. reboot
reboot: reboot，重新启动系统

reboot，重新启动系统。
1.7. renice
renice: renice，调整正在运行进程的优先级

renice -1 1772，将进程号码为1772的进程优先级改为-1。
1.8. repquota
repquota: report of quota，检查硬盘容量限制

1.9. resize2fs
resize2fs: resize file system，调整文件系统的大小

df -humount /dev/sda2e2fsck -f /dev/sda2resize2fs /dev/sda2 100Mmount /dev/sda2 /homedf -h 
将文件系统/dev/sda2的大小调整为100MB。
1.10. restore
restore: restore，回存dump所产生的数据

restore -r -f dump.txt，将文件dump.txt还原到dump所备份的位置。
1.11. rlogin
rlogin: remote login，远程登录主机

rlogin 10.1.1.3，默认以当前root用户登录10.1.1.3主机。
rlogin 10.1.1.3 -l mark，使用mark身份登录10.1.1.3主机。
1.12. rm
rm: remove，删除文件或目录

rm -i *.c，删除所有.c文件，删除前逐一询问确认。　　rm -rf finished，将 finished 目录及子目录中所有文件删除，不再确认。
　　 
1.13. rmdir
rmdir: remove directory，删除目录

rmdir AAA，将当前目录下名为 AAA 的目录删除。　　rmdir -p BBB/Test，在当前目录下的 BBB 目录中，删除名为 Test 的子目录。若 Test 删除后，BBB 目录成为空目录，则 BBB 也删除。 
1.14. rmmod
rmmod: remove modules，删除加载的模块

1.15. route
route: route，显示或设置路由

route add -net 192.168.0.0 netmask 255.255.255.0 dev eth0，通过设备eth0在网段192.168.0.0中增加一个路由。
route del default gw 10.1.1.1route add default gw 10.1.1.2
将原网关地址10.1.1.1改为10.1.1.2。
1.16. rpm
rpm: Red Had package management，管理RPM软件包

rpm -ivh postfix-2.5.6-i386.rpm，使用rpm命令安装postfix。
rpm -qi wget，显示wget详细的安装信息。
rpm -e sendmail，移除sendmail。
1.17. rsh
rsh: remote shell，远程登录的shell

rsh matt@10.1.1.5 /bin/ls，使用matt账号登录10.1.1.5并运行/bin/ls。
1.18. runlevel
runlevel: run level，显示目前的运行等级

2. S2.1. scp
scp: secure copy，使用加密连接复制文件

scp testfile john@10.1.1.2:/home/john/，将本机的testfile文件以john的身份复制到10.1.1.2（默认端口为22）上。
2.2. screen
screen: screen，多重窗口管理进程

screen，使用screen命令在同一个终端机下打开两个窗口，并运行不同的进程。
2.3. sed
sed: stream editor，文件内容修改

sed &#39;2,4d&#39; testfile，将testfile的第2~4行删除。
sed &#39;s/is/error/&#39; testfile，将testfile中的每行第一个is字符串换成error。
sed &#39;s/is/error/g&#39; testfile，将testfile中的所有is字符串换成error。
2.4. service
service: service，打开或关闭服务

service --status-all，显示服务器目前的状态。
service postfix start，启动postfix服务器。
2.5. sestatus
sestatus: SELinux status，显示SELinux的状态

sestatus，显示SELinux的当前状态。
sestatus -b，显示SELinux中布尔值的状态。
2.6. set
set: set，查看和设置环境变量

set，查看系统默认的环境变量。
set SHELL &quot;/bin/csh&quot;，将变量名称SHELL设为/bin/csh。
2.7. setenforce
setenforce: set enforce，启用或取消SELinux的限制

setenforce 0，取消SELinux的所有限制。
setenforce 1，打开SELinux的限制。
2.8. setsebool
setsebool: set SELinux boolean，设置SELinux的布尔值

setsebool samba_enable_home_dirs 1，允许samba共享账号的家目录。
setsebool ftp_home_dir 1，允许账号ftp进入自己的家目录。
2.9. showmount
showmount: show mount，显示NFS文件挂载的状态

showmount -e 172.20.11.1，显示172.20.11.1这台NFS服务器所共享的目录。
2.10. shutdown
shutdown: shut down，关闭系统

shutdown -h now，立即关机。
shutdown -h 0，立即关机。
shutdown 5 &quot;system will be off in 5 mins&quot;，5分钟后关机，并在每个终端机窗口提示。
2.11. sleep
sleep: sleep，暂停计时

date;sleep 10s;date，显示目前时间后延迟10秒，之后再次显示时间。
2.12. sln
sln: static link，新建文件间的软连接

sln /etc/hosts newhost，建立一个静态的连接。
2.13. slogin
slogin: SSH login，远程加密的连接

2.14. slocate
slocate: security enhanced locate，寻找文件或目录

2.15. smbpasswd
smbpasswd: samba password，改变samba账号的密码

smbpasswd -a devin，在samba服务器上新建一个devin账号。
smbpasswd -d devin，暂停账号devin对samba服务器的使用权限。
2.16. smbstatus
smbstatus: samba status，显示samba服务器的状态

2.17. sort
sort: sort，将文本文件的内容重新排序

sort -t: /etc/passwd，将passwd中的内容按照账户的字母顺序排序。
ps -ef | sort，对于进程依照运行的账户排序。
2.18. split
split: split，分割文件

split -b 1000 vsftpd.conf，将vsftpd.conf分割为小文件，每个文件最大为1000Byte。
split -l 30 -d vsftpd.conf，将vsftpd.conf分割为小文件，每个小文件的行数最多为30行，且小文件的文件名以数字来区别。
2.19. ssh
ssh: secure shell，远程加密的连接

ssh 10.1.1.2，通过ssh连接到主机10.1.1.2。
ssh 172.20.11.1 -p 12345 -l macro，连接到172.20.11.1，使用端口12345，并使用marco账户。
2.20. stat
stat: status，显示文件或文件系统的状态

stat vsftpd.conf，显示文件vsftpd.conf的状态。
stat -f /dev/sda1，显示文件系统/dev/sda1的状态。
2.21. su
su: substitute user，切换用户

su，默认切换root账号。
su voidking，切换为voidking账号。
2.22. sudo
sudo: substitute user to do something，使用指定的账号权限运行进程

sudo -u www vi /etc/httpd/conf/httpd.conf，使用身份www编辑/etc/httpd/conf/httpd.conf。
2.23. sum
sum: sum，计算并显示文件的标识符

sum squid.conf，显示文件的标识符。
2.24. suspend
suspend: suspend，暂停当前所使用的shell

suspend -f，停止目前的shell。
2.25. swapoff
swapoff: swap off，关闭交换区空间

swapoff /dev/sda2，关闭定义在/dev/sda2上的交换区空间。
swapoff -a，关闭所有定义在/dev/fstab上的交换区空间。
2.26. swapon
swapon: swap on，挂载交换区空间

swapon /dev/sda2，挂载/dev/sda2上的交换区空间。
swapon -a，挂载所有定义在/dev/fstab上的交换区空间。
2.27. sync
sync: synchronize，将内存中的数据存回硬盘

sync，将内存中的数据写回硬盘。
2.28. sysctl
sysctl: system control，设置内核参数

sysctl -a，列出正在使用的内核参数。
sysctl -w net.ipv4.ip_forward=1，将net.ipv4.ip_forward设为1。
3. T3.1. tac
tac: 颠倒的cat，从文件内容从尾到头显示

tac file.txt，将文件从尾到头以反序显示。
3.2. tail
tail: tail，显示文件后面的部分

tail -n 10 file.txt，显示文件最后10行。
tail -f /var/log/maillog，持续监控文件/var/log/maillog，只要文件有新内容，就在屏幕输出。
3.3. tar
tar: tape archive，打包文件

tar -cvf mail.tar /var/spool/mail，将账号的邮件文件打包为mail.tar。
tar -xvf mail.tar，将一个打包文件解压缩到当前目录之下。
tar -xvf mail.tar -C ./tmp/，将打包文件解压缩到/tmp下。
tar -zcvf conf.tar.gz /etc/*.conf，使用tar打包文件，并只用gzip压缩该文件。
tar -zxvf conf.tar.gz -C ./tmp/，将打包文件解压缩到/tmp下。
tar -jcvf conf.tar.bz2 /etc/*.conf，使用tar打包文件，并使用bzip2压缩该文件。
tar -jxvf conf.tar.bz2 -C ./tmp/，将打包文件解压缩到/tmp下。
3.4. tcpdump
tcpdump: TCP dump，显示网络上TCP的状态

tcpdump -i eth0，显示网络设备eth0上的数据包状态。
3.5. tee
tee: tee，读取文件并输出

tee -a file，接着输入附加内容，ctrl+C跳出。
3.6. telinit
telinit: tell init，切换系统目前的运行等级

runleveltelinit 3runlevel
将目前的运行等级改为3。
3.7. telnet
telnet: tel net，远程连接程序

telnet 172.20.11.1，连接到172.20.11.1上的telnet服务。
telnet localhost 25，连接到本机的25端口（邮件服务器使用的端口）。
3.8. tftp
tftp: trivial FTP，文件传输

# tftp localhost&gt; help&gt; quit
连接到本机tftp服务器。
3.9. time
time: time，统计时间消耗

time ps -aux，获得执行ps -aux的结果和所花费的系统资源。
3.10. top
top: top，查看目前的进程状态

top，显示所有的进程与统计信息。
top -u smmsp，显示账号smmsp所运行的进程与统计信息。
3.11. touch
touch: touch，更改文件的时间标记

touch file，新建一个名为file的文件。
touch vsftpd.conf，更改已存在文件的时间标记。
touch -d &quot;6:03pm&quot; file touch -d &quot;05/06/2000&quot; file touch -d &quot;6:03pm 05/06/2000&quot; file 
将 file 的时间记录改成 5 月 6 日 18 点 3 分，公元两千年。时间可以使用 am，pm 或是 24 小时的格式，日期可以使用其他格式如 6 May 2000。
3.12. tr
tr: translation，转换或更改文件中的字符

cat testfile | tr line abcd，将line字符串换成abcd（l换成a，i换成b，n换成c，e换成d）。
echo &quot;this is a test&quot; | tr a-z A-Z &gt; test.txt，转换“this is a test”为大写，并且存入test.txt文件。
cat test.txt | tr -d this，去掉字符串中的t、h、i、s四个字符。
tr -s &quot;this&quot; &quot;TEST&quot;，字符串中的t换成T、h换成E，i换成S，s换成T。
3.13. tracepath
tracepath: trace path，追踪网络连接的路径

tracepath 210.75.20.211，追踪连接到210.75.20.211的路径。
3.14. traceroute
traceroute: traceroute，追踪连接所经过的路由器

traceroute 210.75.20.211，显示连接到210.75.20.211所经过的路由器。
3.15. tune2fs
tune2fs: tune ext2 file system，调整文件系统的参数

tune2fs -l /dev/sda1，列出/dev/sda1的相关信息。
tune2fs -j /dev/sda1，将文件系统由ext2调整为ext3。
4. U4.1. ulimit
ulimit: user limit，控制系统资源

ulimit -a，显示当前的系统资源使用限制。
ulimit -l 102400，设置所有用户所能使用的内存为102400KB。
4.2. umask
umask: user file creation mode mask，设置新建文件的屏蔽权限

umask，显示默认的文件权限。
umask -S，用较易阅读的方式显示文件权限。
4.3. umount
umount: un-mount，卸载文件系统

umount /cdrom，卸载已挂载的的目录/cdrom。
umount -a，卸载所有定义在/etc/mtab中的文件系统。
4.4. unalias
unalias: un-alias，删除别名设置

unalias ll，将已定义的别名ll删除。
4.5. uname
uname: UNIX name，显示系统信息

uname -a，显示所有信息。
uname -i，显示硬件平台。
4.6. uncompress
uncompress: un-compress，解压缩Z格式的压缩文件

uncompress -v test.Z，解压缩test.Z文件并且显示详细信息。
4.7. uniq
uniq: unique，删除文件中重复的行

uniq testfile，将文件唯一化（重复的行显示一行）。
uniq -d testfile，显示文件中重复出现的行。
4.8. unset
unset: un-set，删除变量设置

unset color，将变量color的设置删除。
4.9. unzip
unzip: un-zip，解压缩zip文件

unzip -l file.zip，显示压缩文件file.zip中的文件列表。
unzip file.zip，解压缩file.zip。
4.10. uptime
uptime: up time，显示系统已经运行的时间

uptime，显示系统已运行的时间。
4.11. useradd
useradd: user add，新建账号

useradd -m max，新增一个用户max。
useradd -u 1001 -d /opt/jerry -m jerry，新增一个用户jerry，并指定该用户UID为1001，且家目录为/opt/jerry。
4.12. userdel
userdel: user del，删除账号

userdel peter，删除用户peter。不加任何参数，仅删除该用户，不会删除该用户的家目录。
4.13. usermod
usermod: user mode，修改账号设置

usermod -d /data/john john，将用户john的登录目录改为/data/john。
usermod -e 01/31/2018 alex，将用户alex的有效期限设为2018年1月31日。
usermod -l alex peter，将账号peter改为alex。
usermod -g users alex，将用户alex的群组改为users。
4.14. users
users: user status，显示登录账号

users，显示登录的用户名称。
5. V5.1. vi
vi: view，文本编辑

vi install.log，编辑文件install.log。
vi/vim编辑器常用命令与用法总结
5.2. view
view: view，文本编辑

view是一个连接到vi的连接文件，与vi完全相同。
5.3. vim
vim: vi improved，文本编辑

vim是vi的升级版，用法参考vi。
5.4. vlock
vlock: virtual lock，锁定虚拟控制台的使用权

vlock，将目前的窗口锁定。
5.5. vmstat
vmstat: virtual memory status，显示虚拟内存的状态

vmstat，显示虚拟内存目前的使用状况。
vmstat -d，显示磁盘的使用状况。
vmstat -s，显示多样的数据与统计信息。
6. W6.1. w
w: who，显示目前登录的账号信息

w，显示所有用户的数据。
6.2. wait
wait: wait，等待程序传回信息

wait 3305，等待进程为3305的程序传回值。
6.3. wall
wall: wall，广播信息

wall hi，传讯息”hi”给每一个用户。
6.4. watch
watch: watch，全屏输出命令的运行结果

watch -n 5 tail /var/log/messages，每个5s运行一次tail /var/log/messages。
6.5. wc
wc: word count，计算文件的字节数、字数或行数

wc -c install.log，显示install.log的字符数。
wc -l install.log，显示install.log的行数。
6.6. wget
wget: WWW get，从指定的网站下载文件

wget http://apache.mirror.com/httpd/httpd-2.2.8.tar.gz，下载httpd-2.2.8.tar.gz。
wget -t 5 http://apache.mirror.com/httpd/httpd-2.2.8.tar.gz，下载httpd-2.2.8.tar.gz，最多尝试5次。
6.7. whatis
whatis: what is，查找在线帮助的位置

whatis kill，寻找命令kill的在线帮助所在的位置。
6.8. whereis
whereis: where is，查找相关的文件

whereis kill，查找命令kill的相关文件。
whereis hosts.allow，查找host.allow的相关文件。
6.9. which
which: which，查找指定的文件

which cat，寻找文件cat。
6.10. who
who: who，显示目前登录的账号信息

who，显示当前登录系统的账户。
who -q，显示当前登录的用户名称及总人数。
who -r，显示当前的运行等级。
6.11. whoami
whoami: who am i，显示账号名称

whoami，显示自己的用户名称。
6.12. write
write: write，发送信息给其他账号

write Rollaend，传讯息给Rollaend，此时 Rollaend 只有一个连线。接下来就是将讯息打上去，结束按 ctrl+c。
write Rollaend pts/2，传讯息给 Rollaend，Rollaend 的连线有 pts/2，pts/3。注意：若对方设定 mesg n，则此时讯息将无法传给对方。
7. X7.1. xauth
xauth: X-authentication，编辑X服务器的授权信息

xauth，进入命令行互动模式。
7.2. xhost
xhost: X-host，管理存取X服务器的权限

xhost，显示目前的xhost状态。
xhost +192.168.0.1，允许地址192.168.0.1对本机的X服务器有访问权限。
7.3. xset
xset: X-set，设置X-window的参数

xset s on，打开屏幕保护功能。
xset q，显示目前状态。
8. Y8.1. yes
yes: yes，响应相同的字符串

yes hello，利用yes命令重复响应字符串hello。
8.2. yum
yum: yellowdog updater modified，RPM软件包的高级管理

yum check-update，列出所有可升级的软件包。
yum install lynx，安装lynx软件包。
yum update webalizer，升级webalizer软件包。
# yum shell&gt; update vino&gt; run&gt; exit
使用shell升级vino软件包。
9. Z9.1. zcat
zcat: zip concatenate，列出压缩文件中的文件内容

zcat file.zip，查看压缩文件中的内容，只能列出压缩文件内的第一个文件的内容。
9.2. zgrep
zgrep: gz grep，查找gz或Z文件中特定的字符串

zgrep line3 file.Z，用zgrep命令查找压缩文件中包含line3的字符串。
zgrep line3 file.gz，用zgrep命令查找压缩文件中包含line3的字符串。
9.3. zip
zip: zip，将文件压缩为zip格式

zip file.Z file*，将文件名file开头的文件压缩为file.Z。
zip -d file.Z file1.zip，承接上例，移除压缩文件file.Z中的file1.zip。
9.4. zipgrep
zipgrep: zip grep，查找zip文件中特定的字符串

zipgrep Th file1.zip，寻找压缩文件file1.zip中包含字符串Th的文件，并列出该行。
9.5. zipinfo
zipinfo: zip information，显示zip压缩文件的信息

zipinfo file.Z，显示文件file.Z的压缩内容。
9.6. zless
zless: zip less，显示gz或Z文件的内容

zless file.gz，查看压缩文件的内容。
9.7. znew
znew: Z new compression，将Z文件重新压缩为gz文件

znew file.Z，将文件file.Z转换为file.gz。若Z文件中包含多个文件，将无法使用这个命令。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux命令大全 LMNOPQ</title>
    <url>/dev-linux-command-all-lmnopq/</url>
    <content><![CDATA[本文摘自《Linux/UNIX指令范例速查手册》。
1. L1.1. last
last: last login，显示曾登录的账号

last，显示曾登录的账号。
last -x，显示系统关机参数与运行等级。
1.2. lastb
lastb: last bad login，显示登录失败的账号

lastb，显示登录失败的账号。
lastb -i，显示IP地址，而不显示主机名称。
1.3. ldd
ldd: library dependencies，列出与文件有关的函数库

ldd /bin/netstat，显示/bin/netstat所使用的共享函数库。
ldd /bin/cat，显示/bin/cat所使用的共享函数库。
1.4. less
less: less，显示文件内容

less ezhttp.log，分页查看ezhttp.log文件。使用pgup和pgdn上下翻页，使用q退出。
ps -ef | less，ps查看进程信息并通过less分页显示。
1.5. lilo
lilo: LInux LOader，开机启动程序

lilo -v -v -v，设置完/etc/lilo.conf开机配置后，使之开机时生效，指定显示第三级模式。
1.6. ln
ln: link，新建文件之间的连接

ln -s yy zz，将文件yy产生一个符号链接zz。　　ln yy xx，将文件yy产生一个硬链接zz。
1.7. lndir
lindir: link directory，新建目录之间的连接

lndir /etc/vsftpd/，新建目录的连接。
1.8. lnstat
lnstat: linux network statistics，列出网络数据统计信息。

lnstat -d，列出支持的统计文件。
1.9. locate
locate: locate，在系统中查找包含特定字符串的文件

locate mysql.sock，在整个系统中查找mysql.sock的文件。 
locate -n 100 a.out，在整个系统中查找a.out文件，但最多只显示100个。
locate -u，建立资料库。
系统如果找不到locate命令，需要先安装locate，yum install mlocate，然后更新locate的数据库，updatedb。
1.10. logname
logname: login name，列出登录的账号

logname，显示最开始登录系统的账号。
1.11. logrotate
logrotate: log rotate，定期或定量将日志文件压缩备份

logrotate /etc/logrotate.conf，执行logrotate命令并采用/etc/logrotate.conf中的设置。
1.12. logsave
logsave: log save，将制定程序的输出存为日志文件

logsave ps.txt ps，将ps的输出记录到文件ps.txt中。
1.13. lp
lp: line printer，打印文件

lp file1，将file1通过默认的打印机输入。
1.14. lpq
lpq: line printer queue，列出正在等待打印机的队列

lpq，列出目前打印机的队列状态。
1.15. lprm
lprm: line printer remove，删除正在打印的任务

lprm -，取消所有的打印任务。
1.16. ls
ls: list，列出目录或文件名

ls -ltr s*，列出当前工作目录下所有名称是s开头的文件，新的排后面。　　ls -lR /home，将 /home 目录以下所有目录及文件详细资料列出。　　ls -AF，列出目前工作目录下所有文件及目录。目录名称后会加 “/“，可执行文件名称后会加”*”，链接文件后会加”@”。
1.17. lsattr
lsattr: list attribute，列出ext2或ext3系统中文件的属性

lsattr，列出当前文件的类别。
1.18. lsmod
lsmod: list module，列出内核模块的使用状态

lsmod，列出（部分）内核模块在RedHat与Fedora上的使用状态。
1.19. lsusb
lsusb: list usb，列出所有USB设备。

lsusb，列出目前的USB设备。
1.20. lynx
lynx: 由大学实验室中命名而来，文字界面上显示网页内容

lynx www.google.com，通过lynx命令在终端机上浏览网页。
2. M2.1. mail
mail: mail，收发邮件

mail -s &quot;test mail&quot; voidking@qq.com，信件主题为“test mail”，然后输入右键内容。信件结束时，输入一个点并按enter键。然后输入发件人的email地址，没有就按enter键。 
如果没有mail命令，需要先安装mailx，yum install mailx。
2.2. mailstats
mailstats: mail status，显示目前的邮件状态

mailstats，列出目前的邮件统计表。
2.3. mailq
mailq: mail queque，列出队列中的邮件

mailq，列出所有在队列中尚未寄出邮件。
2.4. make
make: make gcc program，维护或编译程序组

make -C /etc/mail，在RedHad下编译sendmail的配置文件。
cd /usr/src/linuxmakemake modules_installmake install
运行编译内核的编译顺序。
2.5. makemap
makemap: make map files，产生sendmail的数据库文件

makemap -l，列出支持的转换文件类型。
makemap hash /etc/mail/access.db &lt; /etc/mail/access，通过/etc/mail/access产生/etc/mail/access.db转换文件。
2.6. man
man: manual，显示在线帮助信息

man kill，显示kill命令说明。
man -K kill，显示所有与kill有关的说明。
2.7. manpath
manpath: manual path，显示在线帮助的搜索路径

manpath，显示在线帮助的搜索路径。
2.8. md5sum
md5sum: MD5 check sum，计算并显示MD5 sum。

md5sum file1，检验文件file1的MD5 sum。
cat checktxt，md5sum -c check.txt，检验check.txt中所记载的MD5 sum是否正确。
2.9. mesg
mesg: message，控制终端机的写入权限

mesg，查看其他人对当前终端机的写入权限。
mesg n，关闭其他人对当前终端机的写入权限。
2.10. mkbootdisk
mkbootdisk: make boot disk，制作启动盘

mkbootdisk --device /dev/fd0 --verbose 2.6.33，使用2.6.33的内核制作启动盘。
2.11. mkdir
mkdir: make directory，新建目录

mkdir temp，在当前目录下新建temp子目录。
mkdir -p /opt/www/test，新建所有不存在的目录和上层目录。
2.12. mke2fs
mke2fs: make ext2/ext3 file system，格式化为ext2、ext3或ext4的文件系统

mke2fs /dev/sda3，将分区格式化为ext2的文件系统。
mke2fs -j /dev/sda3，将分区格式化为ext3的文件系统。
mke2fs -t ext4 /dev/sda3，将分区格式化为ext4的文件系统。
2.13. mkfs
mkfs: make file system，格式化文件系统

mkfs /dev/sda1，将分区/dev/sda1格式化为默认的ext2文件系统。
2.14. mkfs.xfs
mkfs.xfs: make XFS file system，格式化为xfs的文件系统

mkfs.xfs /dev/sdc3，将分区格式化为xfs的文件系统。
2.15. mkinitrd
mkinitrd: make initial ramdisk images，建立ramdisk的镜像文件

uname -a，创建一个镜像文件。
mkinitrd /boot/initrd-new.img 2.6.33-85.fc13.i686.PAE，创建一个镜像文件。
2.16. mkreiserfs
mkreiserfs: make reiser file system，格式化为reiserfs的文件系统

mkreiserfs /dev/sda1，将分区/dev/sda1格式化为reiserfs的文件系统。
2.17. mkswap
mkswap: make swap，新建swap空间

mkswap /dev/sda2，新建一个swap空间。
2.18. modinfo
modinfo: module information，显示内核模块的信息

modinfo mii，显示mii模块的信息。
modinfo -a snd，只显示snd模块的作者信息。
2.19. modprobe
modeprobe: module probe，从内核中新建或删除模块

modprobe -l ext*，显示名称以ext开头的模块名称。
modprobe --show-depends ext2，显示与ext2有关的模块名称。
2.20. more
more: more，显示文件内容

more -s testfile，逐页显示 testfile 的文件内容，如有连续两行以上空白行则以一行空白行显示。
more +20 testfile，从第 20 行开始显示 testfile 之文件内容。
2.21. mount
mount: mount，挂载文件系统

mount，显示当前的分区状态。
mount -t xfs /dev/sda2 /opt，将分区/dev/sda2挂载到/opt上，并指定文件系统为xfs。
mount -t ext3 server1://data /opt，挂载NFS服务器所共享的文件系统。
mount -t smbfs -o username=tom,password=123 //10.1.1.1/TL /tmp，挂载windows系统的网上邻居中所共享的文件系统。
2.22. mtools
mtools: MSDOS tools，显示mtools所支持的命令

mtools，显示所有支持MSDOS文件系统的命令。
2.23. mutt
mutt: mail user agent，文字界面的邮件工具

mutt -s &quot;A test mail&quot; josfeng@gmail.com，将邮件寄给 &#106;&#111;&#115;&#x66;&#x65;&#110;&#x67;&#64;&#103;&#x6d;&#97;&#105;&#x6c;&#x2e;&#99;&#111;&#109;，信件主题为A test mail。
2.24. mv
mv: move，移动或重命名文件或目录

mv aaa bbb，将文件 aaa 更名为 bbb。　　mv *.c finished，将所有的.c文件移动到 finished 目录中。
3. N3.1. nano
nano: Nano’s another editor，文本编辑

3.2. ncftp
ncftp: new command line FTP，传送与接收文件

# ncftp -u max -p abc123 172.20.11.1&gt; get readme&gt; bye&gt; yes
使用ncftp命令下载一个readme文件。
3.3. netstat
netstat: net status，查询网络目前的状态

netstat -nt，显示目前TCP的连接状态。
netstat -apt，显示目前TCP应用进程所使用的端口号。
3.4. nice
nice: nice，更改优先级

nicenice -n 1 /bin/bashnice
调整shell的优先级。
3.5. nohup
nohup: no hup，后台运行指定的程序

nohup script1 &amp;，在后台运行script1，且在脱机后仍可继续运行。
3.6. nslookup
nslookup: name server lookup，域名与IP地址的对应

# nslookup&gt; www.163.com&gt; exit
查询www.163.com的网站地址。
# nslookup&gt; set type=mx&gt; qq.com
查询qq.com邮件服务器的地址。
4. O4.1. od
od: octal dump，以八进制编码输出文件内容

od file，以八进制编码输出文件内容。
od -t c file，以ASCCII码显示文件file的内容。
5. P5.1. passwd
passwd: password，修改密码

passwd，一般账号修改密码。
passwd mark，修改账号mark的密码。
passwd -l peter，将peter账号停用。
passwd -u peter，将peter账号启用。
5.2. paste
paste: paste，合并文件的内容

paste file1 file2，将两个文件按列合并。
5.3. patch
patch: patch，补丁更新

patch file file.patch，以补丁文件file.patch修补文件file。
patch b file file.patch，以补丁文件file.patch修补文件file，并备份原文件。
5.4. pg
pg: pagewise，显示文件内容

pg aaa，使用pg显示aaa这个文件。
5.5. pgrep
pgrep: process grep，根据PID显示进程

pgrep gdm，列出与字符串gdm有关的PID。
5.6. pico
pico: pine composer，文本编辑

5.7. pidof
pidof: process ID of something，查找进程的PID

pidof nfs，显示进程nfs所用的PID。
5.8. pine
pine: 作者命名，文字界面的邮件工具

新版Linux中，pine已被alpine所取代。
5.9. ping
ping: 乒乓碰撞声，用特定的数据包测试主机是否在线

ping -c 5 www.sina.com.cn，发送5次ICMP echo数据包，并显示统计结果。
ping -s 120 192.168.1.1，使用大小为120Byte的数据包进行测试。
ping -r www.sina.com.cn，不通过网关，直接传送数据包。
5.10. pkill
pkill: process kill，传送信号给指定的进程

pkill -9 sendmail，将正在运行且含有sendmail的进程终止。
5.11. pmap
pmap: process map，显示进程的内存对应

pmap 2245，显示进程2245的运行状态。
5.12. postalias
postalias: postfix aliases，产生postfix的aliases数据库文件

5.13. postmap
postmap: postfix map，产生postfix的access数据库文件

5.14. postqueue
postqueue: postfix queue，postfix队列区的控制命令

mailq，显示在mailq队列中的邮件。
postqueue -f，强制传送队列中的邮件。
5.15. postsuper
postsuper: postfix super，postfix邮件队列的高级管理

mailq，postsuper -d B175，删除邮件B175。
postsuper -d ALL，删除所有在队列中的E-mail。
5.16. pr
pr: print，打印前的重新排版

5.17. ps
ps: process，显示目前的进程

ps，显示当前账号所运行的进程。
ps -ef，完成地列出所有账号的进程。
ps aux，列出所有账号的进程，以及该进程所有的CUP和内存比例。
5.18. pstree
pstree: process tree，以树状表示目前的进程

pstree，以树状表示目前的进程运行状况。
5.19. pwck
pwck: passwod check，检查密码文件的正确性

5.20. pwconv
pwconv: password convert，转换为投影密码

5.21. pwd
pwd: print the working directory，显示当前所在的目录

pwd，显示当前所在的目录。
5.22. pwunconv
pwunconv: password convert，还原投影密码

6. Q6.1. quota
quota: quota，显示并限制账号的硬盘用量

quota，显示自己的硬盘用量。
quota mark，显示账号mark的硬盘用量。
6.2. quotacheck
quotacheck: quota check，检查账号硬盘空间的限制

6.3. quotaoff
quotaoff: quota off，关闭账号硬盘空间的限制

6.4. quotaon
quotaon: quota on，开启账号硬盘空间的限制

6.5. quotastats
quotastats: quota status，显示账号硬盘空间限制的统计数据

]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux命令大全 EFGHIJK</title>
    <url>/dev-linux-command-all-efghijk/</url>
    <content><![CDATA[本文摘自《Linux/UNIX指令范例速查手册》。
1. E1.1. e2fsck
e2fsck: ext2 file system check，检查ext2和ext3文件系统

e2fsck /dev/sda1，检查/dev/sda1的状态是否正常。如果发现异常，则会询问是否修复。
1.2. e2label
e2label: ext2 label，设置ext2和ext3文件系统卷标

e2label /dev/sda1 Boot，将文件系统/dev/sda1的卷标设置为Boot。
e2label /dev/sda1，显示文件系统/dev/sda1的卷标。
1.3. echo
echo: echo，显示文字

echo &quot;This is a test&quot;，将字符串This is a test显示到屏幕上。
echo &quot;Test: \t example1\nTest: \t example2&quot;，将字符串进行格式化的编排。
w，echo &quot;This is a test&quot; &gt; /dev/pts/1，将字符串This is a test显示到其他终端机/dev/pts/1上。
1.4. ed
ed: editor，文本编辑

ed file，编辑文件file。
ed不常用，一般使用vi。
1.5. edquota
edquota: edit quota，编辑账号或组所能使用的硬盘容量。

edquota karen，修改账号karen的quota用量。
edquota -p karen john，将karen的设置套用在john上。
1.6. egrep
egrep: grep -e，查找文件中的特定字符串

egrep 127.0 /etc/*，列出/etc下包含127.0字符串的所有文件。
1.7. eject
eject: eject，光驱的弹出与收回

eject，弹出光驱。
eject -j，收回光驱。
eject /dev/cdrom1，弹出指定光驱。
1.8. emerge
emerge: emerge，软件包安装与管理命令

emerge --sync，同步目前最新软件包名称。
emerge -pv apache，emerge -u apache，将apache升级到最新版本。
emerge -u world，将所有软件包升级到最新版本。
1.9. enable
enable: enable，启动或关闭shell的默认命令

enable -a，显示当前shell的所有启动的命令。
enable -n cd，关闭shell内置的命令cd。
1.10. eval
eval: evaluate，运算求出参数的内容

a1 = &quot;This is a book&quot;a2 = \$a1echo $a2eval echo $a2

1.11. ex
ex: vi in execution mode，文件编辑

ex file1，编辑文件file1。
ex相当于vi -e。
1.12. exit
exit: exit，退出当前shell

exit，退出并关闭当前的窗口。
1.13. export
export: export，设置环境变量

export exp=2.71828，echo $exp，将变量exp设置为2.71828。
export，列出当前的环境变量。
1.14. expr
expr: expression，求表达式变量的值

expr length &quot;this is a test&quot;，计算字符串长度。 
expr 14 % 9，计算余数。
expr substr &quot;this is a test&quot; 3 5，从位置处抓取字串。
expr index &quot;testforthegame&quot; e，计算第一个e出现的位置。
2. F2.1. fc
fc: first command，修改或使用曾经使用的命令

fc -l，列出运行过的指令。
fc -e vi，用vi修改最后运行的指令，修改完自动运行。
2.2. fdisk
fdisk: formatted disk，设置硬盘分区

fdisk -l，列出第一块SCSI硬盘上的分区表。
fdisk /dev/sda，进入分区管理。

输入 m 显示所有命令提示。
输入 p 显示硬盘分割情形。
输入 a 设定硬盘启动区。
输入 n 设定新的硬盘分割区。输入 e 硬盘为[延伸]分割区(extend)，输入 p 硬盘为[主要]分割区(primary)。
输入 t 改变硬盘分割区属性。
输入 d 删除硬盘分割区属性。
输入 q 结束不存入硬盘分割区属性。
输入 w 结束并写入硬盘分割区属性。

2.3. fg
fg: front ground，将进程放到前台运行

tail -f /var/log/maillog &amp;，fg tail，将该进程放到前台运行。
2.4. fgrep
fgrep: grep -f，查找文件中的字符串

fgrep 127.0 /etc/*，列出/etc下文件中包含字符串127.0的所有文件。
2.5. file
file: file，显示文件类型

file /etc/hosts，显示一般文件。
file /etc/view，显示连接文件。
2.6. filefrag
filefrag: file fragment，显示文件的破碎状态

filefrag -v backupfile，检查文件backupfile的破碎状态。
2.7. find
find: find，查找特定的文件或目录名称

find . -name *.c，将目前目录及其子目录下所有扩展名是.c的文件列出来。
find / -name mysql.sock，在整个系统中查找mysql.sock文件。　　find . -type f，将目前目录其其下子目录中所有一般文件列出。　　find . -ctime -20，将目前目录及其子目录下所有最近20分钟内更新过的文件列出。　 
2.8. findfs
findfs: find file system，用标签或UUID查找文件系统

findfs LABEL=/，查找名为/的文件系统。
findfs LABEL=SWAP-sda6，查找名为SWAP-sda6的文件系统。
2.9. finger
finger: finger，远程查询主机上的账号信息

finger scfeng@localhost，查询本机账号scfeng的状态。
finger是早期远程查询命令，近年来由于安全考虑，几乎没有用户安装finger软件包。
2.10. fixfiles
fixfiles: fix files SELinux context，修正文件的SELinux标签

fixfiles restore /etc/vsftpd/*，修正/etc/vsftpd/目录下所有文件的标签。
2.11. fmt
fmt: formatter，文件编排

cat file，fmt -w 30 file，进行固定宽度文件编排。
2.12. fold
fold: fold，修改文件的显示宽度

cat file，fold -w 20 file，进行固定宽度文件编排。
2.13. free
free: free，显示内存使用状况

free，查看内容使用状况。
free -t，查询内存目前的状态，并列出物理内存与虚拟内存的总和。
2.14. fsck
fsck: file system check，检查或修复文件系统

在ext2文件系统下，和e2fsck功能完全相同。
2.15. ftp
ftp: file transferring protocol，文件传输

ftp 10.0.0.2，put file，bye，使用ftp上传一个名为file的文件。
2.16. ftpcount
ftpcount: FTP count，显示当前使用FTP的人数

ftpcount，查看当前登录FTP的人数。
2.17. ftpshut
ftpshut: FTP shutdown，停止ProFTP服务器

ftpshut -d 3，3min后关闭FTP服务器。
2.18. ftpwho
ftpwho: FTP who，显示当前登录FTP的名单

ftpwho，查看当前登录FTP的名单。
2.19. fuser
fuser: file and process user，通过文件或sockets确认进程

fuser -l，列出可以使用的系统信号。
fuser -km /home，删除与/home有关的程序。
3. G3.1. gcc
gcc: GNU cc complier，C语言编译工具

gcc count.c，将文件count.c编译为可执行文件。
./a.out，运行a.out。
gcc count.c -o cc，将文件count.c编译为可执行文件，并指定可执行文件的名称为cc。
3.2. getsebool
getsebool: get SELinux boolean，显示SELinux的布尔值

getsebool ftp_home_dir，显示是否允许通过vsftpd连接到账号的家目录。
getsebool httpd_enable_cgi，显示是否允许httpd执行cgi script。
3.3. gpasswd
gpasswd: group password，管理/etc/group的高级工具

gpasswd elex，修改elex组的组密码。
gpasswd -a feng users，将账号feng到users组中。
gpasswd -d feng users，将feng从users组中删除。
gpasswd -A feng users，将feng设为users组中管理员。
3.4. gpm
gpm: graphic cut and paste manager，设置鼠标的粘贴功能

gpm -t ps2，启动PS/2鼠标。
3.5. grep
grep: global search regular expression，查找文件中的字符串

grep -c sum count.c，显示count.c中包含字符串sum的行数。
grep -v sum count.c，显示count.c中不含字符串sum的行。
grep -f file1 file2，搜寻file2中与file1有相同字符串的内容。
3.6. groupadd
groupadd: group add，新建组

groupadd admin，新建名为admin的组。
groupadd -r super，新建一个名为super的系统组。
groupadd -g 566 spot，新建一个组号为566，名为spot的组。
3.7. groupdel
groupdel: group del，删除组

groupdel admin，删除名为admin的组。
3.8. groupmod
groupmod: group mode，修改组的高级内容

groupmod -n admin super，将组super的名称改成admin。
groupmod -g 666 spot，将组spot的组号改为666。
3.9. groups
groups: groups，显示账号所属的组

groups admin，显示账号admin所属的组名称。
3.10. grpconv
grpconv: group convert，转换为组投影密码

3.11. gunzip
gunzip: GNU un-zip，解压缩gz文件

gunzip -l /var/log/* .gz，显示目录/var/log下所有的gz文件的信息。
gunzip -c file.gz &gt; file2，将file.gz解压缩，并保留原压缩文件。
gunzip -r /home/mark，将/home/mark下的所有gz文件全部解压缩。
gunzip -v file.gz，将file.gz解压缩，并显示过程。
3.12. gzexe
gzexe: GNU zip execution，运行压缩文件

gzexe -d a.out，运行已压缩可执行文件的a.out。
3.13. gzip
gzip: GNU zip，压缩gz的文件

gzip -v output，压缩output，并显示压缩过程。
gzip h*，压缩当前目录下所有文件名以h开头的文件。
gzip -9 backupfile1，指定压缩率压缩文件。
gzip -l /var/log/*.gz，显示目录/var/log/下所有gz文件的信息。
4. H4.1. halt
halt: halt，关闭系统

halt -p，关闭系统并关闭电源。
halt -d，关闭系统，并且不记录在日志文件/var/log/wtmp中。
halt -n，不将内存数据写入硬盘，直接关闭系统。
4.2. hash
hash: hash table

hash -l，显示记忆的命令。
hash -t cat，列出命令cat的路径。
4.3. hdparm
hdparm: hard disk parameter，显示或设置硬盘参数

hdparm -t /dev/sda，评估硬盘的读取效率。
hdparm -d 1 /dev/sda，启动硬盘的DMA模式。
hdparm -I /dev/sda，侦测硬盘的规格。
hdparm -C /dev/sda，侦测IDE硬盘电源管理模式。
4.4. head
head: head of file，输出文件内容前面的内容

head -n 3 install.log，显示前3行内容。
head -c 30 install.log，显示前30字节的内容。
4.5. help
help: help，shell内置命令说明

help alias，显示alias命令的说明。
4.6. history
history: history，列出使用过的命令

history 5，列出5个最近使用过的命令。
4.7. host
host: host，查询主机使用的域名

host www.taobao.com 61.139.2.69，在DNS服务器上61.139.2.69上查询地址www.taobao.com。
host -t mx 126.com 61.139.2.69，在机器61.139.2.69上查询网域126.com的邮件记录。
4.8. hostid
hostid: host id，显示主机ID

hostid，显示主机的识别码。
4.9. hostname
hostname: host name，显示或设置主机名

hostname，显示当前的主机名称。
hostname -d，显示当前的网域名称。
hostname -i，查询主机名对应的IP地址。
4.10. htpasswd
htpasswd: httpd passwd，设置Apache的账户密码

htpasswd -c /etc/htpasswd jack，新建一个Apache登录账号jack。
4.11. httpd
httpd: HTTP deamon，管理Apache网页服务器。

httpd -v，显示当前的apache详细信息。
httpd -f /opt/httpd.conf，使用指定的配置文件启动httpd。
httpd -t，测试配置文件的语法是否正确。
httpd -l，显示httpd编译时所包含的模块。
4.12. hwclock
hwclock: hardware clock，显示或设置硬件时间

hwclock，显示硬件日期与时间。
hwclock --set --date=&quot;5/1/11 12:15:01&quot;，将硬件时钟修改为2011年5月1日12点15分01秒。
5. I5.1. iconv
iconv: internet conversion，字符集的转换

iconv -l，列出所有支持的格式。
5.2. id
id: identity，显示账号与组的ID

id -a，显示所有的账号信息。
id -g，显示账号所属的主组代码。
id -u，显示账号代码。
5.3. ifconfig
ifconfig: interface configuration，设置或查看网络配置

ifconfig，显示当前的网络设备及其状态。
ifconfig eth0 192.168.1.5 netmask 255.255.255.0，将IP地址设置为192.168.1.5，子网掩码设置为255.255.255.0。
ifconfig eth0，显示eth0的状态。
ifconfig eth0 down，将eth0停用。
5.4. info
info: information，显示在线帮助信息

info kill，查看kill的在线帮助信息。
5.5. init
init: initial，改变系统的运行等级

init 0，关闭计算机。
init 6，重新开机。
init 1，进入单用户模式。
5.6. insmod
insmod: insert module，价值模块

insmod brdcom.ko，加载模块brdcom.ko。
5.7. ip
ip: internet protocol，显示或设置网络设备的路由策略


ip link：网络设备设置。
ip address：IP地址的管理。
ip route：路由表的管理。
ip neighbour：邻近地址与ARP表的管理。
ip tunnel：IP通道设置。
ip maddr：组广播地址的管理。
ip rule：组广播地址的管理。
ip mroute：列出组路由地址。

ip address show，显示当前网络地址的设置。
ip route show，显示当前的路由列表。
ip route add 172.16.1.0/24 via 192.168.1.1，多重路由的设置：发往172.16.1.0/24的数据包，一律通过192.168.1.1传送。
5.8. ipcrm
ipcrm: interprocess communication remove，删除指定ID的IPC进程。

ipcs，ipcrm -m 262149，显示内部程序目前的状态，并将其中的共享内存删除。
5.9. ipcs
ipcs: interprocess communication status，显示IPC的状态

ipcs，显示内部程序目前的状态。
5.10. iptab
iptab: IP table，显示子网掩码的种类

iptab，显示子网掩码的种类。
5.11. iptables
iptables: IP tables，数据包处理与安全管理

iptables -L，显示当前iptables的设置。
iptables -F，iptables -X，将iptables中过滤表格的规则清楚。
echo &quot;1&quot; &gt; /proc/sys/net/ipv4/ip_forwardingiptables -t nat -A POSTROUTING -o eth0 -s 10.1.1.1/24 -j MASQUERADE
开启NAT功能，设置10.1.1.1~10.1.1.254可通过本机连接到互联网。
iptables -A INPUT -p tcp --dport 25 -j ACCEPTiptables -A INPUT -p tcp --dport 80 -j ACCEPTiptables -A INPUT -p tcp -i eth0 -j DROP
仅开启SMTP与HTTP的连接，关闭其他端口的连接。
iptables -A input -d 140.111.1.1 -p tcp -j DROP，不得连到IP地址140.111.1.1。
5.12. iptables-save
iptables-save: IP tables save，保存当前iptables的规则

iptables-save，保存当前iptables的规则。
5.13. isosize
isosize: ISO size，显示iso9600格式的文件系统大小

isosize /dev/hdc，显示当前光盘的容量。
6. J6.1. jobs
jobs: job status，显示正在后台运行的任务

jobs，显示在后台运行的任务。
jobs -p，仅列出在后台运行的任务的PID。
6.2. join
join: join，合并两个文件中相同的区域

join -t &#39;:&#39; /etc/passwd /etc/shadow，将两个文件结合，以冒号作为字符串的分隔符。
7. K7.1. kill
kill: kill，传送信息给进程

kill -l，列出所有的信号与代码。
ps -ef | grep mysql，kill -9 6887，查看mysql的PID，并且结束该PID。
7.2. killall
killall: kill all，根据给定名称终止进程

killall -9 ntop，将所有关于ntop命令的程序删除。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>好好学Linux：Linux命令大全 ABCD</title>
    <url>/dev-linux-command-all-abcd/</url>
    <content><![CDATA[本文摘自《Linux/UNIX指令范例速查手册》。
1. A1.1. adduser
adduser: add user，新建系统上的账号

adduser -D，显示新建账号时的默认值。
adduser -m jos，新建名为jos的账号（使用系统默认值）。
adduser位于/usr/sbin/之下，是/usr/sbin/useradd的连接。也就是说，adduser和useradd实际上是同一个命令。


1.2. alias
alias: alias，定义命令及参数的别名

alias，列出现有的别名设置。
alias ua=&#39;uname -a&#39;，将uname -a的别名设置为ua。
alias的优先级高于path（系统搜寻的路径）。
1.3. apachectl
apachectl: apache controller，管理Apache网页服务器

apachectl -l，列出编入apache的模块。
apachectl restart，重启apache。
1.4. apt-get
apt-get: advanced package tool get，APT软件包管理工具。

apt-get install mailx，安装mailx软件包。
apt-get是Linux发行商Debian与Ubuntu上的软件包管理工具，其他版本Linux无法使用。
1.5. ar
ar: archives，打包和解压缩文件

ar -rv afile a*，将以a开头的文件打包为afile文件。
ar -t afile，列出打包文件中的成员文件。
ar -p afile anaconda-ks.cfg，显示打包文件中某一文件的内容。
ar命令已被tar所取代，目前已很少使用。
1.6. arch
arch: architecture，列出处理器的类型

arch，列出处理器的类型。
1.7. arp
arp: address resolution protocol，网卡地址的对应

arp，列出arp的信息。
arp -s 10.1.1.10 00:0F:26:2A:BF:77，将10.1.1.10强制对应到网卡号00:0F:26:2A:BF:77。
arp -d 10.1.1.10，删除IP地址与网卡号的对应。
1.8. arping
arping: ARP ping，网卡地址的测试命令

arping 172.20.11.1，对172.20.11.1的IP地址进行网卡地址测试。
若不在同一个网络，arping不会有回应，这时需要用ping命令。
1.9. at
at: at，在指定的时间运行命令

at 5pm + 3 days /bin/ls，三天后的下午 5 点执行 /bin/ls。　　at 5pm + 3 weeks /bin/ls，三个星期后的下午 5 点执行 /bin/ls。　　at 17:20 tomorrow /bin/date，明天的 17:20 执行 /bin/date。　　at 23:59 12/31/1999 echo the end of world !，在1999年的最后一天的最后一分钟印出 the end of world !  
at -l，列出将要运行的工作。
at -c 1，显示工作编号为1的工作。
at -d 1，删除编号为1的工作。
1.10. awk
awk: Alfred Aho, Peter Weinberger, and Brian Kernighan(作者名)，文字数据的高级处理。

awk &#39;&#123;print&#125;&#39; /etc/passwd，显示/etc/passwd中内容，和cat命令结果相同。
awk -F&quot;:&quot; &#39;&#123;print $1 $3 $6&#125;&#39; /etc/passwd，将/etc/passwd中的内容以冒号分隔，并取出第1位、第3位和第6位。
awk -F&quot;:&quot; &#39;&#123;print $1 &quot;\t&quot; $3 &quot;\t&quot; $6&#125;&#39; /etc/passwd，将/etc/passwd中的内容以冒号分隔，并取出第1位、第3位和第6位，并用Tab作为字段间的分隔符。
awk -F&quot;:&quot; &#39;&#123;print &quot;ID=&quot; $1 &quot;\t 家目录=&quot; $6&#125;&#39; /etc/passwd，将/etc/passwd中的内容以冒号分隔，并取出第1位和第6位，并用Tab作为字段间的分隔符，在第1位前加上“ID=”，第6位前加上“家目录=”。

2. B2.1. badblocks
badblocks: bad blocks，检查硬盘中损坏的区块

badblocks -v /dev/sda1，检查损坏的区块，并显示详细信息。
适用于ext2和ext3文件系统。
2.2. batch
batch: batch，运行批次作业

batch -f com.txt，运行文件com.txt中的命令。
2.3. bc
bc: arbitrary precision calculator，文字型计算器

bc，进入计算器。可以做四则运算，也可以定义变量并做运算。
2.4. bg
bg: background，将进程放到后台运行

cat /var/log/messages | more，然后ctrl+z暂时中断程序。再运行bg 1，其中1为工作编号。
将正在运行的进程移到后台运行，其效果与运行命令后面加上&amp;效果相同。
2.5. bind
bind: bind，显示或设置键盘配置

bind -l | grep kill，列出与kill有关的所有功能名称。
bind -m vi -v，列出vi的按键配置与使用的变量名称。
2.6. blockdev
blockdev: block device，查询区块设备

blockdev -v --getss /dev/sda1，列出/dev/sda1的区块大小。
blockdev -v --getsize /dev/sda1，获取/dev/sda1的区块容量。
2.7. bunzip2
bunzip2: Burrows-Wheeler un-zip file，解压缩bz2格式的压缩文件。

bunzip2 -k afile.bz2，解压afile.bz2文件，不删除原来的压缩文件。
bunzip2 -s afile.bz2，用较少的内存解压afile.bz2文件。
bunzip2是bzip -d的功能连接。
2.8. bzgrep
bzgrep: Burrows-Wheeler zip file grep，查找bz2文件中特定的字符串

bzgrep router ip.txt.bz2，寻找ip.txt.bz2压缩文件中的router字符串。
2.9. bzip2
bzip2: Burrows-Wheeler zip file，将文件压缩为bz2文件

bzip2 afile，压缩文字文件afile为afile.bz2，压缩后afile文件消失。
bzip2 -l pic.png，压缩一般的png图像文件。
bzip2 -d pic.png.bz2，解压文件。
2.10. bzip2recover
bzip2recover: Burrows-Wheeler zip file recover，修复损坏的bz2文件

bzip2recover text.bz2，当bz2文件发生问题无法解压缩时，尝试此命令来还原文件。
2.11. bzless
bzless: Burrows-Wheeler zip file less，列出bz2文件的内容

bzless afile.bz2，列出压缩文件afile.bz2中的内容。

3. C3.1. cal
cal: calendar，显示日历

cal，显示本月的月历。
cal 2000，显示2000年年历。
cal 5 2001，显示2000年5月月历。
cal -m，以星期一为每周的第一天方式，显示本月的月历。 
cal -jy，以一月一日起的天数显示今年的年历。
3.2. cat
cat: catenate，列出文件内容

cat -n textfile1 &gt; textfile2，把textfile1的内容加上行号后，转存为textfile2。
cat -b textfile1 textfile2 &gt;&gt; textfile3，把textfile1和textfile2的内容加上行号（空白行不加）之后，将内容附加到textfile3的最后。
3.3. cd
cd: change directory，切换目录

cd /usr/bin，进入/usr/bin/目录。
cd ~，回到home directory。
cd ../..，跳到目前目录的上上两层:
3.4. cfdisk
cfdisk: curses formatted disk，设置硬盘分区

cfdisk，进入分区界面。
cfdisk -P S /dev/sda，按照扇区排序，显示第一块硬盘的分割情况。
cfdisk是传统命令fdisk的进化版。
3.5. chage
change: change user password expiry info，改变密码的有效期

cat /etc/shadow | grep sherry，chage -E 2018-12-31 sherry，设置sherry账号的密码设置在2018年12月31日失效。
chage -M 5 sherry，要求账号sherry必须在5天内变更密码。
chage -l sherry，显示账号的密码设置。
3.6. chattr
chattr: change attributes，改变文件属性

chattr +a file1，lsattr file1，增加文件的属性，使之可以附加数据，而无法被修改。
chattr +i file1，改变文件属性，无法修改和删除。
3.7. chcon
chcon: change security context，修改SELinux标签

chcon -R -t httpd_sys_content_t www/，将www目录类型改为httpd_sys_content_t。
3.8. chgrp
chgrp: change group，改变文件或目录所属的组

chgrp users afile，修改afile的组为users。
chgrp -h users tt，修改符号连接tt的组为users。
可以使用chmod实现同样的效果，因此chgrp使用频率较低。
3.9. chkconfig
chkconfig: check configurate，设置系统在不同运行等级下的服务。

chkconfig --list sendmail，列出sendmail在不同运行等级下的状态。
chkconfig --level 35 named on，使DNS服务器在运行等级为3和5时启动。
chkconfig --level 0123456 vsftpd on，使FTP服务器在所有等级下启动。
chkconfig --lis | grep 3:启用，列出runlevel3中所有开启的服务。
3.10. chmod
chmod: change mode，改变文件或目录的权限

chmod ugo+r file1.txt ，将file1.txt设为所有人可读取。
chmod a+r file1.txt ，将file1.txt设为所有人可读取。　　chmod ug+w,o-w file1.txt file2.txt，将file1.txt与file2.txt设为文件拥有者和其所属同一个群体者可写入，但其他以外的人则不可写入。　　chmod u+x ex1.py，将ex1.py设定为只有该文件拥有者可以执行。　　chmod -R a+r * ，将目前目录下的所有文件与子目录皆设为任何人可读取。
chmod 777 file，三个7，分别表示User、Group及Other的权限。r=4，w=2，x=1。若要rwx属性则4+2+1=7；若要rw-属性则4+2=6；若要r-x属性则4+1=7。 
chmod a=rwx file和chmod 777 file效果相同。
chmod ug=rwx,o=x file和chmod 771 file效果相同。
chmod 4755 filename，可使此程序具有root的权限。
3.11. chown
chown: change owner，改变文件或目录的拥有者或组

chown jessie:users file1.txt ，将文件file1.txt的拥有者设为users群体的用户jessie。　　chmod -R lamport:users *，将当前目录下的所有文件与子目录的拥有者皆设为users群体的用户lamport。
3.12. chroot
chroot: change root，切换根目录所在的路径

chroot /mnt/disk /bin/bash，将根目录切换到/mnt/disk，并将/bin/bash作为使用的shell。
3.13. chsh
chsh: change shell，改变账号登录系统时所使用的shell

chsh -l，列出所有可用的shell。
chsh，然后指定使用的shell。
chsh -s /bin/bash peter，指定peter账号的shell。
3.14. clear
clear: clear，清除画面

clear，清屏。 
3.15. clock
clock: clock，调整RTC（Real Time Clock）时间

clock，显示目前硬件时钟的时间。
clock --set --data=&quot;2/27/11 22:15&quot;，将目前硬件时钟的时间设置为2011年2月27日22:15。
clock --hctosys，让系统时间和硬件时钟一致。
clock --systohc，将系统时间写入硬件时钟。
3.16. cmp
cmp: compare，对比两个文件的差异

cmp test.txt text.txt，对比两个文件。
一般使用diff命令来进行文本内容比较，cmp使用较少。
3.17. col
col: column，过滤特殊字符

col -f &lt; testfile，过滤testfile中的RLF字符。
man kill | col -b &gt; kill.txt，过滤所有控制字符（RLF和HRLF）。
3.18. colrm
colrm: column remove，删除指定的列

cat file | colrm 7，删除第6列以后的字符。
cat file | colrm 2 5，删除第2~5列的字符。
3.19. compress
copress: compress

compress -f source.dat，将 source.dat 压缩成 source.dat.Z，若 source.dat.Z 已经存在，内容则会被压缩档覆盖。　　compress -vf source.dat，将 source.dat 压缩成 source.dat.Z ，并列印出压缩比例。
compress -c source.dat &gt; target.dat.Z ，指定压缩档名。　　compress -b 12 source.dat ，-b 的值越大，压缩比例就越大，范围是 9-16 ，预设值是 16 。 
　　
compress -d source.dat compress -d source.dat.Z 
由于系统会自动加入 .Z 为延伸档名，所以 source.dat 会自动当作 source.dat.Z 处理。
将 source.dat.Z 解压成 source.dat ，若文件已经存在，用户按 y 以确定覆盖文件，若使用 -df 程序则会自动覆盖文件。
3.20. cp
cp: copy file，复制文件或目录

cp aaa bbb，将文件aaa复制命名为 bbb。
cp *.c finished，将所有的.c文件复制到finished目录中。
3.21. cpio
cpio: copy in, copy out，文件备份

ls | cpio -o -O ./backupfile，将目录下的所有文件（不包含子目录）备份到backupfile。
cpio -t -v -I backupfile，查看备份文件backupfile中的文件信息。
3.22. crontab
crontab: cron table，设置计划任务

crontab -l，列出自己的计划任务设置。
crontab -e，编辑自己的计划任务。若要在每周六运行/usr/bin/w &gt;&gt; /root/login.txt，可设置如下：
* * * * 6 /usr/bin/w &gt;&gt; /root/login.txt
若要改为每天23:55运行以上命令，可设置如下：
55 23 * * * /usr/bin/w &gt;&gt; /root/login.txt

crontab -u adm -r，删除adm账号的计划任务设置。
1、确认crontab是否安装crontab，如果报 command not found，就表明没有安装2、安装 crontabyum install -y vixie-cron3、确认是否安装成功:执行 crontab -l4、看是否设置了开机自动启动chkconfig --list crond5、启动crontabservice crond start
3.23. csplit
csplit: content split，分割文件

csplit -n 3 vsftpd.log 3000，以3000行为界分割为两个文件，并指定列出的文件名位数为3。
csplit -f file vsftpd.log 3000，以3000行为界分割为两个文件，且指定分割的文件名以file开头。
csplit vsftpdlog 1000 &#123;7&#125;，以1000行为界分割为7个文件。
3.24. ctrlaltdel
ctrlaltdel: control alt del，设置Ctrl+Alt+Del快捷键。

ctrlaltdel hard，设置为不保存数据立即重启。ctrlaltdel soft，设置为保存数据、停止服务、卸载文件后重启。
3.25. cut
cut: cut，截取文本内容的指定范围

cat log1，正常查看文件。
root    pts/0        2013-04-29 00:52(192.168.222.1)root    pts/0        2013-04-29 00:52(192.168.222.1)root    pts/0        2013-04-29 00:52(192.168.222.1)

cut -b 3,10 log1，只取出第3、10个字节。
opopop

cut -b -3 log1，取前3个字节。
roorooroo


4. D4.1. date
date: date，显示或修改日期时间

date，显示当前日期和时间。
date +%B%d，显示月份与日数。
4.2. dd
dd: standard input, standard output，转换并列出数据

dd if=file.txt of=/dev/fd0，将文件file.txt写入到软盘。
dd if=boot.img of=/dev/fd0 bs=1440k，制作启动盘，其中，boot.img为开机的镜像文件。
dd if=test.txt of=out.txt conv=ucase，将文件test.txt中的英文字母全部转换为大写后，存储为out.txt。
4.3. debugfs
debugfs: debug file system，ext2和ext3的文件系统改错工具

debugfs /dev/sda7，dump install.log /root/bkp.txt，将/dev/sda7下的install.log文件导出一份放到/root/bkp.txt中。
4.4. declare
declare: declare，声明环境变量

declare，显示当前的shell变量。
declare -x，显示所有的环境变量。
declare -i number=100+200，echo $number，如果不加-i，系统会以字符串方式来处理100+200。
declare命令与export命令相比，区别在于declare声明的是shell变量，export声明的是环境变量。shell变量只能给shell只用，环境变量可以给shell以及外部命令使用。declare加上-x参数，则与export的作用相同。
4.5. depmod
depmod: dependence of module，分析可加载模块的关联性

depmod -a，检测模块的关联性。
4.6. df
df: display file system，显示文件系统的使用情况

df，显示当前文件系统的使用状况。
df -m，以MB为单位来显示当前文件系统的使用状况。
df -a，显示所有文件系统的使用状况。
df -h，以较易读取的方式显示文件系统的使用状况。
df -i，显示系统inode的状态。
4.7. diff
diff: diffrence，比较并显示文件差异

diff file1 file2，对比file1和file2。
diff -c file1 file2，对比file1和file2，并列出文件的异同。
diff -y file1 file2，对比file1和file2，并以并列的方式显示对比结果。
diff -B file1 file2，对比file1和file2，不对比空白行。
diff /etc/mail/ mail/，比较两个目录的差异。
4.8. diffstat
diffstat: diffrence statistics，根据diff的比较结果显示统计数字

diff /etc/mail/ mail/ | diffstat，对比两个目录的差异，并通过diffstat命令列出。
4.9. dig
dig: dig，显示域名的高级信息

dig sina.com，查询域名sina.com。
dig 163.com -t MX，查询163.com的邮件名称记录（MX record）。
4.10. dir
dir: directory，列出目录或文件名

dir，列出当前目录的文件。
dir -l，以长列表列出当前的文件。
dir命令和ls命令的功能完全相同。
4.11. dirname
dirname: directory name，列出当前路径下的路径名称

dirname /opt/httpd，显示/opt/httpd下的路径名称。
dirname file.txt，显示file.txt文件的路径名称。
4.12. dpkg
dpkg: Debian package，Debian软件包管理工具

dpkg -L postfix，列出postfix安装的文件。
dpkg -i ./unzip_6.0-1_i386.deb，安装当前路径下的unzip_6.0-1_i386.deb。
dpkg是Debian和Ubuntu上的软件包安装指令，类似于RedHat与Fedora上的rpm，但一般较常使用apt-get。
4.13. du
du: display units，显示目录或文件的大小

du，显示当前目录的使用情况。
du -sk /var/*，显示/var目录下所有文件的容量，仅显示总和，默认以KB为单位。
du -sh /*，以可读性高的方式显示根目录下的目录容量。
du --max-depth=2 /var，显示/var目录下两层子目录所占用的空间。
du -b backupfile，显示文件占用的空间。
4.14. dump
dump: dump，文件系统的备份

dump -0 -f /opt/backup /boot，将/boot下的数据备份到/opt/backup中，并更新/etc/dumpdates中的记录。
cat /etc/dumpdates，查看更新后的记录。
restore -r -f /opt/backup，还原backup到备份的位置。
dump命令常用来备份ext2和ext3文件系统。
restore命令是dump命令的逆命令。
]]></content>
      <categories>
        <category>engineering</category>
        <category>devops</category>
        <category>linux</category>
      </categories>
      <tags>
        <tag>linux</tag>
        <tag>好好学Linux</tag>
      </tags>
  </entry>
  <entry>
    <title>IE首页被篡改解决办法</title>
    <url>/hobby-ie-homepage/</url>
    <content><![CDATA[解决办法1:使用360。
解决办法2:1.起始页的修改。展开注册表到HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer\Main，在右半部分窗口中将”Start Page”的键值改为”about:blank”即可。
同理，展开注册表到HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main，在右半部分窗口中将”Start Page”的键值改为”about:blank”即可。
注意:有时进行了以上步骤后仍然没有生效，估计是有程序加载到了启动项的缘故，就算修改了，下次启动时也会自动运行程序，将上述设置改回来，解决方法如下:运行注册表编辑器Regedit.exe，然后依次展开HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run主键，然后将下面的”registry.exe”子键(名字不固定)删除，最后删除硬盘里的同名可执行程序。退出注册编辑器，重新启动计算机，问题就解决了。
2.默认主页的修改。运行注册表编辑器，展开HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer\Main\，将Default-Page-URL子键的键值中的那些恶意网站的网址改正，或者设置为IE的默认值。
注:如果无法修改,请进入安全模式。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
  </entry>
  <entry>
    <title>ArtCursors使用教程</title>
    <url>/hobby-artcursors/</url>
    <content><![CDATA[1. 左手使用鼠标你是左撇吗，以至用鼠标都是左手，或者你像我一样，有兴趣经常改变单调的习惯，又或者有意锻炼右边大脑，而有试过把鼠标调成左手习惯？那么你一定会发现，你可以把鼠标拿到键盘左边，把左右键调换，但系统却没有左手方向的指针。
微软似乎从一开始就没有照顾想用左手拿鼠标的人，从win9x年代开始，到现在的vista，从键盘的布局（不清楚是否微软设计），系统快捷键，到鼠标指针，甚至整个系统，没有哪一点是为了方便左手习惯的。也难怪网上会有人说微软歧视左撇子了。
我很理解使用左手但鼠标指针却仍向右撇的别扭－－不要说习惯了也一样～～
之前的确找到过xp下的反向鼠标指针，可是找了很久也没有找到vista下的，又不想牺牲外表用回以前xp下的指针，怎么办呢？
今天找了下，找到个软件，叫ArtCursors。
ArtCursors小档案软件版本：4.21 软件大小：1051KB软件性质：共享软件 适用平台：Windows 9x/ME/NT/2000/XP/2003下载地址：http://www.onlinedown.net/soft/11132.htm


2. 鼠标指针换个方向2.1. Step 1运行ArtCursors，在弹出的New Cursor（新建指针）窗口中单击“Cancel（取消）”按钮，因为我们要打开已有的鼠标指针进行编辑。选择菜单命令“File→Open（文件→打开）”或按下Ctrl+O组合键，展开X:\Windows\Cursors（X:表示系统安装盘符）目录，找到某个需要调整方向的指针文件，比如“aero_arrow.cur”，这是“Windows Aero（系统方案）”中的“正常选择”指针。单击“打开”按钮将此指针文件打开。
 
2.2. Step 2在工作区域中单击右键，选择“Flip→Horizontally（翻转→水平方向）”，即可将指针转换成适合左手使用的方向。在窗口右下角可以看到转换后的实际效果图。
 光把图标翻转是不够的，还要设置“热点”。所谓热点就是指针用来触发事件的点，对于箭头来说，热点当然就是箭头的尖端了。对于默认的32×32像素的指针，ArtCursors将左上角的坐标定义为（0,0），右下角为（31,31）。因为我们对指针进行了水平翻转，所以将热点的水平坐标改为“31-X（X是原坐标）”，垂直坐标保持不变即可。以“aero_arrow.cur”文件为例，将热点坐标从（0,0）改为（31,0）。
 
2.3. Step 3选择菜单命令“Cursor→Hot Spot（指针→热点）”，在对话框中为翻转后的鼠标指针设置新的热点。
做完以上两步，一个左手的指针就完成了，是不是很简单？但请不要覆盖保存系统自带的图片，选择菜单命令“File→Save as（文件→另存为）”，在打开的保存对话框中，为翻转后的指针命名为你想要的名字，如“aero_left.cur”，以便和原来的右手用指针文件区分开来。 对于有些动态指针，并不是翻转一次就完成，而是要翻转多次，因为它是多帧的，每一帧都要处理。
3. 光标设置使用ArtCursors，我们制作出了自己喜欢的光标。接下来，就可以应用到我们的计算机上了。
1、把自己喜欢的这一套光标放进文件夹（名称随意），郝同学给这个文件夹起名mycursor。然后把mycursor复制到C:\Windows\Cursors文件夹下面。
2、桌面右击，个性化，更改鼠标指针。
3、单击自定义（C）里面的正常选择，浏览，进入mycursor文件夹，找到合适的光标，打开。依次逐个替换自定义（C）里面的光标。
4、修改完成后，另存为mycursor，以后就可以很方便的切换整套光标了。
 
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
      </tags>
  </entry>
  <entry>
    <title>无法停止通用卷设备</title>
    <url>/hobby-could-not-stop-the-general-volume-equipment/</url>
    <content><![CDATA[安全删除U盘时，大家常常会遇到提示“无法停止‘通用卷’设备，请稍候再停止该设备。”
这种情况下可以强行拔下U盘吗？当然不可以！这时候如果强行拔除的话，很容易损坏计算机U口或者你的U盘。如果你的U盘上有重要的资料，很有可能就此毁坏了。那么应该怎么办呢？
1. 第一种方法：（这也是我建议大家最常用的方法）往往我们在把U盘的文件或者数据取出来的时候，都是用的“复制”“粘贴”方式，而如果复制的是U盘上的文件，这个文件就会一直放在系统的剪切板里，处于待用状态。而如果这种情况下我们要删除U盘的话，就会出现上面的无法停止U盘的提示。
相应的解决办法就是：清空你的剪切板，或者在你的硬盘上随便进行一下复制某文件再粘贴的操作，这时候你再去删除U盘提示符，看看是不是顺利删除了？ 
2. 第二种方法：如果觉得上面那种方法还无效的话，可以使用下面这个方法：
同时按下键盘的”Ctrl”＋”Alt”＋”Del”组合键，这时会出现”任务管理器”的窗口，单击”进程”标签，在”映像名称”中寻找”rundll32.exe”进程，选择”rundll32.exe”进程，然后点击”结束进程”，这时会弹出任务管理器警告，问你确定是否关闭此进程，点击”是”，即关闭了”rundll32.exe”进程。再删除U盘就可以正常删除了。
使用这种方法时请注意：如果有多个”rundll32.exe”进程，需要将多个”rundll32.exe”进程全部关闭。
3. 第三种方法：这种方法同样是借助了任务管理器，同时按下键盘的”Ctrl”＋”Alt”＋”Del”组合键，出现”任务管理器”的窗口，单击”进程”，寻找”EXPLORER.EXE”进程并结束它。这时候你会发现你的桌面不见了，请不要惊慌，继续进行下面的操作，在任务管理器中点击“文件”——“新建任务”——输入EXPLORER.EXE——确定。再删除U盘，你会发现可以安全删除了。
4. 第四种方法：这种方法最简单，但最耗时，那就是，重启你的电脑。
5. 第五种方法：一个国外的十分小巧实用的软件，只有191KB，叫unlocker，十分好用！这个软件能解锁USB连接设备！就能实现100%安全删除USB连接了！ 
6. 预防方法：如果觉得出现问题的时候才解决有些麻烦，可以采用下面这个提前的预防措施：关闭系统的预览功能。
方法：双击我的电脑——工具——文件夹选项——常规——任务——使用windows传统风格的文件夹，然后点击确定就行了。这样一劳永逸了。
小窍门：目前有些U盘的技术很到位了，有的U盘直接插拔也可以的。但无论是多好的U盘，有一种时候是绝对不能直接插拔的，那就是仔细看你U盘的那个小红灯，小灯在不停闪的时候表示正在不停地读写数据，这时候千万不能拔，否则轻则损坏数据，重则U盘报废。
]]></content>
      <categories>
        <category>computer</category>
      </categories>
      <tags>
        <tag>windows</tag>
      </tags>
  </entry>
</search>
