[{"content":"前言 当使奴太久了，从cod8 - cod21，前几天卖了PS5，打算彻底脱坑，再也不玩了\n之前被折磨的太多了，现在真的不想玩了，动视的匹配机制真的是个迷\n有时候打一把，狂杀50人+，第二把露头就死，第三把杀人个位数，总感觉煞笔动视控制了什么\n对面杀我和杀孙子一样\n这几天读了一下动视的技术白皮书，搞懂了一些东西，那么，便有此记录\n纪念一下cod的这几年，和花出去的好几千块钱，over。\n1. COD的段位和匹配机制 1.1 COD的段位等级 ​​段位名称​​ ​​玩家分布​​ ​​玩家分布​​ 青铜 (Bronze) 新手/低活跃玩家 所有人起点段位 白银 (Silver) 入门级竞技玩家 允许与青铜/黄金组队 黄金 (Gold) 主流玩家集中段 可跨3段组队（青铜至铂金） 铂金 (Platinum) 实力前30%玩家 组队跨度收缩（银-钻） 钻石 (Diamond) 顶尖玩家守门员（前10%） 仅能与黄金/铂金/钻石组队 深红 (Crimson) 职业级预备役（前3%） 仅限钻石/深红组队 虹彩 (Iridescent) 全服Top 500（含Top 250榜单） ​​实际匹配按钻石+处理 重要说明\nTop 250玩家在匹配系统中视为钻石+段位（非独立段位），因人数过少需合并匹配池 段位转换严格依赖​​SR数值门槛​​（如黄金→铂金需2500 SR） 1.2 段位的隔离机制 ​组队硬约束​​策略 ​1. ​跨段检测​​\n系统实时计算队伍\u0026quot;段位差\u0026quot;（Rank Disparity） 例：钻石（差值为0） + 黄金（差值2） → 整队段位差=2 ​​ 2. 动态组队锁​​\n钻石玩家邀请黄金队友 → 队伍​​立即禁止​​邀请白银玩家 虹彩玩家组队 → 全员必须为深红/虹彩\n匹配池分层策略 优先同段位匹配 系统首先寻找同段位玩家（如铂金vs铂金）\n超时降级机制 搜索开始 --\u0026gt; 仅匹配同段位 --\u0026gt; 超时 --\u0026gt; 放宽至±1段 --\u0026gt; 超时--\u0026gt; 放宽至±2段 --\u0026gt; 超时 --\u0026gt; 启用跨区匹配+高延迟容忍 高级段位容易触发降级：铂金以上玩家常遇跨段匹配 TOP250处理 单独匹配池 TOP250玩家自动进入候选池\n匹配更新机制 TOP250玩家匹配时视为**钻石+**段位，最低会匹配到钻石玩家\n1.3 段位和Raw Skill（真实战力）的关系 在玩游戏的时候，我们经常会进行掉分操作，例如自雷或者自杀，或者不停的频繁去送\n让系统对我们的技术评级降低，这样就能匹配到比较菜的玩家，大杀四方（我没这么干过就是了）\n根据白皮书的描述，可以看这张图，其实动视内部有个机制，是来判定你的真实战力（Raw Skill）的\n首先先说结论\n段位 不等于 游戏的真实实力\n看这张图，\n​​1. 高段位含金量高​​：深红(Crimson)以上玩家100%集中在技能前20%（0.8-0.9十分位） ​2. ​低段位鱼龙混杂​​：青铜(Bronze)中混有​​高技能玩家​​（0.7+技能分占比超5%） ​​3. 系统强制干预痕迹​​：黄金(Gold)段聚集大量​​被系统压制的中高技能玩家​​（0.5-0.7占比达13%）\n我们继续分析这张图\n在我玩游戏初期，经常能碰到一局杀几十个人的大神，甚至还有混战码头100多杀的大牛，这点我一开始就非常疑惑\n因为本身我的KD只有0.9，或者1，这种大牛我能匹配到真的非常逆天\n其实这张图就说明了很多东西\n技能分 青铜段占比 ​​实际分布段位 0.7 0 黄金(2%)/白银(5%) 0.6 0 黄金(3%)/白银(5%) 0.5 1% ​​青铜最高技能分​​ 这里可以看出来，动视其实是不允许技能分高于0.7的玩家到青铜段炸鱼的\n那么动视是如何让0.7+的战力玩家不能在青铜呆着呢？\n白皮书上也说了\n加速晋级（战力校准机制） 如果Raw Skill ≥0.7的时候，那么就触发机制\nif current_rank == \u0026#34;Bronze\u0026#34; and raw_skill \u0026gt;= 0.7: SR_gain *= 2.5 # SR获取效率翻倍 matchmaking_pool = \u0026#34;Gold/Platinum\u0026#34; # 强制匹配更高段位 白皮书上描述：\n​​1. 0.7技能玩家在青铜段 单场胜利SR可超100点（普通玩家20-30点）\n5连胜内必升入白银\n越段战斗（跨阶段战斗）\n在某个阶段内，给你匹配高段位玩家，根据你的raw_skill数值来判定你匹配的真实段位玩家\n有些时候会炸鱼，可能是你的匹配，把你匹到高段位玩家的池子了\n有一种经典的情况：\n我上一把杀的嘎嘎爽，下一把马上成哈皮了\n不是动视针对你 → 是系统检测到Raw Skill≥0.7\n绝壁你被划到高段位池子了，至于为什么这样做\n青铜段存在大量高玩 → 新手退游\n游戏属实就没法玩了，人全跑光了\n所以一般会安排一大堆傻人机哈哈哈哈。\n1.4 玩家成长和段位关系 从上面的分析可以看出来\n段位晋升≠线性成长 黄金→铂金：需突破\u0026quot;SR收益衰减墙\u0026quot;（胜场SR从50→20） 钻石→虹彩：强制\u0026quot;地狱检验\u0026quot;（仅匹配前10%玩家）\n系统强制干预场景 现象 触发条件 ​​玩家感受 连胜后突然连败 当前SR \u0026gt; 目标SR+15% 妈的搞我 青铜局遇大神狂杀 高潜力玩家检测（Raw Skill\u0026gt;0.7 妈的搞我 钻石段匹配超时 段位差规则降级失败 妈的鬼服了 2. COD的SR（Skill Rating）机制 SR我们已经在前面提过了\n这逼玩意儿简单来说，就是控制你什么时候该升到什么段位，或者是你该获取多少经验值的一种动态干预算法\n是不是有点子抽象，换个简单的语句来描述\n这表面是段位系统，实则是动视的 使奴驯化装置。\n2.1 动视内部的段位操作 首先，动视会给你进行一个评估\n根据你每局的杀敌数/死亡数，算出一个真实的隐藏分数（Raw Skill），这是你无法看见的\n这里的计算公式是\nraw_skill = (击杀×0.25 + 助攻×0.15 + 目标分×0.45 + 占点×0.15 - 死亡×0.3) / √(对局时长) ​​1. 目标分权重最高(0.45)​​（控制点位/炸弹模式核心指标） ​2. ​对局时长开平方​​（防止刷时长影响）\n假设你现在的Raw Skill=0.6\n然后根据你的真实水平（Raw Skill），动视会算出一个 ​​你应该在的段位​（Target SR）​（比如黄金需要2500分）\n动视内部有个数值，我称之为：行为调整因子，这里是分析玩家行为数据的核心数据\n这个数值的主要影响因素有：\n影响因素 原描述 修正公式（编程级精准） 高活跃度(日\u0026gt;4h) 负向 -0.15 * log2(当日游戏小时数) 近期付费(7天消费) 正向 min(0.1, 消费金额($)/2000) 弃游风险 负向 -0.3 * (1 - min(1, 当日活跃/周平均)) 这里的Target SR计算公式是\nbase_SR = min(4500, Raw_Skill × 4500) behavior_effect = behavior_factor × 0.25 × base_SR/1000 Target_SR = base_SR + behavior_effect 写一个伪代码\n# 计算Raw Skill（每局更新） def calc_raw_skill(match_data): kills = match_data.kills assists = match_data.assists objectives = match_data.objective_points # 炸弹/点位控制分 zone_time = match_data.zone_seconds # 占点秒数 deaths = match_data.deaths duration = match_data.duration_seconds / 60 # 转为分钟 return (kills*0.25 + assists*0.15 + objectives*0.45 + zone_time*0.15 - deaths*0.3) / math.sqrt(duration) # 计算目标SR（每日更新） def calc_target_SR(raw_skill, behavior_data): base_SR = min(4500, raw_skill * 4500) # 基础映射，上限4500 # 计算行为影响值（每1000分±25%） bh_factor = calc_behavior_factor(behavior_data) behavior_effect = bh_factor * 0.25 * (base_SR / 1000) return min(5500, max(0, base_SR + behavior_effect)) # 全段位边界保护 2.2 动视关于输赢的操作 SR系统本质是 ​​动态难度调节器​​——它决定你何时能赢，何时必须输。\n连续胜 def on_win_streak(streak_count): if streak_count \u0026gt;= 3: # 触发三连胜协议 # 1. 匹配系统启动\u0026#34;段位跃升测试\u0026#34; matchmaking.set_opponent_rank(current_rank + 2) # 匹配高2段车队 # 2. SR收益压制系统激活 win_reward = max(10, base_SR * 0.4) # 基础分打4折 # 3. 表现分封顶机制 performance_bonus = min(5, raw_performance) # 杀50人只算5分 return win_reward + performance_bonus 当你三连胜时，下局必遇 ​​钻石车队打黄金局​​（白皮书第5.2.3节称其为“confidence-based calibration”） 就算你力挽狂澜赢了 原可获25基础分+15表现分=40分 现强制压制为(25 * 0.4)+5=15分（砍掉62.5%） 所以你赢了好几把之后，你是一定会输的，你打不到那前100，你输的概率都是非常大的。\n连续输 def on_lose_streak(streak_count, player_type): if streak_count \u0026gt;= 3: # 三连败触发救济 if player_type == \u0026#34;NEWBIE\u0026#34;: # 新手保护 spawn_bot_lobby() # 生成人机局 return 30 # 固定+30分（希望药剂） else: # 老油条处置 reduce_next_opponent_skill(40) # 下局对手强度-40% return -10 # 扣分减半（从-18→-10） elif player_type == \u0026#34;WHALE\u0026#34;: # 付费玩家特殊关怀 return abs(base_SR) * 0.3 # 扣分再打7折 白皮书上也说了：\nLoss streak interventions boost retention by 22%\n玩家类型 三连败救济方案 萌新 人机局​​ → +30分 老玩家 对手强度-40% \u0026amp; 扣分减半 充值玩家 扣分再打7折 2.3 赛季控制的操作 赛季重置 ​1. ​软性段位重置 (Soft Rank Reset)​ 将所有玩家的可视段位重置至初始等级（如青铜），但保留隐藏实力分（Raw Skill）的90%\n2.​​差异化匹配处理 (Tiered Matchmaking Treatment)​ 根据玩家历史实力划分匹配池，高竞争力玩家仍与原段位玩家对战\n​​3.加速晋升协议 (Accelerated Promotion Protocol)​ 高竞争力玩家在重置后获得额外SR增益系数\n动视白皮书描述（Sec 7.2）：​​\n\u0026#34;Rank resets apply cosmetic demotions while preserving core skill metrics, enabling high-skill players to rapidly return to appropriate tiers.\u0026#34; （段位重置仅应用表面降级，同时保留核心技能指标，使高水平玩家能快速返回适当段位） 控制晋级 1.​​动态难度校准 (Dynamic Difficulty Calibration)​ 根据玩家实时状态调整晋升赛对手强度\n2.受控晋升率 (Controlled Promotion Rate)​ 系统精确控制不同段位间的晋升成功率（通常53-58%）\n3.检验性匹配 (Validation Matching) 晋升关键局分配高强度对手验证玩家实力\n动视白皮书描述（Sec 5.3）：​​ \u0026#34;Promotion matches trigger validation protocols, exposing players to marginally superior opponents to confirm tier readiness.\u0026#34; （晋升赛触发检验协议，使玩家面对略强对手以确认段位准备度） 所以你会直接感受到如下的游玩体验\n玩家视角 技术术语 真实目的 新赛季新开始 软性段位重置 维护高玩炸鱼特权 晋级赛遇大神 检验性匹配 压制自然晋升率 刚升段就连败 段位适应性调节 迫使玩家停留当前段位 赛季初被血虐 差异化匹配处理 用低段玩家供养高玩留存 3. 三个重要匹配机制 3.1 网络优先机制 排位赛强制 ​​Delta Ping≤50ms​​（红线），而普通模式无限制 当匹配超时后会强制放宽至15ms 等于说玩游戏的人受两种折磨\n第一个就是，你会在低分段，受到来自匹配超时大佬的暴击（可能\n第二个就是，在高段位，你等待排队的时间只会更长/延迟更大\n3.2 段位/SR双过滤机制 坐标轴 技术含义 白皮书关联机制 ​​横轴 Lobby Max SR​​ 当局所有玩家中的​​最高SR值​​ 决定匹配强度（3.2.1节） ​​纵轴 Count​​ 遭遇该SR值的​​对局次数占比​​ 组队惩罚算法（5.3节） 如果你是单排玩家，并且你的SR非常高（2000-3000R），你的匹配速度会更快，因为系统会加速校准 如果你是组队玩家，你的SR出现异常高峰（队内有钻石大佬），就会触发跨段惩罚机制 # 单排青铜 if solo_player and bronze_rank: lobby_max_sr = target_sr * 1.2 # 系统校准性匹配 # 结果：集中于2000-3000（黄金段） # 组队 if partied_with_diamond: lobby_max_sr = max(diamond_sr)*1.5 # 惩罚系数 # 结果：5000-8000异常峰（虹彩段） # 当青铜与钻石组队 → 按钻石SR×1.5匹配 → 青铜面对7500SR虹彩战神 3.3 Raw Skill干预机制 坐标轴 技术含义 ​​横轴 Lobby Max SR​​ 当局最高SR值 → 代表对局强度等级 ​​纵轴 Probability​​ 玩家在该强度对局中的​​存活概率​​ # 底层玩家封锁（白皮书4.3节） if player.percentile \u0026lt; 0.25: target_sr = min(1500, raw_skill*5000) # 锁白银 allow_high_tier = False # 禁止接触高端局 # 顶尖玩家特权 if player.percentile \u0026gt; 0.95: bypass_rank_constraints = True # 可突破段位隔离 这个的核心意思就是，后25%玩家永远在青铜困住，真玩家胜率只会越来越低\n4. 结尾 动视的白皮书，一共出了三个部分\n排位赛匹配机制详解 Ping机制详解 SBMM（Skill-Based Matchmaking，基于技术水平的匹配机制） 我这段时间，会依次把这些文章读完并且发出来\n脱坑cod之后，刹那天地宽\n再也不玩了，再也不买了，靠\n","permalink":"https://yemilice.com/posts/cod%E4%BD%BF%E5%91%BD%E5%8F%AC%E5%94%A4-%E6%8E%92%E4%BD%8D%E8%B5%9B%E5%8C%B9%E9%85%8D%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e当使奴太久了，从cod8 - cod21，前几天卖了PS5，打算彻底脱坑，再也不玩了\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"alt text\" loading=\"lazy\" src=\"https://blog-1256169066.cos.ap-chengdu.myqcloud.com/%E6%8E%92%E4%BD%8D%E8%B5%9B%E5%8C%B9%E9%85%8D%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3/image-7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e之前被折磨的太多了，现在真的不想玩了，动视的匹配机制真的是个迷\u003c/p\u003e","title":"COD(使命召唤)-排位赛匹配机制详解"},{"content":"CI/CD的概述 良好的CI/CD应该拥有哪些功能 自动化构建 自动化构建触发：每次代码提交、合并请求、或其他触发事件时，自动启动构建过程。 依赖管理：自动处理项目依赖，确保构建环境的一致性。 可复用的构建脚本：使用脚本或配置文件（如Makefile、Gradle、Maven等）来定义构建过程，确保构建步骤的一致性和可复用性。 版本控制：构建过程中自动生成版本号或构建标签，以便于版本管理和追踪。 自动化测试 自动化测试是CI/CD系统确保代码质量的重要功能，通过自动化测试，开发团队可以快速发现和修复问题。\n单元测试：每次构建后自动运行单元测试，确保代码功能的正确性。 集成测试：在代码合并到主分支前运行集成测试，验证不同模块的协同工作。 端到端测试：模拟用户行为，验证应用的整体功能和性能。 代码覆盖率：生成代码覆盖率报告，确保测试覆盖了足够的代码。 自动化部署 自动化部署功能使应用能够快速、安全地部署到各种环境（如开发、测试、生产环境）。\n多环境部署：支持将应用部署到不同的环境（开发、测试、UAT、生产），每个环境有独立的配置。 蓝绿部署/金丝雀发布：在生产环境中进行蓝绿部署或金丝雀发布，以降低部署风险。 配置管理：通过配置文件或环境变量管理不同环境的配置。 基础设施即代码：使用工具（如Terraform、Ansible等）自动化管理和部署基础设施。 监控与反馈 监控与反馈功能确保开发团队能够及时了解系统的健康状态和性能，并迅速响应问题。\n实时监控：监控应用的性能、日志和资源使用情况，及时发现和响应问题。 报警系统：设置监控报警，及时通知相关人员处理异常情况。 反馈循环：将监控和测试结果反馈给开发团队，持续改进代码质量。 可视化仪表板：提供可视化的监控和报告仪表板，便于快速查看系统状态和构建结果。 回滚机制 回滚机制确保在出现问题时能够快速恢复到稳定状态，减少对业务的影响。\n自动回滚：在新版本部署失败或出现严重问题时，自动回滚到上一个稳定版本。 手动回滚：提供手动触发回滚的能力，确保在自动回滚失效时能够人工干预。 版本管理：保存所有发布版本的记录，便于快速回滚和问题排查。 数据库迁移回滚：在回滚应用代码时，能够同时回滚数据库迁移，确保数据一致性。 CI/CD在Kubernetes中的最佳实践方案梳理 根据前期提到的优秀的CICD的功能，结合网络上的方案\n梳理出如下最佳方案\nGitlab CI/CD 简介 GitLab CI/CD 是一个内置于 GitLab 中的持续集成和持续交付/部署解决方案.\n它通过 GitLab Runner 执行定义在 .gitlab-ci.yml 文件中的任务（如构建、测试和部署）.\nGitLab CI/CD 提供了一个强大的框架，用于自动化软件开发生命周期中的各个阶段，从代码提交到生产环境的部署。\n先决条件 需要配置Kubernetes集群的访问权限 需要配置aws权限 流程 流程如图所示\n详细解释：\n开发人员提交代码 开发人员将代码推送到 GitLab 代码库（GitLab Code Repo）。代码库包含源代码、Dockerfile 和 GitLab CI 配置文件（.gitlab-ci.yml）。 测试代码：运行代码测试，确保代码逻辑正确。 单元测试（UT）：执行单元测试，验证各个模块的功能。 Lint 检查：运行代码风格检查（Lint），确保代码符合规范。 在通过初始测试后，GitLab CI 管道继续执行以下步骤： 构建镜像：使用 Dockerfile 构建 Docker 镜像。 扫描镜像：对构建的镜像进行安全扫描，检查潜在的漏洞。 推送到AWS ECR：将通过扫描的镜像推送到 ECR。 部署阶段，部署deployment 到kubernetes集群 example 根据上述流程完成了一个CI/CD流程\n.gitlab-ci.yml示例\ntest: stage: test script: - echo \u0026#34;Test Docker image $DOCKER_IMAGE\u0026#34; build: stage: build image: $CONTIANER_TOOL_SET_IMAGE script: - cifab build-to-ecr -i $ECR_IMAGE_URI -t $CI_COMMIT_TAG -d $CI_PROJECT_DIR/Dockerfile -r us-east-1 - cifab build-to-ecr -i $ECR_IMAGE_URI_CN -t $CI_COMMIT_TAG -d $CI_PROJECT_DIR/Dockerfile -r cn-north-1 tags: - ut only: - tags deploy: stage: deploy script: - sed -i \u0026#34;s|ECR_IMAGE_URI_PLACEHOLDER|$ECR_IMAGE_URI:$CI_COMMIT_TAG|g\u0026#34; $CI_PROJECT_DIR/k8s/deployment.yml - kubectl apply -f k8s/deployment.yaml tags: - ut only: - tags environment: name: dev url: http://127.0.0.1 执行部署\n部署成功\nGitlab CI + ArgoCD 简介 先决条件 需要ArgoCD搭建 需要在ArgoCD配置gitlab的校验 流程 流程如图所示\n详细解释：\n开发者将代码推送到GitLab代码库（GitLab Code Repo），代码库包含源码、Dockerfile和GitLab-CI配置文件（.gitlab-ci.yml） 进行代码测试（Test Code）、单元测试（UT）和代码风格检查（Lint Check）。 构建Docker镜像（Build Image）、扫描镜像（Scan Image）、将镜像推送到镜像仓库（Push to Registry），并更新清单文件（Update Manifest）。 构建的Docker镜像被推送到ECR镜像仓库（ECR Registry）。 在GitLab中进行部署更新，更新清单文件（Update Manifest）。 使用ArgoCD同步GitLab代码库中的清单文件（Sync Repo），将更新部署到Kubernetes集群中（Deploy to Cluster）。 Kubernetes从ECR镜像仓库中拉取最新的镜像（Pull Image），部署应用。 example job如下 手动部署deployment\n修改deployment.yml的内容\nargocd自动部署\nGitlab + jenkins 简介 先决条件 需要jenkins搭建在内网（已经搭建完毕） 需要在jenkins上安装多个插件 （gitlab，cicd，k8s，pipline） 需要kubernetes提供token 需要在kubernetes上部署代理，连接jenkins 需要aws权限 需要gitlab的token权限 流程 开发者将代码推送到GitLab代码库（GitLab Code Repo） 代码推送到GitLab后，通过Webhook触发Jenkins任务 Jenkins收到Webhook触发的任务后，开始构建Docker镜像 (通过Docker插件) 构建完成的Docker镜像通过docker插件推送到ECR镜像仓库（ECR Registry） Kubernetes插件从ECR镜像仓库中拉取最新的镜像，部署应用 example 设定一个Jenkins的webhook\n编写jenkinsfile\n设置好jenkins pipline（步骤较为麻烦，这里不再论述）\n点击部署\n部署成功 CI/CD的方案对比 方案对比表 优缺点梳理 Gitlab 优点\n完整的平台，可以自定义所有的CI/CD流程 内置的CI/CD功能适合大部分的部署场景 管理比较简单，所有的功能都在Gitlab上 缺点\n扩展性比较差，没有插件系统 对于复杂的CI\\CD流程还是需要外部工具帮忙 Gitlab + ArgoCD 优点\n通过 Argo CD 实现声明式配置管理和自动化同步 专注Kubernetes管理，提供强大的 Kubernetes 部署和监控功能 自动化同步，持续监控 Git 仓库的配置文件，发现修改及时更新 和gitlab的集成，结合gitlab的代码托管，形成完整的CI/CD流程 缺点\n需要对项目进行配置，项目越多，配置越多 专注支持Kubernetes管理，其他环境支持比较弱 Gitlab + Jenkins 优点\n插件系统强，插件多，可以覆盖所有CI/CD需求 通过编写pipline code，进行自定制pipline 和gitlab的集成非常方便 缺点\n过于复杂，维护成本高 配置复杂，k8s的配置token比较复杂 学习成本较高，需要花费大量时间学习。 推荐方案 推荐Gitlab + ArgoCD\n推荐原因 此次需求定义的项目全是EKS项目，正好符合ArgoCD的专属kubernetes 已经配置好了ArgoCD，编写pipline只需要基础的GITLAB支持 GitOps 模型使得变更记录清晰可追溯，方便审计和回滚 Argo CD 可以持续监控 Git 仓库中的配置文件，当检测到配置变更时，自动将这些更改应用到 Kubernetes 集群 结合 GitLab 的代码托管和 CI/CD 功能，形成完整的 DevOps 工作流，提升了开发和运维的效率 设计job job梳理 test 运行Golang项目的单元测试和集成测试，以确保代码的质量和正确性\ntest: stage: test script: - echo \u0026#34;Running tests...\u0026#34; - go mod tidy - go test ./... -v security_check 进行安全扫描和检查，以发现代码中的潜在安全漏洞和问题\nsecurity_check: stage: security_check script: - echo \u0026#34;Running static code analysis...\u0026#34; - go get -u golang.org/x/lint/golint - golint ./... - echo \u0026#34;Running dependency scanning...\u0026#34; - curl -LO https://github.com/jeremylong/DependencyCheck/releases/download/v6.5.3/dependency-check-6.5.3-release.zip - unzip dependency-check-6.5.3-release.zip - ./dependency-check/bin/dependency-check.sh --project \u0026#34;test\u0026#34; --scan . build 构建Docker镜像，并且上传到AWS ECR\nbuild: stage: build image: $CONTIANER_TOOL_SET_IMAGE script: - echo \u0026#34;Build success\u0026#34; - cifab build-to-ecr -i $ECR_IMAGE_URI -t $CI_COMMIT_TAG -d $CI_PROJECT_DIR/Dockerfile -r us-east-1 - cifab build-to-ecr -i $ECR_IMAGE_URI_CN -t $CI_COMMIT_TAG -d $CI_PROJECT_DIR/Dockerfile -r cn-north-1 only: - tags deploy 修改deployment.yml文件，并且提交修改，这里为手动触发\ndeploy: stage: deploy only: - tags script: - echo \u0026#34;Deploying to production\u0026#34; - sed -i \u0026#34;s|ECR_IMAGE_URI_PLACEHOLDER|$CI_REGISTRY_IMAGE:latest|g\u0026#34; deployment/deployment.yaml - git config --global user.email \u0026#34;test_work\u0026#34; - git config --global user.name \u0026#34;test_user\u0026#34; - git add deployment/deployment.yaml - git commit -m \u0026#34;Update deployment image to $CI_REGISTRY_IMAGE:latest\u0026#34; - git push origin HEAD:main when: manual ","permalink":"https://yemilice.com/posts/gitlab-argocd-jenkins%E6%96%B9%E6%A1%88%E6%A2%B3%E7%90%86%E5%92%8C%E5%AF%B9%E6%AF%94/","summary":"\u003ch2 id=\"cicd的概述\"\u003eCI/CD的概述\u003c/h2\u003e\n\u003ch3 id=\"良好的cicd应该拥有哪些功能\"\u003e良好的CI/CD应该拥有哪些功能\u003c/h3\u003e\n\u003ch4 id=\"自动化构建\"\u003e自动化构建\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e自动化构建触发：每次代码提交、合并请求、或其他触发事件时，自动启动构建过程。\u003c/li\u003e\n\u003cli\u003e依赖管理：自动处理项目依赖，确保构建环境的一致性。\u003c/li\u003e\n\u003cli\u003e可复用的构建脚本：使用脚本或配置文件（如Makefile、Gradle、Maven等）来定义构建过程，确保构建步骤的一致性和可复用性。\u003c/li\u003e\n\u003cli\u003e版本控制：构建过程中自动生成版本号或构建标签，以便于版本管理和追踪。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"自动化测试\"\u003e自动化测试\u003c/h4\u003e\n\u003cp\u003e自动化测试是CI/CD系统确保代码质量的重要功能，通过自动化测试，开发团队可以快速发现和修复问题。\u003c/p\u003e","title":"GitLab/ArgoCD/Jenkins CI/CD方案梳理和对比"},{"content":"\n一个写代码的说唱歌手，也是一名长期在技术、音乐和表达之间来回折返的人。\n我是谁 你好，我是 Yemilice。\n白天写代码，长期关注 Golang、架构设计、搜索技术、分布式系统，以及游戏开发里那些容易被忽略但很有意思的问题。\n晚上偶尔写歌、编曲、混音，也会把一些生活里的情绪、想法和阶段性的变化，留在这个博客里。\n对我来说，这里不是单纯的文章列表，更像一个持续更新的个人样本库。\n我在做什么 写技术文章，记录踩坑、思考和总结 做音乐，主要是编曲、beat 制作和混音 观察游戏与系统设计，尤其对多人游戏网络机制很感兴趣 把那些不太适合发在朋友圈的话，留在博客里 技术这条路 我大概从 2014 年开始正式写代码。\n第一份工作是实习，那时候还是大二学生。我买了一台二手苹果笔记本，开始到处投简历，几乎抱着“不要钱干活都行”的心态入场。后来进了一家小公司，最开始做 iOS 开发，也兼职做后端开发，程序员这条路就这样慢慢走上了正轨。\n这些年我写过 Python、Golang、Java，也做过多个从零开始的项目。相比“掌握了多少技术栈”这种说法，我更在意的是能不能把一个系统真正做出来、稳定跑起来、并且把过程中踩过的坑讲清楚。\n音乐这件事 音乐对我来说不是副业，更像一种长期存在的内核。\n小时候学过手风琴和钢琴，后来因为家庭原因中断了系统学习，但一直没有真正离开音乐。进入大学之后，我开始慢慢接触更多风格，从 country music、摇滚、哥特，到 hiphop、电子，再到爵士。\n我参加过一些比赛，也做过不少编曲和混音相关的事情。唱得不算多，但我很喜欢“把感受变成作品”这件事，所以后面也会继续把自己的音乐、翻唱和一些片段放到博客里。\n为什么写博客 写博客对我来说有几个很简单的理由：\n我是个想法很多的人 我习惯通过写作整理自己 我相信知识和文字会留下痕迹 我需要一个不被社交平台节奏绑架的地方 每天我们会遇到很多人、听到很多话，但大多数东西很快就会散掉。能被留下来的，往往是那些认真写下来的东西。\n所以这个博客既是我的学习基地，也是我阶段性的生活切片。\n近况 我已经成功跳槽到 育碧，后面也会继续在技术和音乐这两条线上慢慢往前走。\n接下来的计划大概会是：\n持续更新更扎实的技术内容 和更多 Rapper 合作，继续做 beats 和混音 开始写自己的 R\u0026amp;B 情歌 把更多作品慢慢放到博客里 联系我 如果你刚好看到了这里，说明我们已经以一种很偶然的方式认识了。\n如果你有问题、建议，或者只是想随便聊聊，都可以联系我：\n邮箱：Yemiliceklichko@outlook.com GitHub：https://github.com/Alexanderklau 感谢你来过。\n2022-2-10 UPDATE\n","permalink":"https://yemilice.com/aboutme/","summary":"aboutme","title":"关于我"},{"content":"前言 很久不更新Blog了，是自己懈怠了很多，所以在思考了一下之后明白，我需要继续加强学习\n加入ubisoft之后其实也没做什么值得称道的事儿\n只是一个基础的开发人员，不过工作当中有一些可以被记录和梳理的地方\n我也会更新到Blog当中\n我会把学习捡起来的，我会的。\n开发多人游戏需要了解的概念 首先，开发多人游戏会涉及到非常多的网络概念和机制，所以这里会梳理所有相关的网络机制\n梳理这些的原因，是将知识进一步拆分，免得掉入庞大的知识深渊，也可以帮助我们去定点学习\n在梳理完这些知识之后，我会根据每个技术单独写Blog（虽然已经开了很多坑了）\nServer 和 Client 这个其实比较简单，因为无论是游戏，还是普通的web开发，或者是基础的网络操作，都离不开server和client\nServer 游戏的服务器，存储所有玩家数据，记录玩家行为，托管游戏数据，通常是由一组高性能的服务器组成（或者云）\nClient 从游戏的角度上通俗来讲，这里指的是玩家的个人设备，例如电脑，手机，主机等等\n一般玩家通过Client端连接到Server端进行游戏，它还负责根据从服务器收到的信息向玩家展示游戏世界中发生的事情。\n工作原理 玩家在他们的设备上（Client）开始游戏。 客户端连接到游戏服务器(Server)。 服务器(Server)运行游戏，跟踪所有玩家和游戏状态。 客户端(Client)将它们的操作（如移动角色或开火）发送到服务器。 服务器(Server)处理这些操作，更新游戏世界，并将结果发送回所有客户端。 客户端(Client)向玩家显示更新后的游戏世界。 带宽（Bandwidth） 带宽就像是你的互联网连接的容量或“管道大小”。\n它决定了在任意给定时间内有多少数据可以通过你的互联网连接传输。\n假设你有一根水管。管道的直径代表着你的带宽。\n一根较宽的管道可以让更多的水同时流过，而一根较窄的管道则只能让较少的水通过。在互联网的情况下，较高的带宽意味着更多的数据可以通过你的连接传输，而较低的带宽则意味着较少的数据可以通过。\n在多人游戏的背景下，带宽是至关重要的，因为它直接影响了：\n数据传输： 多人游戏之间需要在玩家和服务器之间交换大量信息。这些数据包括玩家位置、动作、聊天消息和游戏更新。较高的带宽可以让这些数据快速而顺畅地流动。 减少延迟： 低带宽可能导致游戏出现延迟。如果你的带宽太窄，数据在玩家和服务器之间传输的时间就会更长，从而导致游戏体验不够响应和愉快。 多个玩家： 在多人游戏中，服务器经常需要同时与多个玩家通信。当有许多玩家参与时，较高的带宽至关重要，确保数据可以发送给所有人而没有显著的延迟。 流媒体： 如果你在网络上直播游戏或分享你的游戏体验，较高的带宽可以让你以更高的质量将游戏体验广播给其他人。 带宽通常以每秒比特数（bps）来衡量。 例如，当你订阅互联网服务时，你可能会看到不同连接速度的优惠，比如10 Mbps、100 Mbps或1 Gbps。\n这些数字代表了在一秒钟内可以传输的数据量。数字越高，带宽越大，你的互联网连接传输数据的速度就越快。\n延迟 （Latency） 延迟就像是多人游戏中的延迟或卡顿。\n换句话说，它是你的操作到达游戏服务器（Server）（或其他玩家）并且服务器的响应返回到你的设备所花费的时间。\n你在玩一款赛车游戏，你按下“加速”按钮。在你按下按钮后，游戏中你的车加速所花费的时间就是延迟。它是你的动作和游戏反应之间的延迟。\n低延迟对于多人游戏是有益的，因为它意味着动作几乎会立即发生。相反，高延迟会导致延迟，并使游戏的乐趣降低。在某些情况下，高延迟甚至可能使游戏无法进行，特别是在需要分秒必争的快节奏游戏中。\n以下是一些可能影响延迟的因素：\n距离： 你距离游戏服务器越远，数据往返的时间就越长。可以把它想象成向一个远方的朋友发送消息 —— 得到回复需要更长的时间。\n互联网速度： 互联网连接速度可以影响延迟。更快的连接可以更快地传输数据，从而减少延迟。\n服务器性能： 服务器的性能也起着重要作用。如果服务器负载过重或速度较慢，它可能会给游戏引入延迟。\n数据包 信息被划分成数据包以通过网络发送。\n可以将其想象成拼图的小块。它们被发送，在另一端被组装起来，并相应地采取行动。\n数据包的工作原理工作原理如下：\n数据分割： 当你通过互联网发送信息时，它会被分解成小数据包。每个数据包包含你正在发送的数据的一部分，比如视频的一部分、一封电子邮件或游戏命令的一部分。\n寻址： 每个数据包都带有发送者的地址、接收者的地址和其他细节，就像你在信封上写地址一样。\n发送和路由： 这些数据包被发送到互联网上，它们可以经过不同的路由到达目的地。想象每个数据包就像一辆汽车在不同的道路上行驶，以到达它的投递地址。\n重组： 当数据包到达目的地时，它们将按正确的顺序重新组装，以重建原始数据。这就像收到拼图的所有部分并将它们组合在一起以看到完整的图像。\n数据包在网络中被用于确保数据可以高效可靠地通过互联网发送。\n它们小巧易管理，这使得数据能够快速通过互联网复杂的设备和连接网络。\n如果一个数据包丢失或延迟，系统可以处理这个问题，而不会影响整个传输。\n这样，即使一些数据包遇到障碍或采取更长的路线，你的数据仍然可以到达目的地。\n协议 TCP（传输控制协议）和UDP（用户数据报协议）是两种不同的通信协议，各自具有优缺点。\n游戏开发者通常根据游戏的特定需求在它们之间进行选择。以下是它们与多人游戏相关的情况：\nTCP（传输控制协议）： 可靠性： 这个协议就像一次可靠的对话。它确保数据包按正确的顺序到达目的地。对于那些事件顺序至关重要的游戏（如策略游戏或回合制游戏），这是至关重要的。它保证了所有的动作都以正确的顺序被服务器和其他玩家接收到。 确认： TCP要求接收方确认数据的接收，以便发送者知道数据是否已传送。在游戏中，这种确认有助于确保所有的动作、聊天消息和游戏更新都能可靠地接收和处理。 较慢： 由于其可靠性机制，与UDP相比，TCP可能会引入一些延迟，称为延迟。在快节奏的动作游戏中，这种额外的延迟可能会被注意到，并影响玩家的体验。\nUDP（用户数据报协议）： 更快： UDP更像是一张快速的便笺。它更快，但可靠性较低。当设备使用UDP时，它们发送数据时不太担心数据是否完美到达。它更快，因为它没有与TCP相同的可靠性和确认机制。在快节奏的动作游戏中，其中分秒必争的反应至关重要时，UDP可以提供更具响应性的体验。\n可靠性较低： UDP不保证数据会按正确的顺序接收，并且不保证每个数据都会到达目的地。一些数据包可能会丢失，或者它们可能会以错误的顺序到达。\n用于实时游戏： 许多实时和动作导向型的游戏，如第一人称射击游戏、赛车游戏和大逃杀游戏，使用UDP，因为速度和响应性比确保每个数据都被接收更为重要。\n总结 总之，TCP更可靠但可能会引入一些延迟，因此适用于准确性和事件顺序至关重要的游戏。\nUDP牺牲了一些可靠性，以换取速度，因此更适用于快节奏、实时多人游戏。\n游戏开发者通常根据游戏的特定要求和设计选择其中之一，甚至结合两者使用。\n往返时间（RTT） RTT（Round-Trip Time）是多人游戏中的一个重要概念，指的是数据包从你的计算机到游戏服务器再返回的时间。\nRTT对多人游戏的影响如下：\n延迟测量： RTT是延迟的一种度量。在游戏中，较低的RTT意味着较少的延迟，而较高的RTT意味着更多的延迟。较低的RTT值是理想的，因为它们使游戏感觉更加响应。\n游戏响应性： 在快节奏的多人游戏中，比如第一人称射击游戏、赛车游戏或实时策略游戏，低RTT至关重要。它确保你的动作，比如射击对手或移动角色，能够被服务器快速注册，并且你能够迅速在屏幕上看到结果。\n服务器选择： 当你连接到一个多人游戏时，通常会选择一个RTT最低的游戏服务器。这确保你在一个物理上距离你更近的服务器上进行游戏，减少了数据往返所需的时间。许多游戏允许你选择服务器，或者自动连接到RTT最佳的服务器。\nPing： “Ping”是用来测量RTT的命令。当你“ping”一个服务器时，你实际上是向它发送一个小消息，并测量接收到回复所需的时间。结果就是你的ping时间，它经常显示在在线游戏中，以帮助你评估你的连接质量。\n序列化 序列化是将复杂的游戏数据，比如玩家位置、角色属性或其他游戏中的信息，转换成一种可以在网络上传输的格式的过程。\n可以将其想象成将数据打包，以便可以从一个设备高效地发送到另一个设备，类似于将消息翻译成通用语言以便于交流。\n以下是序列化的重要性及其工作原理：\n数据传输 在多人游戏中，玩家的动作和游戏更新需要实时地传达给其他玩家和游戏服务器。这些数据可能非常复杂，包括角色的三维位置、游戏对象的状态等等。 效率 序列化旨在使这些数据紧凑且高效地进行传输。它将数据转换为一种标准化的格式，易于通过网络发送，从而最小化数据量，并确保它能够快速传输。 反序列化 在接收端，序列化的数据会被“反序列化”，也就是将其转换回其原始、可用的形式。这就像接收者将消息翻译回他们的语言以便理解一样。 同步机制 在多人游戏中，同步指的是确保所有玩家在游戏中看到相同的游戏世界，并在同一时间经历相同的事件的过程。\n以下是同步的重要性及其工作原理：\n一致性： 在多人游戏中，确保所有玩家对游戏世界有一致的视角至关重要。这意味着如果一个玩家击中了一个敌人，所有其他玩家应该看到这个敌人被击中并做出相应的反应。如果同步没有正确发生，玩家可能会看到在游戏中发生不同的事情，这可能会令人沮丧和不公平。\n时间： 同步还涉及时间。游戏中的事件和动作应该按照正确的顺序和在正确的时刻发生。例如，在赛车游戏中，确保所有玩家在同一时间经历相同的比赛事件（如转弯、障碍和道具），从而确保公平竞争。\n延迟补偿： 为了实现同步，游戏开发者通常需要补偿玩家和服务器之间传输数据时产生的延迟。例如，如果一个玩家的连接速度较慢，游戏可能需要预测该玩家的动作，以使游戏对所有参与者仍然感觉顺畅。\n丢包 数据包丢失发生在你的计算机（客户端）和游戏服务器之间发送的某些数据包未被接收或确认的情况下。\n这些丢失的数据包可能是由于网络拥塞或其他网络问题导致的。\n在多人游戏的背景下，数据包丢失可能会产生显著影响：\n游戏状态不一致： 当数据包丢失时，游戏服务器和玩家可能会失去同步。例如，如果你的角色移动命令丢失，你的游戏内位置将不会与服务器的视图匹配，导致你的动作与其他人看到的不一致。\n瞬移或抖动： 在某些情况下，数据包丢失可能导致玩家在游戏中出现“瞬移”或移动不稳定的情况。这是因为服务器和其他玩家正在尝试根据最后接收到的数据来估算你的位置。\n不公平的游戏体验： 在竞争性游戏中，数据包丢失可能导致不公平的优势或劣势。例如，如果一个玩家持续经历数据包丢失，他们的角色可能会变得更难击中，因为他们的动作是不可预测的。\n航位推测（DR） DR是多人游戏中用来估计其他玩家的位置和动作的技术，当网络数据延迟或丢失时使用。\n这就像在游戏中预测某人将会在哪里，当你在一段时间内没有收到他们位置的更新时。\n以下是DR在多人游戏中的工作原理：\n预测： 当玩家的设备（客户端）向游戏服务器发送数据时，服务器知道他们的当前位置和动作。然而，数据需要一些时间通过网络传输，在这段延迟期间，游戏中的情况可能会发生变化。 估算： 为了保持游戏运行的流畅，服务器估算当数据到达时玩家将会在哪里。这种估算是基于玩家之前的动作和当前游戏状态。就像根据朋友之前的移动来预测在游戏中标签游戏中朋友将会在哪里一样。 校正： 当延迟的数据最终到达时，服务器将其与自己的估算进行比较。如果存在差异，服务器可以纠正玩家的位置或动作。这有助于确保游戏世界对所有玩家保持一致。 可扩展性（Scalability） 多人游戏中的可扩展性指的是游戏处理越来越多玩家的能力，确保随着更多参与者加入，游戏体验依然愉快顺畅。\n以下是可扩展性的重要性及其工作原理：\n玩家基数增长： 多人游戏的目标是随着时间吸引更多的玩家。随着更多人加入并开始游戏，游戏必须能够适应这种增长，而不会出现显著的性能下降。\n服务器容量： 负责管理游戏世界和玩家互动的游戏服务器需要具备支持更大玩家群的能力。它应该处理更多的同时连接，处理更多的游戏数据，并在玩家数量增加时保持响应性。\n网络基础设施： 可扩展性也涉及到网络基础设施。这意味着网络技术和服务器架构应设计为能够处理更多的流量和连接，随着玩家基数的增长。\n负载测试： 游戏开发者通常进行负载测试来确定游戏的可扩展性。这涉及模拟大量玩家，并分析在高负载下游戏及其基础设施的表现。\n信息安全 多人游戏中的安全性涉及保护游戏、玩家和整体游戏体验免受各种威胁，特别是作弊、黑客攻击和未经授权的访问。以下是安全性的重要性及其工作原理：\n防作弊： 作弊可能会严重干扰多人游戏。作弊者可能使用第三方软件或黑客手段获取不公平的优势，比如透视墙或自动瞄准。安全措施，如反作弊系统，被设置为检测和阻止此类活动。\n账号保护： 玩家通常拥有带有有价值的游戏内物品、角色和进度的账户。这些账户需要防止未经授权的访问。安全功能，如双因素认证（2FA）和强密码要求有助于保护玩家账户。\n服务器安全： 游戏服务器本身需要安全保护，以防止未经授权的访问、数据泄露和其他网络攻击。一个受到威胁的服务器可能会导致游戏体验受到损害。\n安全交易： 在玩家可以购买游戏内物品或货币的游戏中，安全的支付处理对于保护金融信息并防止欺诈交易至关重要。\n玩家举报： 游戏通常提供给玩家举报不端行为（如作弊或骚扰）的工具。有效的审核和响应机制是保持积极友好的游戏社区的安全策略的一部分。\n网络安全： 支持多人游戏的网络基础设施也应该是安全的，以防止分布式拒绝服务（DDoS）攻击和其他基于网络的威胁。\n游戏代码和数据安全： 保护游戏代码和数据的完整性对于防止篡改、黑客攻击和引入恶意代码以干扰游戏或伤害玩家至关重要。\n","permalink":"https://yemilice.com/posts/%E5%A4%9A%E4%BA%BA%E6%B8%B8%E6%88%8F%E6%8A%80%E6%9C%AF%E5%9F%BA%E7%A1%80/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e很久不更新Blog了，是自己懈怠了很多，所以在思考了一下之后明白，我需要继续加强学习\u003c/p\u003e\n\u003cp\u003e加入ubisoft之后其实也没做什么值得称道的事儿\u003c/p\u003e\n\u003cp\u003e只是一个基础的开发人员，不过工作当中有一些可以被记录和梳理的地方\u003c/p\u003e","title":"多人游戏网络技术基础"},{"content":"前言 大家好，好久不见！\n最近太忙了，实在没时间上来写blog，昨天收获了最后一个offer（育碧），自此，我的跳槽计划完全结束\n我在一个月的时间拿下了育碧，动视，EA的Offer\n此处育碧Offer镇楼！哈哈哈，其他的我就不放了，其实还有个雷蛇也到了最后一轮，我的名字打码了，请见谅！\n我也要重新换一个地方开始我的人生了，再见了！我工作4年多的老公司！\n言归正传\n大家都知道我，我喊着要跳槽要跳槽，结果从去年（2021）年初到年底，都是雷声大，雨点小\n最后基本约等于没跳，看着以前的老兄弟们一个个离职，最后只有我一个老员工了，感觉百感交集\n加上12月中旬原公司的团队重组，这边换了新上司，新上司希望大展宏图，并且引入新的团队管理制度\n开始大规模的任用新人，引入新人管理\n自然而然，我就开始被边缘化了，也待的没意思，也感觉我会越来越down，心情也越来越坏\n所以，在12/25日，我决定跳槽，并且下定决心。\n面试准备 面试流程 基础的面试流程，这里面有两家都是这个流程，另外一个流程比较快\n电话沟通 (HR) 笔试 (codifity/算法平台) 电话面试（技术） 视频面试（技术） 视频面试 (HRBP/Team leader) 电话面试 (HR) Offer发放 基础技术栈的准备 首先是面试准备，这部分我已经准备了大概半年的样子，我从6月开始复习八股文\n八股文复习的方向，主要是和你的技术栈有关，例如，我的技术是Python/Golang后端开发\n首先是基础技术栈，相对于我这种工作几年的老哥来说，一般不会问什么\nimport 之后是个什么情况 ___main___代表什么 这种是对初级程序员来进行询问的，我们要认清楚自己的工作年限进行准备，而不是上来抱着基础梭哈，这属于浪费时间。\n这里我举几个面试里面的例子（此处我不会透露是哪家公司的！\n比如Python\n1. 多线程使用过吗？平常在什么时候使用？和多进程相比有什么好处？缺点在哪？ 2. 多线程之间的通信方式了解吗？ 3. 实现一个装饰器的timeout逻辑 4. 协程是什么？解决了什么问题？你会在什么情况下使用协程？或者进程？或者线程？ 比如Golang\n1. interface的反射机制了解吗？一般你会在什么情况下调用？ 2. 如果一个协程被系统调用，那么相对应的M和P会怎么操作？ 3. 说一下slice扩容的方案 4. map是如何扩容的？ 5. map的hash冲突如何解决？ 6. map的负载因子是什么？ 大概就是举了几个例子，我们都是工作3-5年的老哥了，问的问题一般都会倾向实战或者高级八股\n如果不了解，或者没用过，这可不行！无论如何，就算背也要背下来！找工作，这事儿不寒碜，千万别整那些高尚因子，如果你不学习，好工作就只能被别人找走，你就只能继续跌落下去，有毛意思！\n这里我梳理了一些面试的基础资料，放置到我的Github当中。\n也会推荐几本书，我放到这下面了，大家看完，跳槽指定是没问题了。\n推荐书籍（必看！）\nGolang底层原理剖析（主力看协程部分） Golang专家编程（短小精悍的一本书） Python cookbook （主要看生成器，迭代器） Python 进阶编程 （快速随便看一遍） 这里梳理了一些我的八股文，放置在github当中了！\nhttps://github.com/Alexanderklau/prepare_materials\n希望大家都能好好使用！\n其他技术的准备 其他技术，也就是指：你使用过的框架，你使用过的第三方服务\n例如：redis，Elasticsearch，Mysql，Rabbitmq，kafka之类的\n这部分也是八股文居多，但是涉及到实战的也不少\n如果你没有核心使用，只是会用，那么你需要进行八股文的背诵\n比如redis，面试其中某个就问到了（我不会说是哪家的\n1. Redis 的缓存击穿？雪崩？预热？降级？ 2. Redis 为什么要用单线程？ 3. Redis 6.0为什么引入多线程？ 4. Redis 的内存优化该怎么做？ 5. 设计一个读多写少的高并发系统，用Redis进行缓存，如何设计？ 这种就是典型的八股，这种你背就完事儿了，这里我也会梳理一下然后推送到Github上\n如果，如果你只是为了面试，那么我建议只背诵八股文就好了\n此处梳理一些经典问题，都是面试遇到的\n1. 你一般如何设计一个es的Mapping 2. 遇到oom，你一般怎么处理？ 3. Redis 缓存雪崩是什么，怎么处理它？ 4. Etcd的raft协议描述一下 5. Elasticsearch脑裂如何发生的？怎么规避？ 6. 讲一下你开源的几个Es的基础库 7. 描述一下snowflake算法 8. 分布式锁的实现（redis，etcd） 9. 描述一下es的选主 10. rabbitmq的topic描述下 11. rabbitmq，kafka，rocketmq你是如何选择的？ 12. rabbitmq高可用的实现机制？ 大概就是这些八股文，你背就完事儿了。\n我整理的八股文\nhttps://github.com/Alexanderklau/prepare_materials\n推荐几本书，这几本书都是我看过的，希望大家也去看看吧\n我在Es方面下了很多功夫，大家也可以选择自己的专精点进行突破\nElasticSearch搜索引擎构建和实战 ElasticSearch源码剖析 Etcd技术内幕 Rabbitmq实战指南 Redis实战 英文准备 这部分要看你之前的积累，可以看到，我投的基本都是外企\n国内的企业，例如网易，腾讯我看都不看，我不想进去996，也不想进去当人肉电池，我有我的人生和生活\n所以英语这方面，对大家伙儿还是挺重要的，这部分我只能说\n自我介绍 （尽量写好点，不要写中式英文） 问答部分 把自己的简历用英文写一遍，然后大概就知道人家说什么了，poor English也行，说清楚就可以了，词组不行的话就往外蹦词 讲真，如果不学好英语，你是无法进入外企打工的，平常如果想要进外企，你需要持续性的进行外语学习，不能停止 算法准备 没说的，leetcode直接上，没有捷径\n我去年开始刷题，到今年才敢直接上面试（其实是我太笨）\n一般面试的算法都是比较基础的\n梳理一下我这次被问到的几个算法\n最大岛屿面积 （bfs） 翻转二叉树 LRU算法 布隆过滤器 （这个只是描述了一下） 设计多线程队列 （不能使用自带的列表类型） 黄金矿工问题 （dp算法） 推荐几本书\nlabuladong的算法小抄 Python数据结构与算法分析 算法（第四版） HR面试准备 我因为跳槽不多，这部分其实没有被怎么问到\n这几家的HR都非常专业，问的问题都是非常核心的\n例如\n你在上家公司做了很久，为什么要跳槽？ 你和同事们的关系如何？你平常如何和沟通能力差的同事进行技术对接？ 你对自己的职业规划？ 你认为自己在团队是一种什么样的角色？ 你可以描述下你的工作嘛？ 你对自己的评价？ 描述你自己的优点和缺点？ 到了这一步，其实你已经稳了，如果你跳槽太多了，那你会被问的更多。\nTeam Leader面试准备 这部分主要是针对高级programer或者是team leader进行面试的\n我这里经历了两家公司都会有此项面试\n主要是对你的工作定位进行询问，对你的疑问进行答疑\n此处面试不难，但是你的简历经历最好不要作假，因为会询问很多公司业务部分的东西（如果你简历乱写，会背调的\n这部分不会询问技术细节，但是会考察你的架构能力，和你的沟通，分析问题的具体能力，举个例子\n在项目中，你遇到过棘手的问题吗？如何去处理的？ 项目管理部分，你是如何团结团队进行开发的？ 团队有多少人？你主要的定位？ 你对自己的评价 平常有带过新同事吗？怎么帮助他快速融入团队？ 大概就是这样，这部分不难。\n心态的调整 我12月经历了很多事，导致自己心态一度药丸\n具体我也就不说了，反正基本都是工作上的\n这个项目是我从无到有做起来的，现在调整业务线\n相当于这个迭代之后，业务就不会再更新功能了\n这部分属实就很难受。\n所以心态很差，不过至少顶着压力完成了所有笔试面试\n其实不是想给大家讲鸡汤\n如果撑不下去，就回家休息休息，不要把自己压垮\n可以看一些喜剧，武林外传之类的，开心点\n结尾 粗粗一看，又写了三千多字的流水账\n哈哈，仔细想想，一路走来，压力真的太大了\n我给自己立下誓言，跳槽不成功，我就不玩游戏\n现在我成功了，所以我又可以玩了\n过两天就过年了，希望大家都开开心心，可以得到自己想要的东西\n希望大家明年，跳槽加油！学习加油！\n爱你们！\n时间又不会为我赖着不走 干嘛停下来为了选择头疼 我的新目标就在下个路口 现在要做的就是加点油门 当我穿过拥挤的人群 穿过低谷时被冲洗的人情 感谢所有为我亮起的灯在我丧气的时候 总是仗义的过来按我门铃 ","permalink":"https://yemilice.com/posts/2022%E5%BC%80%E5%B9%B4%E7%A4%BC%E7%89%A9-%E6%8B%BF%E4%B8%8B%E8%82%B2%E7%A2%A7-%E5%8A%A8%E8%A7%86-eaoffer%E7%9A%84%E9%9A%8F%E6%83%B3/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e大家好，好久不见！\u003c/p\u003e\n\u003cp\u003e最近太忙了，实在没时间上来写blog，昨天收获了最后一个offer（育碧），自此，我的跳槽计划完全结束\u003c/p\u003e\n\u003cp\u003e我在一个月的时间拿下了育碧，动视，EA的Offer\u003c/p\u003e","title":"2022开年礼物-拿下育碧/EA/动视Offer的随想"},{"content":"前言 这几天攻关了一下协程通信，也顺利完成了\n我觉得每次学习都是一个新的经验\n狗公司今年没有年终\n我肯定是要走的，没必要继续停留\nGolang协程基础 （已经完成！） Golang协程调度 （已经完成！） Golang协程通信 (已经完成) Golang协程控制 Golang垃圾回收机制 CSP思想 要了解协程通信，首先就要了解CSP思想\nCSP全称是 Communicating Sequential Processes，意思是通信顺序过程\n这个的核心就是并发过程中进行交互，需要通过通道传递信息\n在CSP的设计思想当中，通过指定的通道发送信息或者接收信息完成通信\nCSP思想是Go并发的设计思想，所以Go语言里面定义了通道这种重要机制，这也是我们必须面对和掌握的\n我们也可以这么理解：Go的通道是实现Go协程通信的重要媒介\nGo语言的通道是什么 直接了当的说明吧\n我们会在Go代码里面看到很多类似下面这种代码\nvar work chan T chan \u0026lt;- float \u0026lt;-chan string 但凡带有chan的，全都是通道的东西，也就是说，当你们看到chan的时候\n就要明白，通道它来了。\nGo预言通道的使用方法 我在这里默认大家都懂Go，都写过Go，知道Go的基本语法。所以基础的语法，我这就不讲了。。大家不懂得回去再学一阵\n声明通道 声明一个名叫work的chan，通道里存储的数据类型为int\nvar work chan int 声明一个chan 存储int，不带箭头表示可读可写\nchan int 声明一个chan，只能写入int，不能读\nchan \u0026lt;- int 声明一个chan，只能读int，不能写\n\u0026lt;- chan int 初始化通道 我们声明完通道之后，不能马上使用，如果你用了，往里面写东西，就类似\npackage main import \u0026#34;fmt\u0026#34; func main() { var message chan string go func() { message \u0026lt;- \u0026#34;work\u0026#34; }() msg := \u0026lt;-message fmt.Println(msg) } 你就会得到一个错误返回\nfatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send (nil chan)]: main.main() ..... exit status 2 这他喵就是Go的一个固定逻辑，当你不初始化的时候，你是没发往里写东西的\n初始化的操作就是\nmessage := make(chan string) 修改下上面的代码\n...... message := make(chan string) go func() { message \u0026lt;- \u0026#34;work\u0026#34; }() msg := \u0026lt;-message fmt.Println(msg) 现在的返回完全正常了\n通道写入数据 前面我已经举了个简单的代码例子了\n其实再讲明白点，箭头代表你要干嘛\n通道变量 \u0026lt;- 值 把信息写入到channel里面\nmessages \u0026lt;- \u0026#34;ping\u0026#34; 这就是写入，简单吧\n通道读取数据 读取，一般的用法，是把取出来的值赋值出去\n类似\nmsg := \u0026lt;-message 读取分为两种方式\n阻塞式接收 这种就类似上面那种形式\nmsg := \u0026lt;-message 这里的message如果为空，是不会退出的，会持续性阻塞在这里\n直到获取到message之后，才会完全退出\n非阻塞接收 msg, ok := \u0026lt;-message 这里的ok是一个bool类型，如果获取到bool为false，则通道完全关闭\n关闭通道 很简单，直接close通道\nclose(message) 如果你往一个close的通道里面持续写入\nfunc main() { message := make(chan string) go func() { message \u0026lt;- \u0026#34;work\u0026#34; close(message) }() msg, ok := \u0026lt;-message message \u0026lt;- \u0026#34;sda\u0026#34; fmt.Println(msg, ok) } 这里会报错\npanic: send on closed channel goroutine 1 [running]: main.main() .... exit status 2 你不可以向一个已经关闭的通道写数据，但是！但是！但是！\n你可以向一个已经关闭的通道读数据！\nfunc main() { message := make(chan int) go func() { message \u0026lt;- 1 close(message) }() msg := \u0026lt;-message fmt.Println(msg) time.Sleep(time.Second * 2) msg2 := \u0026lt;-message fmt.Println(msg2) } 这里返回\n1 true 0 false 看到没，即使已经关闭，我们都可以读到一个0值\n如果我们不去判断通道是否关闭，而是只获取值，那么函数将永远不会结束\n所以这里就需要我们上面的判断了\n改写一下代码\nmsg2, ok := \u0026lt;-message if !ok { fmt.Println(\u0026#34;stop\u0026#34;) } 这样写不够优雅，我们后面更新一个优雅的写法\n通道只读/只写 这个前面咱们讲过，可以根据箭头指定写入还是读取\n那么建立的时候，咱们也可以通过箭头给它规定死到底是读还是写\n只写通道 var c = make(chan \u0026lt;- int) 这句的意思就是，这个通道只能写入不能读取整数\n只读通道 var c = make( \u0026lt;- chan int) 普通通道（可读可写） var c = make(chan int) 通道缓冲（限制通道大小） 这里的通道缓冲，可以理解为控制消息数量，你将它理解为队列机制\n假设我们的机器处理能力有限，需要限制接收的消息\n限制为最大接收3个消息\nwork := make(chan int， 3) 当超过这个消息的时候，则会发生阻塞\n所以我们可以利用chan来控制消息数量\n或者限制协程执行数量\n或者实现一个简单的协程池\n根据用例深入了解通道 上面我们讲了什么是通道，或者通道是怎么使用的，下面我们找一段代码来逐渐分析下\n通道一般在代码里面是怎么使用的\n写一段简单的生产者-消费者模型代码\npackage main import \u0026#34;fmt\u0026#34; func work() { // 创建一个无缓存的chan 队列，往里面写int数据 g := make(chan int) // 创建一个quit，传输状态 quit := make(chan bool) // 创建一个全局判断状态，传输状态 quitwork := make(chan bool) // 启动一个线程开始监听 go func() { // 走一个无限循环 for { // select循环机制 select { // 如果我们可以持续获取到g case v := \u0026lt;-g: // 打印一发 fmt.Println(v) // 如果我们检测到quit == true，则退出 case \u0026lt;-quit: fmt.Println(\u0026#34;读取协程 end\u0026#34;) // 将主状态定为true quitwork \u0026lt;- true return } } }() // 启动一个写入协程，开始写数据 go func() { // 往g里面塞数据逻辑的协程 for i := 0; i \u0026lt; 10; i++ { g \u0026lt;- i } // 给quit赋个值为true fmt.Println(\u0026#34;写入协程，读取协程退出\u0026#34;) quit \u0026lt;- true }() \u0026lt;-quitwork fmt.Println(\u0026#34;work end\u0026#34;) } func main() { work() } 上面我完成了一个协程写入，一个协程监听的逻辑\n所以说，如果，我们要进行chan的开发\n我们需要两个协程\n一个往里写\n一个往外读\n往里写我已经讲明白了，现在我讲一下往外读的逻辑\n各位发现了没，协程监听逻辑里面有一段代码\n// 启动一个线程开始监听 go func() { // 走一个无限循环 for { // select循环机制 select { // 如果我们可以持续获取到g case v := \u0026lt;-g: // 打印一发 fmt.Println(v) // 如果我们检测到quit == true，则退出 case \u0026lt;-quit: fmt.Println(\u0026#34;读取协程 end\u0026#34;) // 将主状态定为true quitwork \u0026lt;- true return } } }() 这段代码就是往外读的核心机制\n我把它改写一些，让它不要看着那么复杂\nfunc ReadChan(g chan int, quit chan bool, quitwork chan bool) { for { // select循环机制 select { // 如果我们可以持续获取到g case v := \u0026lt;-g: // 打印一发 fmt.Println(v) // 如果我们检测到quit == true，则退出 case \u0026lt;-quit: fmt.Println(\u0026#34;读取协程 end\u0026#34;) // 将主状态定为true quitwork \u0026lt;- true return } } } 好了，核心就是在那个for-select部分，下面，我就来讲一下这里吧。\nselect 首先，当我们需要和多个通道进行通信，或者需要对通道进行判断的时候\n我们一定会用到select，select使用的核心就是一个通道读写阻塞的时候，不会影响其他通道进行work\nselect的使用方法有些像switch\nselect { case \u0026lt;-ch1: ... case \u0026lt;-ch2: ... default: ... } select的随机性 首先，select，意思是选择，如果我们有多个管道待执行，同时准备好操作\n那么select将会随机选择管道执行，举个例子\nc := make(chan int, 1) c \u0026lt;-1 select { case \u0026lt;-c: fmt.Println(\u0026#34;work 1\u0026#34;) case \u0026lt;-c: fmt.Println(\u0026#34;work 2\u0026#34;) } 这里的返回，有时候输出 work 1，有时候输出work 2，这就是它的随机机制。\nselect的阻塞 继续上面的代码，我把它改写一下\nc := make(chan int, 1) // c \u0026lt;-1 select { case \u0026lt;-c: fmt.Println(\u0026#34;work 1\u0026#34;) case \u0026lt;-c: fmt.Println(\u0026#34;work 2\u0026#34;) } 如果select处没有任何通道能够符合要求，则会持续性阻塞下去，直到传入一个新的值为止\n那么如何处理这种问题呢？\n加一个default就行，这个意思就是，当都不满足要求，执行这个分支\nc := make(chan int, 1) // c \u0026lt;-1 select { case \u0026lt;-c: fmt.Println(\u0026#34;work 1\u0026#34;) case \u0026lt;-c: fmt.Println(\u0026#34;work 2\u0026#34;) default： fmt.Println(\u0026#34;work 3\u0026#34;) } select的控制 我们可以利用select控制管道，也可以自己指定规则进行退出，或者是下一步操作\n例如，假设我们要实现一个，如果300s没有消息传入，那么我们就退出这个select\n报告超时\n超时机制实现\nc := make(chan int, 1) select { case \u0026lt;-c: fmt.Println(\u0026#34;work 1\u0026#34;) case \u0026lt;-c: fmt.Println(\u0026#34;work 2\u0026#34;) case \u0026lt;-time.After(5 * time.Second): fmt.Println(\u0026#34;timeout.....\u0026#34;) } for-select循环 这也就是我上面写的那个逻辑\n一般情况下，我们不希望它马上退出，而是希望它不断循环操作\nfor { // select循环机制 select { // 如果我们可以持续获取到g case v := \u0026lt;-g: // 打印一发 fmt.Println(v) // 如果我们检测到quit == true，则退出 case \u0026lt;-quit: fmt.Println(\u0026#34;读取协程 end\u0026#34;) // 将主状态定为true quitwork \u0026lt;- true return } } 这种一般适用于生产者消费者模型，协程池等场景\n还有一种定时发送的场景\n// 每隔2s发送一个信息，写入通道数据 tick := time.Tick(time.Second * 2) for { // select循环机制 select { // 如果我们可以持续获取到g case v := \u0026lt;-g: // 打印一发 fmt.Println(v) // 往里写东西 case \u0026lt;-tick: fmt.Println(\u0026#34;tick\u0026#34;) // 如果我们检测到quit == true，则退出 case \u0026lt;-quit: fmt.Println(\u0026#34;读取协程 end\u0026#34;) // 将主状态定为true quitwork \u0026lt;- true return } } Go通道的原理 又到了喜闻乐见看源码的时间了，我每次看源码看完脑瓜仁都会痛。。。\n那就开始呗\nchan的源码，放置在/go/src/runtime/chan.go 下\nchan的结构体 type hchan struct { qcount uint // 通道中的消息个数 dataqsiz uint // 通道中的数据大小 buf unsafe.Pointer // 存放实际数据的指针 elemsize uint16 // 通道类型大小 closed uint32 // 通道是否关闭 elemtype *_type // 通道类型 sendx uint // 发送者的序号（ID） recvx uint // 接受者的序号（ID） recvq waitq // 读取的阻塞队列 sendq waitq // 写入的阻塞队列 lock mutex\t// 并发的保护锁 } type waitq struct { first *sudog //代表等待列表中的一个g（开始） last *sudog //代表等待列表中的一个g（结束） } *sudog这个函数指的是某个g，这个g用于发送和接收\n这部分代码在/go/src/runtime/runtime2.go里\n我将chan结构体都做了解释翻译，看注释就行了\n其实，将chan理解为一个环形队列\n数组，序号recvx和revcq组成了一个环形队列\nrecvx代表元素在通道里面的位置（读取位置）\nsendx代表写入通道时元素所在的位置（写入位置）\nrecvx到sendex的距离就是通道里面的消息个数总数（读取位置+写入位置 = 消息总数）\n举个例子\n一个chan一共有4个缓存\n[0, 0, 0, 0]\n现在写入数据\n[1, 2, 3, 4]\n那么sendx = 1，recvx = 3\n所以count = sendx + recvx\n这块非常复杂，我看了源码和书都不太能整明白，只有后面继续学习了。\n初始化chan的源码 func makechan(t *chantype, size int) *hchan { // 检查 ... // 计算需要分配多少大小，根据你传入的size来决定 mem, overflow := math.MulUintptr(elem.size, uintptr(size)) if overflow || mem \u0026gt; maxAlloc-hchanSize || size \u0026lt; 0 { panic(plainError(\u0026#34;makechan: size out of range\u0026#34;)) } var c *hchan switch { // 当分配的大小为0 case mem == 0: //在内存中进行gc // mallocgc是分配内存的函数 c = (*hchan)(mallocgc(hchanSize, nil, true)) // Race detector uses this location for synchronization. c.buf = c.raceaddr() // 如果元素不包含指针 case elem.ptrdata == 0: // hcahan 元素 和 size元素全相加 c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) c.buf = add(unsafe.Pointer(c), hchanSize) default: // 默认情况,包含指针,单独分配内存空间 c = new(hchan) c.buf = mallocgc(mem, elem, true) } ... } 这里最核心的部分就是分配内存的地方\n首先,如果我们指定分配长度为10，那么就会计算10长度需要多少mem，分配指定大内存大小，单独分配内存空间才可以进行gc回收\n写入chan的源码 func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { // 如果通道关闭 if c.closed != 0 { unlock(\u0026amp;c.lock) panic(plainError(\u0026#34;send on closed channel\u0026#34;)) } // 如果有正在等待的读取协程 if sg := c.recvq.dequeue(); sg != nil { // 发送协程 send(c, sg, ep, func() { unlock(\u0026amp;c.lock) }, 3) return true } // 如果队列的总数小于通道的数量（缓冲区有可用空间） if c.qcount \u0026lt; c.dataqsiz { qp := chanbuf(c, c.sendx) if raceenabled { racenotify(c, c.sendx, nil) } // 分配内存，加入队列 typedmemmove(c.elemtype, qp, ep) // 修改链表的next + 1 c.sendx++ // 如果增加之后就缓冲区满了 if c.sendx == c.dataqsiz { // 将sendx置换为0 c.sendx = 0 } // 将总量 + 1 c.qcount++ unlock(\u0026amp;c.lock) return true } // 阻塞通道操作 gp := getg() mysg := acquireSudog() mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } // No stack splits between assigning elem and enqueuing mysg // on gp.waiting where copystack can find it. mysg.elem = ep mysg.waitlink = nil mysg.g = gp mysg.isSelect = false mysg.c = c gp.waiting = mysg gp.param = nil // 发送消息到queue里面 c.sendq.enqueue(mysg) } 写入协程的时候，有三种不同状态，将进行下面的分析\n直接写入协程 如果我们不分配协程的缓存直接写入，会走到这个分支\n首先，hchan的recvq维护了一个协程链表\nrecvq指的是等待的协程链表，每个协程就是一个*sudog，这个前面讲过，就是一个协程的表示方法\n首先，recvq会获取协程的元素指针，默认获取链表中的第一个协程\n会直接将sudog这个玩意儿复制给对应协程，然后进行协程唤醒操作（执行协程）\n缓冲区写入协程 我们在meke协程的时候，如果指定了缓冲区，那么就会走到这个分支\n首先判断现有队列里面元素的总量：c.qcount\n然后获取到缓冲区的总量： c.dataqsiz\n如果小于总量，则认为缓冲区还有空间\n这时候执行分配内存的操作（向缓冲区写入数据）\n然后调整sendx,让它+1(理解链表)\n阻塞协程（缓冲区无空余，阻塞操作） 这个操作就是咱们提到的阻塞\n如果缓冲区满了,或者协程通道没准备好,就会造成阻塞\n这里将sudog进行了赋值修正,然后调用c.sendq.enqueue(mysg),这里是将sudog放入到链表的末尾,然后协程就会进入休眠状态 gp.waiting\n读取chan的源码 func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { // 如果为空 if c == nil { if !block { return } gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2) throw(\u0026#34;unreachable\u0026#34;) } // 如果获取到了等待的协程（可以读取） if sg := c.sendq.dequeue(); sg != nil { recv(c, sg, ep, func() { unlock(\u0026amp;c.lock) }, 3) return true, true } // 如果缓冲区里面有元素 if c.qcount \u0026gt; 0 { // Receive directly from queue qp := chanbuf(c, c.recvx) if raceenabled { racenotify(c, c.recvx, nil) } if ep != nil { typedmemmove(c.elemtype, ep, qp) } typedmemclr(c.elemtype, qp) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.qcount-- unlock(\u0026amp;c.lock) return true, true } gp := getg() mysg := acquireSudog() mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } mysg.elem = ep mysg.waitlink = nil gp.waiting = mysg mysg.g = gp mysg.isSelect = false mysg.c = c gp.param = nil c.recvq.enqueue(mysg) } 其实读取的代码和和写入的有点像，直接分析\n读取正在等待的协程 还是默认操作，从协程链表rescv里面获取第一个协程\n然后复制，最后唤醒阻塞的写入协程\n读取缓冲区里的协程 缓冲区里面有数据，直接读取，然后写入当前的读取协程中\n阻塞协程（无法读取，缓冲区为空） 缓冲区没数据，把sudog放入链表末尾，然后休眠协程，等待写入并且重新执行\nGo通道的几个使用场景 因为篇幅，下面讲一下几个重要的应用场景，我这边会给出大致代码，实现的话我会附上我的GITHUB链接，如果需要的话直接去GITHUB里DOWN下来就行了\n控制协程数 channel可以控制协程的数量，换句话说，我们可以通过控制channel缓存，来控制有几个协程执行\n所以我们可以根据这个特性弄个协程池出来\n首先，协程池已经有相关的第三方开源实现\n例如ants就非常好用\n未来我会详细的读一下ants，然后出个blog给大家看\n我这里只是举个简单的例子，也就是通过chan去控制同时执行的协程\n假设我们现在写个代码，去并发执行一些逻辑\n//Read 假装在执行一个逻辑 func Read(i int) { fmt.Printf(\u0026#34;go func: %d\\n\u0026#34;, i) time.Sleep(time.Second) } func main() { userCount := math.MaxInt64 for i := 0; i \u0026lt; userCount; i++ { go func(i int) { go Read(i) }(i) } } 这时候，你不要去跑这段代码，因为你一定会卡死。因为你开了太多的协程。。。\n然后我们拿chan控制一下这个孙子\n说下我的思想\n首先make chan 造一个有缓存的通道 传输通道， wg.add 一个协程 通过通道控制并发 改写一下代码，如下\nvar wg = sync.WaitGroup{} func Read(ch chan bool, i int) { defer wg.Done() ch \u0026lt;- true fmt.Printf(\u0026#34;go func: %d, time: %d\\n\u0026#34;, i, time.Now().Unix()) time.Sleep(time.Second) \u0026lt;-ch } func main() { userCount := 10 ch := make(chan bool, 2) for i := 0; i \u0026lt; userCount; i++ { wg.Add(1) go Read(ch, i) } wg.Wait() } 这就是大概的协程控制逻辑\n超时操作 这个前面我记得我讲过\n固定的time.After这个逻辑就行\n这部分代码套用了\nhttps://eddycjy.gitbook.io/golang/di-1-ke-za-tan/control-goroutine#chang-shi-chan-+-sync\nfunc doWithTimeOut(timeout time.Duration) (int, error) { select { case ret := \u0026lt;-do(): return ret, nil case \u0026lt;-time.After(timeout): return 0, errors.New(\u0026#34;timeout\u0026#34;) } } func do() \u0026lt;-chan int { outCh := make(chan int) go func() { go work() }() return outCh } 结尾 其实这篇文章写挺久了。，。。\n但是我还是没把它好好写完\n我太懒了，不行，我要加倍努力，加油加油！！！！\n","permalink":"https://yemilice.com/posts/golang%E5%8D%8F%E7%A8%8B%E9%80%9A%E4%BF%A1%E6%8E%A2%E7%A9%B6/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这几天攻关了一下协程通信，也顺利完成了\u003c/p\u003e\n\u003cp\u003e我觉得每次学习都是一个新的经验\u003c/p\u003e\n\u003cp\u003e狗公司今年没有年终\u003c/p\u003e\n\u003cp\u003e我肯定是要走的，没必要继续停留\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ca href=\"https://yemilice.com/2021/10/27/golang%E5%8D%8F%E7%A8%8B%E5%9F%BA%E7%A1%80%E6%8E%A2%E7%A9%B6/\"\u003eGolang协程基础 （已经完成！）\u003c/a\u003e\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ca href=\"https://yemilice.com/2021/11/08/golang%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6%E6%8E%A2%E7%A9%B6/\"\u003eGolang协程调度 （已经完成！）\u003c/a\u003e\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eGolang协程通信 (已经完成)\u003c/li\u003e\n\u003cli\u003eGolang协程控制\u003c/li\u003e\n\u003cli\u003eGolang垃圾回收机制\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"csp思想\"\u003eCSP思想\u003c/h2\u003e\n\u003cp\u003e要了解协程通信，首先就要了解CSP思想\u003c/p\u003e","title":"Golang协程通信探究"},{"content":"前言 最近暴露出一个炸弹级别的漏洞 log4j\nLog4j的GitHub公开披露了一个影响 Apache Log4j 2 实用程序多个版本的高严重性漏洞 (CVE-2021-44228)。\n该漏洞影响了 Apache Log4j 2 的 2.0 到 2.14.1 版本。\n2.0 \u0026lt;= Apache log4j2 \u0026lt;= 2.14.1\n具体详情可以参看 https://logging.apache.org/log4j/2.x/\n首先，受到影响的Es家族系列有\nElasticsearch Logstash Elastic Cloud APM Java Agent Elastic Cloud Enterprise Elastic Cloud on Kubernetes Swiftype 很不幸，鄙人维护的多个Es项目都在此内，需要立马处理并且解决问题\n处理方案 处理方案（1） 检查所有使用了 Log4j 组件的系统，并且立即使用log4j-2.17进行替换 重启 Elasticsearch 节点 检查所有使用了 Log4j 组件的系统，并且立即使用log4j-2.17进行替换\n原来是2.15版本。但是2.15版本被暴露出有新的bug。。。\n这个修复起来属实没完了，所以我这里是最新的2.17版本！\n官方给出了具体链接：https://logging.apache.org/log4j/2.x/changes-report.html#a2.17.0\n但是我寻思估计大家都比较懒。。。\n所以我写了个脚本去处理这个问题\n我把脚本和包都放置到了github里，直接调用bash就行了，应该是最简单的办法\n具体使用方法在下面\n处理方案（2） 如果不方便替换jar，则必须修改系统变量\n修改 JVM 参数 -Dlog4j2.formatMsgNoLookups=true 修改配置 log4j2.formatMsgNoLookups=True 将系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS 设置为 true 重启 Elasticsearch 节点 处理方案（3） 立马将线上机器下线，并且进行网关配置\n例如nginx其他API访问阻隔请求，这里我不说了，因为我这里没涉及到。\n脚本处理方案 首先把我的脚本下下来\n地址：https://github.com/Alexanderklau/Amusing_python/tree/master/Network_security/log4j-fix\n或者直接拖那个rar下来，都是一样的\nhttps://github.com/Alexanderklau/Amusing_python/blob/master/Network_security/log4j-fix/log4j-fix.rar\n解压完之后\n首先进行检查\n此处显示无风险，因为我已经修复过了，如果你的jar版本低于15，应该会显示有风险\n再运行脚本进行替换\n替换完毕\n手动重启Es服务\nsystemctl restart elasticsearch 结束\n","permalink":"https://yemilice.com/posts/elasticsearch%E9%87%8D%E5%A4%A7%E6%BC%8F%E6%B4%9E-log4j%E7%9A%84%E5%A4%84%E7%90%86%E6%96%B9%E6%A1%88/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e最近暴露出一个炸弹级别的漏洞 log4j\u003c/p\u003e\n\u003cp\u003eLog4j的GitHub公开披露了一个影响 Apache Log4j 2 实用程序多个版本的高严重性漏洞 (CVE-2021-44228)。\u003c/p\u003e\n\u003cp\u003e该漏洞影响了 Apache Log4j 2 的 2.0 到 2.14.1 版本。\u003c/p\u003e","title":"ElasticSearch重大漏洞-log4j的处理方案-12月20日更新！！！！！"},{"content":"前言 翻唱，听听就得\n原版\nhttps://www.youtube.com/watch?v=-OE1DvOaq7o\n歌曲 歌词 내가 널 울린 적 있니 我曾把你弄哭\n넌 항상 옳은 선택을 했니 你总是做正确的选择\n넌 이미 그런 길에 서 있어 你已经处于那样的一条路上\n나 역시 낭떠러지에 서 있어 而我果然还是站在了悬崖边\n바람도 기억하는 가운데 风也看向记忆中\n사랑을 살인한 우리 가슴을 보는 게 曾嗜杀爱情的我们的心\n그깟 소주 몇 잔에 널 잊어 보려 해 那种东西就让它在推杯换盏中被遗忘吧\n충동적이고 가볍고 서럽고 그래 我那样冲动轻浮过而伤心过\n너도 충동적이고 가볍고 그래 你也曾那样冲动不安过\n그 예민함은 도가 지나칠 정도로 타이트해 那种敏感而脆弱让我们过度用力地紧握爱情\n개 뿔 이게 무슨 소강 상태 这算什么正常的状态\n최악이야 불행이야 불이 너무 번진 상태 最坏的不幸的太过火的状态\n인간이기에 사랑은 멀어진대 听说人世间的爱情已变稀物\n사랑은 결국 사람을 떠나간대 听说爱情终究也离我们而去\n강렬한 감정에 휩쓸려 사랑할 땐 被强烈的感情充盈着去爱的时候\n우린 조심했어야 해 我们那时应该更小心翼翼的\n눈물이 나도 꾹 참아 볼게 即使流泪也要强忍着\n가슴이 아파도 견뎌 볼게 即使心痛也要坚持着\n아직도 그대로인데 即使现在还是这样\n정말 난 그대로인데 我真的到现在还是这样\n내일이면 다시 볼 것만 같아 明天或许就能再见到了吧\n나를 울리지마 不要让我流泪\n너라는 여자는 하얗고 하얀색인데 你这样如雪纯净洁白的女人\n나란 새끼는 운이 참 나쁜 새끼야 遇到我这样倒霉的真正的混蛋\n매번 엔딩은 검게 그을리고 끝나 让结局每回都变得那样漆黑\n아니 꽃밭을 싹 불태워야 끝나 非要把花丛烧光才罢休\nresponsibility 책임감 어디 판거니 责任感何在\n그렇게 잃고도 웃음이 나냔 말이다 即使这样还笑得出来吗\n시간을 끌든지 연락을 끊든지 해 让时间过去断了联系\n그거 알아 이별은 연습이 불가능해 这些我都知道但是离别如何练习\n우리는 거품 거품 거품 거품 거품 我们是恋人啊恋人啊\n기억속에 사라질 커플 记忆里消失不见的恋人啊\n고개를 끄덕 끄덕 끄덕 끄덕 끄덕 头一点一点的样子\n서로가 이미 이별에 중독 彼此已经濒临分手告别之际\n","permalink":"https://yemilice.com/posts/black-hole%E9%BB%91%E6%B4%9Ecover-mc%E6%A2%A6--ailee/","summary":"\u003ch1 id=\"前言\"\u003e前言\u003c/h1\u003e\n\u003cp\u003e翻唱，听听就得\u003c/p\u003e\n\u003cp\u003e原版\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.youtube.com/watch?v=-OE1DvOaq7o\"\u003ehttps://www.youtube.com/watch?v=-OE1DvOaq7o\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"歌曲\"\u003e歌曲\u003c/h2\u003e\n\n\u003caudio src=\"https://blog-1256169066.cos.ap-chengdu.myqcloud.com/music/%EB%B8%94%EB%9E%99%ED%99%80%EF%BC%88%E9%BB%91%E6%B4%9E%EF%BC%89%EF%BC%88Cover%20MC%E6%A2%A6%20%EF%BC%8F%20Ailee%EF%BC%89.mp3\" controls muted loading=\"lazy\"\u003e\u003c/audio\u003e\n\n\u003ch2 id=\"歌词\"\u003e歌词\u003c/h2\u003e\n\u003cp\u003e내가 널 울린 적 있니\n我曾把你弄哭\u003c/p\u003e\n\u003cp\u003e넌 항상 옳은 선택을 했니\n你总是做正确的选择\u003c/p\u003e\n\u003cp\u003e넌 이미 그런 길에 서 있어\n你已经处于那样的一条路上\u003c/p\u003e\n\u003cp\u003e나 역시 낭떠러지에 서 있어\n而我果然还是站在了悬崖边\u003c/p\u003e","title":"Black Hole（黑洞）（Cover MC 몽 ／ Feat. Ailee(에일리）"},{"content":"前言 这几天攻关了一下go协程的调度，然后写了这篇文章\n这篇文章的工作量真挺大的，我看的也很累，找资料也很累\n这篇文章，需要感谢《Go语言底层原理剖析》这本书的作者，这本书给了我很大启发\n并且这本书写的非常好，这本书类似我这篇文章的大纲，对我非常有帮助！\n希望大家支持这本书的作者！\n我的Go系列已经完成如下部分\n分别为\nGolang协程基础 （已经完成！） Golang协程调度 （已经完成！） Golang协程控制 Golang协程通信 Golang垃圾回收机制 所以我会持续更新，大家请期待吧，爱你们！\n总是笃定成功必有收获 每次都是竹篮打水得过且过 总在幻想能写出什么旷世杰作 咽下苦果，传道而授业解惑 协程的几种状态 Gidle 表示协程刚开始创建的状态\nGdead 当新的协程初始化后,会转为Gdead状态,被销毁的时候也是这个状态\nGrunnable 表示协程在运行队列中,正在等待运行\nGrunning 表示协程正在运行,已经分配好了线程\nGwaiting 表示协程被所动,不能执行代码,一般垃圾回收,channel通信的时候会出现这种状态\nGsyscall 表示协程正在执行系统调用\nGpreempted 表示协程被强制抢占的状态\nGcopystack 表示发现需要扩容/缩小协程栈空间,转移到新栈的状态\n协程的状态转移 首先看个图\n解释下这个图,首先,协程被创建,状态为Gidle\n然后协程的状态转为Gdead,再转为Grunnable,等待运行\n然后执行协程,状态为Grunning,此时可能出现多个状态\n第一个,协程被抢占,转为状态Gpreempted\n第二个,协程正在调用,转为状态Gsyscall\n第三个,协程正常执行结束,转为状态Gdead\n这就是基础的协程状态转移过程,这也是协程的生命周期\ng0协程是什么 在Golang当中，协程分为两种\n一种是主协程main\n另一种是子协程\n主协程只能有一个，但是翻阅一下Golang的源码，每个线程里面都有一个g0协程\n首先看下m（线程）的源码\ntype m struct { g0 *g // 带有调度栈的goroutine gsignal *g // 处理信号的goroutine tls [6]uintptr // thread-local storage mstartfn func() curg *g // 当前运行的goroutine caughtsig guintptr p puintptr // 关联p和执行的go代码 nextp puintptr id int32 mallocing int32 // 状态 spinning bool // m是否out of work blocked bool // m是否被阻塞 inwb bool // m是否在执行写屏蔽 ......... } 可以看到g0在工作线程m当中，也就是g0协程运行在操作系统栈上\ng0，类似于一个特殊角色，每一个m都会有一个g0如影随形，这是每个m被创建开始的第一个协程\n它的主要作用就是执行协程调度部分的代码，也就是控制协程切换的\n下面我就讲一下g0是怎么搞协程调度和切换的\ng0和协程切换 首先先复习一下GMP原理\n这个上一篇博客我有讲过,翻一下就得了\nGolang协程基础探究\n我们可以知道，P是产生M的，而M产生G，g0是创建的第一个协程goroutine\n看个图，最初始的创建状态应该如下所示\n然后我们假设现在开始执行一个协程g1，如下图所示\n现在g1出现了问题（超时，停止等）\ng2通过g0进行调度，替换g1为最新的执行goroutine\ng2成为正在执行的goroutine\n所以它和协程的关系大概就类似这样，如图所示\n协程的切换过程就是 g1 -\u0026gt; g0 -\u0026gt; g2\n协程的切换，被称为协程的上下文切换\n协程g1执行切换的时候，需要保存当前的执行现场，这是保证协程切换回来的时候能够正常执行\n执行现场的源码存储在gobuf的结构体当中，这里面分别保存了rsp，rbp， rip等重要信息，它们是CPU重要的寄存器值。\n执行现场gobuf的源码如下\ntype gobuf struct { // 保存CPU的rsp的寄存器值 sp uintptr // 保存CPU的rip寄存器值 pc uintptr // 记录gobuf 属于哪个 协程goroutine g guintptr ctxt unsafe.Pointer // 保存调用的返回值 ret uintptr lr uintptr // 保存rbp寄存器的值 bp uintptr // for framepointer-enabled architectures } 解释下几个重要名词\nrsp：始终指向函数调用栈栈顶\nrip：执行程序要执行的下一条指令的地址\nrbp：存储函数栈帧的起始位置\n这几个东西相当于犯罪现场的笔录\n当你重返犯罪现场的时候，开启笔录，你依旧可以开始调查，不会停止。\ng0的总结 所以，调度协程g0和普通的协程g完全不同\ng0作为调度协程，执行的函数和流程是固定的，并且为了避免栈溢出\ng0栈是会重复使用的\ng0不仅负责协程调度，还负责\n垃圾回收 动态栈增长 这就是调度协程g0的主要功能\n工作线程的绑定 工作线程是什么鬼\n通俗来说，GMP里面，M就是工作线程\n一般Go的调度器，使用线程本地存储将操作系统线程和代表线程的M结构体绑定在一起，然后才能继续其他的操作\n所以我们现在就来研究下线程本地存储这个东西\n首先看下M结构体的源码\ntype m struct { ...... tls [tlsSlots]uintptr // thread-local storage (for x86 extern register) ...... } 我这里截取了关键代码，tls指向的是本地存储的线程的地址\n艹，太绕了，我们来追一下下本地存储的线程的地址是个什么鸟玩意儿\n首先，线程本地存储是一种计算机方法，使用线程本地的静态和全局内存，线程本地存储的变量值，仅仅只对当前线程可见\n所以这种线程存储变量是私有的，操作系统使用的是FS/GS存储线程本地变量\n现在，替换一下我们前面的理解\n结构体M存储的是线程本地存储中线程的地址，所以在线程内部，我们可以获取当前线程的协程g, 逻辑处理器p等信息。\n结构体M存储在Fs寄存器当中，FS寄存器里面又存储了本地的线程变量，因为这个关系，从而实现工作线程和结构体M之间的绑定\n太TM饶了这个\n调度循环 那啥，这里不是重复一遍，上面讲的是协程切换\n这里是协程调度循环，是一个循环的流程，全称叫做调度循环\n我简单说一下吧\n就是从协程调度g0开始，找到要运行的协程g1进行切换\n然后切换回g0，通过不通的调度策略，获取其他的g，进行切换，直到没有g为止\n这里和协程切换不一样的一点是，协程切换关心的是协程状态，调度循环关注的是调度流程\n我拿蒋校长微操举个例子\n蒋校长告诉你我们要打徐州会战\n协程切换管的是切换的具体状态，\n协程切换就是开打，部队冲，打输了，退下来，优势在我这种状态\n协程调度循环管的是切换的具体流程，\n也就是怎么打，换谁上，先打哪里，先调谁，后用谁，机枪右移50米，空投手令之类的。\n这就是协程调度循环\n细化协程调度循环 调度循环的流程，可以参看下图，我将它们分为两个阶段进行解说\n看图，这里是个具体流程\n开始分析\n首先，协程g0切换到用户协程g，经历了schedule函数到execute函数再到gogo函数的过程\n其中用到的函数功能如下\nschedule函数 调度策略合集，处理调度 execute函数 状态转移，g和m之间的绑定 gogo函数 操作系统函数，完成栈切换和CPU寄存器的恢复等 这里细化一下流程图\n接下来进入第二阶段\n切换到协程g之后，g开始执行，当g出现了主动让渡，抢占，退出之后，将会执行到协程g0开始第二轮调度\n首先，从协程g切换到协程g0的时候，mcall用来保存当前协程的执行现场，这个我们上两节说过，gobuf存储了执行现场。然后进行切换，切换到g0之后，会根据切换的原因进行不同的函数选择\n具体的状态有如下几种\n主动让渡 这时候需要调用Gosched函数 协程退出 这时候需要调用Goexit函数 执行完毕之后，将会切换回g0函数，执行第一步阶段，开始下一个循环，形成闭环，无限重回\n调度策略 上面我讲了调度循环，里面讲到一个函数schedule\n这个函数是总管协程的调度策略的，相当于国防部\n这个代码的放置位置是 \u0026ldquo;runtime/proc.go\u0026rdquo;\n这里简单给大家看几个重要的函数\nfunc schedule() { // 获取到当前的goroutine _g_ := getg() ... var gp *g var inheritTime bool // 检查goroutine是否需要唤醒 tryWakeP := false if trace.enabled || trace.shutdown { gp = traceReader() if gp != nil { casgstatus(gp, _Gwaiting, _Grunnable) traceGoUnpark(gp, 0) tryWakeP = true } } if gp == nil \u0026amp;\u0026amp; gcBlackenEnabled != 0 { gp = gcController.findRunnableGCWorker(_g_.m.p.ptr()) if gp != nil { tryWakeP = true } } if gp == nil { // 检查全局队列，每隔一段时间 // Check the global runnable queue once in a while to ensure fairness. // Otherwise two goroutines can completely occupy the local runqueue // by constantly respawning each other. if _g_.m.p.ptr().schedtick%61 == 0 \u0026amp;\u0026amp; sched.runqsize \u0026gt; 0 { lock(\u0026amp;sched.lock) gp = globrunqget(_g_.m.p.ptr(), 1) unlock(\u0026amp;sched.lock) } } if gp == nil { gp, inheritTime = runqget(_g_.m.p.ptr()) // We can see gp != nil here even if the M is spinning, // if checkTimers added a local goroutine via goready. } if gp == nil { gp, inheritTime = findrunnable() // blocks until work is available } } 在这个函数里面，首先会检测程序是否处于垃圾回收阶段，然后再检测是否需要标记协程\n接下来是几个重点概念\nGo会使用队列，将等待执行的协程存放在其中 Go的协程队列分为局部运行队列和全局运行队列 Go的运行队列是一个先入先出的队列 来看一下Go调度器P的源码\n局部队列源码\ntype p struct { id int32 status uint32 // one of pidle/prunning/... ..... // Queue of runnable goroutines. Accessed without lock. // 可运行的协程队列 runqhead uint32 runqtail uint32 // 256个局部运行队列 runq [256]guintptr // 指向下一个要执行的协程 runnext guintptr ..... } 全局队列源码\n// 有一个头，看起来类似next的链表 type gQueue struct { head guintptr tail guintptr } // 这里是全局列表的代码 type schedt struct { // accessed atomically. keep at top to ensure alignment on 32-bit systems. goidgen uint64 // Global runnable queue. // 全局队列源码 runq gQueue runqsize int32 .... } 现在我们有了全局队列和局部队列\n调度的优先级 上面我们引入了两个很重要的概念，全局队列 和 局部队列\n那么我们原有的Golang的GMP模型就可以进行一下修改\n看个图\n首先，M生成P，P会去局部队列里获取G，然后全局队列在此处Stand By。\n所以可以总结出一般的调用逻辑\n根据上述的那张图，一般的思路是先去查找每个P的局部队列获取G，当局部队列为空的时候，再从全局队列中获取G，直到都没有G为止\n但是这里涉及一个很大的问题，就是很可能如果循环执行局部队列的G，就可能导致全局队列的G永远不会被运行获取。\n差了一些资料，Go里解决这个问题的策略是：\n当P中执行了61次调度之后，就必须从全局队列获取一个G，到当前P中执行 这里看下源码\n源码位置在：src/runtime/proc.go ，依旧还是那个调度函数schedule\nif gp == nil { // Check the global runnable queue once in a while to ensure fairness. // Otherwise two goroutines can completely occupy the local runqueue // by constantly respawning each other. // 如果可以被61整除并且全局队列的数量大于0 if _g_.m.p.ptr().schedtick%61 == 0 \u0026amp;\u0026amp; sched.runqsize \u0026gt; 0 { lock(\u0026amp;sched.lock) // 获取一个g gp = globrunqget(_g_.m.p.ptr(), 1) unlock(\u0026amp;sched.lock) } } 我们分析了队列调度的先后，那么就可以规划出来队列调度的优先级顺序了\n梳理了一下，有个图，大家伙看看\n首先，P执行调度的时候，首先会通过runnext获取下一个可执行的G，如果获取不到\n那么将尝试从局部队列寻找G，如果局部队列没有G了,就会从全局队列寻找G\n如果全局队列也没有G，那么将会从其他的P中窃取G进行运行\n如果都找不到G，那么P将会解除和M的绑定，M也进入休眠状态中\n获取局部队列 上一节我们梳理了什么是局部队列，什么是全局队列，以及他们的优先度，这段我就来说下他们是怎么获取的\n先看一下源码\nfunc runqget(_p_ *p) (gp *g, inheritTime bool) { // 获取下一个可用的G next := _p_.runnext // 如果下一个next地址不等于0并且cas成功，直接返回一个G // 如果下一个next地址不等于0并且cas失败，那么它只能被另一个P抢夺 // 其他P可以设置为0，当前只有P可以设置非0 // 只有CAS失败，则不需要重试 if next != 0 \u0026amp;\u0026amp; _p_.runnext.cas(next, 0) { return next.ptr(), true } for { // 获取头部 h := atomic.LoadAcq(\u0026amp;_p_.runqhead) // 加载，获取，和其他P同步 // 获取尾部 t := _p_.runqtail // 如果头尾相等，那么没有协程可以运行 if t == h { return nil, false } // 访问加锁 gp := _p_.runq[h%uint32(len(_p_.runq))].ptr() if atomic.CasRel(\u0026amp;_p_.runqhead, h, h+1) { // 推送执行了多少G return gp, false } } } 首先这里先检查runnext是否为空，如果不为空就直接拿出G\n如果为空就从局部队列进行查找，从队列头部获取到一个G然后返回\n循环获取头部尾部，头尾相等，证明没有协程可用，返回nil和false\n返回有个访问加锁，这里是避免其他P窃取任务时和当前P进行同时访问\n这里大概就是获取局部队列的一些相关知识了，这里看的很累，比较复杂\n获取全局队列 前面说过，当P执行了61次调度的时候，就会从全局队列里面拿取G优先执行\n全局队列，其实就是全局链表，每个P都可以从里面拿东西，有点类似消费者生产者的模型\n梳理一下队列转移的方法\n首先，我们要先统计P的数量，然后根据P的数量平均分配全局队列的G， 但是要拿取的数量不能超过局部队列的一半，当前是 256 / 2 = 128， 也就是说，不能一次性拿走超过128个G放到自己的局部队列里 获取到G之后，再通过循环将全局队列的G放入平均分配的P的局部队列当中 下面看一些源码\n// Try get a batch of G\u0026#39;s from the global runnable queue. // sched.lock must be held. func globrunqget(_p_ *p, max int32) *g { assertLockHeld(\u0026amp;sched.lock) if sched.runqsize == 0 { return nil } // 平均分配 // n就是每个p获取的g的数量 n := sched.runqsize/gomaxprocs + 1 if n \u0026gt; sched.runqsize { n = sched.runqsize } if max \u0026gt; 0 \u0026amp;\u0026amp; n \u0026gt; max { n = max } if n \u0026gt; int32(len(_p_.runq))/2 { n = int32(len(_p_.runq)) / 2 } // 全局队列 - n sched.runqsize -= n // gp = 队伍的最后一个 gp := sched.runq.pop() // 递减 n-- // 如果 n \u0026gt; 0 for ; n \u0026gt; 0; n-- { gp1 := sched.runq.pop() // 上传 runqput(_p_, gp1, false) } return gp } 协程窃取 如果一个P，获取不到它自己的局部队列的G，并且全局队列的G他也拿不到，那么他就可以去其他的P里面拿G过来跑\n这是Go很巧妙的一个设计逻辑，避免的持续性等待，空放P在那什么也不做\n首先这里分为两个部分讲解\n第一个是从其他P获取到G\n第二个是窃取G到自己的P\n那么，开始吧\n从其他P获取G 当自己的局部队列和全局队列都拿不到G的时候，则会开启这个分支剧情\n首先需要获取到所有的P，然后通过P获取到他们的局部队列\n这里最重要的几个特性就是\n随机性 公平性 首先P存储在全局的allp[] 里面，这是一个全局变量\n获取P的源码如下\n//从任何P中进行窃取的函数 func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) { //获取到G pp := getg().m.p.ptr() ranTimer := false //重复4次操作 const stealTries = 4 for i := 0; i \u0026lt; stealTries; i++ { stealTimersOrRunNextG := i == stealTries-1 // 二层循环，找到可窃取的P马上返回 for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() { if sched.gcwaiting != 0 { // GC work may be available. return nil, false, now, pollUntil, true } p2 := allp[enum.position()] if pp == p2 { continue } // 从P2里面进行窃取 if stealTimersOrRunNextG \u0026amp;\u0026amp; timerpMask.read(enum.position()) { tnow, w, ran := checkTimers(p2, now) now = tnow if w != 0 \u0026amp;\u0026amp; (pollUntil == 0 || w \u0026lt; pollUntil) { pollUntil = w } if ran { // 检查现在是否要运行的本地G if gp, inheritTime := runqget(pp); gp != nil { return gp, inheritTime, now, pollUntil, ranTimer } ranTimer = true } } //如果P2空闲，不进行窃取 if !idlepMask.read(enum.position()) { if gp := runqsteal(pp, p2, stealTimersOrRunNextG); gp != nil { return gp, false, now, pollUntil, ranTimer } } } } // 没有发现的协程G，持续等待或者推出 return nil, false, now, pollUntil, ranTimer } 这里的代码比较难整，简单说一下意思\n第一层For循环，意思就是重复执行4次循环逻辑，至于循环什么呢。。。\nconst stealTries = 4 for i := 0; i \u0026lt; stealTries; i++ { stealTimersOrRunNextG := i == stealTries-1 // 二层循环，找到可窃取的P马上返回 for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() { if sched.gcwaiting != 0 { // GC work may be available ..... 这里有一个二层循环\n二层循环里面用了一个随机算法，这里我直接讲一下这个随机算法吧\n我们用一个例子来说明， 假设一共有8个P，\n第1步: fastrand 函数 选择一个随机数并对8取模， 算法选择了一个0 - 8之间的随机数，假设为6\n第2步，找到一个比8小且与8互质的 数。 比8小且与8互质的数有4个：\ncoprimes=[1，3，5，7]，\n代码中取coprimes[6%4]= 5， 这4个数中任取一个都有相同的数学特性。\n大概就是这样，这部分我直接参考了 Go语言底层剖析这本书，我属实没看明白这段。\n窃取G到现在的P 这部分的源码如下\nfunc runqgrab(_p_ *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 { for { h := atomic.LoadAcq(\u0026amp;_p_.runqhead) // load-acquire, synchronize with other consumers t := atomic.LoadAcq(\u0026amp;_p_.runqtail) // load-acquire, synchronize with the producer n := t - h // 偷取一半的G个数 n = n - n/2 ..... // 放入自己的队列 for i := uint32(0); i \u0026lt; n; i++ { g := _p_.runq[(h+i)%uint32(len(_p_.runq))] batch[(batchHead+i)%uint32(len(batch))] = g } if atomic.CasRel(\u0026amp;_p_.runqhead, h, h+n) { // cas-release, commits consume return n } } } 这里窃取了其他指定P一般的G的个数到自己队列当中，这段核心代码大概就是这个意思\n上一节讲了调度策略，讲的是大战略方针，调度时机就是讲的什么时候发生调度\n还是拿蒋公举个例子\n蒋公定了徐州战役，现在要打，订好了策略，那什么时候打？\n调度时机就是说明了时间，也就是什么时候发生调度，生效调度策略之类的。\n一般来说，调度时机分为几种\n主动调度 被动调度 抢占调度 下面就分别来讲一下这三种调度时机\n主动调度 协程可以主动选择过渡自己的执行权利，也就是让出自己的执行权利给其他的G协程。\n在大多数情况下，我们（开发人员）是不会去执行这种调度的，因为Go会默认的主动检查调用函数，判断G是否被抢占\n主动调度的核心代码如下\nfunc goschedImpl(gp *g) { // 判断g的状态 status := readgstatus(gp) // 如果g不是_Grunning状态 if status\u0026amp;^_Gscan != _Grunning { // 获取g的详细信息 dumpgstatus(gp) // 输出提示 throw(\u0026#34;bad g status\u0026#34;) } // 取消g和m之间的绑定关系 casgstatus(gp, _Grunning, _Grunnable) dropg() // 加锁g lock(\u0026amp;sched.lock) // g放入全局运行队列 globrunqput(gp) // 关闭锁 unlock(\u0026amp;sched.lock) // 新一轮调度 schedule() } 这里的大致意思就是，首先检查g的状态，然后从当前协程切换到g0\n首先取消g和m的绑定关系，然后把g放入全局队列，然后开始新一轮循环（调用schedule函数）\n被动调度 这里就是可以被我们（开发人员）所控制的调度了\n一般我们在开发里面会遇到例如应用休眠，timeout，堵塞等情况\n所以应用内部的协程这时候就会被动的将自己的执行权限交出去\n因为不同的原因，调度器可能执行的操作也不同\n被动调度是协程内部发起的操作\n它的源码如下\nfunc gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) { if reason != waitReasonSleep { checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy } mp := acquirem() gp := mp.curg status := readgstatus(gp) if status != _Grunning \u0026amp;\u0026amp; status != _Gscanrunning { throw(\u0026#34;gopark: bad g status\u0026#34;) } mp.waitlock = lock mp.waitunlockf = unlockf gp.waitreason = reason mp.waittraceev = traceEv mp.waittraceskip = traceskip releasem(mp) //执行被动调度 mcall(park_m) } 这个函数会去执行park_m这个函数\nfunc park_m(gp *g) { _g_ := getg() if trace.enabled { traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip) } // 解除G和M的关系 casgstatus(gp, _Grunning, _Gwaiting) dropg() // 判定执行被动调度的原因 if fn := _g_.m.waitunlockf; fn != nil { ok := fn(gp, _g_.m.waitlock) // 执行waitlock _g_.m.waitunlockf = nil _g_.m.waitlock = nil if !ok { if trace.enabled { traceGoUnpark(gp, 2) } casgstatus(gp, _Gwaiting, _Grunnable) execute(gp, true) // Schedule it back, never returns. } } // 重新调度 schedule() } 首先，还是老样子，切换协程到g0，然后解除G和M的关系，根据执行被动调度的不同原因，执行不同的wautunlockf函数\n但是要注意一点，被动调度的函数不会放入全局队列\n协程的状态会从_Gwaiting转换为_Grunnable\n然后放入当前P的局部队列当中，开始执行。\n抢占调度 抢占调度的源码如下\nfunc retake(now int64) uint32 { n := 0 lock(\u0026amp;allpLock) // 重新获取allp for i := 0; i \u0026lt; len(allp); i++ { _p_ := allp[i] if _p_ == nil { // This can happen if procresize has grown // allp but not yet created new Ps. continue } pd := \u0026amp;_p_.sysmontick s := _p_.status sysretake := false if s == _Prunning || s == _Psyscall { // Preempt G if it\u0026#39;s running for too long. t := int64(_p_.schedtick) if int64(pd.schedtick) != t { pd.schedtick = uint32(t) pd.schedwhen = now } else if pd.schedwhen+forcePreemptNS \u0026lt;= now { preemptone(_p_) // 系统调用 没有连接到P的M sysretake = true } } if s == _Psyscall { // 如果存在1个sysmon，从系统调用中获取P t := int64(_p_.syscalltick) if !sysretake \u0026amp;\u0026amp; int64(pd.syscalltick) != t { pd.syscalltick = uint32(t) pd.syscallwhen = now continue } // 防止进入深度睡眠无法huan\u0026#39;x if runqempty(_p_) \u0026amp;\u0026amp; atomic.Load(\u0026amp;sched.nmspinning)+atomic.Load(\u0026amp;sched.npidle) \u0026gt; 0 \u0026amp;\u0026amp; pd.syscallwhen+10*1000*1000 \u0026gt; now { continue } unlock(\u0026amp;allpLock) // 较少空闲锁定M的数量 // 增加nmidle报告死锁 incidlelocked(-1) if atomic.Cas(\u0026amp;_p_.status, s, _Pidle) { if trace.enabled { traceGoSysBlock(_p_) traceProcStop(_p_) } n++ _p_.syscalltick++ handoffp(_p_) } incidlelocked(1) lock(\u0026amp;allpLock) } } unlock(\u0026amp;allpLock) return uint32(n) } 这段代码头晕了，大致意思就是如果协程运行时间过长，或者处于系统调度阶段\n则会抢占当前G的执行\n总结 这篇文章其实挺费力的，我本来就不是什么勤奋的人\n但是写下来没少花功夫，最近感觉自己在逐渐懈怠\n这样不太行，不太可，还是要继续hold on\nchill man\n","permalink":"https://yemilice.com/posts/golang%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6%E6%8E%A2%E7%A9%B6/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这几天攻关了一下go协程的调度，然后写了这篇文章\u003c/p\u003e\n\u003cp\u003e这篇文章的工作量真挺大的，我看的也很累，找资料也很累\u003c/p\u003e\n\u003cp\u003e这篇文章，需要感谢《Go语言底层原理剖析》这本书的作者，这本书给了我很大启发\u003c/p\u003e","title":"Golang协程调度探究"},{"content":"前言 最近我把Go重新过了一遍，特别是Go的协程这一块，我感觉任何事都是从简单到复杂\n包括现在，所以我重新开始学习基础，后序我会出一个系列\n分别为\nGolang协程基础 （已经完成！） Golang协程调度 Golang协程控制 Golang协程通信 Golang垃圾回收机制 所以我会持续更新，大家请期待吧，爱你们！\nGo里的协程是什么 写Go这么长时间了，在开发项目当中，感觉Golang的好处还是很多的\nGolang为什么被推崇，核心就是在并发和协程方面有很大的优势\n协程这个概念其实不陌生，我在大学看Python的时候就看过这方面的资料\n就是轻量级的线程\n但是Go的协程其实和Python又不太一样了，这里我还是认真讲一下协程是个什么玩意儿吧\n进程和线程 协程是轻量级的线程，但是线程又和进程有不能说的PY交易\n所以我首先来过一遍进程和线程的基础概念\n首先直接说概念，这个是核心，记住就完事\n线程是进程的组成部分 一个线程只能有一个进程 但是一个进程可以有多个线程 进程死了，线程一起死 进程之间相互独立 进程开销比线程大 可以这么理解，线程是进程的崽，进程是独立个体，可以随意下崽（创建线程）\n操作系统里面，调度到CPU中执行的最小单位就是线程\n有多核处理器的计算机，线程可以分布在多个CPU上，实现真正的并行逻辑。\n看图，这就是多核处理器和单核处理器的差别（Python就是上面那个。。。GIL让他永远用不了多核\n线程的切换和调度 这里还是分析线程，是为后序分析协程逻辑打好基础，要是大家伙不想看，直接跳过即可\n线程好用，但是不能一下开无数个，也不能全开，也不能不返回就持续等待\n所以，为了最大化利用CPU资源，操作系统需要通过定时器，IO设备，上下文切换等动作去控制线程\n一般的切换逻辑是\n当A线程发生切换的时候，会从用户态转移到内核中，记录寄存器值，进程状态之类的信息到操作系统线程的控制块当中\n然后进行切换，切换到下一个执行线程B，加载刚刚保存那得那些寄存器值，然后从内核转移到用户态中\n如果线程A和线程B不属于同一个线程，切换的时候将会更新额外的状态信息和内存地址，然后导入页表到内存里面。\n可以看见一点，就是线程切换需要记录一大堆东西\n这就是线程切换的一点点知识。\n线程和协程的关系 协程，轻量级线程，相当于线程PLUS\n但是它和线程不同的一点，人家切换或者是做别的操作\n不依赖操作系统内核，而依赖自身（Go）的调度器\n其实就是内部执行的一串代码\n协程其实是线程的从属\n下面分析下他们的具体区别还有联系\nGMP模型-线程和协程的核心关系 首先看个图\n这个图就是GMP模型的核心图，这个图其实说的挺明白了\n首先\nG就是协程\nP就是Go的调度器\nM就是线程\n可以看见，Go的协程依托线程\n一个P可能包含了多个协程，一个P在任何时候只能有一个M\n这就说明了线程和协程的关系应该是 m:n\n相关的知识我也在其他的文章里说过\ngolang的并发机制探究\n但是我后续还是会细化一下GMP模型\n协程的调度方式 首先看个图\n协程和线程的关系为M：N，为多对多关系\n调度器P可以将多个协程调度到一个线程中，也可以切换一个协程到多个线程中运行\n协程的切换 协程为什么叫轻量级线程，因为它的切换速度比线程快，根据上面我讲的线程切换\n线程切换需要操作系统用户态和内核态，并且需要存储寄存器的变量值，保留额外的一些变量值（上面有说\n协程切换只需要保留极少的状态值和寄存器变量值，并且一切都有Go调度器去操作，免去用户态和操作系统态交互的麻烦逻辑\n线程切换的速度大概是 1-2微秒\n协程切换的速度为 0.2 微秒\n协程的调度策略 这里后面我的Blog也会讲，在这我就简单描述一下吧\n线程的调度大部分都是抢占式的，为了平衡资源，操作系统的调度器会定时发出中断信号，来进行线程切换\n协程的调度是协作式的，当一个协程处理完任务的时候，可以将执行权限交还给其他协程，不会被轻易抢占，并且协程切换也是有一定的方法，比如抢占队列，偷窃任务等等，这个我后面会另开一篇好好讲述\n协程栈的大小 线程栈大小，一般是创建时指定的，为了避免溢出，默认一个栈会比较大（2MB）\n这样就大大限制了线程栈的数量，如果1000个线程就需要占用2GB虚拟内存\n但是协程栈大小默认为2KB，这样就可以创建一大堆协程，并且协程可以动态扩容栈大小\n而线程只能固定一个栈大小\n协程栈扩容的算法我后面会说（自己老开坑。。。\n并发和并行 并发，并行是老生常态的话题了\n我这简单说下\n并发：谁先执行任务我不管，但是，某个时间段内，所有的任务都能执行完毕\n并行：一起执行任务，大家一起出发\n在我开发的生涯里面，这种情况不只是单纯的并发并行，一般都是一起用的\n除开Python（有GIL锁）\n基本都是多核并行处理多个线程任务，但是单核里面也在负责多个线程任务\n所以这样的关系类似\n转回Go部分，Go的调度器，会把协程分给多个线程，这些线程又很可能被分发给了不同的进程，这样的关系就类似\n这样也说明，在多核的环境下，Go的并发，并行是同时存在且不冲突的。\n写一个协程实例 这个我相信大家玩Go的人都会写，其实Go的协程非常简单\ngo test() 那么现在咱们要实现一个并发协程，首先实现一个线性流程\npackage main import \u0026#34;fmt\u0026#34; func FmtSmg(name string) { fmt.Println(name) } func main() { z := []string{\u0026#34;yemilice\u0026#34;, \u0026#34;fuck\u0026#34;, \u0026#34;day\u0026#34;} for _, i := range z { FmtSmg(i) } } 这边输出\nyemilice fuck day 现在我们改写成协程模式\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func FmtSmg(name string) { fmt.Println(name) } func main() { z := []string{\u0026#34;yemilice\u0026#34;, \u0026#34;fuck\u0026#34;, \u0026#34;day\u0026#34;} for _, i := range z { go FmtSmg(i) } time.Sleep(time.Second * 2) } 得到输出\nday yemilice fuck 这就是一个简单的协程，这部分我用了time.sleep做通信等待，这个比较low，我后续会在协程通信的blog里面更详细的描述介绍\n例如channel，waitgroup等\nGo的并发模型 上一节我们实现了一个Go的协程实例，但是似乎我们并不了解为什么Go要这么设计\n这一节我将告诉你什么是Go的并发模型，也就是Go执行的流程，这一节也是为下次的文章做好基础，比较重要\n其实Go遵循的是fork-join的一种并发模型\nfork可以指程序中任何地方，这里将子协程和主协程分开执行\njoin指的是某个时候，子协程和主协程执行分支合并在一起\n看个图\n这就是fork-join的并发模型\n举个简单的例子\nfunc work() { fmt.Println(\u0026#34;work\u0026#34;) } func main() { go work() } 这里我们去执行，但是什么都没有返回\n这里如果用fork-join表示，画图如下\n可见我们需要连接点\n将代码改写\nfunc work() { fmt.Println(\u0026#34;work\u0026#34;) } func main() { var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() work() }() // 这是连接点 wg.Wait() } 这里有了连接点，用fork-join表示一下就是\n这里的代码不用深究，这里属于协程控制，这里后面我会单开博客讲\n这里只是为了让大家直观看到fork-join这种并发模型，这是Go预言需要遵循的并发哲学\n总结 这次主要是把协程的基础概念讲了一下\n讲了协程和线程的关系 讲了协程是怎么来的，怎么实现一个协程 讲了Go的并发模型 下集预告 下一节我将深入到Go协程之间的调度当中\n整明白Go协程的调度原理 整明白Go协程的调度策略 整明白Go协程的几种状态 ","permalink":"https://yemilice.com/posts/golang%E5%8D%8F%E7%A8%8B%E5%9F%BA%E7%A1%80%E6%8E%A2%E7%A9%B6/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e最近我把Go重新过了一遍，特别是Go的协程这一块，我感觉任何事都是从简单到复杂\u003c/p\u003e\n\u003cp\u003e包括现在，所以我重新开始学习基础，后序我会出一个系列\u003c/p\u003e\n\u003cp\u003e分别为\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eGolang协程基础 （已经完成！）\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eGolang协程调度\u003c/li\u003e\n\u003cli\u003eGolang协程控制\u003c/li\u003e\n\u003cli\u003eGolang协程通信\u003c/li\u003e\n\u003cli\u003eGolang垃圾回收机制\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e所以我会持续更新，大家请期待吧，爱你们！\u003c/p\u003e","title":"Golang协程基础探究"},{"content":"前言 最近又是学习的爆发期，我戒除了上班划水和看知乎故事会，也戒除了游戏和小说\n也戒除了频繁参加外面的无用活动，也逐渐修复了寂寞侵蚀内心的恐惧\n将心思逐渐稳定下来，所以，我本月开始到年底，将会爆发式更新blog和歌曲\n请大家和我一起学习吧！对了，如果有机会，请不要忘记帮我看看是否有合适的工作\n我最近在换工作，走过路过，也别忘记我找份工作。\n开始！\n从不去关心他人的定位 对我喜欢讨厌或者是敬畏 始终在不停的韬光养晦 为我喜欢的事鞠躬尽瘁 妈妈问我每天是否疲惫 生活似好似坏前路隐晦 希望的濒危物种不令人敬佩 只想和路过的你说声幸会 当我们在说Es索引设计时，我们在说什么？ 在我的开发生涯中，我使用过Mysql，Etcd，Mongodb之类的数据库\n当我拿到一个数据库要进行开发任务的时候，我的第一件事往往就是查看数据结构，或者是查看表结构\n如果是Mysql一类的数据库，数据表中，表的字段类型是否合适，表设计是否合理，涉及到联合查询的机制是否完全？这往往就是数据库表设计的核心。\n嵌套到es当中，es的索引，你也可以理解为表\n当我们要设计索引的时候，我们就是设计表\n当我们说要设计高可用的的索引时，往往就是指索引\n可支撑大数据量 性能影响小 结合业务场景，全维度考虑增，删，改，查 ElasticSearch索引的基础知识 索引，这个东西在数据库里面就是帮助加快检索速度的\n它是一种数据结构，这个我原来提过一点点\n具体的地址，请参看\nElasticSearch检索的核心-倒排索引解读\n今天我们的主题并不是原理，而是如何设计索引\n如何设计索引才能让我们的ES更加健壮，避免后期存入数据过多后还要更改结构\n这个才是这篇文章的核心。\n所以基础知识这里，我将不会表述太多，请大家见谅。\n默认你已经会用Es，就这样，Over。\nMapping的设计 Mapping是什么? Mapping在Es里面，相当于表结构，我举个例子\n\u0026#34;mappings\u0026#34;: { \u0026#34;doc\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;name\u0026#34;: { \u0026#34;ignore_above\u0026#34;: 256, \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;id\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;size\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;long\u0026#34; }, \u0026#34;last_mod_time\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;date\u0026#34; } } } } 我们定义的mapping里面有若干个字段，name，id\u0026hellip; 类型不同，有long，keyword，date Mapping在索引设计中是非常重要部分，就和表设计一样的，你需要定义字段\n一个好的字段能让我们的检索速度飞快，一个差的字段会让我们浪费很多平白无故的存储空间\n首先Mapping的基础知识可以参看这篇文章\n一文搞懂 Elasticsearch 之 Mapping\n这篇文章的大佬把Mapping说的很清楚了\nMapping 分为两种，静态Mapping和动态Mapping\n下面我分别说下静态和动态的几个优缺点\n静态Mapping 事先定义好字段，也就是定义好基础的Mapping格式，就像章节最开始我举的例子。\n静态Mapping，就相当于我们创建Mysql表，事先建表，规定好所有的字段和类型\n静态Mapping的优点如下\n可选可控 节约存储空间 静态Mapping的缺点如下\n设计要求高，后期无法进行更改 将数据格式全部规定死，不够动态 动态Mapping 我们不创表，直接传入数据，Es会根据你传入的数据自动匹配合适的类型，也就是自动识别数据\n说白了，就是你，传个json过来，Es根据你的json自动匹配类型，然后创建Mapping\n动态Mapping这东西，其实我是不推荐用的，因为相当于，你把数据类型的判断逻辑交给了Es，这是很不可取的，因为Es，很傻，说下原因\n当你不想检索一些字段的时候，你可以通过设置mapping事先指定，但是动态的不行 动态Mapping可控性没那么高，你往里导数据，要保证数据的一致性 高可用Mapping设计的流程 查了一些资料，其实大家可以看一下铭毅天下这个老哥的公众号\n他的公众号给了我很大启发！\n铭毅天下-死磕Elasticsearch方法论\n首先，设计Mapping，需要预先考虑如下几点\n是否需要进行全文检索 是否需要排序 数据类型的多重选择 首先，先将Mapping里的几个参数及其基础设置梳理\n参数 具体说明 传入参数 enabled 设置是否需要检索 True/False index 设置是否构建倒排索引 True/False index_option 设置存储倒排索引的哪些信息 True/False doc_values 是否开启聚合分析 True/False dynamic 动态更新mapping True/False data_detection 自动识别日期类型 True/False 进行Mapping设计时，首先要考虑我们到底有多少字段，这些字段的属性是什么\n这里非常重要，因为，当你创建完Index的时候，Es是不许更新表结构，也就是Mapping的，\n如果要选择更新，则需要重建Index，当数据量很大的时候，这种方案肯定是不可以的。\n所以，我这里定义的流程如下\n第一步：确定存储的数据类型 先细分析一波，先根据我们的需求，确定建立好我们的数据类型\n一般的数据类型如下\n根据我们的需求去梳理数据类型\n这里提几点选择策略\n如果你有很多文字，不需要分词，选text，需要分词，选keyword 如果你未来不确定你到底有多少数据，将类型设置为long是比较合适的 keyword检索比较短的字符会更快，如果字符很大，那么会增加存储和检索成本 第二步：确定哪些字段需要检索 如果我们存储的一个json十分巨大，例如content是一篇文章，或者我们的Mapping字段非常的多，那么如果全部默认检索，那我们会搜出一大堆无用的东西\n这样既增加了存储成本，又减弱了检索效率，所以下一步，我们就要根据需求，确定哪些字段需要检索\n一些固定参数，或者不想给用户展示的参数也就没有检索价值\n例如ID一类的参数\n这里如果我们不想ID被检索，那么需要设置\n\u0026#34;id\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;index\u0026#34;: false } 第三步：检索的方式 这里一般指的是分词的设置，这里需要指定分词器\n分词器我以前的blog里面也讲过\n定义自己的分词器\n这里相当于是，给指定的字段，添加分词\n假如我们现在有一个字段，名字叫name，我们需要指定中文分词器IK\n\u0026#34;name\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;ik_smart } 第四步：指定mapping的特殊设置 这里的设置，不针对某个字段，而是针对整个mapping的setting设置\n这里有如下几个常用设置\n{ \u0026#34;settings\u0026#34;: { // 分片数量 \u0026#34;number_of_shards\u0026#34;: 5, // 副本 \u0026#34;number_of_replicas\u0026#34;: 1, // 最好压缩 \u0026#34;codec\u0026#34;: \u0026#34;best_compression\u0026#34;, // 最大展示条数 \u0026#34;max_result_window\u0026#34;: \u0026#34;100000000\u0026#34;, // 刷新时间 \u0026#34;refresh_interval\u0026#34;:\u0026#34;30s\u0026#34; } 一般都是这么几个设置\n甭管那么多，直接上，不要怂\n这张图是铭毅天下那里找来的！爱你！\n这个流程，非常清楚了\nMapping的模版 这里给出我自己开发时候的一个模版，这个模版扛住了千万数据量，直接上，不要怕\n大家拿过来改改字段就行\n{ \u0026#34;settings\u0026#34;: { \u0026#34;number_of_shards\u0026#34;: 5, \u0026#34;number_of_replicas\u0026#34;: 1, \u0026#34;codec\u0026#34;: \u0026#34;best_compression\u0026#34;, \u0026#34;max_result_window\u0026#34;: \u0026#34;100000000\u0026#34;, \u0026#34;refresh_interval\u0026#34;:\u0026#34;30s\u0026#34; }, \u0026#34;mappings\u0026#34;: { \u0026#34;doc\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;id\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;data\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;nested\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;value\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;keyword\u0026#34;: { \u0026#34;ignore_above\u0026#34;: 256, \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; } } }, \u0026#34;key\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;keyword\u0026#34;: { \u0026#34;ignore_above\u0026#34;: 256, \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; } } } } }, \u0026#34;size\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;long\u0026#34; }, \u0026#34;last_mod_time\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;yyyy-MM-dd HH:mm:ss||yyyy-MM-dd || epoch_millis\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;date\u0026#34; }, \u0026#34;user\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;content\u0026#34;: { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34;, \u0026#34;term_vector\u0026#34;: \u0026#34;with_positions_offsets\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34; } } } } } 大数据下索引的切分 当我通过filebeat或者logstash倒入数据到Es中的时候\n传输到Es中，创建的index是可以自己控制的\n一般的划分方法是：\n年 月 日 自定义 举例来说\n假如我们的log索引，名字叫\u0026quot;log-2021-09\u0026quot;\n那么每天的数据量都有千万，那么这个索引就会越来越大，越来越大\n你做检索的时候，就会在一个非常大的索引中进行\n这样检索的时候，卡，慢就出现了\n万一这索引坏了，那。。。丢数据吧\n所以，如何进行切分，或者优化索引，势在必行\n下面我这里将给出一些我自己实战的手法，给大家一些建议。\n动态创建索引 首先，上一节我们讲了Mapping模版，这里我就不再多说\n我们需要结合模版去动态创建索引\n方法如下\nrollver滚动创建索引 现在开始设定 log-2021-09 这个索引\n这步的意思是创一个索引，名字叫做log-2021-09-00001，别名为logs_write\ncurl -XPUT \u0026#39;localhost:9200/log-2021-09-00001 ?pretty\u0026#39; -d\u0026#39; { \u0026#34;aliases\u0026#34;: { \u0026#34;logs_write\u0026#34;: {} } }\u0026#39; 现在开始设置动态索引创建\n这步的意思是，设定logs_write，当天数大于7天，或者文档数量大于100000，或者大小大于5gb，创建一个新的log-2021-09-00002 索引\nPOST /logs_write/_rollover { \u0026#34;conditions\u0026#34;: { \u0026#34;max_age\u0026#34;: \u0026#34;7d\u0026#34;, \u0026#34;max_docs\u0026#34;: 100000, \u0026#34;max_size\u0026#34;: \u0026#34;5gb\u0026#34; } } 定时清理过期数据 Curator索引管理工具 这个工具，可以进行索引管理\n安装的方法，网上找RPM包直接安就得了\nhttps://www.elastic.co/guide/en/elasticsearch/client/curator/current/yum-repository.html\n它的功能有\n关闭索引 创建快照 创建索引 打开索引 \u0026hellip;.. 这里，我们可以通过Curator来进行索引创建，定时配置删除等等\n例如我们要动态删除7天前的索引\n# Remember, leave a key empty if there is no value. None will be a string, # not a Python \u0026#34;NoneType\u0026#34; # # Also remember that all examples have \u0026#39;disable_action\u0026#39; set to True. If you # want to use this action as a template, be sure to set this to False after # copying it. actions: 1: action: delete_indices　# 这里执行操作类型为删除索引 description: \u0026gt;- Delete metric indices older than 3 days (based on index name), for zou_data-2018-05-01 prefixed indices. Ignore the error if the filter does not result in an actionable list of indices (ignore_empty_list) and exit cleanly. options: ignore_empty_list: True filters: - filtertype: pattern kind: prefix value: logs-　# 这里是指匹配前缀为 “order_” 的索引，还可以支持正则匹配等，详见官方文档 - filtertype: age　# 这里匹配时间 source: name　# 这里根据索引name来匹配，还可以根据字段等，详见官方文档 direction: older timestring: \u0026#39;%Y-%m-%d\u0026#39;　# 用于匹配和提取索引或快照名称中的时间戳 unit: days　# 这里定义的是days，还有weeks,months等，总时间为unit * unit_count unit_count: 7 以上命令删除了7天前，以log-*开头的索引\n可以看下这篇文章\nElasticSearch——Curator索引管理\n结尾 这些大概就是我实战遇到的一些问题，结合网上的一些资料\n我输出了这篇blog\n最近我开始疯狂学习，希望未来会有突破吧\n狗公司不发年终，淦，还是赶紧找机会跑路，才是最重要的。\n大家加油吧！写算法Ing！\n加油！\n","permalink":"https://yemilice.com/posts/%E8%AE%BE%E8%AE%A1%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84elasicsaerch%E7%B4%A2%E5%BC%95/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e最近又是学习的爆发期，我戒除了上班划水和看知乎故事会，也戒除了游戏和小说\u003c/p\u003e\n\u003cp\u003e也戒除了频繁参加外面的无用活动，也逐渐修复了寂寞侵蚀内心的恐惧\u003c/p\u003e\n\u003cp\u003e将心思逐渐稳定下来，所以，我本月开始到年底，将会爆发式更新blog和歌曲\u003c/p\u003e","title":"设计高可用的ElasicSearch索引"},{"content":"前言 最近实在是太忙啦，每天都加班，这是开博这么久首次，一个月都没UPDATE，my bad，这个月保证日更三篇，补上补上。\n久违的freestyle时间，（虽然我现在玩爵士了）\n每天reload存档 似乎总会是一样 加班就像是吃饭一样平常 内心是否总想这样 看不见未来的远方 迷雾它层层阻挡 思绪飞扬总在寂寞的晚上 让我来静静的品尝 为什么要写这篇文章 首先是因为我自己，最近非常的忙，但是我又在想，我他喵的忙了个什么\n汇报一下最近（这一个月）的战果\n从无到有编写完成了一个RPC服务，并且开源给全公司的项目使用 完成了一个CEPH多站点数据同步的开发（我在想这玩意到底有什么用） 完成了一个ElasticSearch项目的开发，大概就是优化优化再优化 其实收获最大的还是开发一个RPC Server，当然对于大佬们可能啥也不算\n我其实不建议自己造轮子，因为现有已经有很多的Rpc框架可用，而且都非常稳定\n但是此次开发，涉及到一些特殊的功能，所以不得不自己造轮子\n但是对我很重要，我将这次开发的过程记录下来\n并且，将此次RPC代码抹去项目核心，开源至GITHUB当中，希望大家好好利用\n爱你们！\nRPC服务的基础 首先，需要知道什么是RPC，或者RPC到底是干嘛的\n这里我将会简单的说明一下，RPC服务的核心\n什么是RPC 首先，RPC的全名，叫做Remote Procedure Call\n大白话来说，就是远程过程调用\n更大白话来说，就是A，B两个服务器，我希望在A上控制B进行某些操作，比如 rm -rf（狗头），我们需要一个服务进行交互，这个服务就叫做RPC\n所以RPC其实是一种网络通信的框架，基于HTTP，或者TCP，或者什么有的没的，这不重要\nRPC不是一种协议，它没那么高深，它是一种工具。\n常见的RPC有什么？ 这个说起来就多了\nGolang 有 GRPC，这个我写过文章，可能有个两三篇了吧\n使用Golang的gRPC框架的一点随想\n学习etcd的消息协议gRPC一点随想\n自己个儿去翻一下，哈哈\nJava系的，有福报厂的Dubbo\n这位更是重量级\n还有我曾经用过的Thrift（难用的1B）\n一个完整的RPC框架应该包含什么？ RPC的核心功能 client端 （调用方） server端 （服务提供方） Network Service （底层协议，HTTP/TCP） 这大概就是一个RPC服务的核心功能，其实简单的理解为\n发送方（client） ---\u0026gt; 中间协议 ---\u0026gt; 接收方（server） 这样就可以表示它们的关系了。一个RPC框架，也需要包含这些功能，无论是简单的，还是复杂的，都要有这些功能。\nRPC的功能详解 研究了Grpc，还有其他的一些RPC服务，除了上述三个必须功能以外，还有几个加强功能，方便RPC服务续航前进的。\n下面将这些功能解说一下，帮助大家打好基础，深入学习\n服务端 服务端，也就是Server端，这个东西，一般是以一个服务的形式，常驻在后台，等待客户端的调用\n这个东西相当于大门，他规定了一些很重要的东西\n指定开放的端口 维护一个call id，这个东西映射了一个call id map，call id map是对应的函数指针,这个我后面会说 等待客户端请求 调用本地函数 返回执行结果 server端是常驻在后台的服务，也就是广义上俗称的RPC服务，没了server端，就相当于老虎没牙。\n客户端 客户端，也就是client端，一般就是发起调用的那一方，和server端是好基友，缺了谁都不行的那种\n这个玩意相当于钥匙，告诉你我要调用，我要开门，我要进去那种\n将调用的函数/命令 映射为call id 将调用/发送的数据流 传输给 server 端 指定server的端口，密码，鉴权之类的东西 等待执行结果 获取执行结果 client端通常在我们的业务代码中调用，一般就是你写在业务代码里面的部分，client端相当于是钥匙，打开server端大门，执行我们要的操作。\n序列化 首先，rpc服务是去其他节点执行函数/操作，所以肯定会带有参数\n我们有时候要传输参数，所以就要有个固定格式，然后也要有解析格式的步骤\n但是A -\u0026gt; B 传输的过程中，传输的参数不在一个内存里，这样就没法进行通讯调用了\n从 A 传输参数到 B，需要 A 把参数转换成字节流，传给B\n然后B将字节流转换成自身能读取的格式\n转换成字节流的步骤就叫序列化\n字节流转换成自身能读取的格式被称为反序列化\n例如Grpc服务里面有protobuf，这是很典型的序列化操作，这里兄弟们可以自己去看下\n我也写过。\n学习etcd的消息协议gRPC一点随想\n网络传输 顾名思义，网络传输，就是通过网络进行消息传递，那么这里肯定有一个网络传输逻辑，术语称为网络传输层\n网络传输层的主要工作就是\n把经过序列化/反序列化的字节流传递给服务端/客户端 这里就涉及到网络协议了，一般有什么TCP，UDP，HTTP之类的，基本都是网络的玩意儿\nGrpc用了HTTP2协议\n一般的RPC都是TCP连接，本次开发的RPC服务我也会用TCP协议，请注意\n一个完整的RPC服务的主要流程 上一节给铁子们讲了一下RPC主要的功能，这一节就来说说，RPC的主要功能如何联系起来\n顺便画几个流程图，更加方便大家学习浏览\n这章挺重要的，因为这样类似于我们设计的RPC服务的基础架构\n请大家，务必，认真细致的阅读此章\n因为免得后面我写代码你啥也看不懂！\n认真阅读！\n认真阅读！\n认真阅读！\n重要的事儿说三遍。\n你要不好好读，到时候对不上了，指定没有你好果汁\nRPC服务的流程 上一节我们详细讲述了RPC服务的四个重要功能\n服务端 客户端 序列化/反序列化 网络传输 那么他们之间的联系是什么样的呢？\n首先，我们还是假定我们有两个节点，一个client节点，一个server节点\n假设我们现在要在远端执行一个 \u0026ldquo;Print\u0026rdquo; 函数\n主要目的就是：Client节点请求访问Server节点上的Print函数，并且获取返回的结果\n基本的流程如下\nclient节点，发起调用Print的请求 client节点，找到Print 函数的 call id client节点，序列化调用的参数/call id client节点，根据指定的ip，协议，端口发送字节流到server节点 server节点，接收到client的字节流 server节点，反序列化字节流，拿到id 或者参数 server节点，根据id 去 call map里面寻找函数 server节点，调用print函数 server节点，获取到print函数返回 server节点，序列化返回结果 serve节点，传输返回结果的字节流 client节点，接收返回结果字节流 client节点，反序列化字节流 client节点，输出返回结果 这个大概就是RPC调用函数的具体流程，细化一下，画个图\n流程如下\n这就是基础的RPC流程\n写一个基础的RPC服务 上面，我们已经过了一遍RPC服务的流程\n下面我们就来写代码，实现一个简单的RPC流程吧\n首先给出我的开发环境\nPython环境：3.7 使用包：无第三方包 实现功能\n1. client传递一个Message给server端 2. server端返回传递的Message Server端代码\n# coding: utf-8 __author__ = \u0026#39;Yemilice_lau\u0026#39; from xmlrpc.server import SimpleXMLRPCServer class SimpleRpcServer: # 创建一个指定的rpc 函数 list _rpc_methods_ = [\u0026#39;PrintWork\u0026#39;] def __init__(self, address): # 倒入自有的rpc服务包 self._serv = SimpleXMLRPCServer(address, allow_none=True) for name in self._rpc_methods_: self._serv.register_function(getattr(self, name)) # 主函数 def PrintWork(self, name): print(name) # 持续运行的server def serve_forever(self): self._serv.serve_forever() # Example if __name__ == \u0026#39;__main__\u0026#39;: print(\u0026#34;服务开始.....\u0026#34;) # 指定一个端口 7900 kvserv = SimpleRpcServer((\u0026#39;127.0.0.1\u0026#39;, 7900)) kvserv.serve_forever() client端代码\n# coding: utf-8 __author__ = \u0026#39;Yemilice_lau\u0026#39; from xmlrpc.client import ServerProxy s = ServerProxy(\u0026#39;http://localhost:7900\u0026#39;, allow_none=True) # 发送一个消息给server端 s.PrintWork(\u0026#34;Good job\u0026#34;) 这下可以调用一下server玩玩\npython rpcserver.py 输出\n服务开始..... 现在执行client\npython rpcclient.py 这时server端返回了\n127.0.0.1 - - [30/Aug/2021 17:13:49] \u0026#34;POST /RPC2 HTTP/1.1\u0026#34; 200 - Good job 这样就完成了一个简单的RPC服务流程\n一个优秀的RPC服务应该有哪些特点？ 上面我们简单的实现了一个RPC服务，但是那只是非常非常简单的RPC服务\n相当于你想整个高达，但是最后整出来个卡布达，还是人工智障形态的\n这部分关系到我们后面章节的开发，还是认真看一下\n既然要做，就要做到最好，所以我自己思考了一下\n那么到底什么样的RPC服务才能被称为优秀的RPC服务呢？\n根据我这一阵的开发来看，一个优秀的RPC服务，肯定是有如下优点的\n速度 速度肯定是很重要的\n以前我的博客聊过，影响RPC速度有这么几个重要原因\n序列化/反序列化部分影响 网络环境影响 server代码影响 首先，序列化，反序列化受传输字节流大小影响，转换比较大的字节流是比较花费时间的\n所以Grpc改用了protobuf作为序列化/反序列化的核心，抛弃掉传统的json传输转换，Grpc为什么很快，很大一部分原因来源于protobuf的高效率，所以序列化/反序列化对速度的影响非常重要\n网络环境，这个属实是基础逻辑了，你网慢，自然而然返回，发送的就慢，现在基础的有Tcp传输，http传输，需要针对综合性的问题，进行协议选择\nserver代码，这部分可谈的也很多，例如队列机制，多进程机制，令牌桶一类，都可进行调优。\n可用性 拿我此次开发来说，我面临的是一个集群（机器数量10+），并且需要支持\nRpc直接支持Python库调用 Rpc直接支持Shell命令调用 Rpc直接支持集群级别的消息发送和返回 这里就直接面对自己造轮子的境地，而且未来还不确定是不是会有跨语言的需求，例如某天突然要支持Golang，Java\n这里需要考虑是否跨平台, restful json? http XML?\n也需要考虑是否有异步操作，异步通信？同步通信？\n所以Rpc的可用性，需要在此处就进行详细的考虑，免得后续开发造成很大的麻烦。\n负载均衡 避免单个服务器接收过多的Rpc请求\n隐私性 涉及到鉴权部分了，这里相对来说比较麻烦\n常用的鉴权逻辑有\nPublic Key鉴权 证书鉴权 网关鉴权 需要根据自己的实际情况进行分析，这边扯这个淡时间就太长了\n我这次开发鉴权用的是证书，哥几个心里有数就行。\n容错性 容错性，这个说起来还是很简单，但是你细分析下就觉得这破玩意是真的挺多\n出现错误之后的处理 是否有异常的处理机制 远端和本地数据不一致的问题（一致性问题） 异常事务的回滚机制 首先，一致性问题就够TM头大了，这个原来我写过Raft协议，专门写过这个，这里不再细说\n还有异常情况，这里也需要进行细分\n网络？ 执行返回结果？ 其他错误？ 发生了异常处理之后，还有\n是否重试？ 是否回滚？ 是否抛出？ 这个真的麻烦，这也是我们要考虑的点\n暂时结尾 ok！兄弟们，全体目光像我看齐，看我看我，我宣布个事儿\n咳咳，错了\n基础知识篇这就结束了，咱们马上要进入高阶开发篇了！\n这次开发花费时间太长了，我也太累了，最近都没好好学习！\n这样可不行！\n努把力，继续刷算法，努力！哥们！努力!\n预告一下下一篇：\n高阶开发篇！\n下一篇的主要内容有\n实现Rpc队列 实现Rpc直接调用Python库 实现Rpc直接调用shell命令 实现Rpc传输json 实现协程Rpc 爱你们！\n","permalink":"https://yemilice.com/posts/%E5%86%99%E4%B8%80%E4%B8%AA%E9%AB%98%E5%8F%AF%E7%94%A8rpc%E6%9C%8D%E5%8A%A1%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%AF%87/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e最近实在是太忙啦，每天都加班，这是开博这么久首次，一个月都没UPDATE，my bad，这个月保证日更三篇，补上补上。\u003c/p\u003e\n\u003cp\u003e久违的freestyle时间，（虽然我现在玩爵士了）\u003c/p\u003e","title":"写一个高可用Rpc服务（基础知识篇）"},{"content":"望江亭（remix） 很喜欢王诗安，一直非常喜欢\nlisten\nremix听听就好\n歌词 后面补\n最近忙\n结尾 我\n间歇性写歌\n间歇性录歌\n不忙的时候会参加比赛\n平常在soundcloud发歌多一些\n最近加班太多\n不想干了\n累\n此处应有自拍，但是我不想拍，我烦\nover\n","permalink":"https://yemilice.com/posts/%E6%9C%9B%E6%B1%9F%E4%BA%ADremix/","summary":"\u003ch2 id=\"望江亭remix\"\u003e望江亭（remix）\u003c/h2\u003e\n\u003cp\u003e很喜欢王诗安，一直非常喜欢\u003c/p\u003e\n\u003cp\u003elisten\u003c/p\u003e\n\u003cp\u003eremix听听就好\u003c/p\u003e\n\n\u003caudio src=\"https://blog-1256169066.cos.ap-chengdu.myqcloud.com/rap/%E6%9C%9B%E6%B1%9F%E4%BA%AD-remix.mp3\" controls muted loading=\"lazy\"\u003e\u003c/audio\u003e\n\n\u003ch2 id=\"歌词\"\u003e歌词\u003c/h2\u003e\n\u003cp\u003e后面补\u003c/p\u003e\n\u003cp\u003e最近忙\u003c/p\u003e\n\u003ch2 id=\"结尾\"\u003e结尾\u003c/h2\u003e\n\u003cp\u003e我\u003c/p\u003e\n\u003cp\u003e间歇性写歌\u003c/p\u003e\n\u003cp\u003e间歇性录歌\u003c/p\u003e\n\u003cp\u003e不忙的时候会参加比赛\u003c/p\u003e\n\u003cp\u003e平常在soundcloud发歌多一些\u003c/p\u003e\n\u003cp\u003e最近加班太多\u003c/p\u003e\n\u003cp\u003e不想干了\u003c/p\u003e","title":"望江亭(remix)"},{"content":"前言 去年这个时候面试了一家小公司叫无糖信息，大概是叫这个名字\n面试的体验极差，HR从头到尾都是隐形人，我感觉就是不停的刷KPI的，忽悠你去面试\n我过去先等了半个小时，那面试官都没来\n然后过了一会面试官气冲冲的过来了，搞得好像是我很想来面试似的，是你们找我来的，大哥，不是我没工作一定要来你们公司\n面试官全程鼻孔看天，对我做的东西嗤之以鼻，工作四年还用校招的题考我，全程态度极差，我们说话交流不到 10 句，简历一个问题都没问，然后垮着一张脸，问我 go 的问题\n第一句，你协程用的多吗\n第二句，我看你简历很一般啊\n第三句，你的情况我了解了，你可以走了\n但我当时也没怼，一个是觉得自己怂，再一个是我和你不一样，我有家庭教养，不像这个面试官一样。\n我后面找这个HR说这个事儿，人家也敞亮，诶，就是玩\n就是已读，就是不回\n行吧，就当被狗咬了呗\n当时写了两篇博客\n似乎要沉下心来处理一些事\nGolang语言的一些基础(针对面向基础的笔/面试)\n也间接促进了我沉下心学习和去年的博文爆发\n至于我今天为什么又提起这件事呢。。。\n是因为那个SB的HR又找我面试同一个岗位，哈哈哈哈哈！\n你他妈不知道去年你干了啥么？\n而且你们一个岗位，一年都没招到人？同一个岗位同一个工资同一个水平呗这不是刷KPI这是什么，还想忽悠我过去挨怼啊？就100人小公司，你招聘有那么需求旺盛吗？\n行啊，你自己送上门，那别怪我喷你\n我直接问：\n您又想KPI我啊？\n它回了一个：\n？\n我直接回答：\n当时我去面试，面试官吊炸天气冲冲，我等半小时面试 5 分钟，后面找你们你们也不理，我忘了拉黑你实在是对不起，你们从去年招人到今年还没招到，不是 kpi 是个锤子。\n然后直接拉黑删除一条龙，估摸着您组织好语言的时候，看到那个感叹号，心理也会挺那啥吧，我只是把当时的一些感觉还给你罢了。\n写这篇文章的目的 我本来都要把去年那事儿忘了，但是你把我这思想勾起来，我不聊两句也说不过去对吧。\n陆陆续续面试以来，碰到挺多HR都挺那啥的，如果有HR在看这个博客\n我就想问你们一个问题，为什么你们要别人（面试者）无论如何给你们回复，但是你们如果不过也从来不给面试者回复呢？\n你们可以辩解，说你们太忙，但是谁的时间不是时间？我遇到了好几个这种HR，还有同一家公司，三四个HR过来找你的，面试不过了也不通知，然后后面又来找你面试的，咋的，你拿我当傻子涮着玩儿是吧？无限循环？这种我一律直接拉黑\n我不知道你们怎么看这个问题，无论如何，HR找我我都会礼貌的回复，但是礼貌的回复之后，总是得不到礼貌的回应，难道现在不讲道理才是生活的必要手段？fuck，那你们鼓吹什么绅士风度？要面试的人礼貌，但是你们有礼貌的对待面试的人吗？\n我感觉肯定也有人说，这么屁大点事儿你发什么blog，是，这是我的blog，您要不乐意看您就走人，没求着你啊，再一个，以后自己遇到事儿还是要怼回去，你越讲礼貌，越忍让，别人才会觉得你软弱可欺。\n这事儿是不大，但是这是我的态度，我不是求着你找一份工作的，我们是平等的状态，抛开掉HR和面试者的角度，我们还可能成为朋友，或者是好朋友\n未来我还可能邀请你来听我们的乐团演出，真的不必要像无糖信息这种公司把我得罪的这么死（忘了告诉大家我现在玩爵士了\n结尾 首先先锤一下无糖信息，垃圾公司，垃圾面试官，垃圾HR，看到了没，我就骂你，我就在沈阳大街骂你（狗头\n上网搜了一下，骂这家公司的也有几个\n看这位，也是被那伞兵HR给耍了 在看这位，应该和我遇见了同一个伞兵面试官 说一句，你们不懂面试，就不要面试，面过大厂几家，外企几家，HR和面试官都挺好，都挺给面子的，所谓庙小妖风大，水浅王八多，哈哈哈，你品，你细品。\n先送你一个祝福\n顺便我祝你幸福\n别杠我，杠我就你对\n最后想给看到博客的人说\n遇到垃圾公司，直接曝光 遇到垃圾面试官，直接曝光 遇到垃圾HR，骂他就完了 遇到垃圾面试官，直接骂 写算法了，我的理想是最高的那个台阶，大家一起加油！\n你可悲的自负再次上演刺秦史录 LIL bitch U Fuck UP 别想把事情止住 我手段太过残忍有点担心 视频尺度 阳光大道你不走偏偏TM 自寻死路 宝刀被我放置4年 利刃还未老 水平太差劝你叫上兄弟 立正排队搞 光freestyle 玩你三个回合你歌词才背好 赶紧跪倒 你个废吊 看主角活埋配角 ","permalink":"https://yemilice.com/posts/%E5%88%B7kpi%E7%9A%84hr%E5%B0%B1%E4%B8%8D%E8%A6%81%E7%BB%99%E6%88%91%E5%8F%91%E9%9D%A2%E8%AF%95%E9%82%80%E8%AF%B7%E4%BA%86%E5%90%A7/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e去年这个时候面试了一家小公司叫\u003cstrong\u003e无糖信息\u003c/strong\u003e，大概是叫这个名字\u003c/p\u003e\n\u003cp\u003e面试的体验极差，HR从头到尾都是隐形人，我感觉就是不停的刷KPI的，忽悠你去面试\u003c/p\u003e\n\u003cp\u003e我过去先等了半个小时，那面试官都没来\u003c/p\u003e","title":"刷KPI的HR就不要给我发面试邀请了吧"},{"content":"什么是导音？ What isguide tones?\n在爵士乐中，和弦大部分为七和弦，导音主要是指的七和弦的三音或七音。\n在一个七和弦中，最能体现和弦色彩的音就是三七音。\n三音为稳定音级，七音为不稳定音级，两者决定一个和弦的大，小，属等重要属性。\n其次，导音也可以是和弦除三七音之外音，根据旋律及和声进行的需要而定。\n也可以把导音理解成在一段和声进行中起承前启后作用的重要旋律音。\n比如这首大家熟悉的曲目ALL THE THINGS YOU ARE。\n整个A段旋律的主体就是从上一个和弦的三音移动到下一个和弦的三音，这种三音或七音的随着和声进行所产生的旋律线条称之为导音线条。\n一般情况下，在standard里有三种导音线条。此曲中，每个画了圈的导音与下一个导音的音程关系为上移4度，再下移五度。这种上下跳进的导音移动模式我们可以把它看作是一种跳进的导音线条。\n还有一种流线型的导音线条，比如AUTUMN LEAVES的A段。\n从A-7的3音C（稳定），延伸到第二小节D7七音C（紧张）\n然后从下方以音阶模式上行解决至Gmaj7三音-B（稳定）\n保持B音至Cmaj7第一拍，B音变为Cmaj7七音\n再由音阶上行到F#-7b5的三音A\n保持到B7变为B7不稳定导音A\n最后音阶上行解决到E小的三音G\n整个导音线条呈流线型下行趋势\u0026mdash;C,C,B,B,A,A,G.\n除以上两种导音线条之外，还有一种导音线条随着和声进行保持一个音不变， 所以可以把它看作为一种直线导音线条。\n比如这首ONE NOTE SAMBA。\nA段的旋律就建立在一个F上。\nD-7中F为3音，Db7中F为3音，皆为稳定音级。\n在C-7中F为11音，在B7b5中F为b5音，为不稳定音级。\n在第五小节解决回D-7稳定音级，三音F，然后循环。\n推荐曲目： All The Things You Are\nSquaresville\nSomething Cool\nthe Way You Look Tonight\n结尾 最近很久没吹小号了，其实生活真的需要慢下来，但是我真的慢不下来\n爵士对于我来说，很像得不到，但是又想得到的东西\n其实做说唱的最大原因是简单。。。。（暴露了\n好了，爱你们呀！\n","permalink":"https://yemilice.com/posts/%E7%88%B5%E5%A3%AB%E4%B9%90%E5%92%8C%E5%A3%B0%E8%BF%9B%E8%A1%8C%E7%9A%84%E4%B8%AD%E5%BF%83%E4%B8%8E%E7%B2%BE%E5%8D%8E-%E5%AF%BC%E9%9F%B3/","summary":"\u003ch2 id=\"什么是导音\"\u003e什么是导音？\u003c/h2\u003e\n\u003cp\u003eWhat isguide tones?\u003c/p\u003e\n\u003cp\u003e在爵士乐中，和弦大部分为\u003cstrong\u003e七和弦\u003c/strong\u003e，导音主要是指的\u003cstrong\u003e七和弦\u003c/strong\u003e的\u003cstrong\u003e三音\u003c/strong\u003e或\u003cstrong\u003e七音\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e在一个七和弦中，最能体现和弦色彩的音就是三七音。\u003c/p\u003e\n\u003cp\u003e三音为稳定音级，七音为不稳定音级，两者决定一个和弦的大，小，属等重要属性。\u003c/p\u003e","title":"爵士乐和声进行的中心与精华-导音"},{"content":"前言 首先，ElasticSearch 7，也就是Es 7， 变动还是有点儿大，改了很多东西，例如取消了type，修改了选主算法之类的操作\n正好几天在钻研一些选主算法一类的东西，看了ETCD，rabbitmq，kafka之类的一些选主算法，想起来似乎对于Es，我还没有细致研究\n于是产生了写这篇文章的动力，这篇文章，也是一篇新老选主算法的对比文章\n大概会描述一下Es的两种选主算法，然后分析一下新老算法的差异\n好了，我们开始把。\n老样子，一段freestyle\n黑暗笼罩着我的眼 看不清到底是黎明还是黑夜 时间依旧不会停歇滴答滴答 本质是仅仅如此还是始终如一？ 我问自己到底想要何物 夸父逐日 终于大泽之尾 停下来的风景似乎更美 老选主算法 Bully算法（在Es7之前的所有版本使用） Bully算法，在ElasticSearch 7.0之前，都是Es的选主算法，在7.0之后被替换。bully，顾名思义，霸道选举，也叫霸道选举算法（自己编的）\nBully算法的原理 首先，一句话概括：\nBully算法的基本原理就是，根据节点的ID大小来判定谁是leader\n这个直接点明本质\nBully算法的消息类型 Bully算法在选举的时候会发送三种消息类型\n选举消息 （Election Message: Sent to announce election.） 应答消息（Answer (Alive) Message: Responds to the Election message.） 选举成功消息 （Coordinator (Victory) Message: Sent by winner of the election to announce victory.） 这三种消息类型组成了Bully的基础消息类型，这也是Bully算法选举必须要了解的东西。请注意。\nBully算法的选举流程 还是用最熟悉的《黑社会》电影举例\n假设我们现在有node1（大D）, node2（阿乐）, node3（吉米）三个进程，现在开始选主。\nnode1的进程号比node2，node3都大，他会直接通知node2，node3,发送选举成功（Coordinator Message）消息，如果它的进程ID小于node2，node3，那么他将发送选举消息 （Election Message） 如果发送选举消息没有回应，这时候有两个原因，一个是其他节点的进程ID大于它，另一个是其他节点还在向集群内节点广播选举消息，也就是互相发送选举消息，就类似，大D和阿乐都自认自己是话事人。 如果node2的进程ID大于node1，那么node1就会收到应答消息（Answer (Alive) Message），表明，选举失败，你不是话事人，等待其他节点的选举成功（Coordinator Message）消息 如果node2进程ID小于node1，node1会返回一个应答消息（Answer (Alive) Message），启动选举进程，向更高的进程发送选举消息 （Election Message） 如果node1接受到了node2节点选举成功（Coordinator Message）消息，则说明，node1看node2是master节点。 为了方便阅读，这里有个图将bully算法选举流程列出如下\n分步解释\n节点1向节点，节点3发送选举，并且带上自己的序号1 节点2，3接收到消息之后，进行序号比较，发觉自己的序号更大，向节点1返回应答消息Answer (Alive) Message，告知节点1被踢出选主序列（大概是这个意思） 节点2向节点3发送选举请求，节点3找不到更高序号的节点发送选举请求了 节点3向节点2返回应答消息，节点3收不到其他节点的应答消息了 节点3被认为是leader，向其他节点发送Coordinator Message，选举成功的请求，将自己是master节点广播到节点1，节点2 Bully算法在ElasticSearch中如何运用（如何选出es的Master） 上面我已经把bully算法如何选举详细的说了，聪明的你应该明白了吧？\n要不明白可以给我发邮件或者留言\n下面说一下Es里面，bully如何选举出master的\n首先看一下基础步骤，这里很可能会贴出java源码，看不懂的我也米办法了，凑合看\n找到活动的active node 可以选举的节点，这个是在配置文件 elasticsearch.yml 里面的 discovery.zen.ping.unicast.hosts 定义的\n首先Es的Java实例会去直接调用ping逻辑检查可用的节点\nList\u0026lt;DiscoveryNode\u0026gt; activeMasters = new ArrayList\u0026lt;\u0026gt;(); for (ZenPing.PingResponse pingResponse : pingResponses) { // We can\u0026#39;t include the local node in pingMasters list, otherwise we may up electing ourselves without // any check / verifications from other nodes in ZenDiscover#innerJoinCluster() if (pingResponse.master() != null \u0026amp;\u0026amp; !localNode.equals(pingResponse.master())) { activeMasters.add(pingResponse.master()); } } 这里会直接去ping 那些节点，然后把除本节点以外的，能ping通的，可用的，正在活动的节点加入到activeMasters这一个链表里面\n找到可以成为master的node // nodes discovered during pinging List\u0026lt;ElectMasterService.MasterCandidate\u0026gt; masterCandidates = new ArrayList\u0026lt;\u0026gt;(); for (ZenPing.PingResponse pingResponse : pingResponses) { if (pingResponse.node().isMasterNode()) { masterCandidates.add(new ElectMasterService.MasterCandidate(pingResponse.node(), pingResponse.getClusterStateVersion())); } } 这里是将所有的可以选举的节点（包括本节点），加入到了一个新的masterCandidates链表里面\nBully算法选举 看下代码\n如果刚才的那个活动选举的链表activeMasters为空，也就是说不存在活着的master节点，然后再去再去配置文件里面elasticsearch.yml，有个重要属性，discovery.zen.minimum_master_nodes，这代表着最小选举节点，当activeMasters为空，并且minimum_master_nodes \u0026gt; masterCandidates，满足匹配数量，直接走bully的选举，选举出最小ID的节点成为master节点。\nif (activeMasters.isEmpty()) { if (electMaster.hasEnoughCandidates(masterCandidates)) { final ElectMasterService.MasterCandidate winner = electMaster.electMaster(masterCandidates); logger.trace(\u0026#34;candidate {} won election\u0026#34;, winner); return winner.getNode(); } else { // if we don\u0026#39;t have enough master nodes, we bail, because there are not enough master to elect from logger.warn(\u0026#34;not enough master nodes discovered during pinging (found [{}], but needed [{}]), pinging again\u0026#34;, masterCandidates, electMaster.minimumMasterNodes()); return null; } } else { assert !activeMasters.contains(localNode) : \u0026#34;local node should never be elected as master when other nodes indicate an active master\u0026#34;; // lets tie break between discovered nodes return electMaster.tieBreakActiveMasters(activeMasters); } Bully算法的优点 其实Bully算法，简单粗暴，好处就是简单\n并且算法的难度低啊，怪不得Es最初几个版本要用它，简单就完事儿了\n而且bully的选举速度很快，相互通知，比对大小，就得了，所以我想这就是es最初几版用它的原因吧。\nBully算法的缺点 缺点的话。。。我感觉bully算法相比于其他主流算法，相对来说简单一些\n但是简单很可能就是一种原罪\n当有节点频繁加入或者退出的时候，主节点会频繁的进行切换，保存的节点信息的元数据也就会越来越大，越来越大\n所以Bully算法的缺点，从我的角度上来看的话，也就是无法满足复杂场景下的选主需求，因为复杂场景下的选主，需要强力的选主算法支撑\n因为主节点，能力越大，责任越大！必须保持稳定！包租婆，你说是吧！\n为什么要替换Bully算法 Es7 版本算是大改，不仅改了选主算法，连type都删了，这算是一个大变动，我记得很多人的Es都是5，或者6\n我查询了一些资料，发觉在Es官方的文章上说明了为什么替换的原因\nElasticsearch 6.x 及之前的版本使用了一个叫作 Zen Discovery 的集群协调子系统。 这个子系统经过多年的发展，成功地为大大小小的集群提供支持。 然而，我们想做出一些改进，这需要对它的工作方式作出一些根本性的修改。 Zen Discovery 允许用户通过设置 discovery.zen.minimum_master_nodes 来决定多少个符合主节点条件的节点可以形成仲裁。 在每个节点上一定要正确地配置这个参数，如果集群进行动态扩展，也需要正确地更新它，这一点非常重要。 系统不可能检测到用户是否错误配置了这个参数，而且在实践当中，在 添加或删除节点之后很容易忘记调整这个参数。 Zen Discovery 试图通过在每次主节点选举过程中等待几秒来防止出现这种错误配置。 这意味着，如果所选的主节点失败，在选择替代节点之前，集群至少在几秒钟内是不可用的。 如果集群无法选举出一个主节点，有时候很难知道是为什么。 这个翻译太蹩脚了，我来直接简单扼要的说明一下为什么要替换吧\n老版本里面有个discovery.zen.minimum_master_nodes，这个很重要，但是动态扩展的时候有些时候可能会忘记设置这个东西 如果不设置这个东西，Zen Discovery会在每次选举过程中等待一阵，大概是几秒时间来防止这种错误配置，这就造成集群暂时不可用 也有可能造成无法选主的问题，这个非常致命，所以我们要换这套算法。 新选主算法 类Raft算法（Es7之后更新） 接上节，那么，Es不用Bully之后，到底要用什么呢？\n官方给的文章也说明了这个问题\n我们重新设计并重建了 Elasticsearch 7.0 的集群协调子系统： 1. 移除 minimum_master_nodes 参数，让 Elasticsearch 自己选择可以形成仲裁的节点。 2. 典型的主节点选举现在只需要很短的时间就可以完成。 3. 集群的伸缩变得更安全、更容易，并且可能造成丢失数据的系统配置选项更少了。 4. 节点更清楚地记录它们的状态，有助于诊断为什么它们不能加入集群或为什么无法选举出主节点 这边说的很清楚，原有的Zen Discovery被替换了，不再使用\n那么新版采用的算法是什么呢？\n这个在官方文档里也有\n我们经常被问到的一个问题是，为什么不简单地“插入”像 Raft 一样的标准分布式共识算法。 有很多公认的算法，每种算法都有不同的利弊权衡。 我们仔细评估了所有可以找到的文献，并从中汲取灵感。 在我们早期的概念验证中，有一个概念便使用了非常接近 Raft 的协议。 所以，我将Es7的选主算法命名为类Raft算法，也就是类似Raft的算法。但是不是完全是Raft算法，这个我会在后面详细说明。\nRaft算法的原理 首先Es的选主算法基础是来源于Raft，还是有必要分析下Raft算法的机制和原理\n才有助于后期对Es 7 的核心源码进行解读分析\n什么是Raft算法？ 这个又涉及到了一致性问题，这是分布式系统的一个老调常谈的部分\n首先，Raft算法是用来解决分布式一致性问题，而编写出来的一种算法。\nRaft算法是如何工作的？ 在Raft算法中，一个节点分别可能有三种角色\nFollower 跟随者 Candidate 候选人 Leader 领导者 当初始化的时候，所有的节点都是Follower跟随者\n此时此刻，如果没有收到leader的消息，那么节点将会自动转换为Candidate候选人的身份\n这时，成为候选人的节点，将向其他节点发送选举投票请求\n这时，接收到信号的节点，将会回复这次投票请求\n如果大多数节点都赞成选举这个节点为leader，那么这个节点将会成为集群leader节点\n这就是领导者选举的步骤，通俗来讲，也就是选主步骤，这也是我今天要讲的核心。\n这里我画了个图，将步骤完全简化，大家，凑合看下吧，聪明的你，一定一看就懂！\n开始选举的条件是：\n当follower节点接收不到leader节点的心跳 leader节点超时 但是要注意，当follower节点收不到leader节点心跳的时候，需要等待一点时间切换为Candidate候选人，才可以开始选举。\n看下图吧，大概，也许差不离，就是这么选举。\n类Raft算法在ElasticSearch中如何运用 大概知道了Raft算法的选举原理，那么在Es里面，如何使用新的算法去选举leader呢？\n这里就需要追一下源码了，下面又到了追源码的路子了，大家要看不了的话，真没办法了！\n首先，ElasticSearch的选举算法套了Raft的壳子，但是不是真的Raft逻辑\n相比于Raft算法，Es的选主算法有如下不同\n初始为 Candidate状态 允许多次投票，也就是每个有投票资格的节点可以投多票 候选人可以有投票的机会 可能会产生多个主节点 举例来说，如果node1，node2，node3进行选主\n如果node1当选leader，但是node2发来了投票要求，那么node1无条件退出leader状态，node2选为主节点，但是node3也发来了投票要求，那么node2退出leader状态，node3当选主节点。\n说明白了，就是保证最后当选的leader为主leader\n那么，综上所述，选举的流程为\n节点的初始状态，为Candidate，当加入集群的时候，如果我们的discovery发现集群已经存在 leader，那么新加入的节点自动转换为follower。\n类似下图：\n假设Candidate开始，那么节点收到足够的投票数量，转换为leader，假设其他节点发送了拉票请求，此节点辞去leader，转换为Candidate，直到选出leader，转换为follower。\n角色转变类似这张图：\n类Raft算法的优点 这个资料上面有，我就直接说了\n免去了 Zen Discovery 的 discovery.zen.minimum_master_nodes 配置，es 会自己选择可以形成仲裁的节点，用户只需配置一个初始 master 节点列表即可。也即集群扩容或缩容的过程中，不要再担心遗漏或配错 discovery.zen.minimum_master_nodes 配置 新版 Leader 选举速度极大快于旧版。在旧版 Zen Discovery 中，每个节点都需要先通过 3 轮的 ZenPing 才能完成节点发现和 Leader 选举，而新版的算法通常只需要在 100ms 以内 修复了 Zen Discovery 下的疑难问题，如重复的网络分区可能导致群集状态更新丢失问题 类Raft算法的缺点 节点多的情况下，会造成重复选主多次，选主缓慢 暂时缺少资料支撑，只有些英文的。并且还TM不全。。回头补上。\n其他Raft算法的实现（哪些知名项目也使用了Raft） 这个就有话说了\n除开我研究过一阵的ETCD，ETCD是把Raft协议用到核心层的分布式数据库\n这个我原来写过文章\n学习etcd核心机制Raft协议的一点随想\n可以自己去翻一下。\n还有kafka\nkafka原有的一致性算法是Zookeeper提供的ZAB协议，但是2.8被替换成了Raft，说明Zookeeper已经被抛弃了，Raft的一些机制的确是好于ZAB\n当然也有RocketMQ，内部核心来源于dledger源码，也是基于Raft的。\n总结 草草写完，过于浅显，入个门也够了，也作为我自己的笔记\n希望有大佬看见，能给予指点，这次借鉴了张超老师的很多博客，包括他写的Es源码解析这本书，对我的帮助非常非常大！感谢您！张超老师！\n6月第一篇blog，我还在不断努力，你们呢？\n希望自己，努力的这段时间，会有好的结果，希望自己可以换个好点的工作。希望大家也是！看到博客的人，爱你们！\n踌躇不前， 总是妄想能一步登天 登上山峰发觉 我依旧在山脚前沿 抬目望眼 所见皆是虚无贪念 停或走？ 或是将生活翻面 直面内心懦弱的另一边。 参考文章\n深入理解 Elasticsearch 7.x 新的集群协调层 Elasticsearch 新版选主流程\n","permalink":"https://yemilice.com/posts/elasticsearch-%E6%96%B0%E8%80%81%E9%80%89%E4%B8%BB%E7%AE%97%E6%B3%95%E5%AF%B9%E6%AF%94/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e首先，ElasticSearch 7，也就是Es 7， 变动还是有点儿大，改了很多东西，例如取消了type，修改了选主算法之类的操作\u003c/p\u003e\n\u003cp\u003e正好几天在钻研一些选主算法一类的东西，看了ETCD，rabbitmq，kafka之类的一些选主算法，想起来似乎对于Es，我还没有细致研究\u003c/p\u003e","title":"ElasticSearch-新老选主算法对比"},{"content":"前言 ElasticSearch为什么适合做检索服务器？\n因为快啊，大佬！\n为什么快啊？\n因为倒排索引啊！\n什么是倒排索引？\n这个，就要细细分析一下了，这篇文章可能写的不是那么全，但是我也会尽量总结所有重点！希望能帮到大家，爱你们！\n索引是什么？ 首先，从基础玩起来，倒排索引，分为倒排+索引\n索引这个词汇，在数据库的出镜率非常高，我基础比较差，所以，我在这里会从头进行学习解读，希望大家理解，如果对索引非常了解的兄弟，请直接跳过这一段\n首先，什么是索引。\n索引是一种特殊的数据库数据结构\n首先，一般我们认为的查找，就是从头检索到尾，也就是从开头遍历到结尾，这样的时间复杂度是O(n)\n索引，就是将数据库表中的某一列或几列以特定的数据结构存起来，比如B-Tree，Hash等，这样我们去查找的时候，复杂度就会下降到O(log)或者O(1)\n把索引想成目录，把数据库想成书的内容。所以索引的核心作用就是加快数据的检索和查询速度。\n上面那句话我感觉就是说了和没说一样。。你用目录来找内容肯定比一页页遍历翻书快多了，下面咱们就该走一下索引的原理了，睁大眼睛，我们走起。\n索引的原理 首先，上面说了，索引是一种数据结构，这种结构很可能是B-Tree，Hash等等\n一般的数据库索引数据结构基本都是Tree，Tree这个东西，涉及一种叫做平衡多叉树的数据结构\n也有一些数据库索引使用hash桶做数据结构，但大部分都是Tree\n平衡多叉树就是b-tree，b+ tree的核心，这玩意儿就是搞查询用的\n平衡多叉树的基本原理\n这不是二叉结构，有一种树叫做平衡二叉树，多叉和二叉是不同的玩意。 B树和B+树的核心都是平衡多叉树 所有节点关键字是按递增次序排列，并遵循左小右大原则 树中每个节点最多含有m个节点（m\u0026gt;=2） 这样似乎有点抽象。。。不过这个只是理论\n后面我单独开贴来说一下B树,B+树系列吧\n常规的索引数据结构如下\n这篇文章不是讲B树，hash的，所以我就直接进入正题了，直接讲ElasticSearch。\nElasticSearch的倒排索引是什么？ 上面说了，索引相当于是目录，是查询内容的指向标一类的东西\n那么，倒排索引又是什么？和一般的索引有什么不一样的嘛？\n现在我来和大家一起复习一下。\n首先索引的机制我已经简单的介绍了，是一种数据库中使用的数据结构。\n一般是B树，hash之类的。\n但是ElasticSearch用的既不是B树也不是hash，用的是倒排索引\nElasticSearch的核心就是搜索，所做的一切都是为了提高搜索的效率\nElasticSearch是一个非关系型的数据库，和Mysql之类的关系型数据库有本质区别。\n所以用的索引技术也不相同\n倒排索引，就是为了方便检索的一种数据结构\n倒排索引检索 VS 传统检索 如果我们在关系型数据库中，例如Mysql中进行查找，我们只能用like来实现模糊检索\n例如，我们现在要检索一篇含有 “Ak47” 的文章，需要编写如下sql语句\nselect * from article where content like \u0026#39;%Ak47%\u0026#39;; 这样的查找，我们是没法使用索引的，因为我们需要进行全扫描，你不可能将整个文章内容都作为索引\n这样检索效率很差，也没法完成复杂性检索。\n更别提复杂的全文检索了，所以，mysql不适合用来做检索引擎。\n更多情况下，很多数据库是以一个主键，作为每个文档的唯一标识\n例如传统索引下\n文档ID content 1 Ak47突击步枪 2 Ak74u突击步枪 3 Ak74s突击步枪 4 Akm突击步枪 倒排索引就不一样了，他是以单词作为索引，然后将对应的文档id和索引进行关联，类似链表，然后生成倒排索引结构\nword 文档ID Ak 1,2,3,4 Ak47 1 Ak74 2,3 Akm 4 突击 1,2,3,4 步枪 1,2,3,4 突击步枪 1,2,3,4 所以，检索的时候只要去通过词汇，直接就能找到匹配的文档\n相当于，假设你要搜索 “Ak47”，直接根据word去找匹配的词汇，然后取到文档ID就行了。\n这个不就很快？所以倒排索引的好处就体现出来了。\n所以倒排 Vs 传统，倒排完胜。\n词汇的形成是通过分词进行切分的，分词的核心在这里就不做讨论了，如果想看，可以看下我以前的一篇blog\n中文分词的算法分析\n倒排索引的数据结构 ElasticSearch的核心首先是 Apache Lucene，而倒排索引也是出自Lucene的特殊的数据结构\n首先我们看上一节，倒排索引其实可以用一个字典逻辑来表达\n{ \u0026#34;Ak\u0026#34;:[1,2,3,4], \u0026#34;AK47\u0026#34;:[1], \u0026#34;Ak74\u0026#34;:[2,3] } 这个字典里面的key值是分词后的单词，对应的value是文档的ID\n根据Lucene的概念，这个key值被称为 Term\n如果要进行检索，我们通过term去找对应的值\n一般如果文档数量很大，那么对应的term表也一样很大，全都放到内存会出事的，所以，Lucene定义了一个索引，叫做 Term Index，汉语词典索引\n一般我们不能把所有的Term表都放到内存里，所以这个是指向第二层索引表指路牌\n第二层，全称 Term Dictionary，汉语叫做索引表\n根据lucene的概念，这个value值被称为 Postings List，汉语被称为倒排表。\n记录表不仅仅只是一个文档ID的list，不要被我的简写搞混淆了，记录表里面包含了大概如下信息\n字段 字段功能 文档ID 包含单词的所有文档唯一id 词频（TF） 记录 Term 在每篇文档中出现的次数，用于评分 位置（Position） 记录 Term 在每篇文档中的分词位置（多个），用于做词语搜索 偏移（Offset) 记录 Term 在每篇文档的开始和结束位置，用于高亮显示 看个图\n这是他们三者之间的关系\n这就是倒排索引的大概数据结构。\nTerm Index解析 Trem Index, 走检索的时候，这个相当于是入口，也就是目录。\n通过Terms Index能够快速地在Terms Dictionary中找到你的想要的Term。\n存储模式 Terms Dictionary 存储 Term 的索引文件叫做 Terms Index，存储的格式是 .tip\n数据结构 Term Index是由多个FST组成的，FST这个东西展开说太多了，咱们这篇文章不太够，所以我这里就简单描述下，后面再开团讲这个。\nTrem Index里面主要包含\n字段 字段功能 header header头 IndexStartFP 当前的FSTIndex信息的起始位置 FSIndex 起始位置索引，FST算法存储 它的数据结构如下：\nTrem Index的结构是Burst-Trie实现的，这里面主要包含了Term的一些前缀\n它的结构类似\n这里简单的画了个图，您就凑合看吧\n当然也有复杂的，类似这样\n当然这里不可能是包含所有的term，这里和mysql之类的索引是不一样的，这里存的只是term的前缀。\n所以，当我们搜索\u0026quot;Ak47\u0026quot;的时候，这里面大概率会存储着\u0026quot;A\u0026quot;的前缀，找到A之后再去匹配Term Dictionary\nTerm Dictionary解析 Term Dictionary 存储了所有的Term值\n如果用精准一点的描述，Term Dictionary存储了Term和对应的Postings List指针。\n存储模式 Terms Dictionary 的文件存储格式为 .tim，存储了Term和对应的Postings List指针。\n数据结构 先看个图\ntim主要包含了如下字段\n这里面，Nodeblock是核心，先看这张图，Nodeblock其实类似一个树\nTerms Dictionary 通过 .tim 后缀文件存储，\n其内部采用 NodeBlock 对 Term 进行压缩前缀存储，\n处理过程会将相同前缀的的 Term 压缩为一个 NodeBlock，\nNodeBlock 会存储公共前缀，然后将每个 Term 的后缀以及对应 Term 的 Posting 关联信息处理为一个 Entry 保存到 Block\nEntry是一个关联的信息，一般记作元数据\nBlock可以互相包含，因为前缀很可能相同\n具体流程类似：\nPostings List解析 Postings List就是前面我们说的，包含文档ID，文档位置，词频等信息，这些数据相互独立，并且在Postings List中也有这样的表现\n存储模式 Postings List 被拆成三个文件存储：\n.doc后缀文件：记录 Postings 的 docId 信息和 Term 的词频 .pay后缀文件：记录 Payload 信息和偏移量信息 .pos后缀文件：记录位置信息 数据结构 这三个文件是做查询使用的，所以用的最多的应该.doc文件\n我这里举一个.doc的数据结构的例子，进行分析\n.doc文件里存储了Term的文档对应的ID，.doc的数据结构如下\nTermFreqs 存储文档号和对应的词频 SkipData是搜索的时候判断对应的文档ID在不在一个交集当中，进行跳表的 倒排索引的查询逻辑 首先接着上面那个数据结构，我们把数据结构连接起来，看一下下面的图。\n通过Term index，这里面记录着前缀 通过前缀快速定位到Term dictionary的offset 通过offset定位到Postings List 这就是大概的倒排索引查询逻辑，这里面较为抽象，但是将基本的查询步骤都刻画出来了。\n结尾 这篇文章，我感觉写的不好，我预计，我后序还是会修修补补，大家看到了，如果我有错误，马上指出来！\n最近学习进入了一个新的阶段，希望我自己能打通任督二脉，希望我可以做到吧。\n太多的感谢说不出口 我知道是你们在我的背后 不断鞭策 信念不会随时间陈旧 停不下执笔的手 踌躇茫然也只是短暂停留 最后感谢一些文章的作者，你们帮了我很多\ntim\u0026amp;\u0026amp;tip文件 Lucene 倒排索引原理 Elasticsearch索引原理\n","permalink":"https://yemilice.com/posts/elasticsearch%E6%A3%80%E7%B4%A2%E7%9A%84%E6%A0%B8%E5%BF%83-%E5%80%92%E6%8E%92%E7%B4%A2%E5%BC%95%E8%A7%A3%E8%AF%BB/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003eElasticSearch为什么适合做检索服务器？\u003c/p\u003e\n\u003cp\u003e因为快啊，大佬！\u003c/p\u003e\n\u003cp\u003e为什么快啊？\u003c/p\u003e\n\u003cp\u003e因为\u003cstrong\u003e倒排索引\u003c/strong\u003e啊！\u003c/p\u003e\n\u003cp\u003e什么是\u003cstrong\u003e倒排索引\u003c/strong\u003e？\u003c/p\u003e\n\u003cp\u003e这个，就要细细分析一下了，这篇文章可能写的不是那么全，但是我也会尽量总结所有重点！希望能帮到大家，爱你们！\u003c/p\u003e","title":"ElasticSearch检索的核心-倒排索引解读"},{"content":"消息队列是什么？ 化繁为简一下，分拆一下这个词汇，首先，先来看队列（queue）\n队列是一种常用的数据结构，特点是先进先出\n消息队列，顾名思义一下，就是把消息放在队列当中进行处理\n这个消息，肯定是等待处理的消息，我们应该要获取消息做什么事\n比如获取一个返回结果，接受一个参数等等。\n所以这里引出了新的概念，消费者和生产者\n生产者就是往队列里面塞数据\n消费者就是往队列里面取数据\n这就是消息队列的基础概念。\n消息队列解决了什么问题 按理说，消息队列只是一个队列类数据结构，这个我们完全可以自己实现\n例如Python的队列queue，或者java的Deque都可以实现队列的功能\n再去实现一个消费者／生产者逻辑就好了，那为什么要用消息队列呢？\n这边举几个我开发中遇见的例子\n异步 拿初始化服务来说，初始化服务的步骤如下\n初始化开始 --\u0026gt; 服务A初始化 --\u0026gt; 服务B初始化 --\u0026gt; 服务C初始化 --\u0026gt; 返回成功 来看个图，大概类似这样。\n初始化的流程类似是一个链表，一走到底，这样你让人家客户等很久，客户心理也不开心啊，不能这么搞，所以得想个好办法处理一下。\n如果有了消息队列，我们就可以把这个处理方法优化一下\n如图所示\n现在我们利用了消息队列作为中间件，当传入ip给消息队列之后，可以直接返回一个初始化进行中的状态，立马返回，减少等待。\n各个服务会自己去消息队列中获取ip进行操作，这样就避免了一步步调用造成的时间浪费，这里就将整个流程异步化。\n这是消息队列的第一个好处。\n解耦/复用 解耦，这个翻译一下，说人话就是，解除相互的依赖，让大家能独立运行。\n还是老样子，举个例子。\n现在我们有一个处理订单的程序\n主要的功能有获取订单，订单入库，订单校验，费用计算，记录日志等\n这个画个图，流程如下\n现在可以看出，订单入库，订单校验，费用计算，日志记录这几个功能都依赖订单处理这个模块\n写一个伪代码来看的话\n//Order_process 订单处理 func Order_process() { //获取订单的函数 Order, _ := GetOrder() // 获取到订单去做几个功能 // 订单入库 _ := OrderStorage(Order) // 订单校验 _ := OrderVer(Order) // 订单金额 _ := OrderAmount(Order) // 日志 - := OrderLog(Order) } 现在只有四个功能，如果后序功能越来越多，那么Order_process这个函数也会越来越长，并且每次都要重新编译主函数，这个是非常不可取的。\n假设我们现在用了消息队列，它的流程就变成了这样\n首先，订单处理模块，解除了其他四个功能对它的依赖，只将订单传输进消息队列当中\n其他几个功能模块要做的就是去消息队列中获取订单，分别处理就好了\n这样我们的伪代码就变成了\n//Order_process 订单处理 func Order_process() { //获取订单的函数 Order, _ := GetOrder() // 订单入消息队列 mq.put(Order) } 完全破处了其他几个功能模块的依赖，如果未来需要进行功能添加，只需要单独开发，并且订阅mq队列就可以。\n也降低了维护成本，处理问题也针对到模块本身，减少了排错占用的人力成本。\n这是消息队列的第二个好处。\n削峰/限流 说人话就是，当访问量激增，咱们的数据库或者是服务顶不住的时候，先往消息队列里面写，等着后面数据库/服务根据自身设置，从消息队列里面取数据消费。\n这个其实说起来比较简单，画个图吧（突然发觉我已经不讨厌画图了\n现在有个大批量的访问来了，直接去访问服务A，服务A假设上限是1000，结果你访问量是10000，好了，直接凉凉\n现在加上消息队列，访问先存放到消息队列里面，然后服务A直接去消息队列取1000条出来先消费了，然后继续从队列里面取就行了，避免服务直接挂掉。\n不过这不是缓存嘛？？？？\n我寻思redis好像也是干这个的。。。。不过的确还是有不一样的地方，我在下面的会详细说下。\n为什么要选择消息队列中间件？ 这个其实是我曾经一直思考过的一个问题\n首先\n在Python里，我们有collections 的 queue\n在Java里，我们有JDK 的 Deque\n在Golang里，我们有slice作为简单队列\n我们还可以多线程，多进行处理一些问题。\n如果只是为了实现一些功能，我们甚至可以用redis这个更简单的玩意儿。\n那么，为什么我们还是要选择例如rabbitmq，rocketmq，kafka这类做消息队列服务呢？\n下面我将从我自己收集到的一些信息，总结一些自己的感悟，希望可以帮助大家。\n持久化 首先，持久化就可以把前面说的那几种干死一大片\n对，你可以用queue实现一个队列，但是万一断电了，你队列里面的消息不就没了吗？\n对，你也可以往硬盘里写，你甚至可以另写一个定时同步的脚本去做这件事\n但是。。。这不麻烦吗。。\n万一你是分布式的，你存一份同步一份，妈耶，那工作量。\n类似redis这种，可以把数据存在磁盘上。同理，消息队列的数据也是要存起来的。这样才可以减少意外情况带来的损失。\n所以，你要自己写的话，简单的队列还是可以滴，但是消息队列嘛，免了。\n高可用 用消息队列的都是他娘的分布式服务，如果你是个自己写的单机的消息队列，你这台机器挂了，岂不是消息队列服务就没了？\n像rabbitmq这种，带着主从模式的，可以保证多少个节点down掉之后，消息服务依旧可用\n一般的内存消息队列可没有这个功能哈，you know？\nRedis Vs 消息队列 上面两个还是比较好理解，完全排除了那些内存数据库，也就是断了你自己写消息队列的这条心\n因为你的多线程，多进程，queue，deque，永远都是单节点内存队列\n但是有个特殊的玩意儿，Redis\n上面我说过了，其实削峰的功能，还挺像Redis的，因为Redis也有这个功能，而且用的人还不少。\n在互联网，大家都把这个叫做Redis缓存。\nRedis其实可以覆盖很多消息队列的功能，但是！它终究有些功能是做不到的，下面我还是来详细讲一下redis和消息队列的对比不同吧。\n首先，Redis也可以做消息队列\n这个毋庸置疑，并且Redis的消息队列实现起来还挺简单的。\n老版本的Redis，功能需求不多的，直接可以上 Redis Pub/Sub\n如果Redis的版本大于5.0, 可以直接上Redis Stream，这个就是Redis专门用来做消息队列的实现方案。\nRedis Stream的概念如下\nRedis Stream 提供了消息的持久化和主备复制功能 可以让任何客户端访问任何时刻的数据 并且能记住每一个客户端的访问位置，还能保证消息不丢失 既然，Redis已经这么厉害了，那为什么还要用消息队列的中间件呢，例如rabbitmq，rocketmq\n其实Redis和消息队列中间件还是有区别的，下面我把我总结的几个点拿出来，给大家讲讲\nRedis的机制，本来就是做缓存的 Redis严格上来讲是一个内存数据库 Redis如果pop任务从队列出去，失败了不会回到队列重试，需要手动重新push Redis自己本身有一个基于Redis源码的消息队列disque 一般的消息队列中间件都有持久化设置，Redis需要手动设置 一般的消息队列中间件处理之后可以手动ack，也可以自动ack 以rabbitmq为例，如果rabbitmq没有收到ack，会将消息放回消息队列进行处理，保证消息不丢失 一般的消息队列中间件具有更完善的MQ机制。 消息队列在项目上的使用 一般什么情况下才会用消息队列呢，这个问题我也反复问过我自己，下面我就从我自己做过的几个项目来分析一下一般什么情况才会上消息队列吧，可能并不完善，不过胜在都是自己的经验，也算有一点点可取之处吧。\n需要异步的情况下 这种情况我在上面的章节举过例子，但是我举一个我开发的项目的例子吧，这样更加直观。\n我开发了一个文件同步系统，类似百度网盘，需要从远端下载文件到本地，并且要对每个文件的同步结果进行日志记录。\n分析一下，我的大概步骤就是\n获取文件 -\u0026gt; 文件传输 -\u0026gt; 下发文件 -\u0026gt; 同步文件 -\u0026gt; 记录状态 -\u0026gt; 结束同步 -\u0026gt; 日志记录 可以看到，这是一个很长的步骤，并且可以通过异步逻辑去改装这个步骤\n所以这里调用了消息队列做异步服务\n修改过的服务步骤如下\nRabbitmq服务 —————————————————————————————— 获取文件 -\u0026gt; |文件传输 -\u0026gt; 下发文件 -\u0026gt; 同步文件| -\u0026gt; 记录状态 -\u0026gt; 结束同步 -\u0026gt; 日志记录 首先，从获取文件开始，后面的任务状态直接修改为“待同步”，等rabbitmq服务跑外内部的步骤，再去修改服务状态，然后结束同步，记录日志。\n这样做就可以减少业务在前端的等待时间。\n这样就是我的大概想法。\n微服务的架构下 公司的微服务架构我参与了设计，其实主要的核心就是解耦，这个我上面也说过了\n主要的核心就是降低每个业务之间的互相依赖，比如A依赖B，B依赖C，这样如果A丢失或者挂掉，那么B，C都会一起完蛋，这是高可用的系统中非常不可取的。\n所以，消息队列中间件，起到一个链接各方，传递消息的作用，以前是互相依赖传递消息，现在是用电话（消息队列）去传递消息，并且只用记住电话号码（订阅的消息队列）就可以了。\n实时性要求不高的情况下 以rabbitmq为例，rabbitmq在上传消息到队列的时候是有一些延迟的，并不是实时操作的\n而且rabbitmq还有ack机制消费者获取消息之后才会ack通知消息队列，如果没有ack通知，消息会重新放入消息队列，再次等待消费\n所以消息队列，例如rabbitmq，kafka等，都不是实时的，如果需要追求实时性操作，你需要redis。\n几种常见的消息队列盘点 这里直接上个图，这个图的原地址是\nhttps://zhuanlan.zhihu.com/p/60288391\n感谢您！您的图让我醍醐灌顶！\n并且将消息队列优缺点总结如下\nActiveMQ 优点 单机吞吐量：万级 topic数量都吞吐量的影响： 时效性：ms级 可用性：高，基于主从架构实现高可用性 消息可靠性：有较低的概率丢失数据 功能支持：MQ领域的功能极其完备 缺点 ActiveMQ 5.x维护越来越少，较少在大规模吞吐的场景中使用。\nKafka 号称大数据的杀手锏，谈到大数据领域内的消息传输，则绕不开Kafka，这款为大数据而生的消息中间件，以其百万级TPS的吞吐量名声大噪，迅速成为大数据领域的宠儿，在数据采集、传输、存储的过程中发挥着举足轻重的作用。\nApache Kafka它最初由LinkedIn公司基于独特的设计实现为一个分布式的提交日志系统( a distributed commit log)，之后成为Apache项目的一部分。\n目前已经被LinkedIn，Uber, Twitter, Netflix等大公司所采纳。\n优点 性能卓越，单机写入TPS约在百万条/秒，最大的优点，就是吞吐量高。 时效性：ms级 可用性：非常高，kafka是分布式的，一个数据多个副本，少数机器宕机，不会丢失数据，不会导致不可用 消费者采用Pull方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次 有优秀的第三方Kafka Web管理界面Kafka-Manager 在日志领域比较成熟，被多家公司和多个开源项目使用 功能支持：功能较为简单，主要支持简单的MQ功能，在大数据领域的实时计算以及日志采集被大规模使用 缺点 Kafka单机超过64个队列/分区，Load会发生明显的飙高现象，队列越多，load越高，发送消息响应时间变长 使用短轮询方式，实时性取决于轮询间隔时间 消费失败不支持重试 支持消息顺序，但是一台代理宕机后，就会产生消息乱序 社区更新较慢 RabbitMQ RabbitMQ 2007年发布，是一个在AMQP(高级消息队列协议)基础上完成的，可复用的企业消息系统，是当前最主流的消息中间件之一。\n优点 由于erlang语言的特性，mq 性能较好，高并发； 吞吐量到万级，MQ功能比较完备 健壮、稳定、易用、跨平台、支持多种语言、文档齐全 开源提供的管理界面非常棒，用起来很好用 社区活跃度高 缺点： erlang开发，很难去看懂源码，基本职能依赖于开源社区的快速维护和修复bug，不利于做二次开发和维护。 RabbitMQ吞吐量相比其他消息队列低 需要学习比较复杂的接口和协议，学习和维护成本较高。 性能差，每秒只能处理几万到十几万 消息堆积的时候，性能会马上下降 RocketMQ RocketMQ阿里的开源，用Java语言实现，在设计时参考了Kafka，并做出了自己的一些改进。\nRocketMQ在阿里集团被广泛应用在订单，交易，充值，流计算，消息推送，日志流式处理，binglog分发等场景。\n优点 单机吞吐量：十万级 可用性：非常高，分布式架构 消息可靠性：经过参数优化配置，消息可以做到0丢失 功能支持：MQ功能较为完善，还是分布式的，扩展性好 支持10亿级别的消息堆积，不会因为堆积导致性能下降 源码是java，可以自己阅读源码，定制MQ 缺点 支持的客户端语言不多，目前是java及c++，其中c++不成熟 社区活跃度一般 没有在 mq 核心中去实现JMS等接口，迁移需要修改大量代码 选择消息队列的建议 如果你的只是轻量级使用mq消息队列\n数据量可能每秒10W以下，对于持久化要求不高\n上Redis\n如果你只是轻量级使用mq消息队列\n数据量10w以下，需要一定的持久化操作\n上Rabbitmq\n如果你是互联网业务，并且数据量时高时低，高的时候特别高，低的时候几乎没有（好像都是这样。。）\n你要求数据不能丢失\n你要求分布式\n你要求高吞吐\n上kafka\n结尾 差不多就这样些，作为5月第一篇blog，我要加油！大家也要加油！\n我们一起刷题，一起离开！\n","permalink":"https://yemilice.com/posts/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E6%9C%8D%E5%8A%A1/","summary":"\u003ch2 id=\"消息队列是什么\"\u003e消息队列是什么？\u003c/h2\u003e\n\u003cp\u003e化繁为简一下，分拆一下这个词汇，首先，先来看队列（queue）\u003c/p\u003e\n\u003cp\u003e队列是一种常用的数据结构，特点是\u003cstrong\u003e先进先出\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e消息队列，顾名思义一下，就是把\u003cstrong\u003e消息\u003c/strong\u003e放在\u003cstrong\u003e队列\u003c/strong\u003e当中进行处理\u003c/p\u003e","title":"为什么要使用消息队列服务"},{"content":"前言 这次不废话，直接接上次的BFS，直接来看DFS。\n什么是DFS算法 DFS，全名深度优先搜索\n用大白话来说，其实就是 一条路走到黑，走不通再回来，直到无路可走\n举个简单的例子，现在我们有一个树，就像下面这样\nA / \\ B C / \\ / \\ D E F G 假设我们要用DFS算法来进行遍历／搜索\n它的步骤如下\n从初始节点A出发，并且将A标记为已访问 查找A的一个临接顶点B。 如果B存在，继续执行访问，否则回退到上一步，继续查找临接顶点 将B标记为可访问，继续执行查找临界点D的操作 重复操作，直到所有的值都访问完了，无值可以访问为止。 那么，遍历树的顺序应该是\nA -\u0026gt; B -\u0026gt; D -\u0026gt; E - \u0026gt; C -\u0026gt; F -\u0026gt; G\n是不是有点像树的前序遍历，其实差不了多少\n它的遍历步骤用图来表示，就像下面这样\n其实换一种走迷宫的说法，就是把所有能走的路都给走了。\n来个图，一看你就明白了\n很感谢这个图的作者！我找不到您的出处了，如果您看到，请联系我，我会加上引用！感恩！感恩！\n这就是俗称的一条路走到黑，撞墙了要么回去，要么继续找路。\nDFS的实现 现在弄明白原理了，问题就在实践上了\n其实看下上面的步骤，基于DFS\n首先，我们需要标记一个已经访问的对象，并且持续性访问子节点，直到无法访问为止。\n这种一般呼之欲出的方法就是递归，我们可以通过递归实现这些操作。\n或者我们也可以利用栈实现，先将根入栈，再将根出栈，并将根的右子树，左子树存入栈，按照栈的先进后出规则来实现DFS\n栈的方法\ndef DFS(root): if root: res = [] stack = [root] # 当 stack 有值 while stack: currentNode = stack.pop() res.append(currentNode.val) if currentNode.right: stack.append(currentNode.right) if currentNode.left: stack.append(currentNode.left) return res 递归的方法\ndef DFS(root): if root is not None: print(root.key) if root.left is not None: return DFS(root.left) if root.right is not None: return DFS(root.right) DFS的几道变种算法题 94. 二叉树的中序遍历 给定一个二叉树的根节点 root ，返回它的 中序 遍历。\n输入：root = [1,null,2,3] 输出：[1,3,2] 这是一道基础的DFS算法题，中序遍历，直接递归走你\nclass Solution(object): def inorderTraversal(self, root): \u0026#34;\u0026#34;\u0026#34; :type root: TreeNode :rtype: List[int] \u0026#34;\u0026#34;\u0026#34; # 搞个全局的列表 z = [] if root == None: return [] # 这边写个递归 def dfs(root): if root: dfs(root.left) z.append(root.val) dfs(root.right) dfs(root) return z 112. 路径总和 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ，判断该树中是否存在 根节点到叶子节点 的路径，这条路径上所有节点值相加等于目标和 targetSum 。\n叶子节点 是指没有子节点的节点。\n输入：root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 输出：true 这是一道典型的DFS题，需要一条路走到黑那种\n并且你需要记录下来到底走了那些路，能不能综合成一个总数\n这边我用的方法是，所有路线走遍，分别记录，一条条再去匹配targetSum，没有匹配到，直接返回false\n把上面那个模版拿过来，加两个参数\n第一个，加一个stact_val，这个是用来记录和的\n第二个，在每一步遍历的的时候，都去做一步加法，最后统计总量对比\nclass Solution(object): def hasPathSum(self, root, targetSum): \u0026#34;\u0026#34;\u0026#34; :type root: TreeNode :type targetSum: int :rtype: bool \u0026#34;\u0026#34;\u0026#34; if not root: return False if root: stack = [root] stack_val = [root.val] # 当 stack 有值 while stack: currentNode = stack.pop() temp = stack_val.pop() if not currentNode.left and not currentNode.right: if temp == targetSum: return True continue if currentNode.right: stack.append(currentNode.right) stack_val.append(currentNode.right.val + temp) if currentNode.left: stack.append(currentNode.left) stack_val.append(currentNode.left.val + temp) return False 129. 求根节点到叶节点数字之和 给你一个二叉树的根节点 root ，树中每个节点都存放有一个 0 到 9 之间的数字。 每条从根节点到叶节点的路径都代表一个数字： 例如，从根节点到叶节点的路径 1 -\u0026gt; 2 -\u0026gt; 3 表示数字 123 。 计算从根节点到叶节点生成的 所有数字之和 。 叶节点 是指没有子节点的节点。 输入：root = [1,2,3] 输出：25 解释： 从根到叶子节点路径 1-\u0026gt;2 代表数字 12 从根到叶子节点路径 1-\u0026gt;3 代表数字 13 因此，数字总和 = 12 + 13 = 25 输入：root = [4,9,0,5,1] 输出：1026 解释： 从根到叶子节点路径 4-\u0026gt;9-\u0026gt;5 代表数字 495 从根到叶子节点路径 4-\u0026gt;9-\u0026gt;1 代表数字 491 从根到叶子节点路径 4-\u0026gt;0 代表数字 40 因此，数字总和 = 495 + 491 + 40 = 1026 这道题的解法，其实就是遍历所有子树，到底，然后把每个子树的数字组成新数字，最后返回一个和。\n分析一下，按照1，2，3这个来看，上层子树永远是下层的10倍数\n也就得出\n(1 * 10 + 2) + (1 * 10 + 3) = 25\n根据这个逻辑，开始写代码\n首先把那个DFS的遍历模版拿出来\ndef DFS(root): if root is not None: print(root.key) if root.left is not None: return DFS(root.left) if root.right is not None: return DFS(root.right) 改一下，先写个伪代码\ndef dfs(root): if not root: return 0 num = 上一个子树的节点 * 10 + root.val if not root.left and not root.right: return num else: return 左节点num + 右节点num 现在我们要获取到的数据就是，上一个子树的节点，这个我们决定，以参数的形式传输进去，因为第一个节点树，都是0\n改一下代码\ndef dfs(root, nodeval): if not root: return 0 num = nodeval + root.val if not root.left and not root.right: return num else: return dfs(root.left, num) + dfs(root.right, num) 把我们的代码嵌入到主代码当中\nclass Solution(object): def sumNumbers(self, root): \u0026#34;\u0026#34;\u0026#34; :type root: TreeNode :rtype: int \u0026#34;\u0026#34;\u0026#34; def dfs(root, nodeval): if not root: return 0 total = nodeval * 10 + root.val if not root.left and not root.right: return total else: return dfs(root.left, total) + dfs(root.right, total) return dfs(root, 0) 这样就完成了这道题。\n总结 这段时间觉得自己越来越不努力了，我不喜欢自己这样，我一点也不喜欢。\n我觉得每天我都要努力前进，后续我会持续性输出算法和其他的东西，请期待吧。\n今天的freestyle说点什么呢？说点开心的事儿吧。\n突然想到的小样，不代表任何事物。哈哈。\ncheck it~ 初见的记忆浮现 是不是爱神射错了离弦的箭？ 曾几何时你我只是互相平行的线 是谁在中间接了莫名其妙的姻缘？ 似乎曾经相见 是石头记下宝玉渊源？ 最近好累，感觉真的挺累，学习不会停止，你，我，我们，都要在春暖花开的地方相见。\n","permalink":"https://yemilice.com/posts/%E5%BC%84%E6%87%82%E9%9A%BE%E7%BC%A0%E7%9A%84dfs%E7%AE%97%E6%B3%95%E5%92%8C%E7%9B%B8%E5%85%B3%E5%8F%98%E7%A7%8D-python%E5%AE%9E%E7%8E%B0/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这次不废话，直接接上次的BFS，直接来看DFS。\u003c/p\u003e\n\u003ch2 id=\"什么是dfs算法\"\u003e什么是DFS算法\u003c/h2\u003e\n\u003cp\u003eDFS，全名深度优先搜索\u003c/p\u003e\n\u003cp\u003e用大白话来说，其实就是 \u003cstrong\u003e一条路走到黑，走不通再回来，直到无路可走\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e举个简单的例子，现在我们有一个树，就像下面这样\u003c/p\u003e","title":"弄懂难缠的DFS算法和相关变种(Python实现)"},{"content":"前言 首先，要在这里说明一个情况，我的etcd的学习进入了第二个阶段\n再一个，未来我将不会更新一些基础代码用法，未来的博客文章将会持续性输出阅读源码和算法之类的文章，因为我的学习也进入了另一个阶段。\n大家共勉吧，希望大家都能找到合适的，适合自己的工作，也希望大家都可以快快乐乐的工作和生活。\n爱你们！\n我以前也写过一篇gRPC鸡翅使用的的相关文章\n如果大家不了解gRPC，请先看看怎么用\n使用golang的grpc框架的一点随想\netcd和grpc的关系 首先，为什么我要把etcd和grpc放到一起，或者说，他们到底有什么PY交易，导致etcd一定要用grpc呢。\n这里就要将一些基础理论了\n首先简单说明他们的关系\netcd v3 使用了gRPC作为了它的消息协议。\netcd 项目包括基于 gRPC 的 Go client 和 命令行工具 etcdctl，通过 gRPC 和 etcd 集群通讯。\n对于不支持 gRPC 支持的语言，etcd 提供 JSON 的 grpc-gateway。这个网关提供 RESTful 代理，翻译 HTTP/JSON 请求为 gRPC 消息。\n这里的资料来源于etcd官方文档中文版\n这里用大白话来说，就是\netcd v3 是通过grpc通信的，并且etcd惯用的管理命令 etcdctl，也是通过grpc进行etcd集群的管理和消息分发的。\n为什么etcd v3要使用gRPC 这里就要了解一下etcd 协议的变迁了\n首先，etcd v2 使用的是传统的 http+JSON 和server端进行交互，http+JSON的组合必须为每个请求建立一个连接，相当于一对一。\netcd v3 就开始完全采用了gRPC进行通信底层的协议消息。\ngRPC是通过了protocol buffer进行定义管理，gRPC在处理网络连接的优势非常明显，因为它使用单一连接的HTTP2， 实现多路复用的RPC，相当于一对多\n为什么原有的http+JSON被替换了呢？\n分开剖析一下。\nJSON 和 protobuf的对比 首先，RPC这个东西，主要就是把消息（内存对象）转换成信息流，发给server端，然后server端再给它转换成需要的数据类型。\netcd v2 是通过JSON作为消息传递的数据格式\netcd v3 是通过protobuf作为消息传递的数据格式\n不同的地方出现了，首先，protobuf 替代了 JSON，那，为什么JSON这种老牌数据格式被替换了呢？\n这里我查阅了一些资料，这篇文章写的真的很好，一看就大概明白了，我在这里感谢这个作者！\n这个作者的文章地址在：https://zhuanlan.zhihu.com/p/331593548\n很感谢您！您是我的指路明灯！\n首先先说一下结论：protobuf的效率高于JSON\n现在分析一下为什么会这样。\n看下这段JSON\n{ \u0026#34;work\u0026#34;:123, \u0026#34;work2\u0026#34;:\u0026#34;456\u0026#34;, \u0026#34;work3\u0026#34;:{ \u0026#34;work31\u0026#34;:789 } } { \u0026#34;work\u0026#34;:789, \u0026#34;work2\u0026#34;:\u0026#34;456\u0026#34;, \u0026#34;work3\u0026#34;:{ \u0026#34;work31\u0026#34;:123 } } 可以看到，JSON中包含很多类型的值，但是有些值是非字符串的，丽日，work的值是123，这个值的类型是int，内存表示只占两个字节，转成 JSON 却要五个字节。bool 字段则占了四或五个字节，所以，JSON其实有很多不必要的内存占用的。\n还有一个问题重复传输字段，可以看见，同样的key，work，只是因为值不同，就要传输两次work这个key值，所以造成了不必要的冗余，所以这就是JSON不足的地方。\n那么protobuf是从哪里解决这个问题的呢？\n首先，protobuf首先需要定义好要传输的字段类型和字段名，例如\nmessage TestWork { string message = 1; int32 code = 2; } 定义好之后，直接编译成二进制文件 .proto\n这部分不明白的可以看一下我以前的blog: 使用golang的grpc框架的一点随想\n编译之后，protobuf对数字之类的编码，使用了VarInts\nVarints是将一个整数序列化为一个或多个Bytes的方法,越小的整数，使用的Bytes越小。所以解决了JSON的第一个问题，非字符串的资源占用效率问题。\n并且看上面，protobuf直接定义好了要传输的字段名，给每个字段指定了一个整数编号。就像上面。这里传输的时候，可以直接传递编号，不用带上字段传输，这样增加了效率，避免了第二个问题：冗余问题\n所以，这就是protobuf替代JSON的必要条件。\nHTTP API 和 gRPC的对比 从上面可以知道，etcd v2 是直接用了http api，etcd v3兼容了两种模式，一种是 http api，一种是gRPC，那么，这两种服务，有什么不同的地方吗？\n这边简单列一个表格，对他们进行比较\n功能 gRPC HTTP API 协定 .proto(必须用) OpenAPI（可以不用） 协议 HTTP/2 HTTP Payload Protobuf(二进制，不可外部读取) JSON（可外部读取） 规定性 非常严格 宽松 流式处理 客户端，服务器，双向 客户端，服务器 浏览器支持 不支持 支持 安全性 TLS TLS 客户端代码生成 可生成 OpanAPI gRPC 替代 HTTP API的一些原因，相信大家在上面那个表里也能看出来。\n我在这里总结一下gRPC的优点\n性能： protobuf序列化字段，负载小 协议：转为HTTP/2 设计，比普通的HTTP紧凑高效，单个TCP可复用多个HTTP/2 调用 代码生成：.proto文件自动生成，并且端到端生成消息和客户端代码 严格规范：避免多平台的情况下出现分歧，各个平台实现一致。 流式处理：支持一元，服务到客户端，客户到服务端，双向流式传输 超时处理支持：支持rpc内部的timeout，并且可以取消timeout的服务 现在，为什么要用gRPC替代HTTP API，我相信，你心里，也应该有数了。\netcd的gRPC源码简单解读 读源码真是个很头大的工作，反正我是觉得自己很菜，读起来很累，不过查询了一些资料，自己也读了一点，也算是明白了那么点点门道，哈哈。\nserver端 首先，阅读gRPC源码，还是要先找到proto\netcd的gRPC的proto，放置的位置在：/etcdserver/etcdserverpb/rpc.proto\n首先先看一下这个文件定义了哪些服务\nservice KV service Watch service Lease service Cluster service Maintenance service Auth 定义了6个服务，服务里面有多个RPC方法，我们选一个最常用的KV来进行简单的分析吧。\n首先看KV这个服务，代码如下\nservice KV { // Range gets the keys in the range from the key-value store. rpc Range(RangeRequest) returns (RangeResponse) { option (google.api.http) = { post: \u0026#34;/v3beta/kv/range\u0026#34; body: \u0026#34;*\u0026#34; }; } // Put puts the given key into the key-value store. // A put request increments the revision of the key-value store // and generates one event in the event history. rpc Put(PutRequest) returns (PutResponse) { option (google.api.http) = { post: \u0026#34;/v3beta/kv/put\u0026#34; body: \u0026#34;*\u0026#34; }; } // DeleteRange deletes the given range from the key-value store. // A delete request increments the revision of the key-value store // and generates a delete event in the event history for every deleted key. rpc DeleteRange(DeleteRangeRequest) returns (DeleteRangeResponse) { option (google.api.http) = { post: \u0026#34;/v3beta/kv/deleterange\u0026#34; body: \u0026#34;*\u0026#34; }; } // Txn processes multiple requests in a single transaction. // A txn request increments the revision of the key-value store // and generates events with the same revision for every completed request. // It is not allowed to modify the same key several times within one txn. rpc Txn(TxnRequest) returns (TxnResponse) { option (google.api.http) = { post: \u0026#34;/v3beta/kv/txn\u0026#34; body: \u0026#34;*\u0026#34; }; } // Compact compacts the event history in the etcd key-value store. The key-value // store should be periodically compacted or the event history will continue to grow // indefinitely. rpc Compact(CompactionRequest) returns (CompactionResponse) { option (google.api.http) = { post: \u0026#34;/v3beta/kv/compaction\u0026#34; body: \u0026#34;*\u0026#34; }; } } 然后我们需要找到对应的服务端的go文件，文件名叫 v3_server.go\n先看下Put方法\nfunc (s *EtcdServer) Put(ctx context.Context, r *pb.PutRequest) (*pb.PutResponse, error) { resp, err := s.raftRequest(ctx, pb.InternalRaftRequest{Put: r}) if err != nil { return nil, err } return resp.(*pb.PutResponse), nil } 看到了吗，有个raftRequest函数，追踪一下\nfunc (s *EtcdServer) raftRequest(ctx context.Context, r pb.InternalRaftRequest) (proto.Message, error) { for { resp, err := s.raftRequestOnce(ctx, r) if err != auth.ErrAuthOldRevision { return resp, err } } } 这部分代码调用raftRequestOnce，大概的意思就是如果出现错误，就进行重试。\nfunc (s *EtcdServer) raftRequestOnce(ctx context.Context, r pb.InternalRaftRequest) (proto.Message, error) { result, err := s.processInternalRaftRequestOnce(ctx, r) if err != nil { return nil, err } if result.err != nil { return nil, result.err } return result.resp, nil } 回到PUT部分的代码，大致意思就是，上传信息，如果错误，重试。\n再看下Range方法\nfunc (s *EtcdServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) { // 判断请求是否是可以 read if !r.Serializable { err := s.linearizableReadNotify(ctx) if err != nil { return nil, err } } var resp *pb.RangeResponse var err error // 检查权限，看看权限是否可用 chk := func(ai *auth.AuthInfo) error { return s.authStore.IsRangePermitted(ai, r.Key, r.RangeEnd) } // 查询kv时候的回调函数 get := func() { resp, err = s.applyV3Base.Range(nil, r) } if serr := s.doSerialize(ctx, chk, get); serr != nil { return nil, serr } return resp, err } 调用了一个doSerialize函数\n看下它\nfunc (s *EtcdServer) doSerialize(ctx context.Context, chk func(*auth.AuthInfo) error, get func()) error { for { // 获取权限相关信息 ai, err := s.AuthInfoFromCtx(ctx) if err != nil { return err } if ai == nil { // chk expects non-nil AuthInfo; use empty credentials ai = \u0026amp;auth.AuthInfo{} } // 回调执行chk函数，校验权限 if err = chk(ai); err != nil { if err == auth.ErrAuthOldRevision { continue } return err } // fetch response for serialized request // 回调get函数，通过authStore读取kv get() // empty credentials or current auth info means no need to retry // 读完，权限没有更改，结束，否则，重试 if ai.Revision == 0 || ai.Revision == s.authStore.Revision() { return nil } // avoid TOCTOU error, retry of the request is required. } } cilent端 server看完了，该看下cilent端的部分代码了\nclient端的代码 放置在：/clientv3/client.go\n下面，将针对几个重要函数进行源码解析\n如果我们要启动一个etcd 的 client连接，我们应该\nclient, err := clientv3.New(cfg) if err != nil { fmt.Println(\u0026#34;连接ETCD失败\u0026#34;) return nil, err } 追踪到核心代码 newClient\n这里为了避免文章太长，将一些不必要的操作打了省略号，请注意！！\nfunc newClient(cfg *Config) (*Client, error) { ...... // use a temporary skeleton client to bootstrap first connection ...... ctx, cancel := context.WithCancel(baseCtx) // 这里检测配置信息，并且创建一个client实例 client := \u0026amp;Client{ conn: nil, dialerrc: make(chan error, 1), cfg: *cfg, creds: creds, ctx: ctx, cancel: cancel, mu: new(sync.Mutex), callOpts: defaultCallOpts, } // 记录账户和密码 if cfg.Username != \u0026#34;\u0026#34; \u0026amp;\u0026amp; cfg.Password != \u0026#34;\u0026#34; { client.Username = cfg.Username client.Password = cfg.Password } ...... // 初始化balancer实例 client.balancer = newHealthBalancer(cfg.Endpoints, cfg.DialTimeout, func(ep string) (bool, error) { return grpcHealthCheck(client, ep) }) // use Endpoints[0] so that for https:// without any tls config given, then // grpc will assume the certificate server name is the endpoint host. // 建立一个网络连接 conn, err := client.dial(cfg.Endpoints[0], grpc.WithBalancer(client.balancer)) if err != nil { client.cancel() client.balancer.Close() return nil, err } client.conn = conn ...... // 初始化多个客户端，前面的介绍过，有6个 client.Cluster = NewCluster(client) client.KV = NewKV(client) client.Lease = NewLease(client) client.Watcher = NewWatcher(client) client.Auth = NewAuth(client) client.Maintenance = NewMaintenance(client) if cfg.RejectOldCluster { if err := client.checkVersion(); err != nil { client.Close() return nil, err } } // 启动一个goroutine，同步集群中的URL go client.autoSync() return client, nil } 最后一步执行了一个goroutine，执行了一个autoSync 方法\n这个方法的代码如下\nfunc (c *Client) autoSync() { ...... for { select { case \u0026lt;-c.ctx.Done(): return case \u0026lt;-time.After(c.cfg.AutoSyncInterval): ctx, cancel := context.WithTimeout(c.ctx, 5*time.Second) err := c.Sync(ctx) cancel() if err != nil \u0026amp;\u0026amp; err != c.ctx.Err() { logger.Println(\u0026#34;Auto sync endpoints failed:\u0026#34;, err) } } } } 这里循环执行了一个Sync方法，方法代码如下\nfunc (c *Client) Sync(ctx context.Context) error { mresp, err := c.MemberList(ctx) if err != nil { return err } var eps []string for _, m := range mresp.Members { eps = append(eps, m.ClientURLs...) } c.SetEndpoints(eps...) return nil } 这里的的操作步骤，是请求当前的节点列表，然后更新本地的缓存。\n下面我们举一个简单的put例子，看一下put的代码怎么写的\n首先，写一个put代码\netcd.client.Put(context.Background(), name, value) 追踪代码 clientv3/kv.go\ntype KV interface { // Put puts a key-value pair into etcd. // Note that key,value can be plain bytes array and string is // an immutable representation of that bytes array. // To get a string of bytes, do string([]byte{0x10, 0x20}). Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error) .... } 持续追踪\nfunc (kv *kv) Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error) { r, err := kv.Do(ctx, OpPut(key, val, opts...)) return r.put, toErr(ctx, err) } 调用了kv.Do部分\n看下kv.Do的代码\nfunc (kv *kv) Do(ctx context.Context, op Op) (OpResponse, error) { var err error switch op.t { // 查询操作 case tRange: ....... // 上传操作 case tPut: var resp *pb.PutResponse r := \u0026amp;pb.PutRequest{Key: op.key, Value: op.val, Lease: int64(op.leaseID), PrevKv: op.prevKV, IgnoreValue: op.ignoreValue, IgnoreLease: op.ignoreLease} resp, err = kv.remote.Put(ctx, r, kv.callOpts...) if err == nil { return OpResponse{put: (*PutResponse)(resp)}, nil } // 删除操作 case tDeleteRange: ....... case tTxn: ....... default: panic(\u0026#34;Unknown op\u0026#34;) } return OpResponse{}, toErr(ctx, err) } 看到了吗，put调用了 KVclient.Put的方法，这个方法在刚刚上面那个位置\n/etcdserver/etcdserverpb/rpc.pb.go里面\ntype KVClient interface { Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*PutResponse, error) } client v3的服务流程，就这样走完了。\n后记 etcd grpc这部分就讲完了\n其实grpc还有很多可以讲的东西，不过这篇blog不是这么玩的。\n下一篇博客将会详细分析gRPC，或者是ElasticSearch的一些原理或者源码解读，或者是算法，请大家期待吧。\n突然想写一首diss 歌曲 内心依旧激昂翻滚从未平息 想到那些不尊重人的faker coder 面试官 竖起中指对你们亲切表达 从来不care他人的看法 评判我的资格你还没有拿下 回去继续敲你那没用的代码 甩你开源5个身位 冒牌faker程序员还有资格坐在高位？ fuck off 垃圾傀儡。 ","permalink":"https://yemilice.com/posts/etcd%E7%9A%84%E6%B6%88%E6%81%AF%E5%8D%8F%E8%AE%AE-grpc%E5%AD%A6%E4%B9%A0%E9%9A%8F%E6%83%B3/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e首先，要在这里说明一个情况，我的etcd的学习进入了第二个阶段\u003c/p\u003e\n\u003cp\u003e再一个，未来我将不会更新一些基础代码用法，未来的博客文章将会持续性输出阅读源码和算法之类的文章，因为我的学习也进入了另一个阶段。\u003c/p\u003e","title":"学习etcd的消息协议gRPC一点随想"},{"content":"前言 最近开始学习k8s相关的东西，不可避免的和etcd搭上了交道，说来使用etcd的日子也不短了\n中途开源过Python和Golang的etcd的api，代码在github上，公布一下\nGolang的代码：\nhttps://github.com/Alexanderklau/Go_poject/tree/master/Go-Etcd 我只能说我会用，但是，会用是远远不够的，所以，这几天看了一些书和博客，将etcd相关的一些重要的知识点进行梳理总结，并且整理输出\n作为自己的笔记，希望可以帮到大家。\n感谢 《etcd技术内幕》的作者！您的书给了我很大的启发，也推荐大家去看看！\netcd到底是什么 是一个分布式的KV存储数据库，通过raft算法保持数据的一致性。\n通常，我们会把etcd作为分布式系统的数据共享，或者是服务发现，服务注册等。\n什么是服务发现？ 当我们开发到后期，一个系统中包含的模块和服务越来越多，所以对需要对系统进行拆分，现在流行的微服务架构就差不多这样。\n有时候多个服务互相依赖，假设，一个检索系统，tika和elasticsearch相互依赖，当tika挂掉之后，elasticsearch并不知道它挂了，所以，这就出现了负载均衡的问题。\n当我们使用了服务发现后，tika和elasticsearch通过定期发送心跳，告知服务是否存活，当elasticsearch调用tika的时候，会调用服务发现组件，保证返回的服务地址可用，或者是良好的负载均衡。\n什么是服务注册？ 服务注册，其实和服务发现相辅相成，如果没有服务注册，那我们一般都是通过读取配置文件获取服务地址，如果有了服务注册，当启动服务，或者安装服务的时候，将信息注册到etcd当中，这时，服务发现就派上了用场了，请继续浏览上面的流程。\n什么是数据共享？ 这个很简单，在分布式系统中，我们经常需要同步配置文件，也有可能，分布式系统中的几个机器需要共用一个文件，例如rabbitmq的cookie。我们可以将这种数据存储在etcd当中，就避免了配置同步需要另写代码，或者是rpc，其他服务出现问题时同步失败的情况。\netcd的基础数据模型 etcd有几个特殊功能，假设我们对一个键值进行修改\n{ \u0026#34;work\u0026#34;:\u0026#34;ak47\u0026#34; } 如果现在我们把ak47换成m16，那么原有的ak47这个值不会消失/删除，而是会记录一个不同的版本号，把ak47和m16进行区分。这就是wathcer机制。\n一个key会对应多个generation（翻译成代，世代的意思，可以记作周期），意思就是，一个key很可能有多个实例，也就是说，可以被修改多次，也可以对应多个值\n当key被创建的时候，会同时创建一个generation实例，和这个key相互关联\n当key被修改的时候，会记录当前版本到generation中\n当key被删除时，会添加一个墓碑（tombstone）到 generation中，标识这个generation已经完蛋，并且创建一个新的generation。\n所以，查询的步骤应该是\n寻找指定的key值 获取全部generation版本号 根据查询的generation从存储中找到具体的Value值 输出Value 所以，etcd不会进行覆盖的操作，而总是生成一个新的数据结构。\n更详细的解释etcd的数据模型 首先，etcd将数据存放在一个持久化的B+树当中\netcd会维护一个字段序的B树索引，是为了加速针对key的范围扫描\n在每个B树索引项中，都存储了一个key值，这也是为了快速定位指定的key或者进行范围扫描\n所以回到上面讲的，etcd的每个key有多个版本，在每个revision的tree里，有多个对应的keys。\netcd保证数据一致性的方法-Raft协议 etcd是一种分布式的数据库，分布式数据库里面，存在的一个很重要的问题就是，保证每个节点的数据一致，也就是数据一致性\n一般的分布式数据库，etcd，elasticsearch之类的都会维护多个副本，这样我们上面的问题就变成了 维护多节点副本的一致性\n那么什么又是一致性呢？\n顾名思义，多个节点的数据一致，并且保持更新的状态，少数集群凉了也不影响整个集群的工作。\n这就是一致性的妙处，Raft协议，就是实现一致性的重要算法。\nRaft协议的大白话解释 首先我看了一下Raft的一致性算法论文，论文的中文地址在\nhttps://www.infoq.cn/article/raft-paper\n我在这里大白话总结一下Raft协议的核心内容\n首先，Raft会进行一个状态（角色）划分\n假设现在我们有四个个节点node1, node2, node3，node4, 副本的协议是单副本\n那么所有的节点就会处于三种角色状态\nleader 主节点，通过follower选举出来的，接收所有的client请求 follower 跟随者，从主节点获取请求，进行相关操作 candidate 当leader出现故障时，选主过程打开，其他follower转为这个角色，直到选出新的leader 在这里可以看到了，leader是带头大哥，follower是小弟，任何时候都要听大哥指挥\n大哥让你更新你就更新，让你删除你就删除。如果大哥被杀（leader挂掉），那么其他小弟（follower）就会竞争大哥的位置\n你们看过《新世界》没，选帮派大哥，是不是有那味了？\n竞选大哥（leader选举）的流程如下：\n其他小弟follower发现，大哥node1的心跳没了（leader节点不通，无返回，心跳机制接收不到leader的存活信息，leader timeout） 开始触发选举 （election timeout） 所有 follower 转变角色为 candidate candidate（node2）开始投票，优先投票给自己（加勒比海盗选海盗王） candidate (node2)给其他节点(node3, node4)发送选举请求（request vote）拉票 其他的candidate(node3, node4)节点接收到请求后，如果他们还没有开始进行投票（上一步的投票给自己），就会投票给拉票节点(node2) node2 获取了半数以上的票数，当选为新的大哥（leader） 但是，竞选大哥（leader）也可能出现一种情况，就是我支持大D，你支持阿乐，那谁当大哥啊？\n这种问题也是有的，就是\n当node2(大D) 得到了 node3 (根叔)的支持，拿到一票，但是node1（邓伯）投了 node4(阿乐)一票，这样就是2对2\n当node2的选举请求（request vote）到达node3，但是node4 的选举请求到了node1的时候，会有一个election timeout的事件，这一步预示选举失败，要搞新和连胜了，重新选举一个大佬，所以上一轮选举失败。\nnode4(阿乐)的选举计时器（election timer）到期，直接timeout，阿乐会触发选举，继续发送选举请求（request vote）给根叔（node3）,邓伯（node4）,大D（node2），这时，邓伯和根叔的选举计时器（election timer也到期，接受到阿乐（node4）的信息，直接投票给阿乐（node4），大D（node4）这时候也到期了，这时node4（阿乐）已经获得了半数投票，加冕话事人，大D那票，也就无所谓了。\n这就是Raft的大白话解读，我反正懂了，你们呢？\n日志复制 首先书接上回，我们选出了大哥（leader）\n大哥（leader）能干嘛\nleader除了给小弟（follower）发号施令（心跳信息），还会接受到client的请求，比如更新，删除，等消息，发送到所有follower节点，进行相应的操作，当leader接受到半数以上的follower操作成功信息的时候，将成功的相应返回，对client进行应答。\n回到本节，什么是日志复制？只是复制日志吗？\nnative了兄弟！在数据库里面，日志里面有数据也有操作，是很重要的东西，万一你把数据整丢了，通过日志也能恢复！\n日志应该是挺重要的东西，etcd是怎么保护日志的那？\n假设三个节点，node1（leader）, node2（follower）, node3（follower）\n假设我们现在往etcd里面写了一条数据\n{ \u0026#34;a\u0026#34;:10 } 首先，这个写入的请求(set a=10)会被传入到node1（leader），然后被传输给node2，node3\nnode2，node3收到Append Entries的消息(set a=10)之后，会记录操作到本地的log当中，并且返回成功的信息\nnode1在收到半数以上的响应消息之后，才会认为集群已经成功记录了本次操作，node1会更新日志，提交对应日志的纪录状态，最后返回消息给client。\n一个etcd集群当中，每个节点都需要维护一个log，除此之外，log维护还需要两个重要的数值\n一个是commitIndex（当前日志索引值）\n另一个是lastApplied （最后日志索引值）\n说起来似乎有点抽象，其实就是，一个是现在日志的位置，另一个是最后一条日志的位置，做记录用的。\nleader节点不仅要维护自己commitIndex，lastApplied，他还需要知道所有节点的commitIndex，lastApplied信息。\n为啥呢，因为leader是老大啊，他需要知道每个follower节点的日志记录到哪了，从而保持大家的一致，也决定下次发送的消息里面包含什么信息。\n所以leader还维护了两个数组，一个叫nextIndex，另一个叫matchIndex。\n这个同样是为了更新用的\nnextIndex记录的是发送给follower的下一条日志的索引值\nmatchIndex是记录了已经发送给follower的最大索引值\n这都是由follower节点上报给leader的\n太绕了，我举个例子吧。\n还是用上面那个node1,node2,node3的环境。\n假设，node3宕机过，现在数据不一致，那么leader节点的记录的三个节点的nextIndex和matchIndex的表示形式如下图，看图\n黑色箭头代表的是nextIndex，红色箭头代表的是matchIndex，这边表示出来就是\n{ \u0026#34;node1\u0026#34;: { \u0026#34;nextIndex\u0026#34;: 3, \u0026#34;matchIndex\u0026#34;: 4 } } 所以leader里面应该是按照这个形式存储的nextIndex和matchIndex\n我们发现node3和node1,node2有误差，是因为node3宕机过，现在在leader中，记录了node3的nextIndex和matchIndex\n{ \u0026#34;node3\u0026#34;: { \u0026#34;nextIndex\u0026#34;: 0, \u0026#34;matchIndex\u0026#34;: 1 } } 这时leader还能知道node3的日志具体位置，也知道该向node3发送哪些日志信息。\n但是有一种特殊情况，leader挂掉之后，新leader接替上位，新leader会把所有的nextIndex和matchIndex都置为空。新leader会以自己本节点的日志为核心，将其他节点的nextIndex置换为本节点提交日志的最后一条，将matchIndex置为0值。\n新的leader会持续向其他节点发送append Entriesx消息，node3并没有2，3这两条日志，node3将会返回给leader追加失败的相应，leader会修改node3的nextIndex,matchIndex，将他们前移一位，继续进行追加，直到返回成功为止。\n可以看下这个图\n日志的新旧对比 上一节提到了日志复制，但是我们如何去保持日志永远都是最新的呢？\n如果需要比较节点之间的日志新旧，就需要找到最后一条日志的索引值和任期号，用来决定谁才是最新的。\n如果任期号比较大，那么认为此日志比较新。\n如果任期号相同，日志索引值比较大的比较新。\n在选举过程中，candidate节点成为leader的过程中，向半数以上的节点都发送了日志信息，所以，leader节点必然和其他节点都有一个相同的日志信息，也就是交集。\n日志压缩与快照 随着etcd服务集群部署的时间越长，数据量也就越来越大，占用的资源也就越多，我们前一阵子，etcd的内存占用率非常高，一看，原来是有人把操作日志全记录在etcd当中了。\n日志不能无限量增长下去，所以需要压缩机制和清除机制来释放空间。\n一般在etcd中，大家都用压缩这种机制，因为压缩非常简单，而且效率也非常高。\n快照包含了节点当前的数据状态，也包含了最后一条日志记录的任期和索引号\n也就是说，假设，我们的日志记录了1-100条日志的任期和索引号，现在我们生成快照文件，只需要记录最后第100条的任期和索引号就得了，1-99条日志记录全部丢弃。\n一般恢复快照的时候，都是leader节点发送快照给follower，follower使用快照恢复数据，这里是通过GRPC发送的网络消息，这里比较复杂，未来我写一篇日志详细讲下。\n后记 etcd的基础过了一遍了，其实我弄明白的差不多了。\n学习嘛，见贤思齐焉，见不贤而内自省也。\n希望能帮助大家。\n描绘人生不用名贵的画笔 我吐字成金 勾勒速写生活的点滴 妈妈总是说你还是个孩子 没有办法处理好自己的事 可我已经开始长胡子 扛起不屈的意志 ","permalink":"https://yemilice.com/posts/%E5%AD%A6%E4%B9%A0etcd%E6%A0%B8%E5%BF%83%E6%9C%BA%E5%88%B6raft%E5%8D%8F%E8%AE%AE%E7%9A%84%E4%B8%80%E7%82%B9%E9%9A%8F%E6%83%B3/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e最近开始学习k8s相关的东西，不可避免的和etcd搭上了交道，说来使用etcd的日子也不短了\u003c/p\u003e\n\u003cp\u003e中途开源过Python和Golang的etcd的api，代码在github上，公布一下\u003c/p\u003e","title":"学习etcd核心机制Raft协议的一点随想"},{"content":"前言 最近感觉自己又陷入了无尽的自我循环和自我否定，不知道自己到底是怎么了\n我，出生于忧患之中，但是告诫自己，不能死于安乐。\n这篇文章是收集的一些Golang进阶的知识，作为我自己更上一层楼的笔记，也希望可以帮助大家\n未来将会持续性输出面试之类的八股文和算法之类的笔记，因为我感觉自己需要时间去沉淀\n怀才不遇只是欺骗自己的安慰剂 胸无点墨想学太白一字千金？ 泛舟池上不如随风远去 人生不止生命不息 Golang进阶需要知道的知识 理解Golang垃圾回收 垃圾回收，一般简称GC，你理解为，释放不需要的资源就行\nGC的核心机制，就是后台维护一个守护线程，监控对象状态，识别不需要的对象，释放资源\nGolang的垃圾回收机制进行了多次演变\n1. v1.0 — 完全串行的标记和清除过程，需要暂停整个程序 2. v1.1 — 在多核主机并行执行垃圾收集的标记和清除阶段 3. v1.3 — 运行时基于只有指针类型的值包含指针的假设增加了对栈内存的精确扫描支持，实现了真正精确的垃圾收集 4. v1.5 — 实现了基于三色标记清扫的并发垃圾收集器,大幅度降低垃圾收集的延迟从几百 ms 降低至 10ms 以下 5. v1.6 — 实现了去中心化的垃圾收集协调器；基于显式的状态机使得任意 Goroutine 都能触发垃圾收集的状态迁移 6. v1.7 — 通过并行栈收缩将垃圾收集的时间缩短至 2ms 以内 7. v1.8 — 使用混合写屏障将垃圾收集的时间缩短至 0.5ms 以内 8. v1.9 — 彻底移除暂停程序的重新扫描栈的过程 9. v1.10 — 更新了垃圾收集调频器（Pacer）的实现，分离软硬堆大小的目标 10. v1.12 — 使用新的标记终止算法简化垃圾收集器的几个阶段 11. v1.13 — 通过新的 Scavenger 解决瞬时内存占用过高的应用程序向操作系统归还内存的问题 12. v1.14 — 使用全新的页分配器优化内存分配的速度 2021年，Golang的垃圾回收机制是三色标记法搭配辅助GC还有写屏障\n三色标记法是标记-清除法的一个增强版本\n那么，什么是标记清除法呢\n简单的说下\n标记-清除法的基础原理\n就是，先停止服务运行，被引用的对象打上标记 没打上标记的对象就直接清除，也就是回收资源，然后恢复程序运行。 说白了，就是先给你停止任务，正在引用的对象被打个标签，告诉GC，这个是我的人，我罩着的，你不能动，其他的我都用过了，随便你干掉吧，GC才会去清除未引用的标记。\n那么三色标记法的原理呢？\n首先，三色，是哪三色，是白色，灰色，黑色\n这三个分别代表三种不同的标识状态\n白色代表可回收，灰色代表被黑色引用，黑色代表被程序引用\n感觉有点绕，这边简单的出个示意图吧\nmain(主程) | | --------------------- A B C D E F G --------------------- |-----| 包含引用 现在我们有个栈，主main引用了A，B两个元素，但是B同时引用了E这个元素，根据三色标记法，三色标记的情况如下\n--- A B 标记的黑色对象 --- - E 标记的灰色对象 - ------- C D F G 标记的白色对象 ------- 所以三色标记法GC的步骤是\n1. 扫描全局数据和当前的栈区域，标记引用的对象（A，B）黑色对象 2. 扫描引用的对象，(E) 标记灰色对象 3. 其他的全打入白色对象（C D F G） 4. 重复步骤2，直到灰色对象为空，清空所有白色对象 写屏障，简单点说，就是GC开始的时候，有一个记录器，名字叫屏障，第一次运行的时候，它会扫描各个对象的状态，第二次扫描时，会拿出来和第一次扫描的结果进行比对，也就是三色法那个记录灰色的步骤，标记被引用对象为灰色，防止丢失。\n辅助GC，如果GC回收的速度过慢，赶不上程序分配对象的速度，那么这边就会暂时停止分配对象，然后将用户线程抢过来执行GC，其实整个程序现在就是停止的状态，这就是辅助GC\n讲了原理，那么我们什么时候去触发GC呢？\n达到内存阈值，阈值是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发 达到定时时间，如果上面的内存阈值一直达不到，那就默认2min触发一次GC。 手动触发GC，runtime.GC()等 理解Golang的内存分配 首先，什么是内存分配\n内存分配的地位？\n内存分配和垃圾回收是Golang内存管理的核心双子 内存分配主要解决什么问题？\n主要解决协程,对象的内存分配问题 Golang内存分配的算法是？主要的思想是什么？\nTCMalloc算法，全程：Thread-Caching Malloc，中文翻译：线程缓存分配 核心的思想就是内存分为多级管理，也就是根据多级缓存，将对象大小分类，根据类别实行不同的分配策略。 每个线程维护独立的内存池，进行内存分配时，首先从独立的内存池申请内存，不足时，才向全局申请内存，避免恶意竞争 对象大小有哪几种分类方法？\n微对象 （0， 168b） 小对象 (16b， 32KB) 大对象 (32KB，无限大) 多级缓存是什么\n看下这张图\n首先看下这个图，这个图里面有三个组件： 1. 线程缓存（Thread Cache） 2. 中心缓存（Central Cache） 3. 页堆（Page Heap）， 这三个组件的作用分别是 线程缓存: 线程缓存和线程上的处理器一一绑定，主要用来缓存用户程序申请的微小对象。 中心缓存：Golang内存分配的中心缓存，访问需要互斥锁，主要是用来管理跨度内存管理单元。 页堆：内存分配的核心结构体，Golang会将其作为全局变量存储，是一个全局的缓存列表。 下面来理解多级： 多级，理解为多层级，每个线程都有一个独立的池，所以不需要进行竞争 也就不需要互斥锁进行保护，能够较少抢锁带来的损耗 当自带的缓存不足时，会直接调用中心缓存解决对象的内存分配 如果是大对象，将会被直接进行分配（页堆分配） 分配小对象的步骤\n1. 确定分配对象的大小 2. 从线程缓存获取空闲的内存空间 3. 假设线程缓存空闲不足，从中心缓存获取 4. 清除空闲内存（调用runtime.memclrNoHeapPointers） 分配大对象的步骤\n1. 获取对象大小，检测对象大小 2. 大于32KB，直接调用（runtime.mcache.allocLarge）分配大内存 理解Golang的runtime runtime是什么东西？\n理解为Golang的基础设施 是一个Golang的内置库 主要就是调度协程，内存分配，GC等一系列基础操作 也是管理goroutine的调度程序 可以理解Golang运行时候系统交互的操作。 详细一点，runtime主要有什么功能\n1. GC() 垃圾回收 2. GOMAXPROCS(n) 控制最大CPU 3. Goexit() 终止调用并且退出 4. Gosched() 让出CPU，让其他协程运行，接力协程 5. NumGoroutine：返回正在执行和排队的任务总数 6. NumCPU：返回当前系统的 CPU 核数量 理解goroutine泄漏 goroutine泄漏是什么？\n结束goroutine有如下几种方法 1. goroutine完成任务退出 2. 遇到错误 3. 通过信号的方式停止 如果不是通过这三种方式结束的goroutine 那就导致goroutine不会正常退出 然后不断的增长，不会释放，占用过多资源 这就叫goroutine的内存泄漏 举几个goroutine内存泄漏的例子\n一般goroutine调度不当，才会出现内存泄漏 一般有如下几个原因可能造成goroutine泄漏 1. 死循环 2. channel机制-持续发送，但不接收 3. channel机制-持续接收，但不发送(空channel) 4. channel机制-缓冲区已满，持续发送 防治goroutine内存泄漏的几个方法\n1. 信号控制goroutine，当创建goroutine的时候就要想着结束goroutine 2. 使用channel的时候，最好不要用无限缓存，规定一个缓存数量 3. 避免死循环操作 如何检查goroutine内存泄漏\ngo pprof， 这个可以看一下我原来的blog 链接在这里\n一次Golang服务占用CPU过大的排查经过\nGolang的一些常用标准库 os\n操作系统功能的相关接口 例如Open, Create, Mkdir, Remove time\n时间相关处理 例如 time.Sleep(time.Second * 1) fmt\n格式化操作 例如 fmt.Println(\u0026#34;12\u0026#34;) strconv\n提供字符串与基本数据类型互转的能力 string\n处理字符串的一些函数集合，包括合并、查找、分割、比较、后缀检查、索引、大小写处理等等。 http\n提供web服务 context\n上下文操作，我blog里有 sync\n提供了基本的同步原语。在多个goroutine访问共享资源的时候，需要使用sync中提供的锁机制。 Golang的package包管理 Go modules管理\n1. go.mod 文件，它与 package.json 或 Pipfile 文件的功能类似。 2. 机器生成的传递依赖项描述文件 ： go.sum。 3. 不再有 GOPATH 限制。模块可以位于任何路径中。 结尾 大概重要的地方都在这里了，我的基础还是稍稍偏差，不过我倒是觉得，学习嘛，不要停下来，每天进步一点点应该也就好了。\n这周六我也要搬到新家去了\n音乐工作室也搭建好了，买了电钢\n编曲那套东西也都准备完毕了，希望我能做的更好吧\n我希望我能越做越好。\n将脚步停滞，生命静止 从不明白自己处于什么位置 人生偶尔痛苦还是始终如此？ 活着才是唯一值得骄傲的事 ","permalink":"https://yemilice.com/posts/golang%E8%BF%9B%E9%98%B6-%E5%BF%85%E9%A1%BB%E7%9F%A5%E9%81%93%E7%9A%84%E4%B8%80%E4%BA%9B%E4%BA%8B/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e最近感觉自己又陷入了无尽的自我循环和自我否定，不知道自己到底是怎么了\u003c/p\u003e\n\u003cp\u003e我，出生于忧患之中，但是告诫自己，不能死于安乐。\u003c/p\u003e\n\u003cp\u003e这篇文章是收集的一些Golang进阶的知识，作为我自己更上一层楼的笔记，也希望可以帮助大家\u003c/p\u003e","title":"Golang进阶-必须知道的一些事"},{"content":"前言 今年我陷入迷茫\n我不知道我的路是否还在远方？\n到底停下还是持续hold on？\n难道这就是生活给予我的前路渺茫？\nONE 算命先生说我天赋异禀，功成名就\n20年后，格子间的工蚁是我难辞其咎\n身边朋友来了又走，life move on\n对影成三人的生活，is my feture\n塞上耳机，beats never stop\n闭上了眼，已经是晚间十点\n身体靠后，看地铁奔赴下一个旅程的终点\n回归生活本质，吃饭得闲饮茶\n所以，将进酒且君杯莫听\n且听一首诗云子曰\nTWO listen\n曾经仰天大笑出门\n可我辈旧是蓬蒿人\n曾经在会当凌绝顶\n但高楼遮住我的眼\n曾经狂放诗百篇\n现在空空如也\n江郎才尽？ye，这是生活做的孽\n回到过去，回到充满希望的年代\n活着也不只是为了活着\n活着很简单，生活很难。\n","permalink":"https://yemilice.com/posts/%E8%AF%BB%E8%AF%97%E7%9A%84%E6%84%9F%E6%82%9F/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e今年我陷入迷茫\u003c/p\u003e\n\u003cp\u003e我不知道我的路是否还在远方？\u003c/p\u003e\n\u003cp\u003e到底停下还是持续hold on？\u003c/p\u003e\n\u003cp\u003e难道这就是生活给予我的前路渺茫？\u003c/p\u003e\n\u003ch2 id=\"one\"\u003eONE\u003c/h2\u003e\n\u003cp\u003e算命先生说我天赋异禀，功成名就\u003c/p\u003e\n\u003cp\u003e20年后，格子间的工蚁是我难辞其咎\u003c/p\u003e","title":"读诗的感悟"},{"content":"前言 这段时间频繁刷题，leetcode真的好难啊！！每次都他娘的做不出来，除了刷题，最近还在复习各种架构，或者是完成公司的开发。这些占据了我过多时间，所以blog其实一直想写，但是实在腾不出时间，今天在针对性刷leetcode的时候，对BFS/DFS有了一点别的感悟，所以就写一篇博客，作为自己的笔记，在记录的同时，也帮助其他兄弟少走弯路，希望，能够帮到大家。\n什么是BFS算法？ 这些百度谷歌都搜的到，不过这里还是简单说一下吧\n首先，BFS的全名，叫做广度优先搜索算法\n搜索，顾名思义，是找寻某个东西，所以叫搜索，在写代码的时候，搜索，其实等同遍历，只是这个遍历是有条件的。\n相对于BFS，它的条件是什么呢？\n举个例子，有个迷宫，有两个点，你要从A 移动到 B，中间带X的表示墙壁，无法通行\nA 0 0 0 X 0 0 X 0 X 0 B 现在走出第一步\nA 1 0 0 X 0 0 X 0 X 0 B 走现在走第二步，发现有两个可以走的地方（分岔路）\nA 1 2 0 X 2 0 X 0 X 0 B 走第三步，依旧有个分岔路\nA 1 2 3 X 2 3 X 0 X 0 B 走第四步，只有一条路\nA 1 2 3 X 2 3 X 0 X 4 B 第五步，走到B点\nA 1 2 3 X 2 3 X 0 X 4 B 我们画出可行的步骤\nA-1-2 3 | | X 2-3 X | 0 X 4-B BFS（广度优先）算法，就是记录所有可行的路径。\n当面临选择和岔路的时候，BFS选择我全都要，全都记录下来，然后选择其中一个进入，如果有死路，它选择返回，选另外的岔路，继续重复这样的操作。\n可以看出，BFS是逐步求解的，由近到远。这里找路程的步骤，用动态图来展示如下\n这个图其实就直接说明了BFS算法的特点\n这个图真的很棒！我一看就大概明白BFS算法了。 感谢此图的作者，如果你看到，请联系我，我会加上你的名字作为引用！谢谢！\n是不是很像钢铁雄心4推进部队的样子？\n持续性突进！\n树的BFS算法 前面你可以看到，BFS算法，其实就是将分支逐步列出。\n其实对于树这个结构来说，BFS算法更像是特殊的遍历整个树结构的一种方法。\n举个例子\n假设我们弄一个树\nA / \\ B C / \\ / \\ D E F G 一般的遍历逻辑就是(前序遍历)\nA - B - D - E - C - F - G\n但是，BFS可不是这么玩的，BFS的遍历逻辑就是 A - B - C - D - E - F - G\n这里来个图，你一看就明白！\n这就是树的BFS遍历，其实说起来也没多难吧？\n树的BFS算法模版 根据上面的逻辑，我们可以看出来，树的BFS算法是一层层的层次搜索算法，并且是先进先出的逻辑。\n如图所示\n那么，遍历树的BFS的模版，应该这么写\n首先，定义一个树的结构体\nclass TreeNode: def __init__(self, x): self.val = x self.left = None self.right = None 由于先进先出的结构特点，这里使用队列来进行存储数据，这是因为BFS算法需要保证优先访问顶点的未访问领接点。\ndef BFS(root): if root == None: return # 队列化root queue = deque([root]) result = [] # 遍历root while queue: # 移去并且返回一个元素，queue 最左侧的那一个 node = queue.popleft() # 获取node的详细情况 result.append(node.val) print(node.val) # 访问左树 left = node.left if left != None: queue.append(left) # 访问右树 right = node.right if right != None: queue.append(right) return result 写一个测试的方法，就按着我们刚刚那个A - G的树来一把\nif __name__ == \u0026#34;__main__\u0026#34;: tree = TreeNode(\u0026#34;A\u0026#34;) tree.left = TreeNode(\u0026#34;B\u0026#34;) tree.right = TreeNode(\u0026#34;C\u0026#34;) tree.left.left = TreeNode(\u0026#34;D\u0026#34;) tree.right.right = TreeNode(\u0026#34;E\u0026#34;) tree.right.right.right = TreeNode(\u0026#34;F\u0026#34;) tree.right.right.right = TreeNode(\u0026#34;F\u0026#34;) print(BFS(tree)) 打印出来\nA B C D E F [\u0026#39;A\u0026#39;, \u0026#39;B\u0026#39;, \u0026#39;C\u0026#39;, \u0026#39;D\u0026#39;, \u0026#39;E\u0026#39;, \u0026#39;F\u0026#39;] 树的BFS算法变种 从上到下打印二叉树 III leetcode地址：https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/\n请实现一个函数按照之字形顺序打印二叉树， 即第一行按照从左到右的顺序打印， 第二层按照从右到左的顺序打印， 第三行再按照从左到右的顺序打印，其他行以此类推。 例如: 给定二叉树: [3,9,20,null,null,15,7], 3 / \\ 9 20 / \\ 15 7 返回其层次遍历结果： [ [3], [20,9], [15,7] ] 分析题目\n这道题，主要就是用BFS去做遍历，然后遍历的同时判断现在遍历到第几层，然后根据层数转换打印的次序，这道题不算太难\n首先先写个BFS模版\nqueue = deque([root]) result = [] while queue: node = queue.popleft() result.append(node.val) print(node.val) left = node.left if left != None: queue.append(left) right = node.right if right != None: queue.append(right) return result 根据需求，首先先解决输出是\n[ [3], [20,9], [15,7] ] 的问题，其实说明内部嵌套多个队列，改写一下，当遍历每一层的时候，添加到新的队列当中\nwhile queue: # 定义一个新的队列 tmp = deque() # 判断队列循环到哪里 for i in range(len(queue)): node = queue.popleft() # tmp 队列 添加 val数据 tmp.append(node.val) left = node.left if left != None: queue.append(left) right = node.right if right != None: queue.append(right) result.append(list(tmp)) return result 接下来就要判断循环的行号是偶数还是计数\n# 如果是偶数 if len(result) % 2: # 添加到队列左端 tmp.appendleft(node.val) else: tmp.append(node.val) 最终的代码是\nfrom collections import deque class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = None def BFS(root): if root == None: return [] queue = deque([root]) result = [] while queue: tmp = deque() for i in range(len(queue)): print(len(result)) node = queue.popleft() if len(result) % 2: tmp.appendleft(node.val) else: tmp.append(node.val) if node.left != None: queue.append(node.left) if node.right != None: queue.append(node.right) result.append(list(tmp)) return result 结尾 我这里只总结了二叉树的几个题目，其实还不是很全，最近实在是太忙啦！后面会补全图算法的BFS和DFS算法，大家多多期待吧！\n","permalink":"https://yemilice.com/posts/%E5%BC%84%E6%87%82%E9%9A%BE%E7%BC%A0%E7%9A%84bfs%E7%AE%97%E6%B3%95%E5%92%8C%E7%9B%B8%E5%85%B3%E5%8F%98%E7%A7%8D-python%E5%AE%9E%E7%8E%B0/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这段时间频繁刷题，leetcode真的好难啊！！每次都他娘的做不出来，除了刷题，最近还在复习各种架构，或者是完成公司的开发。这些占据了我过多时间，所以blog其实一直想写，但是实在腾不出时间，今天在针对性刷leetcode的时候，对BFS/DFS有了一点别的感悟，所以就写一篇博客，作为自己的笔记，在记录的同时，也帮助其他兄弟少走弯路，希望，能够帮到大家。\u003c/p\u003e","title":"弄懂难缠的BFS算法和相关变种(Python实现)"},{"content":"前言 现在是2021年1月4日早上的九点十四分\n这是我2021开年第一篇日志，就为2020做一个回顾和2021年做一个展望吧。\n回顾2020 2020其实是一个特殊的年份，想一想似乎大家都在生活当中挣扎。\n其实我也一样，因为疫情的缘故，1月到3月都是在家办公，在家办公，其实真的挺累，每天没日没夜的编写代码，独立开发项目，并且独立测试，同事们有时候沟通也不足，每天都在疲惫当中度过。\n但是堪堪顶住压力，项目顺利开发完毕并且上线，前几天我居然还在我司官网上看见了我开发的项目，居然还被放在首页拿去招标，而且居然TM的招上了央视的项目。所以说，认真做的东西，必有回响。\n2020年的个人资产，增加了一套房子，其实买房并不在我计划之中，一直是母亲希望我有个地方住，她舍不得看着我搬家，看着我四处跑。一开始我真的是拒绝的，因为我应该会出国，但是疫情打乱了一切部署。\n幸好，家里颇有资产，买房子也不至于伤筋动骨，房贷比起很多大佬都可以忽略不计，所以对于我的未来，我要搬进新家了，不用再租房子了，突然觉得自己多了一份责任，或者说多了一份辛苦。\n2020年，看书方面，依旧保持20+的读书量，今年其实相比去年，懈怠了一些，不知道是不是我年纪大了（大雾\n缺乏一点点耐心和恒心，自己也变懒了，唉，这样可不行啊。\n2021年的展望 阅读/看书方面 还是老样子，读书至少要20+本，并且一定要往深里钻研\n我初步定为\nk8s部分 ElasticSearch部分 Python，java，Golang部分 数据库部分 前端部分 心理学部分 历史学部分 文学部分 经济学/投资部分 这就是我2021要看的书了，希望大家互相监督，互相进步。\n工作/技术方面 我打算往游戏/安全方面转。\n因为原本我做的都是一些偏业务类的东西\n我打算在游戏服务器，或者安全工程师上面发力，并不打算继续做偏业务的东西\n业务代码写久了，我感觉会丢失一些计算机思维，所以，2021跳槽势在必行。\n我会首先选择稳定一些的游戏大厂，小厂是肯定不考虑的。\n在2021年，首先要提升的肯定是外语和算法水平，其他的东西都可以后期弥补，外语和算法是一定要补强的。\n生活/娱乐方面 我打算在新房子里搭建一个音乐工作室\n还是老样子，2020年中国新说唱一轮游之后，我发现我和现在的rapper还是有非常大的差距，补强，补强，非常重要，编曲水平，出歌水平，也是很重要的。所以，搭建音乐工作室，也是自己副业走向的第一步。\n身体方面，每天还是依旧保持半小时的锻炼时间，不能停止！身体第一！\n每年体检一次，自费最贵的体检。\n做饭之类的。。。凑合做吧。\n结尾 2021最重要的三件事\n跳槽 学习 锻炼 希望我都可以做到，2021年12/31日我会回来，看看这篇文章，是否我都做到了上面所说。\nover。\n","permalink":"https://yemilice.com/posts/2020%E7%9A%84%E5%9B%9E%E9%A1%BE%E5%92%8C2021%E7%9A%84%E5%B1%95%E6%9C%9B/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e现在是2021年1月4日早上的九点十四分\u003c/p\u003e\n\u003cp\u003e这是我2021开年第一篇日志，就为2020做一个回顾和2021年做一个展望吧。\u003c/p\u003e\n\u003ch2 id=\"回顾2020\"\u003e回顾2020\u003c/h2\u003e\n\u003cp\u003e2020其实是一个特殊的年份，想一想似乎大家都在生活当中挣扎。\u003c/p\u003e","title":"2020的回顾和2021的展望"},{"content":"前言 ElasticSearch当中，有许多的分词器供我们调用，中文用的最多的就是IK分词器，一些基本的词汇都包含了。\n但是，一些基本的生僻词就很难检索了，比如一些特定专业词汇，或者一些流行词汇之类的，所以这篇文章，我会讲一下自定义分词器词典的设置和扩展，让我们能够检索的词汇变的更多。如果能帮到你，我也会很开心！\n分词器怎么分词的 首先要明白分词器分词的原理，这个我的blog以前说过，中文分词遵循的是最大匹配算法，Maximum\nhttps://yemilice.com/2020/08/21/%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D%E7%9A%84%E7%AE%97%E6%B3%95%E5%88%86%E6%9E%90/\n可以参看一下这篇博客，说的很清楚了。\n我们来举个简单的分词例子\n看下这个分词怎么分的：首先\nGET http://10.0.9.28:9200/_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;年轻人不讲武德\u0026#34; } 分词的结果是\n{ \u0026#34;tokens\u0026#34;: [ { \u0026#34;token\u0026#34;: \u0026#34;年轻人\u0026#34;, \u0026#34;start_offset\u0026#34;: 0, \u0026#34;end_offset\u0026#34;: 3, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 0 }, { \u0026#34;token\u0026#34;: \u0026#34;年轻\u0026#34;, \u0026#34;start_offset\u0026#34;: 0, \u0026#34;end_offset\u0026#34;: 2, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 1 }, { \u0026#34;token\u0026#34;: \u0026#34;人\u0026#34;, \u0026#34;start_offset\u0026#34;: 2, \u0026#34;end_offset\u0026#34;: 3, \u0026#34;type\u0026#34;: \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34;: 2 }, { \u0026#34;token\u0026#34;: \u0026#34;不讲\u0026#34;, \u0026#34;start_offset\u0026#34;: 3, \u0026#34;end_offset\u0026#34;: 5, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 3 }, { \u0026#34;token\u0026#34;: \u0026#34;讲武\u0026#34;, \u0026#34;start_offset\u0026#34;: 4, \u0026#34;end_offset\u0026#34;: 6, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 4 }, { \u0026#34;token\u0026#34;: \u0026#34;武德\u0026#34;, \u0026#34;start_offset\u0026#34;: 5, \u0026#34;end_offset\u0026#34;: 7, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 5 } ] } 这就是最大粒度的分词结果\n举个例子，\n你搜 “武德”，这句话就能搜出来\n你搜 “年轻人”， 这句话也能搜出来\n但是你要搜 “人不”，你铁定毛都搜不出来\n为啥，因为这个没被分词儿，你肯定是什么都搜不到的。\n我估摸着，你没弄明白我说的啥意思？你那么聪明，智慧，美丽，大方，你肯定能懂吧。\n咱们拿生僻词举个例子\n假设，我现在搜 “意带利黑手哥”\n让我们和他比划比划\n让你比划比划，让你知道什么叫黑手！\nGET http://10.0.9.28:9200/_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;意带利黑手哥\u0026#34; } 分词出的结果就是\n{ \u0026#34;tokens\u0026#34;: [ { \u0026#34;token\u0026#34;: \u0026#34;意\u0026#34;, \u0026#34;start_offset\u0026#34;: 0, \u0026#34;end_offset\u0026#34;: 1, \u0026#34;type\u0026#34;: \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34;: 0 }, { \u0026#34;token\u0026#34;: \u0026#34;带\u0026#34;, \u0026#34;start_offset\u0026#34;: 1, \u0026#34;end_offset\u0026#34;: 2, \u0026#34;type\u0026#34;: \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34;: 1 }, { \u0026#34;token\u0026#34;: \u0026#34;利\u0026#34;, \u0026#34;start_offset\u0026#34;: 2, \u0026#34;end_offset\u0026#34;: 3, \u0026#34;type\u0026#34;: \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34;: 2 }, { \u0026#34;token\u0026#34;: \u0026#34;黑手\u0026#34;, \u0026#34;start_offset\u0026#34;: 3, \u0026#34;end_offset\u0026#34;: 5, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 3 }, { \u0026#34;token\u0026#34;: \u0026#34;哥\u0026#34;, \u0026#34;start_offset\u0026#34;: 5, \u0026#34;end_offset\u0026#34;: 6, \u0026#34;type\u0026#34;: \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34;: 4 } ] } 所以你搜 “意带利”,或者 “意带利黑手哥” 是肯定没这几个词儿的。\n那就不能和他比划了。\n但是我们就是要这个词可以被检索\n那么我们现在该怎么做呢？\n分词器词汇的扩展 百度，谷歌每天都会更新热点词汇，像 “意带利黑手”，“三日杀神”这样的人物，早就存在词库里面了。\n当然，我ElasticSearch作为数一数二的检索引擎，能没这功能吗，其实也是有的\n一般我们如果要定义自己的词典，首先就要去修改ik的配置\nik的配置一般放置在\n/etc/elasticsearch/analysis-ik 这下面有个文件叫做 IKAnalyzer.cfg.xml\n打开它\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE properties SYSTEM \u0026#34;http://java.sun.com/dtd/properties.dtd\u0026#34;\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;comment\u0026gt;IK Analyzer 扩展配置\u0026lt;/comment\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展字典 --\u0026gt; \u0026lt;entry key=\u0026#34;ext_dict\u0026#34;\u0026gt;\u0026lt;/entry\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展停止词字典--\u0026gt; \u0026lt;entry key=\u0026#34;ext_stopwords\u0026#34;\u0026gt;\u0026lt;/entry\u0026gt; \u0026lt;!--用户可以在这里配置远程扩展字典 --\u0026gt; \u0026lt;!-- \u0026lt;entry key=\u0026#34;remote_ext_dict\u0026#34;\u0026gt;words_location\u0026lt;/entry\u0026gt; --\u0026gt; \u0026lt;!--用户可以在这里配置远程扩展停止词字典--\u0026gt; \u0026lt;!-- \u0026lt;entry key=\u0026#34;remote_ext_stopwords\u0026#34;\u0026gt;words_location\u0026lt;/entry\u0026gt; --\u0026gt; \u0026lt;/properties\u0026gt; 这注释，很清楚啊，我想。。大家不需要我再解释参数了吧。\n如何定义扩展字典？\n首先看一下目录下面的几个文件\nIKAnalyzer.cfg.xml：用来配置自定义词库 main.dic：ik 原生内置的中文词库，总共有 27 万多条 quantifier.dic：放了一些单位相关的词 suffix.dic：放了一些后缀 surname.dic：中国的姓氏 stopword.dic：英文停用词 这里可以看到，我们有两个方法去增加扩展词\n一个是直接修改main.dic，进行词汇追加，另外一个就是重新写入一个dic文件，直接在IKAnalyzer.cfg.xml里面进行修改。这两种方法都介绍一下吧。\n追加词汇 直接在main.dic里面追加词汇 “意带利黑手”\n然后重启ElasticSearch\n再进行一次检索\nGET http://10.0.9.28:9200/_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;意带利黑手\u0026#34; } 返回如下\n{ \u0026#34;tokens\u0026#34;: [ { \u0026#34;token\u0026#34;: \u0026#34;意带利黑手\u0026#34;, \u0026#34;start_offset\u0026#34;: 0, \u0026#34;end_offset\u0026#34;: 5, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 0 }, { \u0026#34;token\u0026#34;: \u0026#34;黑手\u0026#34;, \u0026#34;start_offset\u0026#34;: 3, \u0026#34;end_offset\u0026#34;: 5, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 1 } ] } 这下就能搜到了。\n新建字典 首先，我们在/etc/elasticsearch/analysis-ik里面建个字典，名字叫 new.dic\n在里面写入我们要检索的词\n然后修改配置\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE properties SYSTEM \u0026#34;http://java.sun.com/dtd/properties.dtd\u0026#34;\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;comment\u0026gt;IK Analyzer 扩展配置\u0026lt;/comment\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展字典 --\u0026gt; \u0026lt;entry key=\u0026#34;ext_dict\u0026#34;\u0026gt;./new.dic\u0026lt;/entry\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展停止词字典--\u0026gt; \u0026lt;entry key=\u0026#34;ext_stopwords\u0026#34;\u0026gt;\u0026lt;/entry\u0026gt; \u0026lt;!--用户可以在这里配置远程扩展字典 --\u0026gt; \u0026lt;!-- \u0026lt;entry key=\u0026#34;remote_ext_dict\u0026#34;\u0026gt;words_location\u0026lt;/entry\u0026gt; --\u0026gt; \u0026lt;!--用户可以在这里配置远程扩展停止词字典--\u0026gt; \u0026lt;!-- \u0026lt;entry key=\u0026#34;remote_ext_stopwords\u0026#34;\u0026gt;words_location\u0026lt;/entry\u0026gt; --\u0026gt; \u0026lt;/properties\u0026gt; 重启elasticsearch\n然后继续检索\nGET http://10.0.9.28:9200/_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;意带利黑手\u0026#34; } 返回如下\n{ \u0026#34;tokens\u0026#34;: [ { \u0026#34;token\u0026#34;: \u0026#34;意带利黑手\u0026#34;, \u0026#34;start_offset\u0026#34;: 0, \u0026#34;end_offset\u0026#34;: 5, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 0 }, { \u0026#34;token\u0026#34;: \u0026#34;黑手\u0026#34;, \u0026#34;start_offset\u0026#34;: 3, \u0026#34;end_offset\u0026#34;: 5, \u0026#34;type\u0026#34;: \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34;: 1 } ] } 这就能和黑手哥比划比划了。\n扩展字典的逻辑，大概就是这样。\n结尾 这两天在做智能检索，自己的ElasticSearch还是要多补补啊，最近成都疫情又变严重了，sad，要老老实实在家待一阵好好学习了。\n希望能够帮到大家，希望大家多提意见，多和我比划比划。\n年底了，大家要快乐呀！\n","permalink":"https://yemilice.com/posts/elasticsearch%E5%AE%9A%E4%B9%89%E8%87%AA%E5%B7%B1%E7%9A%84%E5%88%86%E8%AF%8D%E5%99%A8%E8%AF%8D%E5%85%B8%E6%A3%80%E7%B4%A2%E7%94%9F%E5%83%BB%E8%AF%8D/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003eElasticSearch当中，有许多的分词器供我们调用，中文用的最多的就是IK分词器，一些基本的词汇都包含了。\u003c/p\u003e\n\u003cp\u003e但是，一些基本的生僻词就很难检索了，比如一些特定专业词汇，或者一些流行词汇之类的，所以这篇文章，我会讲一下自定义分词器词典的设置和扩展，让我们能够检索的词汇变的更多。如果能帮到你，我也会很开心！\u003c/p\u003e","title":"ElasticSearch定义自己的分词器词典（检索生僻词）"},{"content":"前言 前阵子开发了一个检索服务器，用了一些Es的高阶功能，网上随便看了一圈，基本没有人公开这些功能的中文版，我寻思咱们不能只满足了自己，不满足其他老哥吧，所以，我将开发中用到的Es高阶功能总结输出.\n此次开发\n我用的是\nGolang 1.13.6\nEs的版本是6.3.2，\nPackage: \u0026ldquo;github.com/olivere/elastic\u0026rdquo;\n请注意。\n定义Mapping模板 首先，先展示一下我定义的mapping模板，这个模板是我们创建index之前定义的，这个十分重要！十分重要！十分重要！切记切记！\n首先我直接展示一下我的mapping定义，接下来我会一点点的说明每个参数的含义\nvar Contentmapping = `{ \u0026#34;settings\u0026#34;: { \u0026#34;number_of_shards\u0026#34;: 5, \u0026#34;number_of_replicas\u0026#34;: 1, \u0026#34;codec\u0026#34;: \u0026#34;best_compression\u0026#34;, \u0026#34;max_result_window\u0026#34;: \u0026#34;100000000\u0026#34; }, \u0026#34;mappings\u0026#34;: { \u0026#34;doc\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;id\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;data\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;nested\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;value\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;keyword\u0026#34;: { \u0026#34;ignore_above\u0026#34;: 256, \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; } } }, \u0026#34;key\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;keyword\u0026#34;: { \u0026#34;ignore_above\u0026#34;: 256, \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; } } } } }, \u0026#34;size\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;long\u0026#34; }, \u0026#34;last_mod_time\u0026#34;: { \u0026#34;format\u0026#34;: \u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;date\u0026#34; }, \u0026#34;user\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;content\u0026#34;: { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34;, \u0026#34;term_vector\u0026#34;: \u0026#34;with_positions_offsets\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34; } } } } }` mapping主字段，我定义了如下几个字段\n字段 字段说明 字段mapping类型 字段举例 name 姓名 keyword YK_example id id keyword 12345678 user 用户 keyword YK content 全文检索字段 text 中国123\u0026hellip;\u0026hellip; size 大小 long 14500 data 嵌套数据 nested [{key:123,value:456}, {key:789,value:101112} last_mod_time 最后修改时间 date 2020-12-02 12:00:00 这样是不是就说的很清楚了，下面我细致说一下mapping里面这种的设计方法吧\n首先是keyword类型，如果你的字段，有全检索的需求，也就是完全匹配的需求，你需要使用这个类型，但是keyword能完全检索的长度有限，也就是说，他只能完全匹配指定长度的数据，我查了一下，大概是2766个UTF-8字节数，所以超过这个数的将不会被检索到 我这里面出现了ignore_above这个东西，这个东西是：最大可被检索字段 意思就是，当超过我定义的ignore_above的字符数的时候，多出来的将不会被检索到，这里是根据业务场景自己划分。 long类型，一般用来存储数字，这种情况，大部分都是存储文件大小之类的，一开始一定要用long定义，因为int类型大小超过100000会直接存不进去的。 text类型，这里一般都是存储那种特别大的文字数据的，比如你存了一个PDF进来，或者存了一本字典进来，就需要用text存储，text理论上支持存储无限大的文字数据。这里不想keyword，它不会被支持全文检索和精准匹配。需要定义分词器/在检索语句上下功夫 nested类型，这里一般是复杂嵌套，类似列表中包含字典的操作，[{key:123,value:456}，类似这样 date类型，这里一般是时间格式，你要自己格式化，\u0026ldquo;format\u0026rdquo;: \u0026ldquo;yyyy-MM-dd HH:mm:ss\u0026rdquo;,这种比对的就是：2020-12-02 12:00:00，如果你是2020-12-02 12:00:00:22，是肯定会导入失败的 大概就是这些，包含的部分也就足够你一般使用了，还有一些mapping的高级用法，后面我会单独写博客分析，本篇内容不是这个，此处进行跳过了。\n定义分词器 上面说了，分词器是做全文检索的时候必须要用的，分词器的功能就是能够让你对text字段进行检索，text字段是不可被全文检索的，因为它的大小不定，但是，用了分词器能让你进行全文检索，就像这样\n所以，定义分词的的参数是\n\u0026#34;content\u0026#34;: { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34;, \u0026#34;term_vector\u0026#34;: \u0026#34;with_positions_offsets\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34; } 加一个analyzer参数，指定某个字段使用ik分词器，这里可以使用\n\u0026#34;analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34;, 也可以使用\n\u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, 这里根据你的业务自己选择\n定义最大展示条数 一般ElasticSearch为了保证一次不拿取过多数据，会进行一个限制，限制最大不能读取10000条数据，但是我们有些时候因为一些特殊原因需要拿取超过10000条数据（翻页\n这时候有两种方法\n第一种直接通过Es的接口\nPUT _all/_settings { \u0026#34;index.max_result_window\u0026#34;:200000 } 这种好处就是随时随地可以改\n第二种方法就是，定义mapping的时候，直接定义\n\u0026#34;settings\u0026#34;: { \u0026#34;max_result_window\u0026#34;: \u0026#34;100000000\u0026#34; }, 我个人还是比较喜欢第二种啦，因为事先定义好总比发现了问题再去调接口好一百倍。\n根据模板创建index mapping现在有了，咱们现在要做的就是，利用mapping创建一个index，涉及一些连接Es之类的，我在这就不说了，我以前写过一个Golang调用Es的接口，大家要不自己去瞅瞅？\nhttps://yemilice.com/2020/05/14/golang%E5%B0%81%E8%A3%85elasticsearch%E5%B8%B8%E7%94%A8%E5%8A%9F%E8%83%BD/ 直接调用一把代码\nfunc (Es *Elastic) CreateIndex(index, mapping string) bool { // 判断索引是否存在 exists, err := Es.Client.IndexExists(index).Do(context.Background()) if err != nil { fmt.Printf(\u0026#34;\u0026lt;CreateIndex\u0026gt; some error occurred when check exists, index: %s, err:%s\u0026#34;, index, err.Error()) return false } if exists { fmt.Printf(\u0026#34;\u0026lt;CreateIndex\u0026gt; index:{%s} is already exists\u0026#34;, index) return true } // 创建index createIndex, err := Es.Client.CreateIndex(index).Body(mapping).Do(context.Background()) if err != nil { fmt.Printf(\u0026#34;\u0026lt;CreateIndex\u0026gt; some error occurred when create. index: %s, err:%s\u0026#34;, index, err.Error()) return false } if !createIndex.Acknowledged { // Not acknowledged fmt.Printf(\u0026#34;\u0026lt;CreateIndex\u0026gt; Not acknowledged, index: %s\u0026#34;, index) return false } return true } func main() { // 初始化连接Es es, err := InitES() if err != nil { return } // 创建ES es.CreateIndex(\u0026#34;text\u0026#34;, Contentmapping) } 然后我们去查看我们的Es\n就可以看到详细的信息了。大概就完成了\n根据模板上传数据 Golang有个很奇葩的地方，就是，你必须要按照规则传递数据，也就是事先定义好的结构体，如果你不按照这个规矩传，那你肯定是传不进去的，我这边模拟一个简单的数据传递逻辑吧，大家都是聪明孩子，肯定一点就通\n首先，观察一下咱们的mapping，定义一个结构体，用来传递数据/输出数据\ntype ContentEsInfo struct { Name string `json:\u0026#34;name\u0026#34;` ID string `json:\u0026#34;id\u0026#34;` Size uint64 `json:\u0026#34;size\u0026#34;` LastModTime string `json:\u0026#34;last_mod_time\u0026#34;` User string `json:\u0026#34;user\u0026#34;` Data []DataType `json:\u0026#34;data\u0026#34;` Content string `json:\u0026#34;content\u0026#34;` } type DataType struct { Key string `json:\u0026#34;key\u0026#34;` Value string `json:\u0026#34;value\u0026#34; ` } //Put 传入index名， typ，还有组合成的结构体 func (Es *Elastic) Put(index string, typ string, bodyJSON interface{}) (bool, error) { _, err := Es.Client.Index(). Index(index). Type(typ). BodyJson(bodyJSON). Do(context.Background()) if err != nil { // Handle error fmt.Printf(\u0026#34;\u0026lt;Put\u0026gt; some error occurred when put. err:%s\u0026#34;, err.Error()) return false, err } return true, nil } func main() { z := ContentEsInfo{Name: \u0026#34;yk\u0026#34;, ID: \u0026#34;ak47\u0026#34;, Size: 10423, Data: []DataType{{Key: \u0026#34;1\u0026#34;, Value: \u0026#34;2\u0026#34;}}, User: \u0026#34;yk123\u0026#34;, LastModTime: \u0026#34;2020-01-01 12:00:00\u0026#34;, Content: \u0026#34;dssads\u0026#34;} es.Put(\u0026#34;content_test\u0026#34;, \u0026#34;doc\u0026#34;, z) } 传完了咱们看一下Es，里面已经有数据了。\n如图：\n输出字段 既然已经有字段了，我们现在要输出字段，看下检索结果，同理，你还是需要结构体，我告诉你，你就逃不开和结构体的孽缘，哈哈哈哈！\ntype ContentEsInfo struct { Name string `json:\u0026#34;name\u0026#34;` ID string `json:\u0026#34;id\u0026#34;` Size uint64 `json:\u0026#34;size\u0026#34;` LastModTime string `json:\u0026#34;last_mod_time\u0026#34;` User string `json:\u0026#34;user\u0026#34;` Data []DataType `json:\u0026#34;data\u0026#34;` Content string `json:\u0026#34;content\u0026#34;` } //GetMsg 获取Msg func (Es *Elastic) GetMsg(indexname, typ string) { var contentinfo ContentEsInfo res, _ := Es.Client.Search(indexname).Type(typ).Do(context.Background()) //从搜索结果中取数据的方法 for _, item := range res.Each(reflect.TypeOf(contentinfo)) { if t, ok := item.(ContentEsInfo); ok { fmt.Println(t) } } } func main() { es, err := InitES() if err != nil { return } es.GetMsg(\u0026#34;content_test\u0026#34;, \u0026#34;doc\u0026#34;) } 这边输出了：\n{yk ak47 10423 2020-01-01 12:00:00 yk123 [{1 2}] dssads} 输出指定字段 有些时候你不想显示太多字段？没问题，可以让Es返回的时候指定只显示某些字段。有些时候如果某个字段特别大，我们可以直接屏蔽它，让它不包装返回。\n假设我们要让content这个字段不返回\n//ShieldAnotherfield 屏蔽指定字段 func (Es *Elastic) ShieldAnotherfield(indexname, typ string) { var contentinfo ContentEsInfo //指定返回的字段 fsc := elastic.NewFetchSourceContext(true).Include(\u0026#34;name\u0026#34;, \u0026#34;type\u0026#34;, \u0026#34;user\u0026#34;, \u0026#34;size\u0026#34;, \u0026#34;last_mod_time\u0026#34;, \u0026#34;data\u0026#34;) res, _ := Es.Client.Search(indexname).Type(typ).FetchSourceContext(fsc).Do(context.Background()) //从搜索结果中取数据的方法 for _, item := range res.Each(reflect.TypeOf(contentinfo)) { if t, ok := item.(ContentEsInfo); ok { fmt.Println(t) } } } func main() { es, err := InitES() if err != nil { return } es.ShieldAnotherfield(\u0026#34;content_test\u0026#34;, \u0026#34;doc\u0026#34;) } 可以查看一下返回\n{yk 10423 2020-01-01 12:00:00 yk123 [{1 2}] } 和上面对比一下，是不是少了dsds那个字段，那个字段就是content，如果当content特别大的时候，它就相当有作用。\n翻页的实现和优化 翻页，这是老生常谈的问题了，我的blog里面写过Es的翻页优化方法，其实很多人无脑推scroll动态翻页，这是不可取的，有些时候，你要根据自己的需求来定义翻页的逻辑，不能说别人用scroll，你就scroll，from+size也能满足一些不一样的需求。\n当你的翻页需要支持跳页，指定页数翻页，最前/最后翻页，随机跳页的时候，我建议你用from+size 当你的翻页是动态的，例如下拉加载，例如往下滑持续加载，动态加载的时候，你要用scroll深度翻页，因为这个才是对你机器负载最低的一种翻页模式。 好了，我们来实现翻页吧。\n我这里因为要支持随机跳页，所以我用了from+size的逻辑\n//FromSize 翻页方法 func (Es *Elastic) FromSize(indexname, typ string, size, from int) { var contentinfo ContentEsInfo res, _ := Es.Client.Search(indexname).Type(typ).Size(size).From(from).Do(context.Background()) //从搜索结果中取数据的方法 for _, item := range res.Each(reflect.TypeOf(contentinfo)) { if t, ok := item.(ContentEsInfo); ok { fmt.Println(t) } } } func main() { es, err := InitES() if err != nil { return } es.FromSize(\u0026#34;content_test\u0026#34;, \u0026#34;doc\u0026#34;, 10, 0) } 这里输出\n{yk ak47 10423 2020-01-01 12:00:00 yk123 [{1 2}] dssads} 这里的size，你可以理解为每页展示条数，\n而from，你可以理解为页数，\n要注意，这是从0开始1计算的，类似列表，第一个下标为0，如果你将size改为1，那么将搜索不到数据，理由是：第11条数据不存在，因为我们的数据库只有1条数据（暂时\n高亮检索关键字 ElasticSearch支持高亮返回，回到刚刚咱们讨论的话题\n类似百度文库那样的搜索逻辑，如果检索到之后返回，高亮我们检索的值，这种一般怎么处理呢。\n这种其实Es也是支持的\n我们现在来一发检索\n//HighlightMsg 高亮方法 func (Es *Elastic) HighlightMsg(indexname, typ string, size, from int, keyword string) { // var contentinfo ContentEsInfo boolQ := elastic.NewBoolQuery() boolZ := elastic.NewBoolQuery() // 定义highlight highlight := elastic.NewHighlight() // 指定需要高亮的字段 highlight = highlight.Fields(elastic.NewHighlighterField(\u0026#34;content\u0026#34;)) // 指定高亮的返回逻辑 \u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;...msg...\u0026lt;/span\u0026gt; highlight = highlight.PreTags(\u0026#34;\u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;\u0026#34;).PostTags(\u0026#34;\u0026lt;/span\u0026gt;\u0026#34;) escontent := elastic.NewMatchQuery(\u0026#34;content\u0026#34;, keyword) boolZ.Filter(boolQ.Should(escontent)) res, _ := Es.Client.Search(indexname).Type(typ).Highlight(highlight).Query(boolZ).Do(context.Background()) // 高亮的输出和doc的输出不一样，这里要注意，我只输出了匹配到高亮的第一个词 for _, highliter := range res.Hits.Hits { fmt.Println(highliter.Highlight[\u0026#34;content\u0026#34;][0]) } } func main() { es, err := InitES() if err != nil { return } // 我们检索一下带有名气的doc，然后高亮输出 es.HighlightMsg(\u0026#34;content_test\u0026#34;, \u0026#34;doc\u0026#34;, 10, 0, \u0026#34;名气\u0026#34;) } 看一下返回\n那就当因为刘先 \u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;名气\u0026lt;/span\u0026gt;太大， 曹操不得不展现出极度的宽容吧， 但是神奇的是后面的“周不疑之死”。 看到了没，“名气”这个词语被高亮输出了。\n全文检索-精准度调整 因为分词器的缘故，我们在检索词汇的时候，经常会搜索到一些不相干的词，例如\n我们搜索\u0026quot;今天是美好的一天\u0026quot;，我们想要的自然是匹配到 “今天是美好的一天” 的所有doc，但是分词器不会这么想，分词器会将这句话进行分词\n切分为：[今天，美好，一天，美好的，美好的一天]\n这样Es在检索的时候，就会把上面分词了的数据也检索到，意思就是，包含有“今天”，“美好”，“一天”。。。。之类的数据都可以被检索出来，这绝对不是我们想要的\n但是我们可以设定短句搜索，并且调整它的精准度\n//Precisesearch 精准检索 func (Es *Elastic) Precisesearch(indexname, typ string, size, from int, keyword string) { // var contentinfo ContentEsInfo boolQ := elastic.NewBoolQuery() boolZ := elastic.NewBoolQuery() // 定义highlight highlight := elastic.NewHighlight() // 指定需要高亮的字段 highlight = highlight.Fields(elastic.NewHighlighterField(\u0026#34;content\u0026#34;)) // 指定高亮的返回逻辑 \u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;...msg...\u0026lt;/span\u0026gt; highlight = highlight.PreTags(\u0026#34;\u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;\u0026#34;).PostTags(\u0026#34;\u0026lt;/span\u0026gt;\u0026#34;) // 短句匹配 escontent := elastic.NewMatchPhrasePrefixQuery(\u0026#34;content\u0026#34;, keyword).MaxExpansions(10) boolZ.Filter(boolQ.Should(escontent)) res, _ := Es.Client.Search(indexname).Type(typ).Highlight(highlight).Query(boolZ).Do(context.Background()) for _, highliter := range res.Hits.Hits { fmt.Println(highliter.Highlight[\u0026#34;content\u0026#34;][0]) } } func main() { es, err := InitES() if err != nil { return } // 搜个冷门词，ik里绝壁没有的 es.HighlightMsg(\u0026#34;content_test\u0026#34;, \u0026#34;doc\u0026#34;, 10, 0, \u0026#34;渔阳三檛\u0026#34;) } 得到返回值\n想想祢衡的类似表演（\u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;渔\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;阳\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;三\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#39;color: red;\u0026#39;\u0026gt;檛\u0026lt;/span\u0026gt;）， 曹操都只能表示惹不起，恭送出许。 那就当因为刘先名气太大，曹操不得不展现出极度的宽容吧，但是神奇的是后面的“周不疑之死”。 这就匹配到了，并且也不会出现乱匹配的问题了。\n总结 大概也就这么多了，其实很多Es的检索逻辑在我以前的blog里面都写过了\n再放送一遍旧文章地址：\nhttps://yemilice.com/2020/05/14/golang%E5%B0%81%E8%A3%85elasticsearch%E5%B8%B8%E7%94%A8%E5%8A%9F%E8%83%BD/ 今天所有写过的代码都在：\nhttps://github.com/Alexanderklau/Go_poject/blob/master/Go-Elasticdb/ElasticSearch_adv_use/Es_adv.go 大家可以自己下下来自己改着玩玩\n这次用Es开发了一个文件检索服务器，作词作曲又是我自己，自认为在Es这个部分，我应该算是接触不少了吧，最近我在看Es源码，准备弄明白Es到底为什么检索那么快，下一篇不是Python三巨头就是Mysql vs Es，大家期待吧！\n年底了，希望大家都保重身体啊。\n翻看着年初自己许下的承诺 到了如今却只有沉默 告诉镜子里的他你已经不小了 你该学会衡量什么是你想要的 你可以无畏自由的向天空宣泄 大喊着我要走自己的路 check~ ","permalink":"https://yemilice.com/posts/elasticsearch%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95golang%E5%AE%9E%E7%8E%B0/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e前阵子开发了一个检索服务器，用了一些Es的高阶功能，网上随便看了一圈，基本没有人公开这些功能的中文版，我寻思咱们不能只满足了自己，不满足其他老哥吧，所以，我将开发中用到的Es高阶功能总结输出.\u003c/p\u003e","title":"ElasticSearch高级用法(Golang实现)"},{"content":"前言 总觉得用了Python那么久，感觉隔一层面纱似的，只是会用而已。\n现在Python更新了3.9，越来越厉害了，多了很多新的语法特性。\n想想自己，也不能总是写一些Golang的东西，Python也要持续跟上，所以打算开一个Python三巨头的坑，把Python的线程，进程，协程都讲明白，这样第一个是让我加深理解，第二个是让我彻底搞懂这三者的机制，我认为这样我才可以走的更远。\n在开发的过程当中，我们总是需要去提高效率，一般在Python里面，我们经常会听到所谓多线程，多进程，加上现在的协程的诸多方法去解决性能问题，那么，到底这些术语到底是个什么意思，我们什么时候用什么方法，才是最合适的呢？这才是促使我写这篇文章的原因吧。\n因为这些东西，应该是Python最核心的了，搞懂这个，Python也就没什么特别难的问题了。所以，我们开始吧\n从来不畏惧到底明天会是什么模样 活在今天告诉你生命不只一种颜色 咬牙挺住却坚信黑暗之后即是黎明 解剖生活才能看到未来的光 Python的多线程是什么 首先，线程是什么，说的简单一点，线程就是进程的儿子，一个进程可以搞出多个线程，但是一个线程只能有一个爹（进程），不允许随便乱认爹（进程）。\n这就好比你看电影，电影是一个进程，里面的画面是一个线程，声音是一个线程，字幕又是一个线程，这就是线程的一种表现。\n如果是系统执行进程，首先需要划分独立的进程空间/内存，但是线程就不会这么麻烦。\n线程是独立执行的，并且也是并发的，占用资源也小，一个线程可以去创建/杀死另一个线程，相当于“子子孙孙无穷匮也”，并且线程之间还支持通信，可以共享其中的数据/内存等等。\n上面这些都是线程的优点，但是！但是！但是！Python的线程和这个不一样！\nPython的线程其实是个假的，为啥呢，因为Python有GIL全局锁。\n不知道你们看过《无限恐怖》这本小说没有，里面有个东西叫做基因锁，因为有基因锁的存在，导致人类的各项机能被限制了，同理，GIL全局锁也是一样，它的逻辑就是，只要你用了带GIL锁的解释器，任何时候，你都只能，也只可以在同一时间执行一个线程。\n带GIL锁的解释器有哪些呢？当然是我现在说的CPython啦！\nCPython用C实现的，用的人也最多，所以为什么市面上的教材都说Python的线程不好使，是因为大家全都是用的CPython。\n有点儿绕吧，其实意思就是，Python的多线程实际上是个伪多线程，并不是真正的多线程，实际上，无论如何，它只能保证一个线程执行。\n我来描述一下Python多线程执行的逻辑\n线程1获取到GIL锁 执行业务代码 线程1释放GIL锁 线程2获取到GIL锁 执行业务代码 线程2释放GIL锁 ...... 这个是不是贼TM绕，我写个代码给大家伙儿瞅瞅到底这个是怎么表现出来的\nimport threading import time exitFlag = 0 # 继承的方法写一个多线程逻辑 class myThread (threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print (\u0026#34;开始线程：\u0026#34; + self.name) print_time(self.name, self.counter, 5) print (\u0026#34;退出线程：\u0026#34; + self.name) def print_time(threadName, delay, counter): while counter: if exitFlag: threadName.exit() # 这里模拟一个阻塞 time.sleep(delay) print (\u0026#34;%s: %s\u0026#34; % (threadName, time.ctime(time.time()))) counter -= 1 # 创建新线程 thread1 = myThread(1, \u0026#34;Thread-1\u0026#34;, 1) thread2 = myThread(2, \u0026#34;Thread-2\u0026#34;, 2) # 开启新线程 thread1.start() thread2.start() # join等待 thread1.join() thread2.join() print (\u0026#34;退出主线程\u0026#34;) 这里用了一个继承的方法，写了一个多线程逻辑\n这里输出\n开始线程：Thread-1 开始线程：Thread-2 Thread-1: Tue Dec 1 09:58:21 2020 Thread-2: Tue Dec 1 09:58:22 2020 Thread-1: Tue Dec 1 09:58:22 2020 Thread-1: Tue Dec 1 09:58:23 2020 Thread-2: Tue Dec 1 09:58:24 2020 Thread-1: Tue Dec 1 09:58:24 2020 Thread-1: Tue Dec 1 09:58:25 2020 退出线程：Thread-1 Thread-2: Tue Dec 1 09:58:26 2020 Thread-2: Tue Dec 1 09:58:28 2020 Thread-2: Tue Dec 1 09:58:30 2020 退出线程：Thread-2 退出主线程 发现了没，这个，有个时间差，这两根本不是同时去运行的，这就是GIL的石锤\n再看个图，这就是GIL的运行流程\n整明白了嘛？\n但是你会说，哎呀，我没感觉到缓慢阿，害挺快的，是，这就是我下面要聊的，它到底用了什么技术，才能让我们感觉到挺快的。\nPython的多线程在代码部分是怎么实现的？ 又涉及到看源码的东西了,这里首先追踪一把，threading的代码放置在\nhttps://github.com/python/cpython/blob/3.9/Lib/threading.py 先从入口的Start追踪\ndef start(self): \u0026#34;\u0026#34;\u0026#34;Start the thread\u0026#39;s activity. It must be called at most once per thread object. It arranges for the object\u0026#39;s run() method to be invoked in a separate thread of control. This method will raise a RuntimeError if called more than once on the same thread object. \u0026#34;\u0026#34;\u0026#34; # 线程是否初始化（这里就是刚那个init函数 if not self._initialized: raise RuntimeError(\u0026#34;thread.__init__() not called\u0026#34;) # 设置线程的开始状态（把线程状态置为start if self._started.is_set(): raise RuntimeError(\u0026#34;threads can only be started once\u0026#34;) # 加锁，调用GIL with _active_limbo_lock: _limbo[self] = self # 去启动新的线程 try: _start_new_thread(self._bootstrap, ()) # 如果启动出了问题 except Exception: # 把锁给del了 with _active_limbo_lock: del _limbo[self] raise # 释放锁，阻塞。直到被唤醒/超时 self._started.wait() 其实threading的代码写的很好，已经大概把流程和步骤都说明白了，涉及到lock的部分是C写的, 首先追踪溯源，看一下启动线程的C代码\nvoid PyEval_InitThreads(void) { if (gil_created()) return; // 创建GIL锁 create_gil(); // 申请GIL锁 take_gil(PyThreadState_GET()); // 主线程 main_thread = PyThread_get_thread_ident(); // 如果没有等待锁 if (!pending_lock) // 创建等待锁 pending_lock = PyThread_allocate_lock(); } 可以看到 核心的GIL代码如下\nstatic void take_gil(PyThreadState *tstate) { MUTEX_LOCK(gil_mutex); // 加锁 if (!_Py_atomic_load_relaxed(\u0026amp;gil_locked)) // 获取 GIL，若已释放，直接获取，跳转 goto _ready; while (_Py_atomic_load_relaxed(\u0026amp;gil_locked)) { // GIL 未释放 int timed_out = 0; unsigned long saved_switchnum; // 记录切换次数 saved_switchnum = gil_switch_number; // 利用 pthread_cond_tim 阻塞等待超时 COND_TIMED_WAIT(gil_cond, gil_mutex, INTERVAL, timed_out); /* 等待超时，仍未释放, 发送释放请求信号 */ if (timed_out \u0026amp;\u0026amp; _Py_atomic_load_relaxed(\u0026amp;gil_locked) \u0026amp;\u0026amp; gil_switch_number == saved_switchnum) { // 设置 gil_drop_request=1，eval_breaker=1 // 释放信号 SET_GIL_DROP_REQUEST(); } } _ready: /* We now hold the GIL */ _Py_atomic_store_relaxed(\u0026amp;gil_locked, 1); // 设置 GIL 占用 _Py_ANNOTATE_RWLOCK_ACQUIRED(\u0026amp;gil_locked, /*is_write=*/1); if (tstate != (PyThreadState*)_Py_atomic_load_relaxed(\u0026amp;gil_last_holder)) { _Py_atomic_store_relaxed(\u0026amp;gil_last_holder, (uintptr_t)tstate); ++gil_switch_number; } if (_Py_atomic_load_relaxed(\u0026amp;gil_drop_request)) { // 重置 gil_drop_request=0 RESET_GIL_DROP_REQUEST(); } if (tstate-\u0026gt;async_exc != NULL) { _PyEval_SignalAsyncExc(); } MUTEX_UNLOCK(gil_mutex); // 解锁 } 这里实现了互斥锁gil_mutex，如果GIL被占用，那么将持续等待，超时后将修改重置变量。\n这点代码给我看的累死了。\n回到本段开始的问题，为什么GIL有时候感觉不到慢？\n首先，GIL类似一个信号锁，意思就像是尚方宝剑，持有它的线程告诉其他线程，都不许动阿，我现在正用着呢，你们要么等着我主动释放，要么等我超时了自己释放，然后继续竞争切换持有GIL的线程。\n核心的意思就是，多线程的好处在于，阻塞并不会影响其他的线程，因为阻塞的线程持有的GIL马上就被释放了，其他线程可以接力马上干活儿，不会出现阻塞的情况。\n这里可以看一下切换线程的代码\nPyObject * _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag){ ... for (;;) { if (_Py_atomic_load_relaxed(\u0026amp;eval_breaker)) { if (_Py_OPCODE(*next_instr) == SETUP_FINALLY || _Py_OPCODE(*next_instr) == YIELD_FROM) { goto fast_next_opcode; } if (_Py_atomic_load_relaxed(\u0026amp;pendingcalls_to_do)) { if (Py_MakePendingCalls() \u0026lt; 0) goto error; } // 如果检测到gil_drop_request，释放GIL if (_Py_atomic_load_relaxed(\u0026amp;gil_drop_request)) { /* Give another thread a chance */ if (PyThreadState_Swap(NULL) != tstate) Py_FatalError(\u0026#34;ceval: tstate mix-up\u0026#34;); drop_gil(tstate); // 继续去调用GIL抢占 take_gil(tstate); // 检查是否退出线程 if (_Py_Finalizing \u0026amp;\u0026amp; _Py_Finalizing != tstate) { drop_gil(tstate); PyThread_exit_thread(); } if (PyThreadState_Swap(tstate) != NULL) Py_FatalError(\u0026#34;ceval: orphan tstate\u0026#34;); } } ... } } 当检测到 eval_breaker、gil_drop_request 时，会被动的释放 GIL，跟其他线程一起再次竞争 GIL，所以它几乎没有阻塞，虽然这狗GIL把你给锁住了，但是也保证了效率，当某个线程执行时间过长，可以迅速切换下一个线程调用。\n类似这样\n如何绕过GIL锁？ 如何绕过GIL锁，这个也是老生常谈的问题了。\n在我这里的话，如果是我，涉及到需要绕过的地方\n我会直接写C代码，用CPython去调用 我会用JPython 我会用Golang 大概就这么几个方法，没别的方法了，再也不用看了\nPython多线程适用于哪些场景？ 前面说了，Python多线程并不是真正意义的多线程，其实是个假的，但是还是有很多老哥不遗余力的推荐它，到底为啥，上一节也说得很清楚了，GIL虽然恶心，但是人家还是支持自动切换比较慢的阻塞线程的，不会影响其他线程运行，所以，你品一下，在咱们的开发岁月中，多线程，到底适用于哪些场景？\n首先，发生阻塞的是什么情况，网络的连接时间过长，或者是处理多个业务，读取多个文件，访问多个网页等等。\n网上很多文章都说： I/O 密集场景，多线程最合适\n你抄我，我抄你，抄到最后就这么一句话，讲真，我看了那么多文章，真的没几个说清楚的，举个例子的都没。\n那行嘛，那我来举例子吧。\n首先，I/O密集型，指的是涉及到网络、磁盘IO的任务，现在互联网的web大部分都是IO密集的场景\n举几个常用的自用例子吧\n网络爬虫，这个都写烂了 web应用，例如多用户访问，多用户登录，多用户下载 数据库写入，Mysql/Es等等 RPC框架 大概就这些常用的，Python多线程，大概就这些比较合适。\nPython多线程的几个基础用例 说了那么多，最后还是加几个基础的用例，拿去举一反三，你们都是聪明人\n基础的多线程框架 这个基础的多线程框架包含的功能\n启动/停止线程 展示线程的状态 import threading import time exitFlag = 0 # 利用继承的方法 class myThread (threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print (\u0026#34;开始线程：\u0026#34; + self.name) print_time(self.name, self.counter, 5) print (\u0026#34;退出线程：\u0026#34; + self.name) def print_time(threadName, delay, counter): while counter: if exitFlag: threadName.exit() time.sleep(delay) print (\u0026#34;%s: %s\u0026#34; % (threadName, time.ctime(time.time()))) counter -= 1 # 创建新线程 thread1 = myThread(1, \u0026#34;Thread-1\u0026#34;, 1) thread2 = myThread(2, \u0026#34;Thread-2\u0026#34;, 2) # 开启新线程 thread1.start() thread2.start() thread1.join() thread2.join() print (\u0026#34;退出主线程\u0026#34;) 多线程的线程通信框架（利用队列） 有些时候我们需要在线程之间交换信息和数据，所以多线程之间需要互相通信\n多线程的线程通信框架包含的功能\n利用队列共享数据（queue） 创建一个生产者消费者模型，启动生产者，消费者线程 这种框架同样适用于爬虫\nimport threading import time from queue import Queue queue = Queue(20) # 生产者 def Producer(): i = 0 print(\u0026#34;开始线程\u0026#34;) while True: i = i + 1 print(\u0026#34;生产数据\u0026#34;, i, \u0026#34;现有数据\u0026#34;, queue.qsize()) time.sleep(1) queue.put(i) # 消费者 def Consumer(): while True: i = queue.get() time.sleep(0.5) print(\u0026#34;消费数据\u0026#34;, i) if __name__ == \u0026#34;__main__\u0026#34;: Th1 = threading.Thread(target=Producer, ) Th2 = threading.Thread(target=Consumer, ) Th2.start() Th1.start() Th1.join() Th2.join() 大概就是这样，这个害挺简单的。\n线程锁框架 当需要修改共享数据的时候，多个线程会造成冲突，所以对执行的部分进行加锁很有必要，这里采用互斥锁逻辑\n线程池加锁框架大概实现的功能\n在程序中加锁，避免竞争冲突 这部分代码我懒得写了，直接用了 https://www.cnblogs.com/tashanzhishi/p/10775641.html\n这位老兄的代码写的很好，看一遍就懂了，感恩!\nimport threading import time count = 0 # 做一个累加的函数 def add_num(): global count if lock.acquire(): # 获得锁，并返回True tmp = count time.sleep(0.001) count = tmp + 1 lock.release() # 执行完释放锁 def run(add_fun): global count thread_list = [] # 累加100次 for i in range(100): t = threading.Thread(target=add_fun) t.start() thread_list.append(t) # 等待线程执行完 for j in thread_list: j.join() print(count) if __name__ == \u0026#39;__main__\u0026#39;: # 添加全局锁 lock = threading.Lock() # 执行函数 run(add_num) 线程池框架 线程池相当于冰箱里的啤酒\n你只要想喝打开冰箱拿就行\n你不喝人家也不会跑\n就在冰箱里，不吵不闹\n等待你的下一次临幸\nfrom socket import AF_INET, SOCK_STREAM, socket from concurrent.futures import ThreadPoolExecutor # 假装跑一个服务器client def echo_client(sock, client_addr): \u0026#39;\u0026#39;\u0026#39; Handle a client connection \u0026#39;\u0026#39;\u0026#39; print(\u0026#39;Got connection from\u0026#39;, client_addr) while True: msg = sock.recv(65536) if not msg: break sock.sendall(msg) print(\u0026#39;Client closed connection\u0026#39;) sock.close() # 假装跑一个server def echo_server(addr): # 定义一个线程池 pool = ThreadPoolExecutor(128) sock = socket(AF_INET, SOCK_STREAM) sock.bind(addr) sock.listen(5) while True: client_sock, client_addr = sock.accept() # 从池子里拿线程消费 pool.submit(echo_client, client_sock, client_addr) echo_server((\u0026#39;\u0026#39;,15000)) 结尾 线程这块，基本核心的东西都弄完了，其实梳理完，觉得还好，最起码我弄明白人家是干嘛的了，以后碰到任何线程的问题，我也不害怕了，这是12月第一篇blog，我要坚持三四个月，直到黎明的到来。\nend。\n","permalink":"https://yemilice.com/posts/%E5%AE%8C%E5%85%A8%E6%90%9E%E6%87%82python%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%9C%BA%E5%88%B6/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e总觉得用了Python那么久，感觉隔一层面纱似的，只是会用而已。\u003c/p\u003e\n\u003cp\u003e现在Python更新了3.9，越来越厉害了，多了很多新的语法特性。\u003c/p\u003e\n\u003cp\u003e想想自己，也不能总是写一些Golang的东西，Python也要持续跟上，所以打算开一个Python三巨头的坑，把Python的线程，进程，协程都讲明白，这样第一个是让我加深理解，第二个是让我彻底搞懂这三者的机制，我认为这样我才可以走的更远。\u003c/p\u003e","title":"完全搞懂Python的线程机制"},{"content":"前言 虽然上两篇文章讲了几个并发示例和Goroutine的使用，但是我仔细看了一下，还是不够清楚，这次这篇文章将会是一篇纯理论的文章，主要讲一下Go的并发机制的原理还有Goroutine还有channel。\n至于为啥要写呢。。。。。\n朋友们好啊，我是Go开发人员Yk，刚才有个朋友问我， Y老师，发生甚么事了，我说怎么回事，给我发了一篇我的博客截图，我一看，嗷！ 原来是前一阵，我写了两篇Go的文章，一个Go并发，一个Go例子， 他们说，唉，Y老师你这并发讲的不清楚，哎，能不能重新写一个说明白， 我说行，我说你在kindle上看死书，不好用。他不服气。我说小赤佬， 你一篇文章来怼我两篇，你搞不动我。他说你这没用， 我说我这个有用，这是Golang精华，隔壁200多页大PDF我都看了。 他非要和我试试，我说可以，我一说他就“啪”就把键盘拿起来了，很快嗷！ 年轻人，我劝你耗子为汁，来偷袭我20多岁的老同志，这好吗？这不好。 我说你不讲码德阿，我们Golang开发人员要以和为贵，不要搞窝里斗，谢谢朋友们！ 还是要感谢这么几本书，《Go并发编程实战》，《Go语言圣经》，《Go高级编程》，感谢这几本书的作者和译者！感谢你们！\nGo的线程模型 Go的并发机制是个什么，主要就是Go有一个特有的线程模型，这个线程模型里面，有个特有的线程，叫做Goroutine，这个东西可以理解为一个可执行可并发的代码块，通过channel管道去进行状态同步或者消息传递。\n首先来看一下那个线程模型，术语叫做\u0026quot;两级线程模型\u0026quot;，名字其实没啥狗屁用，就听听就得了。\nGo的线程模型，有三个核心元素\nM：machine，代表工作线程，你想成僵尸母体就行了 P：processor，上下文环境，意思就是执行Go代码的所需要的资源，或者是存储要执行的goroutine，你想成僵尸传播渠道就行了 G：goroutine，Go代码片段，你要执行并发的那一块儿程序。你想象成被感染的小僵尸就行了 他们的大致关系如下\n这三个元素是互相依赖，互相依存的关系，我用僵尸传播理论来说明一下他们的关系吧。\n首先，G需要P和M的支持，也就是说，一个G的出现，是先有了M（僵尸母体），然后通过渠道（P），感染其他的小僵尸（G）。一个M对应一个P，P对应多个G，G也对应多个P，因为感染人数不只是一个人，所以，他们的关系就像这样\n如图所示\n这么理解一波，M是内核线程，按照一般的逻辑，一个内核线程一般运行一个任务，但是，GOlang比较牛逼，通过调度器，使得M可以运行多个用户线程G，其中，P的作用是，当遇到内核线程阻塞的时候，M可以放弃P，这样，其他的G就可以被调度到其他M上，持续接力执行。\n类似这样\n所以，同样配置的机器，Golang的效率就会成倍增长，并且可以迅速切换goroutine。所以这就是Go为什么比其他语言快的原因，这也是Go最核心的东西，说真的，Go这个玩意儿，比Python那套好使多了，不过不同任务不可同日而语，继续一波。\nGo的调度器 上面我讲到了调度器，什么是调度器？\n顾名思义，就是调度用的，相当于交通警察，看哪儿阻塞了就给你安排到不赌的路上去，这么想是不是就整明白了，但是Go里面的调度器没那么简单，它的功能相对来说复杂一点。例如空闲的M列表，空闲的P列表，需要运行的G列表等等，都属于调度器的管理部分。\n那么调度器是怎么把上面说的G，M，P串联起来的呢？这个翻了一大堆资料，总算弄明白了一点东西，在这里大概讲一下吧。\n首先，调度器调度的主要对象就是M，P，G的实例，每个M在运行的时候都会执行调度任务，看过黑社会吗，调度器就是选老大时候的邓伯那帮人，选阿乐还是选大D都是他们协调说了算的。\n调度器调度了个什么？寂寞吗，看看图，大概就是这样\n调度器是有自己的数据结构的\ntype schedt struct { goidgen uint64 lastpoll uint64 lock mutex midle muintptr // idle m\u0026#39;s waiting for work nmidle int32 // 当前等待工作的空闲 m 计数 nmidlelocked int32 // 当前等待工作的被 lock 的 m 计数 mnext int64 // 当前预缴创建的 m 数，并且该值会作为下一个创建的 m 的 ID maxmcount int32 // 允许创建的最大的 m 数量 ... pidle puintptr // 空闲 p\u0026#39;s ... // 全局的可运行 g 队列 runqhead guintptr runqtail guintptr runqsize int32 // dead G 的全局缓存 gflock mutex gfreeStack *g gfreeNoStack *g ngfree int32 // sudog 结构的集中缓存 sudoglock mutex sudogcache *sudog // 不同大小的可用的 defer struct 的集中缓存池 deferlock mutex deferpool [5]*_defer ...忽略一些没啥必要的参数 gcwaiting uint32 // 是否要因为任务停止调度 stopwait int32 //需要停止但仍未停止的数量 stopnote note //停止的通知机制 sysmonwait uint32 //停止调度期间任务是否还在等待 sysmonnote note //sysmonnotewait 的通知机制 procresizetime int64 // 上次修改 gomaxprocs 的纳秒时间 ... } 下面说一下调度具体干了什么\n调度的具体步骤 调度器结构体里面有几个重要参数，我先在这里整出来，后面你们方便看\n调度器的具体流程这里整了一个图，大家凑合着看一下吧。图的来源是https://www.infoq.cn/article/r6wzs7bvq2er9kuelbqb，很感谢他！\n看一下逻辑，首先将其分为四个阶段，绑定，创建，执行，释放。调度在其中的作用体现在了这里\n首先第一个步骤绑定开始，这里也是M启动的过程，首先从空闲的P列表里面拿取一个P，然后绑定在M上，P里面有两个列表去管理G，一个runq是存放当前P中可运行G的一个队列，另外一个gfee是存放空闲G的一个队列，启动M之后，则会等待拿取可执行的G\n第二个步骤，创建G，首先创建完之后，扔一个G到当前绑定P的runq队列当中\n第三个步骤，执行G，M从绑定的P那里的runq队列中拿取一个G进行执行\n第四个步骤，释放G，执行完G之后，将执行完毕的G放入gfee队列，当再次创建G的时候，从gfee列表中获取，这里是一个复用的逻辑，避免频繁创建G占用系统内存。\n所以这里是类似一个自循环的逻辑，执行完G1之后持续执行，当M1繁忙时，自动开启新的M来执行\n多线程下的调度机制（偷取G机制） 前阵子我在开发的时候，有一种场景是下载大文件，一般这种情况就是划块下载，但是下载时间是不可控的，也是未知的，很可能有的下的快，有的下的慢，就会出现有的下载队列已经空了，但是有的依旧还很满这种情况。\n其实在Go里面，调度器在这一步就会寻找可执行的G，这里是它们的具体流程\n如图\n这里用流程图表示就是\n我将这里的大概步骤编写如下\n从本地P的可运行G队列（p.runq）中获取G. 调度器首先会尝试从此处获取G，并且返回一个结果\n从调度器的可运行G（sched.runq）队列获取G。调度器首先会尝试从此处获取G，并且返回一个结果\n从其他P中可运行的G队列中获取G。 在某些条件下，调度器会使用伪随机算法在全局P列表中选取一个P，并且尝试从他们的可运行G队列中盗取（转移）一半的G到本地的P可运行队列中，这里会重复多次盗取动作，成功之后就把盗取的一个G作为结果进行返回。\n这里就完成了盗取机制。\n那么偷取部分是怎么实现的呢？\n偷取部分的源码如下\n// runtime/proc.go // 从其它地方获取G func findrunnable() (gp *g, inheritTime bool) { ...... // 尝试4次从别的P偷 for i := 0; i \u0026lt; 4; i++ { for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() { if sched.gcwaiting != 0 { goto top } stealRunNextG := i \u0026gt; 2 // first look for ready queues with more than 1 g // 在这里开始针对P进行偷取操作 if gp := runqsteal(_p_, allp[enum.position()], stealRunNextG); gp != nil { return gp, false } } } } 转移部分的代码如下\n// runtime/proc.go // 偷取P2一半到本地运行队列，失败则返回nil func runqsteal(_p_, p2 *p, stealRunNextG bool) *g { t := _p_.runqtail n := runqgrab(p2, \u0026amp;_p_.runq, t, stealRunNextG) if n == 0 { return nil } n-- // 返回尾部的一个G gp := _p_.runq[(t+n)%uint32(len(_p_.runq))].ptr() if n == 0 { return gp } h := atomic.Load(\u0026amp;_p_.runqhead) // load-acquire, synchronize with consumers if t-h+n \u0026gt;= uint32(len(_p_.runq)) { throw(\u0026#34;runqsteal: runq overflow\u0026#34;) } atomic.Store(\u0026amp;_p_.runqtail, t+n) // store-release, makes the item available for consumption return gp } // 从P里获取一半的G,放到batch里 func runqgrab(_p_ *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 { for { // 计算一半的数量 h := atomic.Load(\u0026amp;_p_.runqhead) // load-acquire, synchronize with other consumers t := atomic.Load(\u0026amp;_p_.runqtail) // load-acquire, synchronize with the producer n := t - h n = n - n/2 ...... // 将偷到的任务转移到本地P队列里 for i := uint32(0); i \u0026lt; n; i++ { g := _p_.runq[(h+i)%uint32(len(_p_.runq))] batch[(batchHead+i)%uint32(len(batch))] = g } if atomic.Cas(\u0026amp;_p_.runqhead, h, h+n) { // cas-release, commits consume return n } } } 第三步，我说到\u0026quot;满足一个条件，才可以偷取\u0026quot;，这个条件其实定义的稍微复杂一些，分为两种\n1. 除了本地的P外，其他有不为空的P 2. M一直没有找到G来运行，也就是前两步一直没有找到G，这里的术语被称为“自旋状态” 当满足上述条件的时候，才可以开启偷取机制。\n总之，调度器会权力查找可执行的G，它会调用多方资源来满足当前M，也就是我刚描述的，下载任务有快慢，会占用多余资源，但是调度器解决了闲置的问题，充分发挥了资源优势，这，相当牛逼了。\nGC机制 在Golang里面，垃圾回收是基于CMS算法的，CMS算法我在这里简单描述一下吧\n洋文叫：Concurrent Low Pause Collector，jvm也是这套算法，玩java的一眼就明白吧。\n要说这套算法，Golang也用它，说明人家是经过考验的\nGolang这套CMS算法分为三种执行模式\ngcbackgroundMode 并发垃圾收集/清理 gcforceMode 串行垃圾收集，并发垃圾清理 gcforceBlockMode 串行垃圾收集，串行垃圾清理 一般涉及到并发，调度器部分会自动GC，都是采用了gcbackgroundMode模式，首先会检查Go 程序的内存用量，检测增量过大的时候才会来一发GC。\n一般在Golang当中，我们可以通过环境变量GODEBUG控制GC，一般修改\n// 转为gcforceMode 串行垃圾收集，并发垃圾清理 gcstoptheworld=1 // 转为gcforceBlockMode 串行垃圾收集，串行垃圾清理 gcstoptheworld=2 一般GC的触发，是在Go程序分配的内存翻倍增长时被触发的。如果想要手动GC，可以调用\nruntime.GC() 进行一次手动GC\n串行的GC触发方式为\nruntime.freeOsMemory() 如果手动调用GC，将不会检测原有Go程序的内存使用量，是为强制GC。\n隐藏的特殊成员-g0 在启动Go程序的时候，有一个特殊的隐藏G，叫做g0。\n这里的G，不是Go程序代码里面的那个G，是一开始，初始化流程中自动分配给M的g，这个g0和上面那个GC对应起来了，它负责的就是监控内存，垃圾回收，执行调度。\n一般由Go代码生成的G，称为用户G，而g0，则被称为系统G，一个系统一个用户，谁权限大是不是显而易见了。\n每个M都会生成一个g0，Go运行的时候会进行切换，g0是不会被阻塞的，也不会被垃圾回收监控扫描。\n所以g0，想象成一个守护神，伴随M，和M同生公死，相当于皇帝身边的大太监一样。\n结尾 这篇文章写完，我是真的彻底搞懂了调度器原理和Golang并发核心机制，其实学习最好的方法就是写博客，记笔记，边写边记。其实慢慢来，都会有好的结果，已经到了年底了，今年大家都过的好嘛？或许今年过的很苦，但今天我们可乐。\nend。\n","permalink":"https://yemilice.com/posts/golang%E7%9A%84%E5%B9%B6%E5%8F%91%E6%9C%BA%E5%88%B6%E6%8E%A2%E7%A9%B6/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e虽然上两篇文章讲了几个并发示例和Goroutine的使用，但是我仔细看了一下，还是不够清楚，这次这篇文章将会是一篇纯理论的文章，主要讲一下Go的并发机制的原理还有Goroutine还有channel。\u003c/p\u003e","title":"Golang的并发机制探究"},{"content":"前言 这几天的攻关都是在Golang并发上面，也算是有了一些收获，把一些基础的并发场景和并发模式都过了一遍，觉得自己又提升了一层楼，其实我发觉，在我的开发生涯中，总是纠结实现，却不是去看最底层的东西，任何东西，都是由简到难，或者就是从底层到核心，无论是Python还是Golang，或者是我正在狂刷算法的Java，我都是去纠结实现，但是不会钻研核心，所以我写代码都是面向搜素引擎编程。所以，也是时候该改变一些东西。\n决意想要改变 心情就像离弦的箭 收不回的思绪每天在反复缠绵 并发的超时处理 如果我们在开发一个Web框架，势必在运行一个goroutine的时候，会出现运行时间过长的问题，一般这种情况，如果不进行超时控制，到最后的结果就是阻塞在那里，迟迟不返回，这样肯定是不行的，所以我们要对并发进行一个超时限制，说人话就是，加一个timeout之后自动退出或者报错。\n首先这里有个新东西，或者是新包，叫做\u0026quot;Context\u0026quot;，这个我的博客里面的久文章也写过了，在这不多废话。你不会还想让我介绍一下它吧！\nContext的几个调用方法 寻思了一下，如果不介绍，很可能你们不知道我在说什么，还是讲几个基础用法吧，其实这个还是比较简单的，Golang的语法比起Rust简单多了。\nContext的接口如下\ntype Context interface { Deadline() (deadline time.Time, ok bool) Done() \u0026lt;-chan struct{} Err() error Value(key interface{}) interface{} } 其中有四个方法\nDeadline返回绑定当前context的任务被取消的截止时间；如果没有设定期限，将返回ok == false。 Done 当绑定当前context的任务被取消时，将返回一个关闭的channel；如果当前context不会被取消，将返回nil。 Err 如果Done返回的channel没有关闭，将返回nil;如果Done返回的channel已经关闭，将返回非空的值表示任务结束的原因。如果是context被取消，Err将返回Canceled；如果是context超时，Err将返回DeadlineExceeded。 Value 返回context存储的键值对中当前key对应的值，如果没有对应的key,则返回nil。 简单的超时取消模型 咱们现在来实现一个超时的例子，其实这个直接调用context的timeout逻辑就可以了，有一张图可以明显说清楚超时处理的逻辑\nctx, cancel := context.WithTimeout(context.TODO(), time.Second*2) 看一下context.WithTimeout的源码\nfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } 这个家伙传入了一个时间，并且调用了withDeadline函数，看下它的源码\nfunc WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok \u0026amp;\u0026amp; cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := \u0026amp;timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } // 监听parent的取消，或者向parent注册自身 propagateCancel(parent, c) dur := time.Until(d) if dur \u0026lt;= 0 { // 已经过期 c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } } 这里输出了两个值，一个是ctx，一个是cancel\n你可以这么理解，ctx就是信号，cancel是阀门，ctx的信号结束时，关闭阀门（关闭goroutine）\n写个简单的超时退出模型\n// 定义一个超时时间/开始时间/信号 var ( Timeout = 1 * time.Second starttime = time.Now() signal = make(chan bool) ) // 模拟一个执行时间很长的任务 func do_something_work_1(ctx context.Context, url string) string { // 执行任务 go func() { // 假装阻塞在这里 time.Sleep(time.Second * 10) fmt.Println(url) // 如果执行完，singal为true signal \u0026lt;- true }() select { // 如果接受到关闭信号（超时） case \u0026lt;-ctx.Done(): fmt.Println(\u0026#34;tomeout..........\u0026#34;) fmt.Printf(\u0026#34;%.2fs - workers(%s) killed\\n\u0026#34;, time.Since(starttime).Seconds(), url) // 杀掉 return \u0026#34;tomeout........\u0026#34; case \u0026lt;-signal: // 如果正常关闭，没超时 fmt.Println(\u0026#34;success\u0026#34;) // 正常输出，正常杀掉 return \u0026#34;success\u0026#34; } return \u0026#34;success\u0026#34; } func main() { // 定义一个Timeout，超时时间为1s ctxt, cancel := context.WithTimeout(context.Background(), Timeout) defer cancel() // 开始执行goroutine go do_something_work_1(ctxt, \u0026#34;hallo\u0026#34;) //假装在做别的任务 time.Sleep(time.Second * 5) // 结束主程 fmt.Println(\u0026#34;END\u0026#34;) } 这里的输出结果是\ntomeout.......... 1.00s - workers(hallo) killed END 解析一下这个框架吧，这个框架实现了一个简单的超时功能.\n首先，我定义了一个假设执行时间很长的task，利用context，我给这个task定义了一个超时时间，并且通过select去监控它，当我监控到ctx有输出的时候，那么我就认为task超时，则直接输出超时并且杀死task，如果task直接run到底，那么会有一个信号signal被赋值，select接受到signal被赋值之后，将会认为任务执行成功，输出成功并且杀死task，大概的逻辑就是这样。\n并发的取消和退出 并发的退出，其实又回到了上面的一个例子，刚才我演示了一个叫做**cancel()**的东西，这个东西就相当于是杀死goroutine的侩子手。来瞅瞅这个东西到底是啥\n首先又回到刚那个逻辑\nctx, cancel := context.WithTimeout(context.TODO(), time.Second*2) 它输出了一个ctx，一个cancel，我们看下cancel到底是个什么东西\n在源码里面，它是个\ntype CancelFunc func() 是不是很蒙蔽，这具体是个啥呢，官方文档里面说了\nA CancelFunc tells an operation to abandon its work. A CancelFunc does not wait for the work to stop. A CancelFunc may be called by multiple goroutines simultaneously. After the first call, subsequent calls to a CancelFunc do nothing. 大意就是这个参数是放弃操作用的，并不等待工作停止，多个任务可调用多个cancel，第一个任务调用cancel之后，其他调用的操作不起作用。 还是蒙蔽阿，瞅一眼其他调用代码，继续追踪，最后在withdeadline找到了\nfunc (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic(\u0026#34;context: internal error: missing cancel error\u0026#34;) } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err if c.done == nil { c.done = closedchan } else { close(c.done) } for child := range c.children { // NOTE: acquiring the child\u0026#39;s lock while holding parent\u0026#39;s lock. child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } } 这部分就相对来说清楚一些了，这里主要是用了一个互斥锁问mutex.lock，看起来是访问共享内存，保护和协调内存访问的，首先先对ctx进行加锁，然后去遍历子goroutine，执行close，没有child之后再进行锁释放，这里的代码真的值得一看。\n并发的退出（context代码）\n// 定义一个int值，这个值没啥意义，单纯就是做了一个传输，看看就得 var c = make(chan int) func do_something_work(ctx context.Context) string { i := 0 for { select { // 接收到退出信号 case \u0026lt;-ctx.Done(): fmt.Println(\u0026#34;Task Exit\u0026#34;) return \u0026#34;Task Exit\u0026#34; // 做点事儿 case c \u0026lt;- i * i: i++ } } return \u0026#34;Task Success\u0026#34; } func main() { ctx, cancel := context.WithCancel(context.Background()) // 开始干活儿 go do_something_work(ctx) for i := 0; i \u0026lt; 5; i++ { fmt.Println(\u0026#34;Next square is\u0026#34;, \u0026lt;-c) } // 杀掉任务 cancel() // 假装还在干别的活儿 time.Sleep(time.Second * 3) fmt.Println(\u0026#34;END\u0026#34;) } 再来一个其他模式的代码，还有一种是通过chan管道传递，其实实现的逻辑也差不多，这里也贴出来，然后给予讲解\nchan类型退出框架\nfunc do_somethings(done chan bool, url string) { go func() { for { select { // 接受到信号关闭 case \u0026lt;-done: fmt.Println(\u0026#34;退出\u0026#34;) return default: fmt.Println(url + \u0026#34; \u0026#34; + \u0026#34;执行中\u0026#34;) time.Sleep(1 * time.Second) } } }() } func main() { done := make(chan bool) // 假装干个活儿 do_somethings(done, \u0026#34;hello\u0026#34;) // 假装正在干别的活儿 time.Sleep(3 * time.Second) // 关闭 close(done) } 这里首先定义了一个chan，通过close chan来进行协程的退出，还是老样子，每次一定要有一个select去监控chan，当执行close时，chan会有输出，当获取到chan输出时，则结束杀死任务。\ngoroutine的心跳模式 有些时候，我们需要一个后台常驻任务，去做一些有的没的，但是有些时候不确定goroutine是否还活着，所以你需要每隔一段时间通知一下，报告情况，虽然存在静默状态，但是会隔固定的时间进行一次通知，这里对并发代码很有用，避免了异常挂住或者zombie的问题。\n一般我写心跳代码都是后台常驻的服务，比如监控服务，比如时不时跳出来的告警服务，比如前阵子我写了个网络服务，一直就后台挂着等着别人来访问，有些时候不确定啥时候来人访问，就写了个心跳代码，所以我感觉并发里面，心跳还是很有必要的，特别是分布式的系统。\n首先看图，心跳在Golang里怎么表现的\n分析下心跳的实现方法\n我们建立一个空的channel，设定时间去关闭它 按照时间间隔定时向空的channel发送一个值，类似定时通知机制，并且每次都能读到channel的内容 func dowork(done \u0026lt;-chan interface{}, pulseInterval time.Duration) (\u0026lt;-chan interface{}, \u0026lt;-chan time.Time) { // 设定心跳通道 heartbeat := make(chan interface{}) //1 results := make(chan time.Time) go func() { defer close(heartbeat) defer close(results) // 心跳间隔 pulse := time.Tick(pulseInterval) //2 // 工作间隔 workGen := time.Tick(2 * pulseInterval) //3 // 发送心跳 sendPulse := func() { select { case heartbeat \u0026lt;- struct{}{}: default: //4 } } // 发送结果 sendResult := func(r time.Time) { for { select { case \u0026lt;-done: return case \u0026lt;-pulse: //5 sendPulse() case results \u0026lt;- r: return } } } // 循环，如果关闭就renturn，获取pulse就续租 for { select { case \u0026lt;-done: return case \u0026lt;-pulse: //5 sendPulse() case r := \u0026lt;-workGen: sendResult(r) } } }() return heartbeat, results } func main() { done := make(chan interface{}) time.AfterFunc(10*time.Second, func() { close(done) }) const timeout = 2 * time.Second heartbeat, results := dowork(done, timeout/2) for { select { case _, ok := \u0026lt;-heartbeat: if ok == false { return } fmt.Println(\u0026#34;pulse\u0026#34;) case r, ok := \u0026lt;-results: if ok == false { return } fmt.Printf(\u0026#34;results %v\\n\u0026#34;, r) // 没有收到心跳 case \u0026lt;-time.After(timeout): fmt.Println(\u0026#34;worker goroutine is not healthy!\u0026#34;) return } } } 峰值限制（防止过大的并发请求数） 又是抽象的一B的东西，说人话就是，把系统的稳定和平衡性控制在可控范围内。举个简单的例子，如果你是一个动漫网站站长，你每天只允许100个人访问你的网站看动漫，那这个100就是你的限制，就是访问限制，如果第101个人想要看动漫，就只能阻塞在外面继续等待，等100个人中的其中几个人退出才可以继续进去。\n这里一般的处理方法是令牌算法\n令牌算法的解释如下\n假设要使用资源，你必须拥有资源的访问令牌。没有令牌，请求会被拒绝。想象这些令牌存储在等待被检索以供使用的桶中。该桶的深度为d，表示它一次可以容纳d个访问令牌。 现在，每次你需要访问资源时，你都会进入存储桶并删除令牌。如果你的存储桶包含五个令牌，那么您可以访问五次资源。在第六次访问时，没有访问令牌可用，那么必须将请求加入队列，直到令牌变为可用，或拒绝请求。 写个代码你看看\nbar24x7 := make(Bar, 10) // 此酒吧只能同时招待10个顾客 for customerId := 0; ; customerId++ { time.Sleep(time.Second) consumer := Consumer{customerId} select { case bar24x7 \u0026lt;- consumer: // 试图进入此酒吧 go bar24x7.ServeConsumer(consumer) default: log.Print(\u0026#34;顾客#\u0026#34;, customerId, \u0026#34;不愿等待而离去\u0026#34;) } } 这个其实相对来说可能比较简单，定义一个队列就可以了，但是这不是最好的办法\n速率限制（设定某一时刻的并发请求数） 和上面那个兄弟的机制没啥两样，其实一个是瞬时请求，一个是总请求，说人话就是，上面那个管全局，下面这个管某一时刻，比如在选课的时候，瞬间就有一大堆人过来抢课，这不把服务器撑爆了。所以，速率限制，常用来限制吞吐和确保在一段时间内的资源使用不会超标。\n写个代码你看看\ntype Request interface{} func handle(r Request) { fmt.Println(r.(int)) } const RateLimitPeriod = time.Minute const RateLimit = 10 // 任何一分钟内最多处理10个请求 func handleRequests(requests \u0026lt;-chan Request) { quotas := make(chan time.Time, RateLimit) go func() { tick := time.NewTicker(RateLimitPeriod / RateLimit) defer tick.Stop() for t := range tick.C { select { case quotas \u0026lt;- t: default: } } }() for r := range requests { \u0026lt;-quotas go handle(r) } } func main() { requests := make(chan Request) go handleRequests(requests) // time.Sleep(time.Minute) for i := 0; ; i++ { requests \u0026lt;- i } } 并发最快回应机制 有时候，一份数据可能同时从多个数据源获取。这些数据源将返回相同的数据。因为各种因素，这些数据源的回应速度参差不一，甚至某个特定数据源的多次回应速度之间也可能相差很大。同时从多个数据源获取一份相同的数据可以有效保障低延迟。我们只需采用最快的回应并舍弃其它较慢回应.\n写个代码你看看\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; ) func source(c chan\u0026lt;- int32) { ra, rb := rand.Int31(), rand.Intn(3) + 1 // 睡眠1秒/2秒/3秒 time.Sleep(time.Duration(rb) * time.Second) c \u0026lt;- ra } func main() { rand.Seed(time.Now().UnixNano()) startTime := time.Now() c := make(chan int32, 5) // 必须用一个缓冲通道 for i := 0; i \u0026lt; cap(c); i++ { go source(c) } rnd := \u0026lt;- c // 只有第一个回应被使用了 fmt.Println(time.Since(startTime)) fmt.Println(rnd) } 差不多就是这样，这里我偷懒了一下，借用了他人的代码，他的环境是，这篇文章给了我很大的启发，感恩！\n通道用例大全\n结尾 细细看了一下，写了不少了，但是其中有一两个东西我也没整的太明白，比如goroutine的心跳之类的，这个东西实在是用的不多，所以我感觉我还是，嗯，需要持续性进步才能解决问题，我写的不那么好，如果有错，请老铁们指出来，感恩！\n予你开心叫我宝宝 予你异见上我镣铐 小人以为我拿它没办法 光脚不惧穿鞋我又有何怕 check~ ","permalink":"https://yemilice.com/posts/%E5%AE%8C%E5%85%A8%E7%90%86%E8%A7%A3golang%E5%B9%B6%E5%8F%91%E6%A8%A1%E5%BC%8F-2/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这几天的攻关都是在Golang并发上面，也算是有了一些收获，把一些基础的并发场景和并发模式都过了一遍，觉得自己又提升了一层楼，其实我发觉，在我的开发生涯中，总是纠结实现，却不是去看最底层的东西，任何东西，都是由简到难，或者就是从底层到核心，无论是Python还是Golang，或者是我正在狂刷算法的Java，我都是去纠结实现，但是不会钻研核心，所以我写代码都是面向搜素引擎编程。所以，也是时候该改变一些东西。\u003c/p\u003e","title":"完全理解Golang并发模式(2)"},{"content":"前言 其实我写Golang有段时间了，平常写一些业务代码，也涉及不到什么高端东西，Golang的核心其实就是在于goroutine这套东西，在处理高并发任务的时候非常强势，甩Python一个车位，所以，写下这篇博客，作为笔记，记录Golang核心的并发机制，并且给出一些自己写的代码作为示例，本次博客大部分代码为自己手写，参考了《Golang并发之道》，《Go语言圣经》这两本书，这两本书是我的进阶之路上的灯塔，感谢这两本书的译者，也希望大家去看看这两本书。\nGolang在并发处理上有什么优势 一个是方便，一个是随用随写。\nGolang的特性goroutine是Go的基础语法，一般调用的方法十分简单\ngo func() 这时的goroutine是一个并发的函数，说起来非常抽象，举个例子，你现在想要执行一个打招呼的逻辑\nfunc Work(){ fmt.Println(\u0026#34;im working!\u0026#34;) } func main() { go Work() } 一个基础打招呼的逻辑就形成了。同理，你可以干多个事儿，类似\nfunc Work(){ fmt.Println(\u0026#34;im working!\u0026#34;) } func Exit(){ fmt.Println(\u0026#34;im go home!\u0026#34;) } func main() { go Work() go Exit() } 这些都是goroutine的好处，就是随便写随便用，混用混写。\nGolang并发模型的标准 Go遵循一种 fork-join的并发模型标准/规则\nfork的意思是程序中的任意一点，goroutine和主程序一起运行，join的意思他们最终合并在一起，用中国话来说叫殊途同归，拿上面那个打招呼的例子详细说一下。\n... func main() { go Work() go Exit() fmt.Println(\u0026#34;END\u0026#34;) } 如果你自己跑一下这个程序，就有可能发现，END的输出，是不会等待你的work和exit完成的，很大概率几乎这两个goroutine都没执行，说白了就是，主程跑起来，跑完了，你子程，我才不管你跑了没呢！像不像个渣男，只顾着自己爽，出来了就完了。\n画个图解释一下\n这里其实是没有join的，这么想一下吧，大家都坐过高铁，高铁上人来人往，把高铁想成主函数main，我们每个人就是goroutine，我们要去的终点是不一样的，如果有人在中途下车上厕所，没上来，不通知高铁一声，高铁就跑了，就不会管你是不是到终点，所以这个join，就类似通知，或者是链接主函数的一个点。现在改写一下这个程序。\n利用WaitGroup来改写一下，对主程进行一个等待和通知的操作\nvar wg sync.WaitGroup Work := func() { defer wg.Done() fmt.Println(\u0026#34;im working!\u0026#34;) } Exit := func() { defer wg.Done() fmt.Println(\u0026#34;im go home!\u0026#34;) } wg.Add(2) go Work() go Exit() wg.Wait() fmt.Println(\u0026#34;END\u0026#34;) 这里写的稍微粗糙了一点，其实很简单，利用了waitgroup建立一个连接点，这里其实就会等待work和exit完全执行完毕再进行END的输出。可以总结成一个图\n总结一下，Go的并发模型，就是无论你goroutine怎么疯，怎么耗时，怎么难搞，都要通过join逻辑通知主程去做一个等待/通知，从而实现并发。\n这个东西应该才是Golang并发模式的核心吧，以后编写的代码，也都是遵循这种框架。\nGolang并发的几个基础用例解析随想 想用Golang实现并发模型，一般有几个固定的组件，例如waitgroup, context, channel等，大家都会根据自己项目的需求选择不一样的并发组件进行开发，我记得我以前针对这三种都写过对应的博客，分别是\ncontext随想 Watigroup随想 Channel随想 这三篇Blog都已经大概说了最常见的几种并发模型，我这几天查了一点资料，看了一下Golang并发的几个用例，发觉遗漏了一些重要部分，这里还是继续更新一下吧。\n我会将基础的使用场景和用例代码贴出，并且加上我自己的解析，如果有错误，请给予我指点，感谢！\n如何处理并发循环问题？ 有时候我们可能会面临这种需求，一次性访问多个网页，或者一次循环切片中多个数据，类似这样\nfunc do_something(url string) { fmt.Println(url) time.Sleep(time.Second * 3) } func main() { url_lists := []string{\u0026#34;baid.com\u0026#34;, \u0026#34;wangyi.com\u0026#34;} for _, url := range url_lists { do_something(url) } fmt.Println(\u0026#34;END\u0026#34;) } 这时的循环模式是单个循环，一循到底部，这里就是一个个循环下去，效率非常低那种。\n我们现在把它改成并发循环\n改写我们的并发循环代码，实现基础并发循环 其实很简单，改动一个地方就行\nfunc do_something(url string) { fmt.Println(url) time.Sleep(time.Second * 3) } func main() { url_lists := []string{\u0026#34;baid.com\u0026#34;, \u0026#34;wangyi.com\u0026#34;} for _, url := range url_lists { go do_something(url) } fmt.Println(\u0026#34;END\u0026#34;) } 在这里，你会发现压根没打印就直接输出了END，看到了吧，问题来了，你没有遵循fork-join的并发逻辑。\n你是不是想加个Sleep去等待它完成，其实你想的没错，但是Sleep不是一个join，它只是一个计时器而已。\n赶紧，上一个WaitGroup，你也可以用chan，这都是一样的。\nwaitgroup写法\nvar wg sync.WaitGroup func do_something(url string) { defer wg.Done() fmt.Println(url) time.Sleep(time.Second * 3) } func main() { url_lists := []string{\u0026#34;baid.com\u0026#34;, \u0026#34;wangyi.com\u0026#34;} for _, url := range url_lists { wg.Add(1) go do_something(url) } wg.Wait() fmt.Println(\u0026#34;END\u0026#34;) } chan写法\nfunc do_somethings(url string) { fmt.Println(url) time.Sleep(time.Second * 2) } func main() { ch := make(chan struct{}) ips := []string{\u0026#34;string1\u0026#34;, \u0026#34;string2\u0026#34;} for _, ip := range ips { go func(ip string) { do_somethings(ip) ch \u0026lt;- struct{}{} }(ip) } for range ips { \u0026lt;-ch } } 并发循环中Wg的控制 上面的代码里，有个函数叫做\nwg.Add(1) 这兄弟是干嘛的呢，它其实就是增加一个计数器，你可以认为他就是增加一个子线程\nwg.Done() 这个兄弟是减去一个计数器，意思就是销毁，干掉\nwg.Wait() 这个兄弟是阻塞主程的，直到计数器为0的时候，再继续往下。\n并发循环中输出错误 有些时候在并发循环中遇到了错误，不能够忽略，要求即刻输出，改写一下我们刚才的代码。\nchan写法\nfunc do_somethings(url string) (err error) { fmt.Println(url) if url == \u0026#34;string2\u0026#34; { return fmt.Errorf(\u0026#34;bad............\u0026#34;) } time.Sleep(time.Second * 2) return nil } func main() { ips := []string{\u0026#34;string1\u0026#34;, \u0026#34;string2\u0026#34;} errors := make(chan error) for _, ip := range ips { go func(ip string) { err := do_somethings(ip) errors \u0026lt;- err }(ip) } for range ips { if err := \u0026lt;-errors; err != nil { fmt.Println(err) return } } } 其实chan就是在各个goroutine中做传递的管道，相当于感情的通讯员~\n控制并发goroutine的数量 举个简单的例子，如果我们的机器太破，咱不可能开无限个goroutine去做事儿吧，有些时候我们需要对并发的数量进行控制，比如说给定一个最大并发量为6，一次性只能并发6个，也就是一次性只能有6个消费者同时进行消费，那么咱们该怎么实现这个操作呢？\n一般人都会说，咱们来个池子，随用随取。但是Golang这么搞就没必要了，显得很没有Go style。\n首先要明确我们的需求，我们的最终目标是，限制并发数，避免过度并发。\n说一下我的解决方案\n首先定义一个有长度限定的channel\nvar jobs = make(chan string, 6) 这里将会存储6个work\n再写一个消费逻辑\nfor url := range jobs { do_somethingss(url) } 这时就大概完成了基础的消费逻辑，那么，我们该如何往这个空channel里传数据呢？\nfor _, url := range worklist { jobs \u0026lt;- url fmt.Println(\u0026#34;add\u0026#34;, url) } 现在我们有了消费者，也有了生产者，那么我们把代码封装一下\n详细代码如下\nfunc do_somethingss(url string) (err error) { fmt.Println(\u0026#34;hello:\u0026#34; + url) if url == \u0026#34;string2\u0026#34; { time.Sleep(time.Second * 10) fmt.Println(\u0026#34;all die\u0026#34;) return nil } time.Sleep(time.Second * 2) return nil } func main() { worklist := []string{\u0026#34;string1\u0026#34;, \u0026#34;string2\u0026#34;, \u0026#34;string3\u0026#34;, \u0026#34;string4\u0026#34;, \u0026#34;string5\u0026#34;, \u0026#34;string6\u0026#34;, \u0026#34;string7\u0026#34;} wg := sync.WaitGroup{} var jobs = make(chan string, 6) //最大6个消费者 for i := 0; i \u0026lt; 6; i++ { go func() { // 消费数据 for url := range jobs { do_somethingss(url) //消费完通知，移除一个已经消费的值 wg.Done() } }() } // 将worklist里的数据上传到jobs当中 for _, url := range worklist { jobs \u0026lt;- url wg.Add(1) fmt.Println(\u0026#34;add\u0026#34;, url) } //增加等待，避免退出 wg.Wait() } 其实很简单，我们捋一下逻辑\n创建一个有长度限制的空channel 空channel是一个消费者队列 创建一个生产者逻辑，生产者就是往channel传递数据 遍历channel进行消费 增加wait等待，避免没消费完自动退出 结尾 这一节给出了几个基础并发模型，讲了一下Golang并发的哲学思维，这里其实是做了一个笔记，可以持续性发散思维解决问题。下一节我会实现一个分布式并发框架，都会用到这里面所有的东西。over。\n并发循环这套东西，说多不多，说少不少，重头戏Context我放到下一节去写了，因为我认为那个还是需要花点功夫的，最近是年底了，学习还是不能停下来，向前吧老铁们！\n","permalink":"https://yemilice.com/posts/%E5%AE%8C%E5%85%A8%E7%90%86%E8%A7%A3golang%E5%B9%B6%E5%8F%91%E6%A8%A1%E5%BC%8F-1/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e其实我写Golang有段时间了，平常写一些业务代码，也涉及不到什么高端东西，Golang的核心其实就是在于\u003cstrong\u003egoroutine\u003c/strong\u003e这套东西，在处理高并发任务的时候非常强势，甩Python一个车位，所以，写下这篇博客，作为笔记，记录Golang核心的并发机制，并且给出一些自己写的代码作为示例，本次博客大部分代码为自己手写，参考了《Golang并发之道》，《Go语言圣经》这两本书，这两本书是我的进阶之路上的灯塔，感谢这两本书的译者，也希望大家去看看这两本书。\u003c/p\u003e","title":"完全理解Golang并发模式(1)"},{"content":"前言 本月20号，我完成了我第三个独立开发项目，并且通过测试顺利交付了。\n首先先总结一下我这半年都干了什么吧。\n回想一下，从去年开始，我就被安排独立开发，从无到有，从惶恐到上手，从不知所措到游刃有余，其实还是度过了一个比较漫长的阶段，还好挺过来了。\n总计开发三个项目，基本从架构到设计，后端的框架/代码都是我开发的，代码量涉及10W+行，\n开发语言横跨Python/Golang/Java,\n涉及框架Flask，Django，Gin等等，\n后端数据库涉及Mysql，ElasticSearch，ETCD。\n我会通过这三个项目，总结一下自己踩坑的问题和自己的心得，做一个记录，也是对自己这半年痛苦的一个回望吧，希望明年会更好。\n写出来才知道自己究竟都干了些什么，不知道这阵996的日子是怎么熬过来的，写一句歌词吧。\n不知道你会不会想起未来的我 在照亮你 在灯下想到现在的我 我知道这封信是给你 给我的Kong 2020年秋 get it 第一个项目-初试牛刀 其实怎么说呢，第一个项目算是野蛮成长的典型，我的习惯就是拿到需求的时候就要分析一下，首先还是老样子，分析需求，这边由于一些隐私相关，我会将项目的核心模糊化处理，请见谅.\n第一个项目-需求分析 你知道，在国内的开发环境里，很多时候都是老板/大领导拍脑袋提一个模棱两可的一句话需求，例如：我们要做一个xxx，大概有什么功能。\n这样就非常坑爹，但是如果咱们遇到了，也要咬牙给它扛了对吧，这时候就需要对接产品，要找产品去详细了解，到底是一个什么样的功能，我们到底该怎么做，这里就衍生出来第一个问题，如何正确去理解需求。\n其实正确理解需求，这句话说起来太容易了，但是做起来就是很难。\n如果有一个低水平PM（产品经理），那真的是个天大的灾难，我这几次开发都遇到了这种，只会抄竞品的，只画图的，或者画图不说清楚功能干嘛的，强行加一些完成不了的功能的，这些都是开发人员要面对的。\n作为独立开发人员，就算势力单薄，也要弄明白到底需求是个什么样，不能完全的盲从产品，这样会给自己带来很严重的开发问题。包括在后期的架构设计上，也会有很大问题。\n如何正确去理解需求呢？首先要明白功能大致是干嘛的，拿我开发这个项目来说，当时，大老板只说了一句，说：我们要做一个数据同步的系统，支持对象存储-本地文件系统互相传输同步。\n好了，这就是我们的所有信息，我们分析一下，首先\n数据同步的系统，支持对象存储-本地文件系统互相传输同步 思考一下，数据同步，系统，互相传输，对象存储，文件系统。\n首先明白是一个数据同步系统，可以对象存储-文件系统同步传输，反推一下，那当然也可以文件系统-对象存储传输了，文件系统同时也分为nfs，cifs，本地文件系统等，对象存储也分为本地，远端两种，这样系统的初步雏形就出来了，为提高效率，很可能是分布式系统，分布式系统还涉及选主，日志记录等，这些都是要注意的部分。预先去准备查询一下这方面的资料，就能在开发中占到先机。\n开发第一条：明白你在做什么，或者说，明白你要完成的是什么样的功能\n第一个项目-拿到PRD图和设计文档后 PRD图这种东西是产品画的，这东西一般就详细说明了你该干什么，做什么，做出来的东西大概什么样子，这东西就相当于，买房子时候的样板房。给你看看的。\n核心其实是在设计文档上，一般专业点的PM，在设计文档上会细化到每个功能是干嘛的，大概是做什么的，这个非常重要，对于开发人员来说，这个就类似结构图。\n这里要细细的去浏览，一定要精确到每个功能，精准到每个功能具体干嘛，因为这里你稍微不看清楚，未来就是大坑，因为如果你做一个系统，你自己都不了解自己在做什么，那真的没有做的必要了，其实我们需要跳出自己固有的技术思维，不要考虑如何去实现，是不是好实现，要在脑子里有一套完全的大概框架，或者说勾画，明白我们到底在干嘛。\n这里衍生出我另一个不足的地方\n我在拿到PRD和设计文档后，没有仔细去看设计文档，就只过了一遍PRD，然后大概知道我要做什么了就完了，这里我犯了个错，设计文档里面东西更细化，我没有注意设计文档中的细节，开发当中有些功能甚至没注意到，淦。\n第一个项目-需求评审会上提出要求 在你大概明白你要做什么的时候，就会召集开发人员/测试人员/产品人员/相关领导 来进行一次需求讨论，所有人都会过一遍需求，并且讨论需求是否合理。\n将会从测试/开发/产品/领导的多方角度进行讨论，这次会议对你来说非常重要，记住，非常重要，这可能是你唯一一次可以砍需求的部分了，如果开发进行到后期你是无法砍需求的，所有人都不会同意。\n如果前面你进行了铺垫，或者你了解你大概要干嘛了，这次需求会，你的核心要在需求评审上，如果有认为自己可能完成不了的需求，需要及时提出质疑，并且说出理由。\n淦，我第一次就掉坑了，有些东西没认真看，糊里糊涂我就答应下来了，后面真的十分痛苦。有些需求做的太难了，不过还好都hold住了。\n第一个项目-进行业务架构设计 架构这狗东西，说真的，我是被折磨够呛的，说句简单的话，你要考虑架构设计，就先抛开技术层面，首先确定业务架构和逻辑架构，技术的东西可以后面再讨论，但是具体做什么，怎么做，流程是什么样的，需要现在就讨论清楚。\n我的逻辑是，先根据PRD和设计文档，划分出具体的功能模块，然后对功能模块进行业务流程规划，然后把功能模块中的业务进行拆分，最后再把他们结合到一起，这就是业务架构。\n这里比较抽象, 举个例子，功能模块如下\n基于功能模块，业务架构如下\n具体流程如下\n这里画的比较简陋，随便看看就行了\n第一个项目-进行技术框架设计 这里就涉及到技术了，主要就是根据项目进行技术选型，数据库选型，开发框架设计，表设计等等。\n这个不用多说了吧，大家都是开发人员，技术可以通过多测试得到结果，太虚的东西是没有说服力的，数据库选型这里我还提出了好几个测试报告。\n第一个项目-开发时间预估 这部分我吃了大亏，开发时间预估，我感觉真的不好判断，因为不确定开发中到底会出现什么事儿，因为我同时还要维护一个检索服务器，或者修改一些bug，或者出差之类的。。。\n我个人认为，对自己得有点B数，如果是你本身比较熟悉的语言/框架，时间估计还是可以自己估摸，如果是你不确定得框架/不熟悉的语言，那这里你的时间应该要再加一半，总结一个公式，就是\n不熟悉语言的开发时间 = （预估时间）/2 + 预估时间 + 测试时间 + 调试时间 时间要的不够，你就996吧，到时候整不完把身体整坏了，那就非常不值得了。\n第一个项目-项目管理部分 其实第一次开发，我印象比较深的还是项目管理部分，按理说项目管理理应产品来做，但是这次产品非常不专业，所以一切还是我自己扛下来了，终于一个人扛下了所有（狗头\n首先我拉了一个Excel表，按照开发开始-结束时间划拉了相应的表格长度，然后把日期填充上去，上面是计划，下面是今日进度，每天来公司我就写计划，回撤我就写进度，中间因为疫情在家开发我也没断过，找了个大概的图大家康康。\n记录每天的开发进度，并且对功能模块开发进行详细划分开发，上面已经细分了功能模块中的功能，针对每个功能去开发就好了，开发完整个模块进行一次模块调度联调，留出两天时间编写测试代码，大概就是这些。\n第一个项目-中期遇到困难的沟通 项目开发到中期，由于一开始我的预估不足，出现了很严重的技术难题，这时候千万不要闭口不要，及时抛出问题，抛出问题的好处就是能找到一帮人来帮你解决，至少你不是单打独斗，在独立开发当中，我感觉我一直是孤独的，但是抛出问题的时候，大家伙儿还是愿意帮你解决，一起讨论，让我觉得我不是那么孤单，这样的感觉就很好啦。\n第一个项目-项目延期的解决方法 项目延期是很正常的事儿，遇到延期，在预估自己可能无法正常完成的时候，需要及时和产品/管理人员沟通，提出项目无法正常完成的原因，我是因为疫情（淦\n大家会表示理解并且愿意给你时间去开发的，开发的质量，永远高于速度。\n第二个项目-逐渐Dark♂化 第一个项目做了差不多三个月，日夜不眠996下终于整完了，没休息几天，第二个项目就来了，TMD是纯粹不让我休息，我算是明白了。有了第一个项目的摧残，对第二个项目我充满信心\n第二个项目-突发状况的处理 这项目其实一帆风顺，但是，开发到最后，遇到一个很大的事儿，被其他同事不小心给删库了！！！！\n你知道这意味着什么，就意味着你两个月等于白干了，996的结果等于你什么也没做，好家伙，那给我气的\n这次删库说明什么，很重要的就是代码管理和备份，因为疫情的原因，我只有一台树莓派，我没把代码存到本地，一直在公司的服务器开发，这就很危险了，所以未来我写了几个脚本，直接定时备份，本地-远端，多个机器一起备份，这次是不会丢了。。。。\n第二个项目-第三方工具的调研 这次开发调用了大量的第三方工具，例如tika，fscrawler等等（你不用整明白这干嘛的），在第三方工具的调用选型部分，踩了一些坑，这里我详细说一下，如果你想要调用第三方工具，你需要一个什么样的调研报告\n工具的具体介绍 工具在本项目内的工作（完成什么） 工具的基础使用方法 工具的测试报告 这里的测试报告很宽泛，主要就是指工具的性能，包括基础的压测，部署，和项目的耦合程度等等等等。\n第二个项目-项目打包与项目发布 各位老铁的公司一般都要运维吧，可惜我这里没有，作词作曲都是老子自己，岂可修！\n打包这块，如果是rpm，相关的spec你要自己写，并且git拉代码自动编译的东西，你也要自己整，这个就相当麻烦，这么麻烦也不见我司给我涨钱啊，fuck。\n这里如果你们想听，我可以重开一篇详细说。\n第三个项目-完全Dark♂化 我已经逐渐习惯一个人干活了。。。我就是孤独猎手独行侠，每次都把很难得活儿抛给我，我真的无语，没有耕坏的地，只有累死的牛（狗头\n第三个项目-心情控制 我在开发这个项目的时候，已经非常浮躁了，因为我一直没有休息过，我想停下来安心看一下算法，或者是其他高级知识，但是繁重的项目和加班让我没有心情去准备，几个面试也失败了。所以，控制好心情很重要\n一个是释放压力，另一个是项目的进度加快，保证项目时时刻刻在可控的范围内，这个对我来说还是比较重要的。\n做表是个很好的方法，真的，你能时刻看到自己的进度，其实类似日报，不过日报这种强制写的狗屁东西，反而还不如自己的计划可控。\n结尾 写的比较潦草，这是我这近半年时间收获的一些经验和接受的教训，期望可以帮助大家吧。下一篇博客应该会是Python一类的高级特性，请期待吧。\n","permalink":"https://yemilice.com/posts/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%92%8C%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86-%E5%86%99%E4%BA%8E%E7%AC%AC%E4%B8%89%E4%B8%AA%E7%8B%AC%E7%AB%8B%E5%BC%80%E5%8F%91%E9%A1%B9%E7%9B%AE%E4%B9%8B%E5%90%8E/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e本月20号，我完成了我第三个独立开发项目，并且通过测试顺利交付了。\u003c/p\u003e\n\u003cp\u003e首先先总结一下我这半年都干了什么吧。\u003c/p\u003e\n\u003cp\u003e回想一下，从去年开始，我就被安排独立开发，从无到有，从惶恐到上手，从不知所措到游刃有余，其实还是度过了一个比较漫长的阶段，还好挺过来了。\u003c/p\u003e","title":"架构设计和项目管理心得-写于第三个独立开发项目之后"},{"content":"前言 十一在家待着玩了两天COD，突然想起来似乎放假前有一个Elasticsearch的问题需要处理，的确，现在距离开工也只剩下四天了，哎，假期的日子过的飞快，又到时候说拜拜，又要开始社畜的生活啦，想到心里就万分悲痛。\n悲痛完了之后，也的确要想办法去解决问题，所以，今天（10／5），我研究了一下ElasticSearch检索／导入大文件的一些方案，并且输出了一些我个人的建议，这些基本都是实战建议，不会那么偏向理论，所以理论家们，就不要挑我的刺啦，想看理论，咱们回头去写源码解析！多多支持！\n问题描述 现在有个ElasticSearch做为基础的检索服务，里面存储很多文档，这些文档多种多样，有PDF，DOC等等，需要对这些文档进行全文检索，所以将这些文档的内容也导入进ElasticSearch当中，文档的内容可能会很大，导入／查询的时候出现了如下问题\n导入一些比较大的文件的时候ElasticSearch节点直接Down机，出现了内存溢出OOM的问题，如下所示 java.lang.OutOfMemoryError: Java heap space 检索的时候，返回的比较慢，当数据量大于100w条，并且基本都要支持全文检索的时候，翻页／高亮，查询非常慢，经常转很久的菊花出不来，翻页也慢，干嘛都慢。 基础环境分析 首先需要分析下我自己的干活儿环境，对症下药。\nElasticSearch的集群配置\ncluster.name: Es_test node.name: es_node1 node.master: true node.data: true index.number_of_shards: 5 index.number_of_replicas: 1 network.host: 10.0.6.244 http.port: 9200 transport.tcp.port: 9300 discovery.zen.ping.unicast.hosts: [\u0026#34;10.0.6.244\u0026#34;] discovery.zen.minimum_master_nodes: 1 http.cors.enabled: true http.cors.allow-origin: \u0026#34;*\u0026#34; network.bind_host: \u0026#34;::\u0026#34; 然后就没了，算是最基础的ElasticSearch配置了\njvm.options设置为\nxms: 1g xmx: 1g 基础的环境配置就是这样\n优化查询和导入的方法 1. 配置优化 有一句话怎么说的来着，当一切都可以通过加钱来解决的时候，那就不是个问题嘛。\n你看看我那个堆内存配置，只有1个G，而且总共测试的机器内存只有8G，这个根本没办法再加，公司的默认测试机都是8G，除开这个还要跑其他服务，例如存储／web等，我想啊，本来就是拖拉机你还想当跑车开？吃肉的东西你让他吃草能行吗，所以再怎么优化，数据量上来了最终还是要沦落到加钱上好机器的最终结局。\n不过现在只有拼命在这个烂机器的基础上去优化了\n所以，需要修改一波jvm.options配置\nxms: 4g xmx: 4g 这里修改了过后，一般就能解决大部分问题，但如果需要支撑大规模的数据检索和近乎实时的查询任务，这个是非常不靠谱的机器配置，完全是无法支撑业务的，但是领导非要往里面塞这个，还要求优化到极限，我能怎么办。\n言归正传，第一步就是\n调整JVM的配置\n将JVM的配置调整为整个机器内存的一半，比如你的机器是64G内存，你设置xms和xmx就设置成32G，这个是官方推荐的，靠谱！\n2. 减少refresh_interval刷新比率 这个可能抽象一点啊，首先这个refresh_interval是干嘛的呢，这玩意儿说白了就是个刷新器，你每次导入数据到ElasticSearch中不能马上被查到，有个固定刷新间隔，也就是refresh_interval控制的，刷新成功之后，数据才能被马上查到。\nrefresh_interval 如果不设置的话，默认是1s一次，像我们这种导入数据比较频繁的，刷新比率过高，会导致CPU／Memory就像开了氮气加速一样极速前进。。。所以把这东西设置一下，如果不是实时比率要求高的，直接改成30s刷新一次，或者1分钟刷新一次，不要害怕丢数据之类的，当你节点down机，Es自己都保不住了，还管刷新比率？\n修改refresh_interval刷新比率，直接在elasticsearch的配置文件修改\n\u0026#34;refresh_interval\u0026#34;: \u0026#34;30s\u0026#34; 3. 查询缓慢，响应时间长的解决方法 这里我更倾向于问题描述是从ElasticSearch获取数据比较慢，比较慢的愿意可能有哪些呢？\n文档过大，可能某个字段非常长，返回的太慢。 ElasticSearch响应慢，CPU／内存占比过高 网络响应慢 我们针对这些问题一个个去解决，对症下药。\n3.1 文档过大，某个字段大，返回慢 首先看看能不能把过长的字段给拆分出去，拆成另外一张表。如果不可以拆分出去，就在显示的部分想办法。\n首先举个例子，假设我们有个字段叫content，里面存的都是解析出来的PDF，DOC文件的文字，这种一般文字都非常多，但是我们需要进行全文检索并且返回检索到的值，Es查询到值以后就会全部返回，这样就非常慢，我们可以通过一些别的手段解决这个问题。\n例如，我们原本的返回字段是\n{ \u0026#34;name\u0026#34;: \u0026#34;xxx\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;xxx\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;............\u0026#34; } 展示部分，不展示此字段，对此字段，也不进行返回，在ElasticSearch请求返回值的步骤时，强制性不返回Content字段（返回指定字段），减少查询压力，这样就无需等待全部字段返回了，加快查询速度。 { \u0026#34;_source\u0026#34;:{ \u0026#34;includes\u0026#34;:[\u0026#34;name\u0026#34;,\u0026#34;id\u0026#34;], \u0026#34;excludes\u0026#34;:[\u0026#34;desc\u0026#34;] } } { \u0026#34;name\u0026#34;: \u0026#34;xxx\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;xxx\u0026#34; } 检索部分，当发现检索关键字的时候，进行全文检索，配置highlight高亮，返回检索结果时只返回highlight结果，只匹配第一个highlight结果，其他结果直接丢弃。加快返回的速度。 这里我感觉我说的更抽象了，其实说白了，我们需要配置ElasticSearch的Highlight返回，指定content字段才可以高亮，当检索到匹配值的时候，Es会返回一个highlight的切片（列表），无论如何，我们只取第一个highlight结果，其他结果全部丢弃，这样可以加快检索，返回给前端的部分也有了，这样岂不是很棒？\n4. 分页部分优化 ElasticSearch的分页机制有两种，第一种是From + Size的机制，这个相对来说比较简单，还可以进行控制，另一种是Scroll机制，这个就复杂一些，我下面说。\nFrom + Size 它的基础逻辑就是，它首先会确定doc的顺序，进行排序，然后再进行返回，逻辑上来说，它也需要取出所有数据，所以当数据量非常大的时候，从ElasticSearch中取出数据会占用大量CPU／内存，如果跳页过大，例如从第1页瞬间跳到第100页，就会取出第1页到第99页的所有数据，然后排序，然后进行切分，然后再展示第100页数据。。。这个又抽象了，说白了就是，这个分页比较适合一页页往后翻，你要闲的没事儿天天跳来跳去，人家谁Hold住.\nScroll的基础逻辑就是，首先，Scroll会维护一个游标，记录你当前读取的doc位置，而不是取出来做除法切分，这个更适合一次性拿一大堆数据出来，它其实类似一个快照，每次你查询的时候，都会记录你上次查询的位置，下次访问就直接从这个位置／快照开始，免去了拿取所有数据的步骤，有更好的检索效率，CPU／内存使用也不会那么大。\n4.1 限定分页的数量 首先无论如何，我们先限定死，分页的数量，最大1000页，多了不让翻。这是为了保证From + Size机制使用时，跳页数量过大，导致CPU／内存飙升导致服务OOM或者是挂掉，这个可以让前端同学强制限制一波。\n4.2 根据数据量大小，逐步替换From + Size 翻页逻辑 如果数量过大，前面我也说了，你整From + Size肯定是不行，而且你把客户想象成熊孩子，没事儿干就乱点，乱跳页，动不动就CPU／内存暴增，那不就拉垮了嘛。所以快速切换 From + Size 为 Scroll就得了。\n5. 建表（索引）时候的优化 在创建表的时候，需要指定一些字段属性，这样会减少查询和检索的内存消耗或者是，这里给出几个我总结的字段优化方法\n全文检索的时候，指定的字段类型一定得是 text 其他检索的时候，需要检索字段完整值，需要使用 keyword，如果想做模糊匹配，需要使用wildcard或者*检索 时间字段可以使用date，可以自己定义数据格式 不确定数据长度，需要用long进行设置，避免以后长度超出。 建表的时候，需要设置索引的压缩功能，减少存储空间占用，设置的方法就像这样，\u0026ldquo;codec\u0026rdquo;: \u0026ldquo;best_compression\u0026rdquo;。 避免自定义Doc的ID，尽量用人家Es自己生成的，你自己整ID一般都是UUID时间戳，还容易重复也慢。 定义分词器没必要所有字段都用，指定几个需要的字段就行了。 确定不会修改的doc需要设置dynamic，禁止更新。 6. ElasticSearch存储位置的转移 安装好ElasticSearch之后，默认的位置是在系统盘。\n我估摸着数据量一大，没多久你就给系统盘整崩溃了，所以，改换系统盘位置，做好数据迁移是非常有必要的。\n修改配置文件，手动切换存储位置, 这里举个简单例子\npath.data: /media/data/elasticsearch 迁移数据文件,修改文件夹权限\nmv /var/lib/elasticsearch/nodes /media/data/elasticsearch chown -R elasticsearch:elasticsearch * 7. ElasticSearch写入性能优化 7.1 推荐使用bulk批量写入 其实按照我现在的使用场景，发现PDF／DOC有更新／上传到文件系统，就传入到ElasticSearch当中，其实ElasticSearch的批量写入bulk的效率比一条条写效率高多了，这里建议批量写入数据，而不是一条条传入。\n7.2 多线程写入ElasticSearch 多线程并发写可以利用集群的资源，我这里用Golang做了后端，直接走了一波协程 + 线程，这样可以减底层fsync开销，可以减少单个Es节点压力。\n7.3 Translog事务日志的优化 translog是用来恢复数据的。Es用“后写”的套路来加快写入速度 — 写入的索引并没有实时落盘到索引文件，而是先双写到内存和translog文件，\nes存储数据时，先把输出存储在内存中，等到refresh(该时间可以在设置mapping时的setting中设置intavel_refresh=xxx)时间后，才把数据存储到lucene中的segment中，清空内存缓冲区，往磁盘里写入commit point信息，文件系统的page cache(segments) fsync到磁盘，之后把translog旧日志删除掉。\n按照逻辑来说，如果降低translog可以提高效率，但是会降低容灾能力。\n修改配置，不需要每次都刷新。\nindex.translog.durability: async index.translog.sync_interval: 3600s 结尾 这次我主要开发了一个ElasticSearch的检索服务器，主要就是用apache tika去读取对象存储中的PDF／DOC等文件，解析文字，传入ElasticSearch当中，主要是全文检索，这个真的费了点功夫，因为我的服务器，实在是太烂了，只有他娘的8个G，还要跑Ceph存储等等等等，优化起来那个麻烦啊，这就相当于你开拖拉机，要我改车给你改成法拉利的速度，你这不是扯嘛。\n预告一下下一篇blog，TIKA提取对象存储中的PDF／DOC中的文字，有点儿绕是吧，绕就对了！哈哈哈，走你，下次见！\n","permalink":"https://yemilice.com/posts/elasticsearch%E5%A4%A7%E6%95%B0%E6%8D%AE%E9%87%8F%E4%B8%8B%E7%9A%84%E4%BC%98%E5%8C%96%E6%96%B9%E6%B3%95/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e十一在家待着玩了两天COD，突然想起来似乎放假前有一个Elasticsearch的问题需要处理，的确，现在距离开工也只剩下四天了，哎，假期的日子过的飞快，又到时候说拜拜，又要开始社畜的生活啦，想到心里就万分悲痛。\u003c/p\u003e","title":"Elasticsearch大数据量下的优化方法"},{"content":"前言 最近心情一直不是太好，写出来的东西感觉也没有灵感，有些时候做了很多事，但是回想起来感觉自己还是什么都没有做。这是9月份第一次更新blog，也更新一篇相对高级一些的技术吧，有一阵没看Python了，想想还是不要落下了，剧透一下，下一篇文章还是针对Elasticsearch或者是前端框架React的，学习还是不能停下来，最近写歌也有问题，感觉自己什么也写不出来，仿佛失去了灵感，是生活还是时间消磨了我的灵气呢？我不愿意这么想，我会努力的，状态会调整过来的。\n什么是协程？ 首先上一个官方的解释\n协程: 协程，又称微线程，纤程，英文名Coroutine。 协程的作用，是在执行函数A时，可以随时中断，去执行函数B，然后中断继续执行函数A（可以自由切换）。但这一过程并不是函数调用（没有调用语句），这一整个过程看似像多线程，然而协程只有一个线程执行. 你觉得抽象吗，那就让我给你一个完美的解释（狗头）\n大白话解释，协程是个什么，其实就是告诉你，在线程的执行中，可以随时停止某个子程序，然后去执行别的子程序，在指定的时候，继续切回来干活，你可以把子程序认定为是Python中的函数，其实就是，几个兄弟干活，一个兄弟拉跨了，其他兄弟把他从工作岗位上扒拉下来，告诉他，小B崽子滚犊子，一边玩去，一会休息好了你再回来，还不耽误其他人干活，休息好了继续投身工作岗位。\n一般协程在涉及到I/O操作的时候特别好用，你可以把协程理解为轻量级别的线程。\n协程的好处是？ 第一个就是解决I/O问题，什么是I/O问题呐，一般就是通过网络或者存储去访问或者写入数据，一般就是数据库取数据，或者是往数据库里面写数据等等，这都属于I/O操作。 协程说自己解决了I/O问题。其实就是协程由程序自己控制，减少线程切换的开销，不存在写变量的冲突，执行效率高于线程。\n一般来说，由于GIL锁的限制，Python的线程相对拉跨，用了协程，就约等于起飞，至少在互联网，协程还是很重要的。\nPython协程的使用场景 一般都是高并发服务，用我自己的使用场景来说，举个例子\n我现在有个服务，登陆的用户，需要定时更新自己的资料，当初的逻辑就是一个用户去开启一个线程访问，但是不停的开启，关闭线程开销太大了，如果登录用户过多，一次性开好几千个，岂不是很xx，这时候利用协程，一个线程开一大堆协程去处理这事儿，第一是减少了开销，第二是增加了效率。所以在频繁的I/O请求当中，协程是非常可取的。也是可靠的。\nPython协程的基础实现 这里分为Python2和Python3，这里的实现方式分很多种\nPython2的协程 Python2的协程支持不太好，但是兄弟们还是可以实现一下\nPython2实现协程的方法就是 yield + send 和 *Gevent\n首先，Python怎么支持协程呐，是通过Generator实现的，也就是生成器，协程也是生成器的一种，只是遵循指定的规则，这边儿兄弟就不说生成器的逻辑了，这个后面再去研究，今儿只说协程。\n在Python2中，指定一个生成器的方法是使用关键字yield，这里写一个简单的生产者消费者模型来说明协程的使用场景\n首先这是一个普通的生产者消费者模型，看代码，这段代码来自于廖雪峰的网站\ndef consumer(): print(\u0026#34;[CONSUMER] start\u0026#34;) r = \u0026#39;start\u0026#39; while True: n = yield r if not n: print(\u0026#34;n is empty\u0026#34;) continue print(\u0026#34;[CONSUMER] Consumer is consuming %s\u0026#34; % n) r = \u0026#34;200 ok\u0026#34; def producer(c): # 启动generator，send（None）是启动协程的必要环节 start_value = c.send(None) print(start_value) n = 0 #生产 while n \u0026lt; 3: n += 1 print(\u0026#34;[PRODUCER] Producer is producing %d\u0026#34; % n) # 这里就是执行消费者的必要逻辑 r = c.send(n) print(\u0026#39;[PRODUCER] Consumer return: %s\u0026#39; % r) # 关闭generator c.close() # 创建生成器 c = consumer() # 传入generator producer(c) 这里其实很好理解\n第一步，创建一个消费者生成器，生产者producer启动，**c.send(None)**的意思是启动/恢复生成器，这边启动一个生成器，开始生产。 第二步，消费者是一个生成器对象，可以被生产者调用，代码在继续执行，执行到生产者的**c.send(n)**时，发送了一个n值给消费者，消费者获取到值，进行消费操作。 当不再执行生产者，调用close，关闭操作。 这里有两个重要参数\nsend(None) ： 启动\nsend(value) ： 传递参数\n生产者生产消息之后，通过yield直接执行消费者，消费者执行完之后立刻切换生产者，同函数内操作，避免了线程锁，队列等待等，还是比较快的。\n旧的生产者消费者模型，是通过lock来控制队列，在协程当中，producer和consumer相互合作，从头到尾没有用到lock，所以，这才是协程的最佳表现呀。\nPython3的协程 Python3引入了牛逼的async，这时候调用起来更加起飞，这个上一篇我写了个读源码的，其实简单一句话，async首先会整一个事件循环的loop，然后轮询任务，直到最后一个任务结束，这个我上一篇写过一个读源码的，这里也就不多废话了。\n写点代码来表述一下Python3的协程怎么用，这里我直接参考了网上的一个兄弟，这里很感谢他，如果代码是你写的，请联系我，我加上你的署名，感恩！\nimport time import asyncio async def taskIO_1(): print(\u0026#39;开始运行IO任务1...\u0026#39;) await asyncio.sleep(2) # 假设该任务耗时2s print(\u0026#39;IO任务1已完成，耗时2s\u0026#39;) return taskIO_1.__name__ async def taskIO_2(): print(\u0026#39;开始运行IO任务2...\u0026#39;) await asyncio.sleep(3) # 假设该任务耗时3s print(\u0026#39;IO任务2已完成，耗时3s\u0026#39;) return taskIO_2.__name__ async def main(): # 调用方 tasks = [taskIO_1(), taskIO_2()] # 把所有任务添加到task中 done, pending = await asyncio.wait(tasks) # 子生成器 for r in done: # done和pending都是一个任务，所以返回结果需要逐个调用result() print(\u0026#39;协程无序返回值：\u0026#39;+ r.result()) if __name__ == \u0026#39;__main__\u0026#39;: start = time.time() loop = asyncio.get_event_loop() # 创建一个事件循环对象loop try: loop.run_until_complete(main()) # 完成事件循环，直到最后一个任务结束 finally: loop.close() # 结束事件循环 print(\u0026#39;所有IO任务总耗时%.5f秒\u0026#39; % float(time.time()-start)) 结尾 基础的协程逻辑就是这些，最近很久没写blog了，下一篇应该是和Elasticsearch或者k8s有关，先这样吧。最近也太累了，想好好休息下，也要思考下换一份工作了。\n","permalink":"https://yemilice.com/posts/python%E7%9A%84%E5%8D%8F%E7%A8%8B%E7%9F%A5%E8%AF%86/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e最近心情一直不是太好，写出来的东西感觉也没有灵感，有些时候做了很多事，但是回想起来感觉自己还是什么都没有做。这是9月份第一次更新blog，也更新一篇相对高级一些的技术吧，有一阵没看Python了，想想还是不要落下了，剧透一下，下一篇文章还是针对Elasticsearch或者是前端框架React的，学习还是不能停下来，最近写歌也有问题，感觉自己什么也写不出来，仿佛失去了灵感，是生活还是时间消磨了我的灵气呢？我不愿意这么想，我会努力的，状态会调整过来的。\u003c/p\u003e","title":"Python的协程知识"},{"content":"前言 其实想了一下，Python有一阵没好好看了，这样不好。刚好前阵子看了一下Golang的协程，我寻思看了那么多协程逻辑，也该看看Python的。\n一看Python，都更新到3.8.3了，真厉害啊！我们现在线上的项目还是2.7，迁移的代价太大了，Python现在更新了asyncio库，这个库可以傻瓜式完成协程，异步等等操作，看起来是相当牛逼，话不多说，去看看它的源码，弄明白它的运行逻辑，顺便学习一下Python官方的代码写作手段，可别到时候，代码没写好，整出一大堆格式问题呀。\n这是我第一次写读源码类的文章，如果写的不好，请多多包涵呀！\nPython中的协程是什么 前阵子我说了一下Golang的协程，现在又跳到Python的协程来了，巧了嘿，golang的协程，其实就是用一个关键字go去完成的\n类似\ngo example() 而Python的协程是怎么表现的呢，其实就是通过一个关键字async定义，就像这样\nasync def async_work(): return 1 理论上来说，Python的协程调用类似\n\u0026gt;\u0026gt;\u0026gt; import asyncio \u0026gt;\u0026gt;\u0026gt; async def main(): ... print(\u0026#39;hello\u0026#39;) ... await asyncio.sleep(1) ... print(\u0026#39;world\u0026#39;) \u0026gt;\u0026gt;\u0026gt; asyncio.run(main()) hello world 它并不像go一样，直接调用关键字就可以执行\n简单地调用一个协程并不会将其加入执行日程，需要用到**run()**这个函数\n所以还是有不一样的。。。这属于只是有Golang那味儿，但是核心不是人家西方那一套。\n其实Python由于自身GIL线程全局锁，在处理一些需要高并发处理的操作时候就显得有些力不从心了，一般这种时候，在我用Python2开发的时候，我会用多进程+多线程混用操作，务必榨干Python2的所有性能，但是Python3之后引入了类似golang的关键字async，可以直接调用协程，这样不就有golang那味儿了嘛，所以我看了一下这个库的文档，也基本弄明白这东西怎么使了。\n协程的创建（asyncio.create_task） create_task函数用来并发运行作为 asyncio任务的多个协程，举个例子\n将 coro 协程 打包为一个 Task 排入日程准备执行。返回 Task 对象\n该任务会在 get_running_loop() 返回的循环中执行，如果当前线程没有在运行的循环则会引发 RuntimeError。\nasync def coro(): worksomething... task = asyncio.create_task(coro()) 基础使用看完了，然后呢。。。\n先去源码库里面看一下create_task的源码(Python的版本是3.8.3)\ndef create_task(self, coro, *, name=None): \u0026#34;\u0026#34;\u0026#34;Schedule a coroutine object. Return a task object. \u0026#34;\u0026#34;\u0026#34; self._check_closed() if self._task_factory is None: task = tasks.Task(coro, loop=self, name=name) if task._source_traceback: del task._source_traceback[-1] else: task = self._task_factory(self, coro) tasks._set_task_name(task, name) return task 这里主要接受到一个协程作为参数，并且还有name\ntask = tasks.Task(coro, loop=self, name=name) 这里会新建一个task实例并且返回。\n协程的休眠（asyncio.sleep） 有些时候需要让协程去等待，或者阻塞指定的时间，就需要调用sleep函数，sleep一般会把任务挂起，然后不影响其他任务运行。\n以下协程示例运行 5 秒，每秒显示一次当前日期\nimport asyncio import datetime async def display_date(): loop = asyncio.get_running_loop() end_time = loop.time() + 5.0 while True: print(datetime.datetime.now()) if (loop.time() + 1.0) \u0026gt;= end_time: break await asyncio.sleep(1) asyncio.run(display_date()) 看下人家的源码\n@types.coroutine def __sleep0(): \u0026#34;\u0026#34;\u0026#34;Skip one event loop run cycle. This is a private helper for \u0026#39;asyncio.sleep()\u0026#39;, used when the \u0026#39;delay\u0026#39; is set to 0. It uses a bare \u0026#39;yield\u0026#39; expression (which Task.__step knows how to handle) instead of creating a Future object. \u0026#34;\u0026#34;\u0026#34; yield async def sleep(delay, result=None, *, loop=None): \u0026#34;\u0026#34;\u0026#34;Coroutine that completes after a given time (in seconds).\u0026#34;\u0026#34;\u0026#34; if delay \u0026lt;= 0: await __sleep0() return result if loop is None: loop = events.get_running_loop() else: warnings.warn(\u0026#34;The loop argument is deprecated since Python 3.8, \u0026#34; \u0026#34;and scheduled for removal in Python 3.10.\u0026#34;, DeprecationWarning, stacklevel=2) future = loop.create_future() h = loop.call_later(delay, futures._set_result_unless_cancelled, future, result) try: return await future finally: h.cancel() 这里的写法让我一瞬间想到golang，粗略说一下把，这里主要是传入一个协程等待时间delay，通过调取get_running_loop()获取事件循环，等待yield，然后暂停执行协程达到的协程阻塞效果。\n这里引出来一个重要概念，get_running_loop()，这个东西是干嘛的？\nget_running_loop函数是干嘛的？ 首先去定位一下人家的源码\nclass _RunningLoop(threading.local): loop_pid = (None, None) _running_loop = _RunningLoop() def get_running_loop(): \u0026#34;\u0026#34;\u0026#34;Return the running event loop. Raise a RuntimeError if there is none. This function is thread-specific. \u0026#34;\u0026#34;\u0026#34; # NOTE: this function is implemented in C (see _asynciomodule.c) loop = _get_running_loop() if loop is None: raise RuntimeError(\u0026#39;no running event loop\u0026#39;) return loop def _get_running_loop(): \u0026#34;\u0026#34;\u0026#34;Return the running event loop or None. 输出一个event loop，主要是一个循环事件的TLS This is a low-level function intended to be used by event loops. This function is thread-specific. \u0026#34;\u0026#34;\u0026#34; # NOTE: this function is implemented in C (see _asynciomodule.c) running_loop, pid = _running_loop.loop_pid if running_loop is not None and pid == os.getpid(): return running_loop 可以看出来一点，get_running_loop（获取事件循环）的主要功能就是返回当前 OS 线程中正在运行的事件循环，这样可能说起来抽象一点，大白话的意思就是事件循环是asyncio的核心，异步任务的运行、任务完成之后的回调、网络IO操作、子进程的运行，都是通过事件循环完成的。所以这个相当于是发动机了。\n协程的结果（asyncio.Future） 这里换种理解方法\n有些时候我们想得到协程的返回值，但是在Python的协程里面任务有些时候会执行，但有些时候不会，所以Future相当于一个最终结果，无论协程是否执行。\nFuture是一个await（可等待）对象，await我下面会说，意思就是，它其实和Task一样，是一个可以被等待的。\n其实理解一下，就是Future是协程的封装函数。\n这部分源码太多了，我直接拿官方例子来说明\nasync def set_after(fut, delay, value): # 设置一个等待时间 depay是秒数 await asyncio.sleep(delay) # 设置一个future的结果 fut.set_result(value) async def main(): # 来一个loop循环 loop = asyncio.get_running_loop() # 来一个新的future fut = loop.create_future() # Run \u0026#34;set_after()\u0026#34; coroutine in a parallel Task. # 创建一个task，去设置future的结果 # We are using the low-level \u0026#34;loop.create_task()\u0026#34; API here because # 开始 # we already have a reference to the event loop at hand. # Otherwise we could have just used \u0026#34;asyncio.create_task()\u0026#34;. loop.create_task( set_after(fut, 1, \u0026#39;... world\u0026#39;)) print(\u0026#39;hello ...\u0026#39;) # Wait until *fut* has a result (1 second) and print it. print(await fut) asyncio.run(main()) 上面那个例子就是创建一个Future对象，创建和调度一个异步任务去设置Future结果，最后再去等待结果。这就是基础的用法。\n协程的等待（await） await其实就是声明协程挂起，其实就是在执行的时候挂起某个协程函数，等待挂起条件结束后，再回来执行。\n它其实是一个关键字，就像async一样\n这里举个例子\n\u0026gt;\u0026gt;\u0026gt; import asyncio \u0026gt;\u0026gt;\u0026gt; async def main(): ... print(\u0026#39;hello\u0026#39;) # 等待1s ... await asyncio.sleep(1) ... print(\u0026#39;world\u0026#39;) \u0026gt;\u0026gt;\u0026gt; asyncio.run(main()) hello world 结尾 这块真的好久没看了，我也是第一次写源码类文章，我估摸着写的的确不太好，不过一回生，二回熟，下次我会写的更好的，哈哈哈\n今天终于周五啦,好开心啊，下一步应该是继续刷算法看书了，大家有什么不懂的可以给我发邮件的。\n","permalink":"https://yemilice.com/posts/%E8%AF%BBpython%E7%9A%84%E5%8D%8F%E7%A8%8B%E5%BA%93asyncio%E6%BA%90%E7%A0%81%E9%9A%8F%E6%83%B3/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e其实想了一下，Python有一阵没好好看了，这样不好。刚好前阵子看了一下Golang的协程，我寻思看了那么多协程逻辑，也该看看Python的。\u003c/p\u003e","title":"读Python的协程库asyncio源码随想"},{"content":"前言 昨天让无糖信息的面试官嘲讽了之后，我火速写了一篇博客嘲讽回去一波，但是嘲讽归嘲讽，该做的事儿还是要做的，所以昨天晚上花了一些时间总结了一些Golang的基础，作为查漏补缺和自己学习。\n还有就是，无糖信息这种公司对待面试者的态度，也决定这个公司的基本格局，反正我是不会去了，如果看到这个blog的人，你们去面无糖信息的Golang还是要用点心，得有个大心脏，因为那个面试官根本就不会care你的感受。\n嘿嘿，不过那个面试官，我不知道你的名字，我也不会care你是否能看到，我只希望你以后懂得尊重这个词语的意义。\n面试官先生，你应该不会看到我的博客，那我用一些歌词回击你吧，不过看你那个样子，应该不懂音乐（狗头）。\n自由的灵魂， 从来都不需要拟行程， 我们的目标在云层。 Golang的理论基础 Golang的保留字段有哪些？ break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var Golang声明变量的方法？ var a int = 1 //第一种: var variable_name variable_name value := 1 //第二种: value_name := 1 var b, c, d = 1, 2, 3 //第三种: 合并声明 var( //第四种: 合并声明 value1 int = 1 value2 string = \u0026#34;hello world\u0026#34; ) Golang声明常量的方法？ const var a int = 1 const var ( b int = 1 c string = \u0026#34;hello world\u0026#34; ) Golang的init函数是什么？ 程序运行前的注册功能，理解为Python的init也可以\ninit函数的特性\n1. init函数先于main函数自动执行，不能被其他函数调用； 2. init函数没有输入参数、返回值； 3. 每个包可以有多个init函数； 4. 包的每个源文件也可以有多个init函数，这点比较特殊； 5. 同一个包的init执行顺序，golang没有明确定义，编程时要注意程序不要依赖这个执行顺序。 6. 不同包的init函数按照包导入的依赖关系决定执行顺序。 defer是什么？ 延迟函数的意思\n举个例子\n下面的函数返回什么值？\nfunc test1() (result int) { defer func() { result++ }() return 0 } 此处应该返回一个1\ndefer后进先出是怎么表现的？ 举个例子\nfunc main() { for i := 0; i \u0026lt; 5; i++ { defer fmt.Printf(\u0026#34;%d \u0026#34;, i) } } 此处会返回，4，3，2，1，0\nGolang的协程是什么？ 这个问题很宽泛，无糖信息的那个面试官问了我一个很玄幻的问题：“你协程用的多吗？”\n我当时很想问，什么叫我协程用的多，Golang有协程，Python也有，用的多不多你看我项目不就完了，分布式，大规模集群的，有几个没用过协程的，这问题就很可笑。\n回到主题，协程是什么，按照golang的语法，创建一个协程只需要\ngo example() 就可以了。\n协程（coroutine）是Go语言中的轻量级线程实现，由Go运行时（runtime）管理。 Golang 的协程本质上其实就是对 IO 事件的封装，并且通过语言级的支持让异步的代码看上去像同步执行的一样。\n协程的控制，一般是channel，waitgroup，context之类的，这个我写过文章详细说了，这里就不再描述，如果你们想看，翻一下我以前的blog就可以了。\nstruct是怎么用的？或者struct是干嘛的？ 这兄弟名字叫结构体，写过Python的话，你理解为一个固定了格式的dict就可以了\n使用的方法\nimport \u0026#34;fmt\u0026#34; type Example struct { Name string Age int } func main() { // 声明 e := Example{} fmt.Println(t) t.Name = \u0026#34;back\u0026#34; t.Age = 10 fmt.Println(t) } 返回的值: { 0} {back 10} interface是什么？ 有些东西名义上是接口，其实啥都能干\ninterface是一种值，它可以像是值一样传递。并且在它的底层，它其实是一个值和类型的元组，interface是一种万能数据类型，它可以接收任何类型的值。\n举个例子\nvar a1 interface{} = 1 var a2 interface{} = \u0026#34;abc\u0026#34; list := make([]interface{}, ) list = append(list, a1) list = append(list, a2) fmt.Println(list) 判断interface的值的类型\nswitch v := i.(type) { case int: fmt.Println(\u0026#34;int\u0026#34;) case string: fmt.Println(\u0026#34;string\u0026#34;) } interface是一个nil，你理解为Python里面的nil就可以了。\nnil也可以调用interfece\npanic是干嘛的？ 类似 try-catch-finally 中的 finally，接收一个interface{}类型的值（也就是任何值了）作为参数，Golang没有try catch，所以panic会直接挂掉程序，如果panic中有defer，那么将先会执行defer，然后，再次抛出panic错误，打印堆栈。\n多个defer的执行顺序 func c() (i int) { defer func() { i++ }() return 1 } 这时候应该返回 2\n一个Go project是如何管理的？ 很简单，go mod，\n创建一个新的工程\n// 初始化go mod go mod init // 下载依赖包/源码包 go mod vendor vendor是一个源码包的集合。这个过于基础了，实在没想到这种题都能考。\nslice切片的基础操作 把切片理解为Python中的list，这样是不是就简单多了\n声明空切片\nvar sliceTmp []int 初始化一个切片\nsliceTmp2 := []string{\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;} 修改一个切片的值\nsliceTmp2[0] = \u0026#34;b\u0026#34; 追加切片值\nsliceTmp2 = append(sliceTmp2,\u0026#34;d\u0026#34;) 截取切片段\nvar sliceTmp5 = sliceTmp4[1:4] 遍历切片\nfor index, value :=range s1{ doSomething } make是干嘛的？ 简单地说，make就是告诉计算机，我要申请内存了，你给我腾地儿。\n因为我们对于引用类型的变量，不光要声明它，还要为它分配内容空间\nmake用于内存分配，但只用于通道chan、映射map以及切片slice的内存创建。\n举个例子\n// 创建一个指定长度的切片 mySlice1 := make([]int, 5) //创建一个初始元素长度为5的数组切片，元素初始值为0，并预留10个元素的存储空间： mySlice2 := make([]int, 5, 10) //创建了一个键类型为string、值类型为PersonInfo myMap = make(map[string] PersonInfo) //也可以选择是否在创建时指定该map的初始存储能力，创建了一个初始存储能力为100的map. myMap = make(map[string] PersonInfo, 100) //创建并初始化map的代码. myMap = map[string] PersonInfo{ \u0026#34;1234\u0026#34;: PersonInfo{\u0026#34;1\u0026#34;, \u0026#34;Jack\u0026#34;, \u0026#34;Room 101,...\u0026#34;}, } //创建有缓存通道 ch := make(chan int, 10) //创建无缓存通道 ch := make(chan int) Golang中Json忽略字段 有些时候有些字段我们要忽略掉，这里要在定义struct的时候做文章，这边采用了定义nil的手段\ntype astruct struct { A string B string C interface{} } func main() { var a = astruct{ A: \u0026#34;hello\u0026#34;, b: \u0026#34;hello\u0026#34;, C: nil } } 定义nil直接忽略值就可。 map的一般用法 想成Python中的dict，不同的是这东西需要你提前定义\nmap的常见操作有：声明、赋值、添加、删除、查询、遍历、清空等\nvarstuMapmap[int]string //声明 var map名称 map[键类型]值类型 mapScore:=make(map[string]float32) //或者这样声明 ​ stuMap=map[int]string{1001:\u0026#34;Tom\u0026#34;,1002:\u0026#34;Tim\u0026#34;} //赋值 stuMap[1003] =\u0026#34;Tem\u0026#34; //添加 delete(stuMap, 1003) //删除 ​ data, flag:=stuMap[1003] //查询该数据是否存在，不存在时flag为false;存在时 //data存储数据，flag为true ​ forkey, data:=rangestuMap{ //遍历键和值 fmt.Println(key, data) delete(stuMap, key) //循环删除清空 } stuMap=make(map[int]string) //或者重新make新的空间以清空stuMap，推荐方法 map的常见方法有：键值存在性、排序、嵌套\ndata, flag:=stuMap[1003] //判断存在性，查询该数据是否存在，不存在时flag为false;存在时 //data存储数据，flag为true ​ import\u0026#34;sort\u0026#34; //利用sort包完成排序功能 varsortSlice[]int //定义sortSlice切片 forkey, _:=rangestuMap{ sortSlice=append(sortSlice, key) //合成切片 } sort.Ints(sortSlice) varstuMap2map[int](map[int]string) //嵌套，即值类型可以嵌套其他类型 Golang中的goroutine并发执行有什么规律？ 首先，并发，不是并行\n如果设置了\nruntime.GOMAXPROCS(n) 这个东西相当于告诉程序，此刻你只能有n个协程去并行，那个n相当于就是一个控制因素\nimport ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func ready(w string, sec int64) { time.Sleep(time.Duration(sec * 1e9)) fmt.Println(w, \u0026#34;is ready!\u0026#34;) } func main() { go ready(\u0026#34;Tee\u0026#34;, 2) go ready(\u0026#34;Coffee\u0026#34;, 1) fmt.Println(\u0026#34;I\u0026#39;m waiting\u0026#34;) time.Sleep(5 * 1e9) } 结果： I\u0026#39;m waiting Coffee is ready! Tee is ready! 结尾 细细一看写了不少了，如果能帮到谁，那我真的非常开心，希望大家的技术都能越来越好。\n也希望有些面试官真的，认清自己的能力，你的确有地方可以，但是，下场比划比划，你也有不是个数的地方，所以，对技术抱有敬畏之心，对未知抱有好奇之心，对他人抱有尊重之心，才是能走的更远的本质，这个我相信您一定能懂，不过我也不希望你懂，因为就您这态度，我希望你继续保持眼高于顶的态度。因为我希望你35岁被裁员的时候也会记得你有一天这么对待过别人。\n嘿嘿，最后用歌词结束这两天不开心的心情吧。\n让时间去给看法 错的会被正义斩杀 你知道天堂很美 孩子们千万别喊害怕 你想要的东西 必须用全力去争取 金钱和权力 才不是你想要的生活真谛 check~ ","permalink":"https://yemilice.com/posts/golang%E8%AF%AD%E8%A8%80%E7%9A%84%E4%B8%80%E4%BA%9B%E5%9F%BA%E7%A1%80-%E9%92%88%E5%AF%B9%E9%9D%A2%E5%90%91%E5%9F%BA%E7%A1%80%E7%9A%84%E7%AC%94-%E9%9D%A2%E8%AF%95/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e昨天让无糖信息的面试官嘲讽了之后，我火速写了一篇博客嘲讽回去一波，但是嘲讽归嘲讽，该做的事儿还是要做的，所以昨天晚上花了一些时间总结了一些Golang的基础，作为查漏补缺和自己学习。\u003c/p\u003e","title":"Golang语言的一些基础(针对面向基础的笔/面试)"},{"content":"前言 今天面试感觉不是特别好\n其实我一直都觉得自己的技术似乎还行，我估摸着\u0026quot;还行\u0026quot;这个定义真的稍微模糊了一点，更多可能就是我什么都懂一些，其实内部什么也不懂\n面试的经过真的就算了，毕竟都没到问我核心的那一步。拿了个笔试题让我做，我看了一下基本都是基础，类似校招题那种，返回什么具体值，该怎么返回，xx是干嘛的之类的，这种我就答的很差，错很多，然后华丽GG。\n主要是我平常干活基本都是多语言开发混用，有时候还写写自动化测试，写写前端（毕竟全栈。。。），对这种基础我觉得是真的8行，人家面试官也挺给力，判完卷子，直接就说了解了，就这样吧（你可以走了）。\n行，那就这样吧，这么多东西你也不问，做了什么你也不问，我做的东西您也不问，算法您也没考，真的不知道怎么说，菜是原罪，我菜我认。\n反思 具体问题总归是要具体分析，不能像歪嘴赘婿一样，动不动就土味打脸，出一时之气，喊一句“莫欺技术不好，将来让我面试你”就完事儿了，但是讲真，今天还是有点那啥，不甘心 这次面试我发现了我几个很严重的问题\n基础技术较差 其实很多东西，IDE都帮咱补全了，特别是JB家的IDE，谁用谁知道，设计流程就完事儿了，这次的面试题基本偏向于具体值返回/某个包是干嘛的，其实这种题真的意义不大，真的要考完全可以请出电脑，咱们现场编码，但是我寻思，如果失去IDE，是不是我就是两眼一抹黑？但是IDE存在的意义不就是解放双手？呸，跑题了，回到基础差的主题。\n其实有些题我真的知道一个大概，但是就是不知道怎么说，讲真，我觉得我可以和那个面试官好好聊一些别的东西，例如系统设计架构，或者是系统处理，业务高效处理之类的。而不是坐在这里聊一些什么值应该返回什么结果的问题，所以我觉得那个面试官，真的，您似乎不怎么了解我，不过您应该也不care我的想法，同理，我也不care你的。\n基础的技术包含了基础的语法，基础的技术思想，或者是一些语法特性，语法糖之类的，如果你是新手，了解这个还是很有必要了，我现在就是复习and查漏补缺，慢慢写手册慢慢复习，对吧。\n不知道谁会看到博客，不过我也不在意你们怎么看我，我真的认为啊，像我们这种工作了3年+的人，在每个项目中都扛了很多事儿，更应该了解一下咱们在项目中怎么能更好的处理业务，而不是纠结一个返回值是什么，我个人感觉这次面试完全表现失常，还Tm不如前段时间的几个电话面试发挥的好。您可以认为我是技术无法展现的不甘心，我倒是认为这是面试人员的一种。。。。怎么说呢，我觉得是一种傲慢。\n其他我也编不出来了 菜，菜，菜，菜菜子的菜，可能就是我太菜，只因为我太菜，哈哈哈。\n谈一谈我想要的面试 借着自己的这个小天地，我说说我想要什么样子的面试吧。\n其实，面试不是考试，评判一个人是否适合公司业务线，或者是是否适合自己的团队，完全可以去引导面试对象，发掘出面试对象的优势。如果答不出来，完全可以走别的逻辑啊，引导到面试官的擅长方向，让面试对象去多说自己的优势，然后再去综合考量是否合适自己的团队。这才是我想要的面试，也是我正在努力的方向。我觉得今天那个面试官就挺xx的，估计一开始也就没认真看，或者就没想诚意招人。\n大家都是平等的，真的，您不用高高在上的，觉得自己似乎在一个公司当一个领导是一个很xx的事儿，我也面试过别人，别人来，至少我都是笑着欢迎的，笑着送走的，人家答不出来，至少我还会给出我自己解答然后听他说，我自己是做到这一点的，所以我敢这么说，我面试几个大厂，人家也是这么对我的。\n因为这是礼貌问题，也代表我的家庭教养，您这个态度吧，算了算了，对了，你要是看到了，对号入座了，那就很对不起了啊。\n我也不知道说什么，就当结尾 面试这种东西，本来就是双向的。有些人说，人家可能就是为了完成kpi，或者人家要的不是我这种人，所以你过去不就是自取其辱嘛，不过我不这么想\n其实我认为这次倒是给我敲响了一个警钟，第一让我明白了自己的水平是个什么样子，第二让我明白了面试官的水平都是良莠不齐的，你可能可以遇到很好的，或者有点耐心的，你可也能遇到那种话都不说的，无所谓了。\n其实说无所谓，心里还是有些不爽，不过这也是自己的一个警示，做事还是要。。以后投简历还是要好好看看，这公司到底需不需要我的技术栈，这次面试全程我和面试官无交流，所以，大家都不要浪费彼此时间了，如果有笔试，请您告诉我，我好好准备，如果没有笔试，咱们好好聊项目，就这样吧。\nend，所以我，最近要努力啦！\n","permalink":"https://yemilice.com/posts/%E4%BC%BC%E4%B9%8E%E8%A6%81%E6%B2%89%E4%B8%8B%E5%BF%83%E6%9D%A5%E5%A4%84%E7%90%86%E4%B8%80%E4%BA%9B%E4%BA%8B/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e今天面试感觉不是特别好\u003c/p\u003e\n\u003cp\u003e其实我一直都觉得自己的技术似乎还行，我估摸着\u0026quot;还行\u0026quot;这个定义真的稍微模糊了一点，更多可能就是我什么都懂一些，其实内部什么也不懂\u003c/p\u003e","title":"似乎要沉下心来处理一些事"},{"content":"前言 起因是一次电话面试，面一个技术比较好的公司，我认为自己玩Elasticsearch还是比较久了，还是能交锋几个回合吧，结果人家一问，中文分词的算法，你有了解吗？\n纳尼？中文分词，不就是调一下中文分词器做index嘛，我说了调用IK分词器，人家让我说一下中文分词的算法，或者你有没有了解过，我只有老实的说，我没。\n后来当然是有点凉，我觉得我需要去详细看一下这方面的东西。所以这两天除了看房子，我也具体看了一下Elasticsearch和其他的，例如tika，fscrawler之类的分词逻辑，加上网上的一些资料，还是有一些收获，这里做个记录，也是未来的博客文章从基础转向高级的一个转折点吧。\n分词是什么？什么是分词？它能干嘛？ 首先三连，这个我认为比较基础，简单的说，在我们搭建检索引擎或者设计搜索的时候，为了方便能更加简便的搜索出我们想要的东西，首先听一个术语，叫做分词粒度，例如，我们搜索 \u0026ldquo;我是吴彦祖\u0026rdquo;，下面是不同的粒度的分析划分。\n我，是，吴彦祖 最细粒度 我是，吴彦祖 正常粒度 我，我是，我是吴，吴彦祖，是吴彦祖 混合粒度 你可以看见，不同的分词效果，带来的分词结果不同，举个例子，我们如果根据正常粒度，搜索 \u0026ldquo;我\u0026rdquo;，这时就搜索不出来，但是根据混合粒度，我们搜索 \u0026ldquo;我\u0026rdquo;，就能得到 \u0026ldquo;我是吴彦祖\u0026rdquo; 的搜索结果，所以，良好的分词，是增加搜索效率和搜索结果的重要因素，同理，分词也是一个搜索引擎的老大部分，这次咱们不说屁股的事儿，咱们光说中文，中文的分词算法组成到底有哪些，您往下看。\n中文分词的几个算法解析 根据我查询的一些资料，和阅读一些技术文档，我总结出了如下几个搜索算法\n词典类分词法 这个大类其实比较好理解，现在大部分的搜索引擎都是根据中文字典作为分词的方法，它首先是基于中文字典，然后根据算法做分词，具体的算法如下\n最大匹配算法（Maximum Matching） 这里分为两种，一种是正向最大匹配，一种是逆向最大匹配，这里我主要说一下正向最大匹配方法，下面我都用MM来代替算法名，后面也都一样。\n正向最大匹配算法 MM正向算法的原理，其实就是将等待分词的文本和字典进行匹配，遵循的是从左往右匹配的原则，如果匹配上了，切分出一个词汇，举个简单的例子\n# 等待分词的列表 wait_seg_word = [\u0026#34;我\u0026#34;,\u0026#34;是\u0026#34;,\u0026#34;吴\u0026#34;,\u0026#34;彦\u0026#34;,\u0026#34;祖\u0026#34;] # 词典 word_dicts ={\u0026#34;我是\u0026#34;, \u0026#34;吴彦祖\u0026#34;, \u0026#34;我\u0026#34;, \u0026#34;是吴彦祖\u0026#34;, \u0026#34;彦祖\u0026#34;} 首先从wait_seg_word开始扫描\nwait_seg_word[0] = \u0026ldquo;我\u0026rdquo;\nwait_seg_word[1] = \u0026ldquo;是\u0026rdquo;\n这时候发现了 \u0026ldquo;我是\u0026rdquo; 已经在word_dicts中了，可以做切分了，但是我们需要做到最大匹配，所以继续切分\nwait_seg_word[2] = \u0026ldquo;吴\u0026rdquo;，发觉 \u0026ldquo;我是吴\u0026rdquo; 不能够组成词组，继续往下\nwait_seg_word[3] = \u0026ldquo;彦\u0026rdquo;, 不能组成任何词组\nwait_seg_word[4] = \u0026ldquo;祖\u0026rdquo;, \u0026ldquo;是吴彦祖\u0026rdquo; 匹配了词典，所以最大匹配为 \u0026ldquo;是吴彦祖\u0026rdquo;\n这就是基础的正向最大匹配算法\n逆向最大匹配算法 MM逆向算法其实就是MM正向算法的逆袭思维，我们简单说一下吧。\n# 等待分词的列表 wait_seg_word = [\u0026#34;我是吴彦祖\u0026#34;] # 词典 word_dicts ={\u0026#34;我是\u0026#34;, \u0026#34;吴彦祖\u0026#34;, \u0026#34;我\u0026#34;, \u0026#34;是吴彦祖\u0026#34;, \u0026#34;彦祖\u0026#34;} 定义一个做大分割值为 4，从右往左切分等待分词的列表\n取得到 \u0026ldquo;是吴彦祖\u0026rdquo;， 发觉它已经在词典中了\n去掉最左边第一个字，得到 \u0026ldquo;吴彦祖\u0026rdquo;\n以此类推，一直到无法被减去为止。\n然后再做一次切分，取得到 \u0026ldquo;我是吴彦\u0026rdquo;\n再次切分，对比。重复上述步骤。\n双向最大匹配算法 双向最大匹配法是将正向最大匹配法得到的分词结果和逆向最大匹配法的到的结果进行比较，从而决定正确的分词方法。\n这个做个比较就好了，我简单说下它是干嘛的吧。\n双向切分算法就是使用正向切分一次、逆向切分一次。如果两次切分结果一样的话就好说了，随便选一个结果就可以。\n但是如果切分不一样的话使用那一次的切分结果呢？这就涉及到了结果的选取原则问题。切分词应该遵守以下原则：\n1：最大匹配原则：上面一直在说这个，使用这个原则的原因是词的字数越多，表示的含义越丰富、对于一条语句分出来的词也就越少，相对的，准确性也就会越高。\n2：词库中没有的单字词越少越好。这个原则有点依赖于词库了，至少词库中应该有一些常用的单字成词的字吧，比如：“你”、“我”、“他”、“和”、“的”、“了”等。使用这个原则的原因可以从上面提到的“我是吴彦祖”这个例子看出来：\n正向结果：我是、是、吴彦祖\n逆向结果：我是、我、是吴彦祖\n虽然分出来的结果单字词都是一个，但是，逆向的单字词”和“在词库中存在，所以我们选择返回逆向切分结果。\n其实说白了就是很依赖词库，如果没词库就是一个哑巴教一个不会说话的孩子念绕口令，纯属烧脑。。。。\n统计类分词法 统计分词是什么？其实说白了，通过相邻的字同时出现的次数越多，就越可能构成一个词，也就是出现的频率。同样的词组出现多次，被统计的概率越大，组成合理词组也就越精准，可信。因此字与字相邻出现的概率或频率能较好的反映词的可信度。\n根据我查询的资料和实践所得，具体有这么几种算法\nN-gram模型算法 这个主要运用在tika当中，tika你不会不知道吧？你不知道？后面我写个文章你瞅瞅去吧。。。。\ntika是我做全文检索插件的时候接触的，它的核心分词算法就是N-gram算法，这个比较重要，我会详细的说一下这个算法。\n首先，N-gram是一种基于统计语言模型的算法。它的基本思想是将文本里面的内容按照字节进行大小为N的滑动窗口操作，形成了长度是N的字节片段序列。（这段出自知乎）\n一个N-gram就是一个长度为N的词语组成的序列。\n简单的流程\nN-Gram 算法具体过程：\n过滤掉文本数据中的标点符号和其他特殊字符；\n对所有单词执行小写转换，并删除单词之间的空格、换行符等标志位；\n使用长度为 N 的窗口对文本内容执行字符级滑动取词，将结果存入有序列表。\n这里直接借鉴了别人的代码，看的懂就行\ndef text_filter(text: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34; 文本过滤器：过滤掉文本数据中的标点符号和其他特殊字符 \u0026#34;\u0026#34;\u0026#34; result = str() for t in text: if t.isalnum(): if t.isalpha(): t = t.lower() result += str(t) return result def slide_word(text: str, l: int = 5) -\u0026gt; list: \u0026#34;\u0026#34;\u0026#34; 滑动取词器 Input: text=\u0026#39;abcd\u0026#39;,l=2 Output: [\u0026#39;ab\u0026#39;,\u0026#39;bc\u0026#39;,\u0026#39;cd\u0026#39;] :param text: 过滤后的文本 （只包含小写数字/字母） :param l: 滑动窗口长度，默认为 5 :return: \u0026#34;\u0026#34;\u0026#34; tf = text_filter(text) result = list() if len(tf) \u0026lt;= l: result.append(tf) return result for i in range(len(tf)): word = tf[i:i + l] if len(word) \u0026lt; l: break result.append(word) return result if __name__ == \u0026#39;__main__\u0026#39;: banner = \u0026#39;abcdefghigkLMN*^%$* \\r\\n)021\u0026#39; print(slide_word(banner)) 返回\n[\u0026#39;abcde\u0026#39;, \u0026#39;bcdef\u0026#39;, \u0026#39;cdefg\u0026#39;, \u0026#39;defgh\u0026#39;, \u0026#39;efghi\u0026#39;, \u0026#39;fghig\u0026#39;, \u0026#39;ghigk\u0026#39;, \u0026#39;higkl\u0026#39;, \u0026#39;igklm\u0026#39;, \u0026#39;gklmn\u0026#39;, \u0026#39;klmn0\u0026#39;, \u0026#39;lmn02\u0026#39;, \u0026#39;mn021\u0026#39;] 隐马尔科夫模型算法 (HMM) 通过模拟人对句子的理解，达到识别词的效果，基本思想是语义分析，句法分析，利用句法信息和语义信息对文本进行分词。自动推理，并完成对未登录词的补充是其优点。不成熟.\n具体概念:有限状态机\\语法约束矩阵\\特征词库\n以往的分词方法，无论是基于规则的还是基于统计的，一般都依赖于一个事先编制的词表(词典)。\n自动分词过程就是通过词表和相关信息来做出词语切分的决策。\n与此相反，基于字标注的分词方法实际上是构词方法。\n即把分词过程视为字在字串中的标注问题。\n由于每个字在构造一个特定的词语时都占据着一个确定的构词位置(即词位)，假如规定每个字最多只有四个构词位置：即B(词首)，M (词中)，E(词尾)和S(单独成词)，那么下面句子(甲)的分词结果就可以直接表示成如(乙)所示的逐字标注形式：\n(甲)分词结果：／上海／计划／N／本／世纪／末／实现／人均／国内／生产／总值／五千美元／ (乙)字标注形式：上／B海／E计／B划／E N／S 本／s世／B 纪／E 末／S 实／B 现／E 人／B 均／E 国／B 内／E生／B产／E总／B值／E 五／B千／M 美／M 元／E 。／S 首先需要说明，这里说到的“字”不只限于汉字。\n考虑到中文真实文本中不可避免地会包含一定数量的非汉字字符，本文所说的“字”，也包括外文字母、阿拉伯数字和标点符号等字符。所有这些字符都是构词的基本单元。当然，汉字依然是这个单元集合中数量最多的一类字符。\n把分词过程视为字的标注问题的一个重要优势在于，它能够平衡地看待词表词和未登录词的识别问题。\n在这种分词技术中，文本中的词表词和未登录词都是用统一的字标注过程来实现的。\n在学习架构上，既可以不必专门强调词表词信息，也不用专门设计特定的未登录词(如人名、地名、机构名)识别模块。这使得分词系统的设计大大简化。\n在字标注过程中，所有的字根据预定义的特征进行词位特性的学习，获得一个概率模型。然后，在待分字串上，根据字与字之间的结合紧密程度，得到一个词位的标注结果。\n最后，根据词位定义直接获得最终的分词结果。总而言之，在这样一个分词过程中，分词成为字重组的简单过程。然而这一简单处理带来的分词结果却是令人满意的。\n总结 我估摸着我还是要好好把这些算法过一遍，未来还有很长的路要走，算法也太难了。。。\n","permalink":"https://yemilice.com/posts/%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D%E7%9A%84%E7%AE%97%E6%B3%95%E5%88%86%E6%9E%90/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e起因是一次电话面试，面一个技术比较好的公司，我认为自己玩Elasticsearch还是比较久了，还是能交锋几个回合吧，结果人家一问，中文分词的算法，你有了解吗？\u003c/p\u003e","title":"中文分词的算法分析"},{"content":"前言 前阵子写了个ETCD选主的代码，持续后台执行，相安无事一阵我就干别的事儿去了，我寻思小爷虽然代码写的一般，但是不至于出错啊。\n但是我想的实在是太单纯了，应了那句话，我还是too young啊，高估了自己的姿势水平，一下搞出来一个大新闻。周六大半夜告警在那里 biubiubiu 的往我邮箱里塞，我当时正在COD战场上挥汗如雨，手机的震动就像电动马达一样给我腿都快整麻了，一看邮箱，好家伙，CPU占用百分之200多！Cgroup都没给它限制住。\n所以我寻思，咱就好好分析下到底怎么回事儿吧。\n工具准备 一般验证一个server详细的CPU和内存占用，Python我会选择PDB，C++我会选择GDB，但是GOlang这个就相对来说比较陌生，经过我查询资料（谷歌）过后得知，Golang有神奇pprof，还可以分析整个函数占用，这就很牛逼了，果断学习了一波之后上手。\npprof的简单使用 首先pprof分为两个大包\nnet/http/pprof runtime/pprof 如果你的go程序是用http包启动的web服务器，你想查看自己的web服务器的状态。这个时候就可以选择net/http/pprof。你只需要引入包_\u0026ldquo;net/http/pprof\u0026rdquo;。\n例如\nimport _ \u0026#34;net/http/pprof\u0026#34; go func() { http.ListenAndServe(\u0026#34;0.0.0.0:8080\u0026#34;, nil) }() 我现在要分析我的go server为什么占用了过大的CPU，所以，我需要在我的Go server的main中添加几行导入pprof的代码，一些业务代码我这里会直接隐去，请见谅\nimport ( _ \u0026#34;net/http/pprof\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ······ ) func main() { // 这里使用了gin框架，模拟我的业务环境 gin.SetMode(gin.ReleaseMode) router := gin.Default() //ETCD选主的逻辑，这里很可能是持续占用CPU的罪魁祸首 go infimanage.Retry(infiapi.InitSyncStart) //调用pprof的逻辑，指定6161端口 go func() { http.ListenAndServe(\u0026#34;:6161\u0026#34;, nil) }() //这里是原本的服务接口,这里只是举个例子，模拟一波 .... router.POST(\u0026#34;/synctask/add\u0026#34;, addwork, AddSyncWorks) router.GET(\u0026#34;/synctask/del\u0026#34;, DeleteSyncWorks) s := \u0026amp;http.Server{ Addr: \u0026#34;:8788\u0026#34;, Handler: router, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } _ = s.ListenAndServe() } 现在走一波\ngo run main.go 现在server就启起来了，然后咱们要来一波分析 首先指定监控main server 30s\ngo tool pprof http://127.0.0.1:6161/debug/pprof/profile -seconds 30 会冒出下面一大堆东西，就相当于进入了pprof，类似pdb，gdb的调试shell下\nFetching profile over HTTP from http://localhost:6161/debug/pprof/profile?seconds=30 Saved profile in /Users/eddycjy/pprof/pprof.samples.cpu.007.pb.gz Type: cpu Duration: 1mins, Total samples = 26.55s (44.15%) Entering interactive mode (type \u0026#34;help\u0026#34; for commands, \u0026#34;o\u0026#34; for options) (pprof) 获取CPU占比前10的函数\n(pprof) top10 Showing nodes accounting for 25.92s, 97.63% of 26.55s total Dropped 85 nodes (cum \u0026lt;= 0.13s) Showing top 10 nodes out of 21 flat flat% sum% cum cum% 23.28s 87.68% 87.68% 23.29s 87.72% syscall.Syscall 0.77s 2.90% 90.58% 21.77s 80.90% runtime.selectgo 0.58s 2.18% 92.77% 0.58s 2.18% runtime.mcall 0.53s 2.00% 94.76% 1.42s 5.35% runtime.timerproc 0.36s 1.36% 96.12% 30.39s 98.47% go.etcd.io/etcd/clientv3/... 0.35s 1.32% 97.44% 0.45s 1.69% runtime.greyobject 0.02s 0.075% 97.51% 24.96s 94.01% main.main.func1 0.01s 0.038% 97.55% 23.91s 90.06% os.(*File).Write 0.01s 0.038% 97.59% 0.19s 0.72% runtime.mallocgc 0.01s 0.038% 97.63% 23.30s 87.76% syscall.Write 这一波就能看出来了，到底谁才是占用CPU过大的罪魁祸首，看到了么，有个select和etcd的keeplive，但是这样似乎还不那么直观，这时候你需要一个叫做火焰图的东西\npprof输出火焰图 安装输出火焰图的server 安装配置FlameGraph\ngit clone https://github.com/brendangregg/FlameGraph.git 配置FlameGraph\nPATH=$PATH:/rootg/go/src/github.com/brendangregg/FlameGraph 配置go-torch，这是生成火焰图的必备工具\ngo get -v github.com/uber/go-torch 生成火焰图 确保都安装完成了之后\n直接执行命令\n此时你要确保第一步那些监控代码已经写入\ngo-torch -u http://127.0.0.1:6161/debug/pprof/ -p \u0026gt; cpu-local.svg 这里会生成一个svg，这个东西可以用浏览器打开，打开是这样的\n看到没，这一下就暴露出来了，谁占比多，你还可以点进去看详细，比如\n这下可以定位了，就是选主逻辑当中的select或者是for循环出了问题\n解决问题 我们回头去反查ETCD的选主逻辑代码\n//ElectMasterNode 争抢主节点服务 func ElectMasterNode() error { ips, err := GetNodeIp() if err != nil { return err } etcd, err := infidb.New() if err != nil { return err } for { _ = etcd.Newleaseslock(ips) } } //Masterwork 选主尝试 func Masterwork() { select { ElectMasterNode() } } 这里的问题就是，在select中重复调用了for循环，应该是出现了for死循环，导致了CPU持续被抢占，这边修改一下代码逻辑\n//ElectMasterNode 争抢主节点服务 func ElectMasterNode() error { ips, err := GetNodeIp() if err != nil { return err } etcd, err := infidb.New() if err != nil { return err } _ = etcd.Newleaseslock(ips) } //Masterwork 选主尝试 func Masterwork() { select { ElectMasterNode() } } 这样问题应该就完全解决了。\n","permalink":"https://yemilice.com/posts/%E4%B8%80%E6%AC%A1golang%E6%9C%8D%E5%8A%A1%E5%8D%A0%E7%94%A8cpu%E8%BF%87%E5%A4%A7%E7%9A%84%E6%8E%92%E6%9F%A5%E7%BB%8F%E8%BF%87/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e前阵子写了个ETCD选主的代码，持续后台执行，相安无事一阵我就干别的事儿去了，我寻思小爷虽然代码写的一般，但是不至于出错啊。\u003c/p\u003e\n\u003cp\u003e但是我想的实在是太单纯了，应了那句话，我还是too young啊，高估了自己的姿势水平，一下搞出来一个大新闻。周六大半夜告警在那里 biubiubiu 的往我邮箱里塞，我当时正在COD战场上挥汗如雨，手机的震动就像电动马达一样给我腿都快整麻了，一看邮箱，好家伙，CPU占用百分之200多！Cgroup都没给它限制住。\u003c/p\u003e","title":"一次Golang服务占用CPU过大的排查经过"},{"content":"出埃及记-前篇-买房理由 有时候时间一晃，就过去了4年多，我2016年毕业，现在已经是2020年。\n按道理说我也是老油条了，不比刚入行的小鲜肉了，但是依旧没个成就，也没个方向。说起买房，无非就是和兄弟朋友推杯换盏中听到的，谁又买了房，谁又结了婚罢了。\n当房东再一次要我搬走的时候，我才觉得，有些时候你根本就没有选择的权力，因为人家的容错率很低，直接赔付你一个月房租即可，突然无比羡慕这些大城市中收取房租的人，有些时候会在加班的深夜反思，当年如果早买了房，是不是人生的选择就会多一点点？\n所以在搬了新家的第一个晚上，我躺在硬板床上，思绪似乎飞到九霄云外，我的亲人，朋友都在身边，但是睁开眼，只有我孤独一人空留在空白的房间徘徊罢了。\n但是有两个字，我记住了，买房。\n是啊，买房吧，那就现在就买吧。\n出埃及记-中篇-买房过程 选定了自己的购房需求，我就要根据自己的预算来选择房子。\n首先需要研读一个比技术文档还要复杂的\u0026quot;限购/购房须知\u0026quot;。\n绕来绕去我弄明白了一点，买房的区域是被严格限制了的。\n说真的，我一直觉得这些限购的政策从来都是治不了本的，好比我在的高新区，不能够买旁边较为便宜的双流区的房子，只能去往买市中心和比较贵的天府新区的房子，哈哈，说白了，你我都懂得，有些人指定的政策完全都是为了消化指定区域的库存和大房子，正在建设的房子未来是哪里的韭菜接盘？\n像哥德巴赫猜一样，我大概猜出了我要买什么样的房子，奈何我的父母也不是大富大贵之人，手头预算有限，我大概也只能买个小房子去生活，并且这样就要掏空我的父母的大部分积蓄，我心里有些难受，一瞬间不想买房子了，但是母亲告诉我：房子，必须要买，买了房子，你才有家的感觉。\n家的感觉？我从6岁开始就没有什么家的感觉了，我的父母很早就分开了，家对我来说永远都是孤独的水泥墙的存在，我更宁愿去相信母亲的意思是我不会被房东随便赶走，也有底气在这个大城市去生活吧。\n手头资金不多，受限于政策，旁边的双流，更远龙泉地区，便宜的房子我是买不了的，我只能在南边去寻找合适的房子，是吧，那就买个合适的小房子吧？\n周六，周天跑了两天，我坐在滴滴上面，半眯着眼看着外面飞驰的树木，车辆，人流，看着远方立起的一座座高楼，会想着，我的家，会在哪里呢？这个钢筋水泥筑成的城市里，是否还有我的一席之地呢？\n出埃及记-后篇-未曾买房 跑了两天，我还是拒绝了中介给我推荐的跃层，那个跃层很好，我很喜欢\n但是他需要我自己将社保转移到双流/龙泉，然后进行别的操作\n我听到哑然失笑，安得广厦千万间？当买个房子都这么麻烦的情况下，我有时候觉得自己就像个提线木偶，一直有人在掐住我的脖子而已。\n不知道什么时候开始，没有以前那么有灵感，我一直不愿意让自己太懒，但是我有些时候有很多想说的，却堵在嘴边。\n今天周一，我心情很复杂，未来，会变得更好吧？！\n","permalink":"https://yemilice.com/posts/%E5%87%BA%E5%9F%83%E5%8F%8A%E8%AE%B0-%E6%99%AE%E9%80%9A%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9A%84%E4%B9%B0%E6%88%BF%E4%B9%8B%E8%B7%AF/","summary":"\u003ch2 id=\"出埃及记-前篇-买房理由\"\u003e出埃及记-前篇-买房理由\u003c/h2\u003e\n\u003cp\u003e有时候时间一晃，就过去了4年多，我2016年毕业，现在已经是2020年。\u003c/p\u003e\n\u003cp\u003e按道理说我也是老油条了，不比刚入行的小鲜肉了，但是依旧没个成就，也没个方向。说起买房，无非就是和兄弟朋友推杯换盏中听到的，谁又买了房，谁又结了婚罢了。\u003c/p\u003e","title":"出埃及记_普通程序员的买房之路"},{"content":"前言 这段时间主要攻关了一下Elasticsearch的一些特性，发觉Elasticsearch还是个挺牛逼的玩意儿，我以前经常用它存日志，还没想着拿来存别的东西，一般不都SQL嘛，但是我看了一下现在流行的检索方案，基本都是elasticsearch做检索引擎，说明这个东西经历了时间的考研，用了的人都说好。\n我以前拿Elasticsearch来存储日志，搜东西相当方便，定义好检索语句直接走你，但是我看百度文库搜索，还可以做到docx，pdf关键字检索并且高亮，这个就很厉害了，这两天紧急攻关了一下，把自己踩的坑和收获记录下来，方便未来自己复习/查看\n需求分析 老样子，写任何blog，我都要做需求分析，这样才能更好的进行开发和写作。\n我们要干嘛 我们要实现一个elasticsearch检索pdf和office家族文件的功能，并且要对pdf/office文件进行一个全文检索，也就是说，搜索“Yemilice”，不仅仅是标题含有“Yemilice”，内容含有“Yemilice”的文件也要检索出来，并且给人家高亮。\n我们的工作环境 底层环境\nCentos7服务器\nElasticsearch的版本\n6.3.2\n现有的检索解决方案 经过我的发力工作（Google/baidu）,现在市面上流行这么几种方案\nElasticsearch 官方插件 ingest-attachment 第三方开源服务 fscrawler 大数据平台 Ambari 接下来麻烦事儿就来了，这三个方案，到底选哪个？才能更那啥呢，我倒不如把他们一一实现一遍，然后给大家伙儿展示一拨，然后最后再选一个合适的不就得了嘛！\n没事儿，苦了我一个，幸福大家伙儿呗！\n1. ingest-attachment插件 这玩意儿是什么呢，说白了就是elasticsearch官方给大家贡献的一个插件，支持你把docx，pdf之类的东西导入到es当中。当然要你自己手动去实现接口。\n1.1 ingest-attachment插件的安装 首先，我认为你现在在CN境内，你就不要看网上那些老哥教的直接执行\n./elasticsearch-plugin install ingest-attachment 这样你会等到地老天荒也下载不完，现在你需要的就是下个离线包，然后再去安装\n下载离线包的网址\nhttps://artifacts.elastic.co/downloads/elasticsearch-plugins/ingest-attachment/ingest-attachment-6.3.2.zip. 注意一点啊，一定要注意！你的ingest-attachment版本必须和你的elasticsearch一致，你要强行不听我的，到时候费心思整完，弄不好，那你就只能来颗华子再来一次了。\n安装离线包\n./elasticsearch-plugin install ingest-attachment-6.3.2.zip 没报错就说明你安上了，不放心的话\n./elasticsearch-plugin list 看一下，有这个插件了说明你已经有了这个插件\n1.2 ingest-attachment插件的基础使用 看了下官网，其实他的逻辑很简单，整个管道，你把你的pdf/doc什么的给转成base64，然后通过管道把base64传进去，es会把所有的东西给你安排好。\n我现在简单的整了个pdf，名字就叫1.pdf，现在开始吧。\n抽取管道的函数编写，这里是告诉es，创建一个抽取管道pipeline\ncurl -X PUT \u0026#34;localhost:9200/_ingest/pipeline/attachment\u0026#34; -d \u0026#39;{ \u0026#34;description\u0026#34; : \u0026#34;Extract attachment information\u0026#34;, \u0026#34;processors\u0026#34;:[ { \u0026#34;attachment\u0026#34;:{ \u0026#34;field\u0026#34;:\u0026#34;data\u0026#34;, \u0026#34;indexed_chars\u0026#34; : -1, \u0026#34;ignore_missing\u0026#34;:true } }, { \u0026#34;remove\u0026#34;:{\u0026#34;field\u0026#34;:\u0026#34;data\u0026#34;} }]}\u0026#39; 创建一个index（表），名字叫pdf，设定1个分片（单节点，当然你自己可以改）\ncurl --location --request PUT \u0026#39;127.0.0.1:9200/pdf\u0026#39; \\ --header \u0026#39;Content-Type: application/json\u0026#39; \\ -d \u0026#39;{ \u0026#34;settings\u0026#34;: { \u0026#34;index\u0026#34;: { \u0026#34;number_of_shards\u0026#34;: 1, \u0026#34;number_of_replicas\u0026#34;: 0 } } }\u0026#39; 将pdf转为base64编码，然后上传到elasticsearch当中\n这里网上那个方法是直接perl调base64，我这里一直报错，如果和我一样的老哥，用我下面给得另一种方法\n方法1\ncurl --location --request PUT \u0026#39;10.0.7.234:9200/pdf/pdf/1?pipeline=attachment\u0026#39; \\ --header \u0026#39;Content-Type: application/json\u0026#39; \\ -d \u0026#39;{ \u0026#34;data\u0026#34;:\u0026#34; \u0026#39;`base64 -w 0 /root/pdf/pdf.pdf | perl -pe\u0026#39;s/\\n/\\\\n/g\u0026#39;`\u0026#39;\u0026#34; }\u0026#39; 方法2\n直接导入base64码\ncurl --location --request PUT \u0026#39;10.0.7.234:9200/pdf/pdf/1?pipeline=attachment\u0026#39; \\ --header \u0026#39;Content-Type: application/json\u0026#39; \\ -d \u0026#39;{ \u0026#34;data\u0026#34;:\u0026#34;JVBERi0xLjcNCiW1tbW1DQoxIDAgb2JqDQo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFIvTGFuZyh6aC1DTikgL1N0cnVjdFRyZWVSb290IDE1IDAgUi9NYXJrSW5mbzw8L01hcmtlZCB0cnVlPj4vTWV0YWRhdGEgMzMgMCBSL1ZpZXdlclByZWZlcmVuY2VzIDM0IDAgUj4+DQplbmRvYmoNC......（这里我不展示完了）\u0026#34; }\u0026#39; 现在去查看index（表）\n显示了作者，之类的杂七杂八信息，而content就是详细的全文，可以直接搜索，因为有我的大名我就马赛克了。。\n这就导入了，搜索也是按一般的搜索方法\nGET pdf/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;attachment.content\u0026#34;: \u0026#34;pdf\u0026#34; } } } 1.3 ingest-attachment插件的性能评测 测试导入PDF（10M大小，无图片）\nbase64命令在perl中执行了大概2s左右，\n并且CPU/内存 在转换-\u0026gt;存储这个阶段，占用了\n可见在抽取文本 ——\u0026gt; 传输存储入es的时候，插件可能会占用少部分CPU\n测试导入PDF（40M大小，图片/文字混用）\nbase64命令在perl中执行了大概10s左右，并且还返回了错误（perl长度过大的错误）\n并且CPU/内存 在转换-\u0026gt;存储这个阶段，占用了\n在图片/文字混用并且pdf文件比较大的情况下，内存占用较大，并且返回较慢，这里需要注意，并且解析的base64会直接塞到系统内存当中，如果做多文件抽取，可能会有CPU/内存占用较大的情况出现。\n1.4 ingest-attachment插件的优缺点 优点：\n安装方便，只需要下载zip安装包，调用elasticsearch本身自带的安装模块即可 使用方便，编辑一条管道抽取命令，可以直接利用linux的base64命令转码存入elasticsearch中 支持的格式比较多，支持ppt，pdf，doc，docx，xls等office常用的办公软件格式导入到elasticsearch当中。 导入后的文档可以直接输入中文进行检索 缺点：\n大文件支持不够，如果超过100M的pdf，base64转码将比较缓慢，并且，存入到elasticsearch中的数据也十分庞大 必须要将文件下载到本地，然后必须进行base64转码才可以存到elasticsearch中，等于说，必须要实体文件，因为需要base64转码，如果文件在对象存储/云上，那就不可以这么操作了 对于pdf，doc中含有图片的情况，没有办法将图片中文文字识别出来，而且如果出现图片较多的情况，转码较慢，含有特殊字符的base64码导入也会无法识别。 1.5 ingest-attachment插件的使用场景 存储的文件不那么大的情况 存储的文件大部分都是纯文字的情况 存储的文件全文搜索精准度要求不那么高 2. fscrawler服务 一个类似filebeat的监控服务，监控某个文件夹下面的文档，定时去遍历一次，如果发现了新添加的文档则会直接写入到elasticsearch当中。\n2.1 fscrawler安装 首先非常重要的一点，确定你的elasticseach版本，\n如果你的版本 \u0026lt;= 6.3.2，那你需要下载2.5 版本的fscrawler，这个非常重要，否则是无法启动任务的！！！！\nhttps://repo1.maven.org/maven2/fr/pilato/elasticsearch/crawler/fscrawler/2.5/fscrawler-2.5.zip 如果你的版本 \u0026gt; 6.3.2, 你就可以随意选择2.6，2.7的fscrawler了，区别还是有一些的，后面我会说。\n下载下来，解压，这个你肯定会\nunzip fscrawler-2.5.zip 尝试跑一下\n./fscrawler 输出\n03:53:39,425 INFO [f.p.e.c.f.c.FsCrawler] No job specified. Here is the list of existing jobs: 03:53:39,433 INFO [f.p.e.c.f.c.FsCrawler] [1] - work01 03:53:39,433 INFO [f.p.e.c.f.c.FsCrawler] Choose your job [1-1]... 说明能用\n2.2 fscrawler的使用方法 启动服务\n查看一下文件结构\n编写指定的json去进行服务监控/设置\n现在导入一个1.pdf的文件\n监控到fscrawler发生了变化\n去查看elasticsearch，已经有了这个1.pdf的文件\n2.3 fscrawler的配置文件解析（彩蛋） 一般来说，咱们fscrawler的配置文件都是默认在/root/.fscrawler/_settings.json下的，但是，如果你的es版本高于6.3.2，你下载的fs版本不是2.5，那么你的settings文件将会是yaml格式的，不知道是不是在向filebeat看齐。。。\n{ // 工程名 \u0026#34;name\u0026#34; : \u0026#34;work01\u0026#34;, \u0026#34;fs\u0026#34; : { // 监控的文件夹 \u0026#34;url\u0026#34; : \u0026#34;/tmp/es\u0026#34;, // 多长时间去扫描一次 \u0026#34;update_rate\u0026#34; : \u0026#34;30s\u0026#34;, // 添加例外 \u0026#34;excludes\u0026#34; : [ \u0026#34;*/~*\u0026#34; ], \u0026#34;json_support\u0026#34; : false, // 将文件名作为ID \u0026#34;filename_as_id\u0026#34; : false, // 文件大小添加 \u0026#34;add_filesize\u0026#34; : true, // 同步删除 \u0026#34;remove_deleted\u0026#34; : true, \u0026#34;add_as_inner_object\u0026#34; : false, \u0026#34;store_source\u0026#34; : false, \u0026#34;index_content\u0026#34; : true, \u0026#34;attributes_support\u0026#34; : false, \u0026#34;raw_metadata\u0026#34; : true, \u0026#34;xml_support\u0026#34; : false, \u0026#34;index_folders\u0026#34; : true, \u0026#34;lang_detect\u0026#34; : false, \u0026#34;continue_on_error\u0026#34; : false, // ocr开启 \u0026#34;pdf_ocr\u0026#34; : true, \u0026#34;ocr\u0026#34; : { // ocr英文 \u0026#34;language\u0026#34; : \u0026#34;eng\u0026#34; } }, \u0026#34;elasticsearch\u0026#34; : { \u0026#34;nodes\u0026#34; : [ { // esip \u0026#34;host\u0026#34; : \u0026#34;127.0.0.1\u0026#34;, \u0026#34;port\u0026#34; : 9200, \u0026#34;scheme\u0026#34; : \u0026#34;HTTP\u0026#34; } ], \u0026#34;bulk_size\u0026#34; : 100, \u0026#34;flush_interval\u0026#34; : \u0026#34;5s\u0026#34;, \u0026#34;byte_size\u0026#34; : \u0026#34;10mb\u0026#34; }, \u0026#34;rest\u0026#34; : { \u0026#34;scheme\u0026#34; : \u0026#34;HTTP\u0026#34;, \u0026#34;host\u0026#34; : \u0026#34;127.0.0.1\u0026#34;, \u0026#34;port\u0026#34; : 8080, \u0026#34;endpoint\u0026#34; : \u0026#34;fscrawler\u0026#34; } } 2.4 fscrawler的性能评测 首先走一波导入文件测试\n导入1M纯文字的pdf\n瞬间就完成了，速度极快\nelasticsearch监控信息如下\nFscrawler占用CPU/内存如下\n导入10M纯文字的pdf\n瞬间就完成了，速度极快\nelasticsearch监控信息如下\nFscrawler占用CPU/内存如下\n导入40M文字图片都有的pdf\n速度很快，但是CPU占比一下就上去了，es还是没有什么变化\nelasticsearch监控信息如下\nFscrawler占用CPU/内存如下\n2.5 fscrawler的优缺点 fscrawler我其实是比较推荐的\n优点\n支持的格式很多，并且自带OCR，如果内存/CPU富裕的情况可以直接上OCR，识别率虽然不是特别高但也够用了。 导入不用自己动手，设置好配置之后，fscrawler会帮你创建好index，然后自动导入文件 支持文件过滤，如果监控的文件夹里面闲杂文件比较多可以写正则过滤掉 CPU/内存占比比较低，我是用的自己的虚拟机，导入40Mpdf完全无问题，CPU占比上去了一瞬间就降下来了。 缺点\n结合我们的使用场景，我们的文件在远端，需要下载下来，下载到指定文件夹，但是下载到指定文件夹后，fscrawler不会马上导入，是去定时扫描，下载到本地的话可能会把系统盘内存撑满，加快扫描时间会让CPU占比上升 安装部署比较麻烦，需要自己写service，自己写维护 配置文件需要自己定义，容错有问题，如果es down机，原有的文件发送一遍失败之后，不会继续发送 3. Ambari服务 这个有个很奇葩的点，是要Es去依赖它，而不是它依赖Es，这东西本身是做大数据的！我感觉咱都有Es了还要啥自行车！\n这个是做大数据用的，安装一个大概1个多G，但是功能比较全面，支持各种大文件导入，还支持图片文字的读取，自动分词之类的，其实还是很好用。\n3.1 使用一波Ambari Ambari的安装就不在这里介绍了。。这个安装比较复杂，但是网上大部分都是docker集成的，我们现在的环境没有docker，所以我只有手动安装了，在我的机器已经安上了，现在需要安装Ambari的Elasticsearch插件\nwget https://community.hortonworks.com/storage/attachments/87416-elasticsearch-mpack-2600-9.tar.gz 安装mpack\nambari-server install-mpack --mpack=/path/to/87416-elasticsearch-mpack-2600-9.tar.gz --verbose 重启ambari-server\nambari-server restart 在ambari-server 上部署elasticsearch\ncurl --user admin:admin -i -H \u0026#39;X-Requested-By: ambari\u0026#39; -X GET \u0026#34;http://ambari.server:8080/api/v1/clusters/$cluster_name/configurations?type=cluster-env\u0026#34; 启动elasticsearch\nsystemctl restart elasticsearch 现在可以在Ambari的管理页面看到es服务器了\n设置ambar的配置文件\nmy-files: depends_on: serviceapi: condition: service_healthy image: ambar/ambar-local-crawler restart: always networks: - internal_network expose: - \u0026#34;8082\u0026#34; environment: - name=my-files - ignoreFolders=**/ForSharing/** - ignoreExtensions=.{exe,dll,rar} - ignoreFileNames=*backup* - maxFileSize=15mb volumes: - /media/Docs:/usr/data 现在尝试导入一个PDF\ncurl -X POST \\ http://ambar/api/files/Books/1984-george_orwell.pdf \\ -H \u0026#39;content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW\u0026#39; \\ -F 1984-george_orwell.rtf=@1984-george_orwell.pdf 这下就完成了导入\n3.2 高级特性 监控S3对象存储\n设定ak，sk\necho ACCESS_KEY_ID:SECRET_ACCESS_KEY \u0026gt; ~/.passwd-s3fs chmod 600 ~/.passwd-s3fs 挂载s3 bucket\nmkdir /mnt/s3-bucket 添加挂载到fstab中\nmybucket /mnt/s3-bucket fuse.s3fs _netdev,allow_other 0 0 修改配置文件\nmy-files: depends_on: serviceapi: condition: service_healthy image: ambar/ambar-local-crawler restart: always networks: - internal_network expose: - \u0026#34;8082\u0026#34; environment: - name=my-files - ignoreFolders=**/ForSharing/** - ignoreExtensions=.{exe,dll,rar} - ignoreFileNames=*backup* - maxFileSize=15mb volumes: - /media/Docs:/mnt/s3-bucket OCR文件识别\nambari自带强势的ocr识别，可以识别多种文字/字母/格式的文件，但是需要富裕的CPU和内存\n3.3 可能会出现的问题 优点\n它可以很好地处理大文件（\u0026gt; 100 MB） 它从PDF中提取内容（即使格式不佳并带有嵌入式图像），并对图像进行OCR 它为用户提供了简单易用的REST API和WEB UI 部署非常容易（感谢Docker） 它是根据Fair Source 1 v0.9许可开源的 开箱即用地为用户提供解析和即时搜索体验。 缺点\n部署可能会相当麻烦，没有docker部署，单独部署的话配置比较复杂 OCR会占用大量的CPU/内存 如果不是大数据，有些大材小用了 4. 总结 我都分析成这样了，该用哪个，心里是不是有点数了，其实很简单\n当你的pdf/doc文件不大的情况下，上ingest-attachment插件\n当你的文件经常发生变动，上fscrawler\n当你是大数据老哥，上Ambari\n今天的文章就到这，憋了个大的，如果帮到你，我很开心。\nend\n2020-07-29\n","permalink":"https://yemilice.com/posts/elasticsearch%E6%A3%80%E7%B4%A2pdf%E5%92%8Coffice%E6%96%87%E6%A1%A3%E7%9A%84%E6%96%B9%E6%A1%88%E6%B5%8B%E8%AF%84/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这段时间主要攻关了一下Elasticsearch的一些特性，发觉Elasticsearch还是个挺牛逼的玩意儿，我以前经常用它存日志，还没想着拿来存别的东西，一般不都SQL嘛，但是我看了一下现在流行的检索方案，基本都是elasticsearch做检索引擎，说明这个东西经历了时间的考研，用了的人都说好。\u003c/p\u003e","title":"Elasticsearch检索PDF和Office文档的方案测评"},{"content":"前言 有时候觉得自己快要撑不下去\n但是依旧不能放弃\n为了爸妈\n为了自己\n为了所有人\nONE 温热的咖啡升腾起轻薄的水雾\n满屏未敲完的代码依旧在提醒\n忙碌和疲惫的一天即将结束\nnext，即将来到下一个季节\n坐上地铁戴上耳机这是属于我的时间\n把今天的不堪和疲惫就在此丢失\n曾今的挚友们一一远去\n他们很多人已把我超越\n盲目的赚钱和生活已经让我麻木\n也曾今质疑过自己走过的路\n但是选择只能让我勇往直前\n脚踏实地是我的人生格言\n等到积累更多，得到更多的时候\n我才能就此歇下，远离喧嚣\nTWO 不知不觉我已经长大\n回想自己经历过的一切\n人生只有努力一条路没有捷径\n无论如何也会勇敢的深入虎穴\n因为要成为能值得被铭记的人物\nso what 成功或者失败\n就让我燃尽自己所有的能量\n然后像灰烬一样洒向远方\n挥洒血汗，我是吞噬金钱的野兽\n抢风头从来不是我的style脚踏实地才是\n和时间之神做个交易吧\n将努力和汗水换为当票\n典当时间，预支痛苦，\n收获的是快乐和幸福\nEND 其实我写这些往往都是DEMO\n一般还要做beat，但是我最近实在是没时间写\n我有时候挺后悔自己没和自己的兄弟去做说唱\n但是世界上是没有后悔药的\n现在就动起来吧。\n","permalink":"https://yemilice.com/posts/grow-up/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e有时候觉得自己快要撑不下去\u003c/p\u003e\n\u003cp\u003e但是依旧不能放弃\u003c/p\u003e\n\u003cp\u003e为了爸妈\u003c/p\u003e\n\u003cp\u003e为了自己\u003c/p\u003e\n\u003cp\u003e为了所有人\u003c/p\u003e\n\u003ch2 id=\"one\"\u003eONE\u003c/h2\u003e\n\u003cp\u003e温热的咖啡升腾起轻薄的水雾\u003c/p\u003e\n\u003cp\u003e满屏未敲完的代码依旧在提醒\u003c/p\u003e\n\u003cp\u003e忙碌和疲惫的一天即将结束\u003c/p\u003e\n\u003cp\u003enext，即将来到下一个季节\u003c/p\u003e","title":"Grow_Up"},{"content":"前言（写文章的原因） 最近开发项目太重了，我有些感觉自己状态不太对，想要去换一个地方生活，但是说到要做的事就一定要做，学习是不能停下来的。\n最近开发使用了一下gRPC，不同于以前我自己写的Python 的 RPC，gRPC相对来说用起来更简单，写起来更容易，可能一开始有点不好使，其实熟悉了也还是不错的，那么，这篇文章主要就写一下，gRPC的基础使用和在我项目中的实际应用吧。\nRPC框架是什么 简单说下什么是RPC，RPC是什么，全称是Remote Procedure Call Protocol，中文名称是远程调用协议，这个就说的很明白了\nRPC干嘛的，远程调用的呗！\n远程调什么？参数呗！函数呗！发消息呗！\n大白话来说，像调用本地服务一样调用远程服务，就是你现在要完成一个动作，你需要一个老哥（机器）和你联动一起完成，所以你就要通过一个渠道告诉他，说，老铁，我们要一起做这个东西，知道吗。\n这个渠道，就叫RPC，一般我们在网络服务，分布式服务，微服务中都需要这个东西。\n从技术的角度上来说，RPC更类似于一种client，server的通信过程，client发送消息给server，server接收到消息，然后返回消息给client。\ngRPC是什么 我是在写Golang网络服务的时候发现了这个框架gRPC，一看就是谷歌亲儿子框架，粗看了一下，感觉其实还好，所以我仔细看了一下这个框架\n首先这货需要一个ProtoBuf序列化工具，是给客户端提供接口的，ProtoBuf这个东西需要另外安装，没他就不能解析接口，这个的好处是，你的开发语言不受这个影响，相当于就是翻译器，能把多种语言编写的接口api翻译出来，给gRPC去用 第二就是需要gRPC的本体，这个如果你有Golang环境直接go get 即可 除此之外，粗看一下gRPC，其实也需要client和server端，也是互相交互的。 gRPC主要的好处 这货的好处\n其实说白了，就是性能高，从Golang的开发来说，就表明了gRPC走的也是高性能的逻辑。 protobuf这东西，也是gRPC钦定的接口编写工具，这东西可以严格的约束住接口，告诉他，你可以传什么，不能传什么，加强了gRPC的安全机制 protobuf可以压缩编码为二进制，二进制你们懂吧，这个东西就更快了，耍起来那就是大杀器，首先相比大刀，他就是手枪，不仅小，威力还大，还可以达到kill 人的目的，因为压缩成二进制，传递的数据量也小了，通过基础的http2还可以实现异步请求，这就很nice了。 来玩玩gRPC吧 安装protobuf 首先你要先去protobuf的大本营 https://github.com/protocolbuffers/protobuf/releases，先找对应的版本下载\n我这里是Centos7 64位， 所以我选了protoc-3.12.3-linux-x86_64.zip 下载，你们要去找对应的版本下载。\n然后你需要解压\nunzip protoc-3.12.3-linux-x86_64.zip CD到目录, 进到目录的bin下，给protoc赋上可执行的权限\ncd protoc-3.12.3-linux-x86_64/bin chmod 777 protoc 这下protoc可用了，你应该把它移动到bin下，方便未来调用\ncp protoc /usr/bin 定义一个protobuf接口 重头戏来了，现在我们从零开始开发了，现在我们先创建一个项目文件夹hellogRPC,这下面现在什么也没有。\n看图\n现在我们创一个名字叫manage的文件夹，在下面创建一个名为manage.proto的接口定义文件，类似这样\n看图\n现在开始编写proto文件\nsyntax = \u0026#34;proto3\u0026#34;; option objc_class_prefix = \u0026#34;HLW\u0026#34;; package manage; //定义服务 service GreeterServer { // 定义一个具体的接口，定义输出和输出的样式 rpc SayHello (HelloRequest) returns (HelloReply) {} } // 定义输入 string message HelloRequest { string name = 1; } // 定义输出 string message HelloReply { string message = 1; } 这里具体的意思就是，我，下一道圣旨，以后，都按着这个给我输入输出，现在我只开了一个sayhello的接口，别的进不来，就这样。\n编译protobuf文件 这一步是要把protobuf生成为二进制文件，就像我们前面说的，这也是人家gRPC的一个有点，就是有点儿操蛋，因为麻烦。。。。\ngo get -u github.com/golang/protobuf/protoc-gen-go 下下来之后，你还得把他移动一下下，到你的go path下面，我这里是/root/go\ncd /root/go/bin cp -r protoc-gen-go /usr/bin 再编译一下，走一波编译\nprotoc --go_out=plugins=grpc:. *.proto 这下你可以看见，多出一个同名得pb.go文件，类似这样\n这就编译成功了。下一步继续干活儿\n编写client服务 client相当于发号施令的大哥，主要就是去找server，告诉server，你们要干嘛，要干嘛这种。\n咱们现在创建一个client.go文件\n现在开写\npackage main import ( \u0026#34;log\u0026#34; pb \u0026#34;/你的目录/hellogRPC/manage\u0026#34; \u0026#34;golang.org/x/net/context\u0026#34; \u0026#34;google.golang.org/grpc\u0026#34; ) //HelloWork 定义一个hello world 方法 func HelloWork(ip string, name string) { conn, err := grpc.Dial(ip, grpc.WithInsecure()) if err != nil { log.Fatalf(\u0026#34;did not connect: %v\u0026#34;, err) } defer conn.Close() c := pb.NewGreeterClient(conn) r, err := c.SayHello(context.Background(), \u0026amp;pb.HelloRequest{Name: name}) if err != nil { log.Fatalf(\u0026#34;could not greet: %v\u0026#34;, err) } log.Printf(\u0026#34;Greeting: %s\u0026#34;, r.Message) } func main() { //传输要发送的ip，传输name HelloWork(\u0026#34;127.0.0.1:4321\u0026#34;, \u0026#34;work\u0026#34;) } 编写server服务 server服务就类似于等待发号施令的小弟，他们会常驻在后台等待，并且会开启一个专用端口去接受client发来的信息，类似这样\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; pb \u0026#34;/你的目录/hellogRPC/manage\u0026#34; \u0026#34;golang.org/x/net/context\u0026#34; \u0026#34;google.golang.org/grpc\u0026#34; ) const ( //定义一个端口 port = \u0026#34;:4321\u0026#34; ) // 定义server type server struct{} //定义SayHello 记住，这里的SayHello要和你proto里面的接口名一致 func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { return \u0026amp;pb.HelloReply{Message: \u0026#34;Hello \u0026#34; + in.Name}, nil } func main() { lis, err := net.Listen(\u0026#34;tcp\u0026#34;, port) if err != nil { log.Fatalf(\u0026#34;failed to listen: %v\u0026#34;, err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, \u0026amp;server{}) s.Serve(lis) } 来跑一下 先启动 server.go\ngo run server.go 再启动 client.go\ngo run clinet.go 发现输出了\n2020/07/07 16:01:10 Greeting: Hello work 这时我们的目录结构如下，基础的gRPC服务也就此完成。\n总结 其实这个还是比较简单的，但是具体到后面使用，你可能会遇到，gRPC和ETCD冲突，编译的时候出现bug，编译的时候编译器版本对不上等等等等，我这次踩了不少坑，不过还好过来了，项目快要结束了，终于要结束了，我想我需要休息一下，然后去找一个适合我的job。\n预告一下，未来我将进行kail linux 和渗透测试的文章更新，基础的技术我应该不会再更了，毕竟我15年入行，16年正式上班，是该有一些蜕变了，未来我将持续输出高质量的文章，请多多期待吧。\n","permalink":"https://yemilice.com/posts/%E4%BD%BF%E7%94%A8golang%E7%9A%84grpc%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E7%82%B9%E9%9A%8F%E6%83%B3/","summary":"\u003ch2 id=\"前言写文章的原因\"\u003e前言（写文章的原因）\u003c/h2\u003e\n\u003cp\u003e最近开发项目太重了，我有些感觉自己状态不太对，想要去换一个地方生活，但是说到要做的事就一定要做，学习是不能停下来的。\u003c/p\u003e\n\u003cp\u003e最近开发使用了一下gRPC，不同于以前我自己写的Python 的 RPC，gRPC相对来说用起来更简单，写起来更容易，可能一开始有点不好使，其实熟悉了也还是不错的，那么，这篇文章主要就写一下，gRPC的基础使用和在我项目中的实际应用吧。\u003c/p\u003e","title":"使用Golang的gRPC框架的一点随想"},{"content":"前言 改 不 了 写 前 言 的 毛 病\n这 个 月 心 情 很 不 美 丽\n来 自 心 底 的 呐 喊\n释 放 出 深 渊 的 野 兽\none 我 得 了 名 为 平 庸 的 不 治 之 症\n就 让 我 在 灰 暗 的 地 底 死 去\n双 手 合 十 祈 求 神 明 的 营救\n看 不 到 的 希 望 的 祷 告 不 会 得 到 回 应\n无 尽 的 在 深 渊 中 反 复 轮 回\n建 起 一 座 名 为 失 败 者 的 宫 殿\n不 要 阻 止 我\n不 要 拯 救 我\n我 根 本 无 药 可 医\n就 算 我 一命 呜 呼\n我 也 要 夺 走 你 的 一切\n这 是 我 的 战 争 宣 言\n匹 夫 的 怒 火 只会 萦 绕 在 你 身 上\n在 战 争 的 前 线 树 立 起 失 败 者 的 旗 帜\n这 就 是 失 败 者 的 战 争 宣 言\ntwo 承 认 自 己 平 庸 不 如 让 我 放 弃 自 己\n我 出 生 的 那 一 瞬 间 就 证 明 我 要 在 这 个 世 间 留 下 痕 迹\n就 算 我 得 了 不 治 之 症\n也 不 要 怀 疑 我 要 成 为 坐 拥 一 切 的 人\n恶 性 循 环 只 会 让 我 越 来 越 恶\n因 为 我 是 来 自 地 狱 的 恶 鬼\n不 会 向 任 何 人 低 头 包 括 上 帝\n双 手 合 十 可 他 并 没 有 出 现\n虽 然 我 活 成 这 样 子\n可 是 我 依 旧 不 赖\n就 算 我 一 命 呜 呼\n也 要 记 得 把 钱 都 给 我\n把 鼓 吹 战 争 的 传 单 散 发 各 地\n这 就 是 我 的 宣 战 宣 言\n高 高 举 起\n梦 想 成 真\n","permalink":"https://yemilice.com/posts/%E6%81%B6%E6%80%A7%E5%BE%AA%E7%8E%AF/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e改 不 了 写 前 言 的 毛 病\u003c/p\u003e\n\u003cp\u003e这 个 月 心 情 很 不 美 丽\u003c/p\u003e\n\u003cp\u003e来 自 心 底 的 呐 喊\u003c/p\u003e\n\u003cp\u003e释 放 出 深 渊 的 野 兽\u003c/p\u003e\n\u003ch2 id=\"one\"\u003eone\u003c/h2\u003e\n\u003cp\u003e我 得 了 名 为 平 庸 的 不 治 之 症\u003c/p\u003e\n\u003cp\u003e就 让 我 在 灰 暗 的 地 底 死 去\u003c/p\u003e\n\u003cp\u003e双 手 合 十 祈 求 神 明 的 营救\u003c/p\u003e\n\u003cp\u003e看 不 到 的 希 望 的 祷 告 不 会 得 到 回 应\u003c/p\u003e","title":"恶性循环"},{"content":"我的自我介绍 hello, 这是我，\n你可能不认识我，\n那么，\n现在由我来进行自我介绍。\ndo it。\n我 叫 Y k，你 可 以 叫 我 l k，来 自 c d c\n出 生 在 钟 鸣 鼎 食 之 家，其 他 都 是 secret\n有 个 很 爱 我 的 妈 妈，更 多 一 点 不 能 告 诉 你\nok，ok，让 我 们 继 续 下 去\n以 前 是 个 Coder 现 在 依 旧 是 Coder\n兼 职 玩 个 Rap 但 是 绝 不 是 Faker\n从 来 不 穿 Aj but 我 只 爱 air force\n嫉 妒 我 的 hater 我 从 来 不 care about\n真 正 的 键 盘 侠 用 键 盘 当 做 武 器\nproject 像 拿 了 绿 卡 一 路 run 到 底\n把 项 目 奖 金 和 fake coder 全 扫 进 包 包 里\n从 不 追 逐 金 钱 因 为 大 家 都 会 show me the money\n不 会 过 多 犹 豫，代 码 就 像 killer shoot you body\nGithub 上 又 多 了 几 颗 星 星，荣 誉 勋 章 记 录 总 不 停\n堆 积 成 山 压 力 从 不 畏 惧 ，制 定 策 略 就 像 指 挥 沙 盘 游 戏\n运 筹 帷 幄 将 代 码 握 在 手 里 ，因 为 我 是 coder 届 的 汤 姆 克 兰 西\n没 有 天 分 我 需 要 更 加 努 力， 感 谢 你 来 我 场 地 看 到 这 里\n照 顾 不 周 随 便 看 看 有 意 见 就 提，回 头 去 你 场 子 battle 乐 此 不 疲。\n","permalink":"https://yemilice.com/posts/%E6%88%91%E7%9A%84%E8%87%AA%E6%88%91%E4%BB%8B%E7%BB%8D/","summary":"\u003ch1 id=\"我的自我介绍\"\u003e我的自我介绍\u003c/h1\u003e\n\u003cp\u003ehello, 这是我，\u003c/p\u003e\n\u003cp\u003e你可能不认识我，\u003c/p\u003e\n\u003cp\u003e那么，\u003c/p\u003e\n\u003cp\u003e现在由我来进行自我介绍。\u003c/p\u003e\n\u003cp\u003edo it。\u003c/p\u003e\n\u003cp\u003e我 叫 Y k，你 可 以 叫 我 l k，来 自 c d c\u003c/p\u003e\n\u003cp\u003e出 生 在 钟 鸣 鼎 食 之 家，其 他 都 是 secret\u003c/p\u003e\n\u003cp\u003e有 个 很 爱 我 的 妈 妈，更 多 一 点 不 能 告 诉 你\u003c/p\u003e","title":"我的自我介绍"},{"content":"Python写一个自动点餐程序 为什么要写这个 公司现在用meican作为点餐渠道，每天规定的时间是早7：00-9：40点餐，有时候我经常容易忘记，或者是在地铁/公交上没办法点餐，所以总是没饭吃，只有去楼下711买点饭团之类的玩意儿，所以这是促使我写点餐小程序的原因。\n点餐的流程 登录 \u0026mdash;\u0026gt; 点餐 \u0026mdash;\u0026gt; 提交\n哈哈，是不是很简单，其实这个还好，说白了，就是登录上去，然后拿到cookie，保持一个登录状态，然后再去点餐，点餐就是构造请求，发送到指定的点餐URL上就可以了。\n登录 首先我们点开\nhttps://meican.com/\n上面要求我们登录，我们这里输入自己的账号密码，登录上去之后可以看见一个请求. 这个请求就是登录的请求，我们看下需要传什么参数，然后我们去完全构造这个请求，也就是参数一致，并且带浏览器头,这里我们也需要去保存cookie，也就是说，我们需要自己的账号时刻保持online状态，所以需要保存cookie，需要时候调用\n所以我们需要实现如下功能\n登录请求构造\n保持登录状态\n保存cookies\n使得后来的访问都带cookie\n代码如下\nimport json import requests import http.cookiejar as HC session = requests.session() session.cookies = HC.LWPCookieJar(filename=\u0026#39;cookies\u0026#39;) def login_meican(): \u0026#34;\u0026#34;\u0026#34; 登录美餐，寻找cookie文件，没cookie文件就重新载入 :return: \u0026#34;\u0026#34;\u0026#34; # 储存cookie作为日后使用，三天clear一次 try: session.cookies.load(ignore_discard=True) except: print(\u0026#39;未找到cookies文件\u0026#39;) save_cookie() def save_cookie(): \u0026#34;\u0026#34;\u0026#34; 如果没cookie，登录逻辑 :return: \u0026#34;\u0026#34;\u0026#34; login_url = \u0026#39;https://meican.com/account/directlogin\u0026#39; # Headers hearsers = { \u0026#34;User-Agent\u0026#34;: \u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36\u0026#34;, \u0026#34;Referer\u0026#34;: \u0026#34;https://meican.com/login\u0026#34;, \u0026#34;Origin\u0026#34;: \u0026#34;https://meican.com\u0026#34;, \u0026#34;Host\u0026#34;: \u0026#34;meican.com\u0026#34;, \u0026#34;Accept\u0026#34;: \u0026#34;*/*\u0026#34; } # Login need data data = { \u0026#34;username\u0026#34;: \u0026#34;xxxxxxxxxxx\u0026#34;, \u0026#34;loginType\u0026#34;: \u0026#34;username\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;xxxxxxxxxxx\u0026#34;, \u0026#34;remember\u0026#34;: \u0026#34;true\u0026#34; } try: r = session.post(login_url, headers=hearsers, data=data) r.raise_for_status() session.cookies.save() except Exception as e: print(\u0026#34;login error!\u0026#34;) return 0 上面的代码实现了登录。\n点餐 找到菜单 这里需要找到菜单，因为截图忘了截，这里就直接公布吧，找到菜单需要两个参数，一个是uuid，另一个是addrid，也就是你登陆的凭证+你所在地区的id，没有这两个是无法找出菜单的，并且也无法继续点餐流程。\n如何获得这两个参数 在登录的时候我发现了一个URL，这个URL是 https://meican.com/preorder/api/v2.1/calendaritems/list?withOrderDetail=false\u0026amp;beginDate=2019-09-04\u0026amp;endDate=2019-09-04，\n这个URL下的返回有我们要的参数，uuid 和 addrid，所以构造请求去获取这两个参数\ndef get_for_my_order(): \u0026#34;\u0026#34;\u0026#34; 找到usertorken, addrid :return: \u0026#34;\u0026#34;\u0026#34; user_dict = {} Now_date = datetime.date.today() z = session.get(\u0026#34;https://meican.com/preorder/api/v2.1/calendaritems/list?withOrderDetail=false\u0026amp;beginDate={Now}\u0026amp;endDate={Now}\u0026#34;.format(Now=Now_date)) x = json.loads(z.text) user_dict[\u0026#34;uuid\u0026#34;] = x[\u0026#34;dateList\u0026#34;][0][\u0026#34;calendarItemList\u0026#34;][0][\u0026#34;userTab\u0026#34;][\u0026#34;uniqueId\u0026#34;] user_dict[\u0026#34;addrid\u0026#34;] = x[\u0026#34;dateList\u0026#34;][0][\u0026#34;calendarItemList\u0026#34;][0][\u0026#34;userTab\u0026#34;][\u0026#34;corp\u0026#34;][\u0026#34;addressList\u0026#34;][0][\u0026#34;uniqueId\u0026#34;] return user_dict 构造获取菜单请求 找到获取菜单的URL\nhttps://meican.com/preorder/api/v2.1/recommendations/dishes?tabUniqueId={uuid}\u0026amp;targetTime={Now}+09:40\n这里需要一个参数uuid，调取我们获取参数的函数\ndef get_menu(): \u0026#34;\u0026#34;\u0026#34; 获取餐单逻辑 :return: \u0026#34;\u0026#34;\u0026#34; menu_dict = {} menu_list = [] Now_date = datetime.date.today() uuid = get_for_my_order()[\u0026#34;uuid\u0026#34;] z = session.get(\u0026#34;https://meican.com/preorder/api/v2.1/recommendations/dishes?tabUniqueId={uuid}\u0026amp;targetTime={Now}+09:40\u0026#34;.format(uuid = uuid, Now=Now_date)) menu = json.loads(z.text)[\u0026#34;myRegularDishList\u0026#34;] for i in menu: menu_dict[\u0026#34;id\u0026#34;] = i[\u0026#34;id\u0026#34;] menu_dict[\u0026#34;name\u0026#34;] = i[\u0026#34;name\u0026#34;] z = copy.deepcopy(menu_dict) menu_list.append(z) return menu_list 输出所有的菜单，以一个list作为输出\n提交 构造点餐请求 首先先找到点餐的URL\nhttps://meican.com/preorder/api/v2.1/orders/add\n查看点餐需要的参数：\ndata = { \u0026#34;corpAddressUniqueId\u0026#34;: addrid, \u0026#34;order\u0026#34;: x, \u0026#34;remarks\u0026#34;: y, \u0026#34;tabUniqueId\u0026#34;: uuid, \u0026#34;targetTime\u0026#34;:target_time, \u0026#34;userAddressUniqueId\u0026#34;:addrid } 构造点餐请求\ndef order_action(): \u0026#34;\u0026#34;\u0026#34; 点餐逻辑 :return: \u0026#34;\u0026#34;\u0026#34; addrid = get_for_my_order()[\u0026#34;addrid\u0026#34;] uuid = get_for_my_order()[\u0026#34;uuid\u0026#34;] menu_list = get_menu() menu_id = choice(menu_list)[\u0026#34;id\u0026#34;] target_time = str(datetime.date.today()) + \u0026#34; \u0026#34; + \u0026#34;09:40\u0026#34; x = str([{\u0026#34;count\u0026#34;:1,\u0026#34;dishId\u0026#34;:menu_id}]) y = str([{\u0026#34;dishId\u0026#34;:menu_id,\u0026#34;remark\u0026#34;:\u0026#34;\u0026#34;}]) data = { \u0026#34;corpAddressUniqueId\u0026#34;: addrid, \u0026#34;order\u0026#34;: x, \u0026#34;remarks\u0026#34;: y, \u0026#34;tabUniqueId\u0026#34;: uuid, \u0026#34;targetTime\u0026#34;:target_time, \u0026#34;userAddressUniqueId\u0026#34;:addrid } headers = { \u0026#34;User-Agent\u0026#34;: \u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36\u0026#34; } try: z = session.post(\u0026#34;https://meican.com/preorder/api/v2.1/orders/add\u0026#34;, headers=headers, data=data) z.raise_for_status() except: return \u0026#34;点餐错误！\u0026#34; 所用的知识点一览 Python requetst的post，session\ncookie的保存和调用\njson的输出和浏览\nrandom.choice 的列表元素随机选择\nPython构造请求和登录逻辑\n","permalink":"https://yemilice.com/posts/python%E5%86%99%E4%B8%80%E4%B8%AA%E8%87%AA%E5%8A%A8%E7%82%B9%E9%A4%90%E7%A8%8B%E5%BA%8F/","summary":"\u003ch1 id=\"python写一个自动点餐程序\"\u003ePython写一个自动点餐程序\u003c/h1\u003e\n\u003ch2 id=\"为什么要写这个\"\u003e为什么要写这个\u003c/h2\u003e\n\u003cp\u003e公司现在用meican作为点餐渠道，每天规定的时间是早7：00-9：40点餐，有时候我经常容易忘记，或者是在地铁/公交上没办法点餐，所以总是没饭吃，只有去楼下711买点饭团之类的玩意儿，所以这是促使我写点餐小程序的原因。\u003c/p\u003e","title":"Python写一个自动点餐程序"},{"content":"Golang中Context使用的一点随想 前言 这一篇是三巨头最后一篇了，前两篇介绍了channel，waitgroup，今天这篇来介绍一下context，相比于其他两种，我倒是更推荐context（上下文）这种控制goroutine的方法，为什呢，下面我就来详细的说一说吧。\n什么是Context（逻辑上下文） Context是一种链式的调用逻辑，一般是用来控制goroutine，例如说，控制goroutine的启动，停止，暂停，取消等等。 我这里举一个简单的例子来说明Context怎么用\n//WorkDir 定义一个遍历目录逻辑，假设这个目录很大，但是咱们需要随时把它结束掉 func WorkDir(ctx context.Context, name string) { //假装这是一个大列表，很大很大那种 filePathList := []string{........} for _, file := range(filePathList) { //假装这里处理一下目录, 打印一下目录 fmt.Println(file) select { //检测到ctx发生了变化 case \u0026lt;-ctx.Done(): fmt.Println(name,\u0026#34;任务退出，骨灰都给你扬了\u0026#34;) return //否则什么都不做 default: } } } func main() { //定义一个waitcancel ctx, cancel := context.WithCancel(context.Background()) //将ctx作为参数传入函数当中 go WorkDir(ctx, \u0026#34;三秒之内撒了你\u0026#34;) //让程序跑个几秒 time.Sleep(3 * time.Second) fmt.Println(\u0026#34;该杀任务了\u0026#34;) //杀任务操作 cancel() //没有监控输出，就表示停止 time.Sleep(2 * time.Second) fmt.Println(\u0026#34;任务让我杀了\u0026#34;) } 怎么样，是不是很简单，简单来说，ctx就是传递的信号，你给每一层传递的ctx，不管传的再多，都只有一个爹\nctx, cancel := context.WithCancel(context.Background()) cancel是来通知goroutine结束的，当爹ctx执行了cancel之后，就表示，我死了，你们得和我一起被株连，大家一起完蛋。\n链式的意思就是，爹ctx是母体，它可以不断的继续下发产生多个子ctx，当cancel通知母体死亡的时候，子ctx也要跟着一起完蛋，所以链式就是，一荣俱荣，一关俱关的，cancel就是丧钟，调用就会释放所有的被感染（ctx下发）的goroutine。\nContext的几个重要方法 WithCancel方法 WithCancel方法的接口\nfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc) 这里会返回一个ctx和cancel，ctx是一个可以被拷贝的context，这个context可以作为父节点继续向下传递，cancel则是通知ctx退出的信号，调用CancelFunc的时候，关闭c.done()，直接退出goroutine。 举个简单的例子，算了我偷个懒，用上面的例子，ctrl c ctrl v一波\n//WorkDir 定义一个遍历目录逻辑，假设这个目录很大，但是咱们需要随时把它结束掉 func WorkDir(ctx context.Context, name string) { //假装这是一个大列表，很大很大那种 filePathList := []string{........} for _, file := range(filePathList) { //假装这里处理一下目录, 打印一下目录 fmt.Println(file) select { //检测到ctx发生了变化 case \u0026lt;-ctx.Done(): fmt.Println(name,\u0026#34;任务退出，骨灰都给你扬了\u0026#34;) return //否则什么都不做 default: } } } func main() { //定义一个waitcancel ctx, cancel := context.WithCancel(context.Background()) //将ctx作为参数传入函数当中 go WorkDir(ctx, \u0026#34;三秒之内撒了你\u0026#34;) //让程序跑个几秒 time.Sleep(3 * time.Second) fmt.Println(\u0026#34;该杀任务了\u0026#34;) //杀任务操作 cancel() //没有监控输出，就表示停止 time.Sleep(2 * time.Second) fmt.Println(\u0026#34;任务让我杀了\u0026#34;) } WithDeadline方法 WithDeadline方法的接口\nfunc WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) 这下机制出现了变化，cancel换成了deadline，参数也换了，换成了time，聪明的你应该想到了什么吧，这个函数其实是设置一个具体的死亡时间（deadline），当到达了指定的deadline时间之后，将所有的goroutine进行退出。咱们简单点儿，告诉你，就是到点儿退出。这里咱们照旧，举个例子，用例子来个详细说明。\nfunc WorkDir(ctx context.Context, name string) { //假装这是一个大列表，很大很大那种 filePathList := []string{........} for _, file := range(filePathList) { //假装这里处理一下目录, 打印一下目录 fmt.Println(file) select { //检测到ctx发生了变化 case \u0026lt;-ctx.Done(): fmt.Println(name,\u0026#34;任务超时了，骨灰都给你扬了\u0026#34;) return case \u0026lt;-time.After(1 * time.Second): fmt.Println(\u0026#34;我还在......\u0026#34;) //否则什么都不做 default: } } } func main() { //定义一个WithDeadline，这里定义了一个具体的时间点 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10 * time.Second)) //将ctx作为参数传入函数当中 go WorkDir(ctx, \u0026#34;从现在开始计时，10s之后撒了你\u0026#34;) //让程序跑个几秒 time.Sleep(3 * time.Second) fmt.Println(\u0026#34;该杀任务了\u0026#34;) //杀任务操作 cancel() //没有监控输出，就表示停止 time.Sleep(2 * time.Second) fmt.Println(\u0026#34;任务让我杀了\u0026#34;) } 千万注意，后面还有个方法叫WithTimeout，它们两个说一样也不一样，WithTimeout是设置一个超时时间，WithDeadline是设定一个具体时间点，比如我上面那个，10s之内撒了goroutine，这个较为抽象，后面WithTimeout讲解的时候我会多费点功夫的。\nWithTimeout方法 WithTimeout方法的接口\nfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 对比一下上面那个WithDeadline，看到没，都需要传入一个时间变量，但是还是那句话，上面的WithDeadline需要的是具体时间，下面的WithTimeout就简单多了，就需要一个时间就行了，说简单点，上面的WithDeadline，要的是一个具体时间，例如 “2020-01-01 12:00:00”，\ntime.Now().Add(5*time.Second) 但是下面的WithTimeout，就很简单，就要一个超时时间就好了，例如5s\ntimeout := 3 * time.Second 类似crontab的用法。好了，继续举个例子\nfunc WorkDir(ctx context.Context, name string) { //假装这是一个大列表，很大很大那种 filePathList := []string{........} for _, file := range(filePathList) { //假装这里处理一下目录, 打印一下目录 fmt.Println(file) select { //检测到ctx发生了变化 case \u0026lt;-ctx.Done(): fmt.Println(name,\u0026#34;任务超时了，骨灰都给你扬了\u0026#34;) return case \u0026lt;-time.After(1 * time.Second): fmt.Println(\u0026#34;我还在......\u0026#34;) //否则什么都不做 default: } } } func main() { //定义一个WithDeadline，这里定义了一个具体的时间点 ctx, cancel:= context.WithTimeout(context.Background(), 5 * time.Second) //将ctx作为参数传入函数当中 go WorkDir(ctx, \u0026#34;不管怎么样，没返回，5s之后撒了你\u0026#34;) //让程序跑个几秒 time.Sleep(20 * time.Second) fmt.Println(\u0026#34;该杀任务了\u0026#34;) //杀任务操作 cancel() //没有监控输出，就表示停止 time.Sleep(2 * time.Second) fmt.Println(\u0026#34;任务让我杀了\u0026#34;) } 其实这两个用法都差不多，就是一个要求指定时间，一个随机时间，都是可以自己定义的.\nWithValue方法 WithValue方法的接口\nfunc WithValue(parent Context, key interface{}, val interface{}) (Context) 这个看一下人家的传入，一个context，一个key, 一个接口interface，这明显就是一个key value类型的参数，这个方法的主要功能就是传递消息用，传递的元数据一般都是在子ctx中要使用到的，这个函数其实我用的不多，我参考了一下其他老哥的写法，总结了一下，具体的例子如下\n//定义一个key var key string = \u0026#34;keywork\u0026#34; func main() { ctx, cancel := context.WithCancel(context.Background()) //附加值 valueCtx := context.WithValue(ctx, key, \u0026#34;【监控1】\u0026#34;) go watch(valueCtx) time.Sleep(10 * time.Second) fmt.Println(\u0026#34;可以了，通知监控停止\u0026#34;) cancel() //为了检测监控过是否停止，如果没有监控输出，就表示停止了 time.Sleep(5 * time.Second) } func watch(ctx context.Context) { for { select { case \u0026lt;-ctx.Done(): //取出值 fmt.Println(ctx.Value(key), \u0026#34;监控退出，停止了...\u0026#34;) return default: //取出值 fmt.Println(ctx.Value(key), \u0026#34;goroutine监控中...\u0026#34;) time.Sleep(2 * time.Second) } } } Context的使用注意事项 Context 始终要以参数的形式进行传递，整个结构体是不太行的，当然，利用map存储进行设计管理还是可以的。 Context 做参数你得永远把它放第一位，无论什么情况都切记 Context 可以在多个goroutine中进行传递，无需担心，它是线程安全的。 结尾 两天终于把这个系列整完了，其实相当于自己复习了一下，也借鉴了一些别人的代码，value那个转手是在太多，我找不着原作者了，要是谁看到说是你写的，马上邮件我啊，我加上你的名字。\n下一步我要开个大坑，我要用Golang 整个大活儿，爬虫之类的我都不想写了，我现在打算用Golang复写我的一些安全工具，或者是写一个Elasticsearch相关的东西，期待吧！\n","permalink":"https://yemilice.com/posts/golang%E4%B8%ADcontext%E4%BD%BF%E7%94%A8%E7%9A%84%E4%B8%80%E7%82%B9%E9%9A%8F%E6%83%B3/","summary":"\u003ch1 id=\"golang中context使用的一点随想\"\u003eGolang中Context使用的一点随想\u003c/h1\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这一篇是三巨头最后一篇了，前两篇介绍了channel，waitgroup，今天这篇来介绍一下context，相比于其他两种，我倒是更推荐context（上下文）这种控制goroutine的方法，为什呢，下面我就来详细的说一说吧。\u003c/p\u003e","title":"Golang中Context使用的一点随想"},{"content":"Golang中WaitGroup使用的一点随想 前言（为什么又要写一篇随想文） 上次我写了一个channel的文章，我寻思，这Golang控制三大巨头，channel，waitgroup，context，我得尽快都安排上，最近工作太忙，压力过大，但是Update Blog还是不能够停下来，所以继续补上，学习还是不能停，那么来吧。\nWaitGroup的简单用法(等待组) 你品一下人家这名字，等待组。等待什么，等待goroutine完成啊。有些时候，我们启动多个goroutine去执行任务，我举个例子\nlistip := []string{\u0026#34;10.0.9.11\u0026#34;,\u0026#34;10.0.9.22\u0026#34;,\u0026#34;10.0.9.33\u0026#34;} for _, ip := range(listip) { //假设我们执行一个ping ip 的逻辑 go PingIPWork(ip) } 我这里执行了一个多ip去ping的逻辑，一般这种时候，你要是执行一波，人家肯定毛都不会返回给你，为什么？因为人家主线程直接就退出了，还是那句话，你又没告诉人家主线程要等这ip全部都ping 完，所以你必须要加个等待，等着Goroutine完成，这里我再举一个网上的例子\npackage main import ( \u0026#34;fmt\u0026#34; ) func main() { go func() { fmt.Println(\u0026#34;Goroutine 1\u0026#34;) }() go func() { fmt.Println(\u0026#34;Goroutine 2\u0026#34;) }() //来个睡眠，等Goroutine结束 time.Sleep(time.Second * 1) } 看到了么，加了一个sleep，用sleep去等着Goroutine跑完，上面我举的那个例子也可以这么来\nlistip := []string{\u0026#34;10.0.9.11\u0026#34;,\u0026#34;10.0.9.22\u0026#34;,\u0026#34;10.0.9.33\u0026#34;} for _, ip := range(listip) { //假设我们执行一个ping ip 的逻辑 go PingIPWork(ip) } time.Sleep(time.Second * 1) 加个sleep可以等待完成，但是万一啊，Goroutine有的跑的快，有的慢，你那sleep就一秒，要是有的Goroutine没跑完不就白瞎了吗，所以咱们需要一个机制，这个机制可以帮助咱们去管理Goroutine，让我们知道Goroutine这东西什么时候停，什么时候完成。所以，WaitGroup这个东西，就可以帮助我们解决这个问题，还是老样子，我举一个简单的例子来说明我的想法。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) func PingIPWork(ip string) { fmt.Println(ip) } func main() { //定义一个等待阿祖 var wg sync.WaitGroup wg.Add(3) // 因为有3个Ip，咱们定义三个动作，所以来三个计数 listip := []string{\u0026#34;10.0.9.11\u0026#34;,\u0026#34;10.0.9.22\u0026#34;,\u0026#34;10.0.9.33\u0026#34;} for _, ip := range(listip) { //假设我们执行一个ping ip 的逻辑 go func(ip string) { //执行一个work PingIPWork(ip) //操作完成之后，done一个计数，也就是3-1 wg.Done() }(ip) } //等待 wg.Wait() // 等待，直到计数为0 } 这里我举了一个简单的例子，其实wg的用法较为简单，在这个例子里面我们用到了\nwg.wait 等待Goroutine结束之后退出主进程 wg.Add 添加Goroutine，其实你可以把它想成，可添加的最大Goroutine数 wg.Done 想象成销毁参数，当Goroutine结束之后调用，意思就是，你没了，我减1 WaitGroup的其他注意事项 将Wg作为参数进行传递的时候，需要使用指针 有些时候，咱们不想写的这么麻烦，就寻思怎么才能简单一点，或者可变性稍微强一点，有些时候我们要把wg最为参数，在函数内部调用，我们该怎么写呢？\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) func PingIPWork(ip string, wg *sync.WaitGroup) { fmt.Println(ip) wg.Done() } func main() { var wg sync.WaitGroup wg.Add(3) // 因为有两个动作，所以增加2个计数 listip := []string{\u0026#34;10.0.9.11\u0026#34;,\u0026#34;10.0.9.22\u0026#34;,\u0026#34;10.0.9.33\u0026#34;} for _, ip := range(listip) { //假设我们执行一个ping ip 的逻辑 go PingIPWork(ip, \u0026amp;wg) } wg.Wait() // 等待，直到计数为0 } 看到了么，如果你把Wg作为参数进行传递，你得要用指针的形式传值，否则就会死锁！！！！！！！！\nWg.Add的数值不能为负 wg.Add()的数值必须为正数，如果为负数，将会抛出异常。\npanic: sync: negative WaitGroup counter goroutine 1 [running]: sync.(*WaitGroup).Add(0xc042008230, 0xffffffffffffff9c) D:/Go/src/sync/waitgroup.go:75 +0x1d0 main.main() D:/code/go/src/test-src/2-Package/sync/waitgroup/main.go:10 +0x54 ","permalink":"https://yemilice.com/posts/golang%E4%B8%ADwaitgroup%E4%BD%BF%E7%94%A8%E7%9A%84%E4%B8%80%E7%82%B9%E9%9A%8F%E6%83%B3/","summary":"\u003ch1 id=\"golang中waitgroup使用的一点随想\"\u003eGolang中WaitGroup使用的一点随想\u003c/h1\u003e\n\u003ch2 id=\"前言为什么又要写一篇随想文\"\u003e前言（为什么又要写一篇随想文）\u003c/h2\u003e\n\u003cp\u003e上次我写了一个channel的文章，我寻思，这Golang控制三大巨头，channel，waitgroup，context，我得尽快都安排上，最近工作太忙，压力过大，但是Update Blog还是不能够停下来，所以继续补上，学习还是不能停，那么来吧。\u003c/p\u003e","title":"Golang中WaitGroup使用的一点随想"},{"content":"Golang中Channel使用的一点随想 前言（为什么要写这篇文章） 在Golang中，搞同步/并发控制的方法有很多，有channel(管道)，WaitGroup(等待线程结束)，context(上下文管理)，我一直想深入研究一下它们，因为这次开发我遇到了很多比较棘手的问题，我认为万变不离其宗，所以我看了一下他们的源码，然后简单的写了几个Demo，结合了我自己的开发经验，写成此文，做记录的同时，希望可以帮到其他兄弟，未来我还会出context随想，waitgroup随想，一点一点来吧。\n什么是channel 首先你要了解两个东西，一个是goroutine，一个是CSP(Communicating Sequential Processes)\ngoroutine：Go协程，比线程小，内存占用小，Go的主打\nCSP:一种模型，并发模型，说白了，就是依赖channel，认为信息的载体更加重要。这里相对来说比较复杂，请大家参考 http://www.usingcsp.com/cspbook.pdf 去学习一个。\n那么channel到底是什么呢，其实就是一种在goroutine之间通用的通信方式，这么理解吧，军队每天都看到人守夜，每天都有不同的口号，比如哨兵a，哨兵b进行交接的时候，就要对暗号，我们理解一下，哨兵a,b是两个goroutine，那么口令就是channel，通过channel，我们可以控制哨兵下岗，上岗，巡逻，那么换算到goroutine中，我们就可以控制goroutine启动，停止，这就是channel的牛逼之处。\nchannel长什么样？（我们怎么定义channel） channel的定义方法非常简单，利用chan 关键字就可以\ntest := make(chan bool) 这里的make，没有定义缓存值，channel可以定义一个缓存值，例如\nmake(chan int, 100) 这里代表channel 的缓存大小为100，如果不设置缓存值，那么channel没有缓存，在没有通讯的情况下，将被阻塞。\n是的，这个就定义好了，现在你就有一个名叫test的bool类型的channel，你可能会问，卧槽，这东西有什么用？行，我马上就举个例子告诉你这玩意儿怎么使。我知道你们大部分都不爱看理论，只爱看解决问题的模型，没问题，我惯你！\n来个channel的并发例子 你细分析分析，一般你什么时候会用到goroutine间通信？那不就是一个不够使，多开几个，增加咱们program的效率嘛。是，你可以用 go example() 这种类型，开它十几二十个，但是你怎么知道他们结束了呢？估摸着你写sleep啊，就像\nlistip := []string{\u0026#34;10.0.9.11\u0026#34;,\u0026#34;10.0.9.22\u0026#34;,\u0026#34;10.0.9.33\u0026#34;} for _, ip := range(listip) { //假设我们执行一个ping ip 的逻辑 go PingIPWork(ip) } time.sleep(time.Second * 5) 这样其实没啥毛病，因为你不加那个sleep，人家估摸着就会一闪而过，你什么消息都收不到，我们这里有三个Ip，相当于你 go PingIPWork(ip)这里执行，外部的主程会直接退出去，人家才不管你搞完没呢，你又没和人家说是吧，所以channel就是干这个的，就是告诉主程，里面还有人呢嘿，别关门！\nlistip := []string{\u0026#34;10.0.9.11\u0026#34;,\u0026#34;10.0.9.22\u0026#34;,\u0026#34;10.0.9.33\u0026#34;} ch := make(chan struct{}) for _, ip := range listip { go func(ip string) { //假设执行一个ping ip的逻辑 go PingIPWork(ip) ch \u0026lt;- struct{}{} }(ip) } for range ips { \u0026lt;-ch } 这里去并发去执行PingIPWork，并且主程会等待所有goroutine完成之后才会彻底退出。发现了没，channel和goroutine一般都是放在一起的。\n在for\u0026hellip;i range中使用channel 有时候我们需要阻塞range，或者是控制循环不退出，这时候就可以用到channel\nlistip := [\u0026#34;10.0.9.11\u0026#34;,\u0026#34;10.0.9.22\u0026#34;,\u0026#34;10.0.9.33\u0026#34;] //创建channel forever := make(chan bool) go func() { for _, d := range msgs { log.Printf(\u0026#34;Received a message: %s\u0026#34;, d) } }() //阻塞，让它不退出 \u0026lt;-forever 在select中使用channel 类似switch的骚操作，一般来说，你给人家整死循环了，你不得负责给人家搞退出，或者说满足必要条件退出\n// 创建 quit channel quit := make(chan string) // 启动生产者 goroutine c := boring(\u0026#34;Joe\u0026#34;, quit) // 从生产者 channel 读取结果 for i := rand.Intn(10); i \u0026gt;= 0; i-- { fmt.Println(\u0026lt;-c) } // 通过 quit channel 通知生产者停止生产 quit \u0026lt;- \u0026#34;Bye!\u0026#34; fmt.Printf(\u0026#34;Joe says: %q\\n\u0026#34;, \u0026lt;-quit) select { case c \u0026lt;- fmt.Sprintf(\u0026#34;%s: %d\u0026#34;, msg, i): fmt.Println(\u0026#34;work....\u0026#34;) case \u0026lt;-quit: quit \u0026lt;- \u0026#34;See you!\u0026#34; return } 有些时候我们不想等那么久，所以我们需要（timeout）机制\nc1 := make(chan string, 1) go func() { time.Sleep(time.Second * 2) c1 \u0026lt;- \u0026#34;result 1\u0026#34; }() select { case res := \u0026lt;-c1: fmt.Println(res) case \u0026lt;-time.After(time.Second * 1): fmt.Println(\u0026#34;timeout 1\u0026#34;) } 关闭一个channel 不用的东西要打包放好，否则你开一大堆channel在那里，搞一大堆panic: send on closed channel很吼么？ 随意关闭channel的姿势你也要学到，其实关闭的逻辑也很简单，关键字close\nc := make(chan int, 10) c \u0026lt;- 1 close(c) 这就关掉了，但是这样关不严谨，你还是能拿到c已经发出去的数据，而且还能不断的读到0值，所以一定要换个方法\nc := make(chan int, 10) close(c) i, ok := \u0026lt;-c fmt.Printf(\u0026#34;%d, %t\u0026#34;, i, ok) 这样就能判断，当close时，读到的值是零值还是正常值，也就避免了上面出现的那种情况，一直读，一直有。\n后记 我感觉我学的还是不够深，我希望我的技术能更进一步，所以我还是要不断学习，我可以的，我会做到的。这里面其实都是些皮毛。。我感觉，我希望未来一定要多学多读多看。哈哈，这篇文章写完了我也要去吃饭了，今天我吃仔肺粉 + 锅盔，我去吃饭啦！有问题给我留言或者邮件吧！\n写于2020-05-25 19:36分\n","permalink":"https://yemilice.com/posts/golang%E4%B8%ADchannel%E4%BD%BF%E7%94%A8%E7%9A%84%E4%B8%80%E7%82%B9%E9%9A%8F%E6%83%B3/","summary":"\u003ch1 id=\"golang中channel使用的一点随想\"\u003eGolang中Channel使用的一点随想\u003c/h1\u003e\n\u003ch2 id=\"前言为什么要写这篇文章\"\u003e前言（为什么要写这篇文章）\u003c/h2\u003e\n\u003cp\u003e在Golang中，搞同步/并发控制的方法有很多，有channel(管道)，WaitGroup(等待线程结束)，context(上下文管理)，我一直想深入研究一下它们，因为这次开发我遇到了很多比较棘手的问题，我认为万变不离其宗，所以我看了一下他们的源码，然后简单的写了几个Demo，结合了我自己的开发经验，写成此文，做记录的同时，希望可以帮到其他兄弟，未来我还会出context随想，waitgroup随想，一点一点来吧。\u003c/p\u003e","title":"Golang中Channel使用的一点随想"},{"content":"分布式数据库如何选择？几种分布式数据库优缺点一览 为什么选择分布式数据库？ 优点如下：\n具有灵活的体系结构 适应分布式的管理和控制机构 经济性能优越 系统的可靠性高、可用性好 局部应用的响应速度快 可扩展性好，易于集成现有系统。 相关的技术概念介绍 什么是分布式数据库？ 常见的分布式系统分为，\n支持持久化存储的分布式存储系统 着重计算的分布式计算框架 分布式消息队列 根据不同的应用的领域，把上述分类细化，常见分布式存储系统分为：\n分布式协同系统（分布式日志复制） 分布式任务调度框架 流计算框架 分布式文件/对象系统 分布式NoSQL存储 分布式关系数据库（OLAP、OLTP）； 各种消息队列mq 不同的分布式数据库如何区分？ Key-value NoSQL : 例如Redis Riak等；\ncolumn family NoSQL(wide column store) : 典型的是Hbase Cassandra\ndocument NoSQL : 典型的是mongodb\n需要什么样的数据库? 支持数据持久化，数据落盘，异常备份，高并发，大数据量存储。 要支持频繁的数据读写 分布式，多节点并行 和以前的数据库不冲突 可选的方法及其特点 根据上述的要求，分布式数据库，符合大数据存储的，支持频繁读写的数据库有如下几个，它们的特点会简单说明。\nElasticsearch数据库 Elasticsearch简介 分布式的实时文件存储，每个字段都被索引并可被搜索，分布式的实时分析搜索引擎\n可以扩展到上百台服务器，处理PB级结构化或非结构化数据\nElasticsearch应用场景 分布式的搜索引擎和数据分析引擎，全文检索，结构化检索，数据分析\n对海量数据进行近实时的处理，站内搜索（电商，招聘，门户，等等），IT系统搜索（OA，CRM，ERP，等等），数据分析\nElasticsearch的优缺点 缺点：没有用户验证和权限控制，没有事务的概念，不支持回滚，误删不能恢复，需要java环境.\n优点：将你的文档分割到不同容器或者分片中，可以存在单个节点或多个节点复制每个分片提供数据备份，防止硬件问题导致数据丢失。\nElasticsearch的持久化方案 gateway 代表 elasticsearch 索引的持久化存储方式，elasticsearch 默认是先把索引存放到内存中去，当内存满了的时候再持久化到硬盘里。当这个 elasticsearch 集群关闭或者再次重新启动时就会从 gateway 中读取索引数据。elasticsearch 支持多种类型的 gateway，有本地文件系统（默认），分布式文件系统，Hadoop 的 HDFS 和 amazon 的 s3 云存储服务。\nElasticSearch是先把索引的内容保存到内存之中，当内存不够时再把索引持久化到硬盘中，同时它还有一个队列，是在系统空闲时自动把索引写到硬盘中。\nRedis数据库 Redis简介 redis是开源BSD许可高级的key-value存储系统(NoSQL)，可以用来存储字符串，哈希结构，链表，集合，因此，常用来提供数据结构服务，Redis支持数据的持久化，可以将内存中的数据保存在磁盘中，重启的时候可以再次加 载进行使用。 支持简单的key-value类型的数据，同时还提供list，set，zset，hash等数据结构的存储。Redis支持数据的备份，即master-slave模式的数据备份。\nRedis应用场景 A）常规计数：粉丝数，微博数\nB）用户信息变更\nC）缓存处理，作为mysql的缓存\nD）队列系统，建有优先级的队列系统，日志收集系统\nRedis的优缺点 优点 (1) 速度快，因为数据存在内存中，类似于HashMap，HashMap的优势就是查找和操作的时间复杂度都是O(1)\n(2) 支持丰富数据类型，支持string，list，set，sorted set，hash\n(3) 支持事务，操作都是原子性，所谓的原子性就是对数据的更改要么全部执行，要么全部不执行\n(4) 丰富的特性：可用于缓存，消息，按key设置过期时间，过期后将会自动删除\n缺点 （1）Redis不具备自动容错和恢复功能，主机从机的宕机都会导致前端部分读写请求失败，需要等待机器重启或者手动切换前端的IP才能恢复\n（2）主机宕机，宕机前有部分数据未能及时同步到从机，切换IP后还会引入数据不一致的问题，降低了系统的可用性\n（3）redis的主从复制采用全量复制，复制过程中主机会fork出一个子进程对内存做一份快照，并将子进程的内存快照保存为文件发送给从机，这一过程需要确保主机有足够多的空余内存。若快照文件较大，对集群的服务能力会产生较大的影响，而且复制过程是在从机新加入集群或者从机和主机网络断开重连时都会进行，也就是网络波动都会造成主机和从机间的一次全量的数据复制，这对实际的系统运营造成了不小的麻烦\n（4）Redis较难支持在线扩容，在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题，运维人员在系统上线时必须确保有足够的空间，这对资源造成了很大的浪费。\nRedis的持久化方案 redis提供两种方式进行持久化，一种是RDB持久化（原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化），另外一种是AOF（append only file）持久化（原理是将Reids的操作日志以追加的方式写入文件）。\nRDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘，实际操作过程是fork一个子进程，先将数据集写入临时文件，写入成功后，再替换之前的文件，用二进制压缩存储。\nMongodb数据库 Mongodb简介 MongoDB本身是一种非关系型数据库。它的每一条记录是一个Document，每个Document有一组键值对组成。MongoDB中的Document与JSON对象相似。 Document中字段的值可能包括其他Document，数组等。\nMongodb应用场景 mongodb的主要目标是在键/值存储方式（提供了高性能和高度伸缩性）以及传统的RDBMS系统（丰富的功能）架起一座桥梁，集两者的优势于一身。mongo适用于以下场景：\na.网站数据：mongo非常适合实时的插入，更新与查询，并具备网站实时数据存储所需的复制及高度伸缩性。\nb.缓存：由于性能很高，mongo也适合作为信息基础设施的缓存层。在系统重启之后，由mongo搭建的持久化缓存可以避免下层的数据源过载。\nc.大尺寸、低价值的数据：使用传统的关系数据库存储一些数据时可能会比较贵，在此之前，很多程序员往往会选择传统的文件进行存储。\nd.高伸缩性的场景：mongo非常适合由数十或者数百台服务器组成的数据库。\ne.用于对象及JSON数据的存储：mongo的BSON数据格式非常适合文档格式化的存储及查询。\nMongodb的优缺点 优点：\n(1) 弱一致性（最终一致），更能保证用户的访问速度\n(2) 文档结构的存储方式，能够更便捷的获取数据\n(3) 内置GridFS，支持大容量的存储\n(4) 在使用场合下，千万级别的文档对象，近10G的数据，对有索引的ID的查询不会比mysql慢，而对非索引字段的查询，则是全面胜出。\n缺点：\n（1）不支持事物\n（2）占用空间过大，会造成磁盘浪费\n（3）单机可靠性比较差\n（4）大数据量持续插入，写入性能有较大波动\nMongodb的持久化方案/异常处理 当执行写操作时，MongoDB创建一个journal来包含确切磁盘位置和改变的字节。因此，如果服务器突然崩溃，启动时，journal会重放崩溃前并没有刷新到磁盘上的任何写操作。\n数据文件每隔60s刷新到磁盘上，默认情况下，因此journal只需要持有60s内的写入数据。journal预分配了几个空文件用于此目的，位于/data/db/journal，命名为_j.0,j.1等等。\nMongoDB运行很长时间情况下，在journal目录下，你会看到类似于_j.6217,_j.6218和_j.6219文件。这些文件是当前的journal文件，如果MongoDB一直运行，这些数字会持续增加。当正常关闭MongoDB时，这些文件将被清除，因为正常关机不在需要这些日志的。\n如果服务器崩溃或kill -9, mongodb再次启动时，会重放journal文件，会输出冗长难懂的检验行，这表明在正常的恢复。\nMysql分布式集群 Mysql分布式集群简介 MySQL集群是一个无共享的(shared-nothing)、分布式节点架构的存储方案，其目的是提供容错性和高性能。\n数据更新使用读已提交隔离级别（read-committedisolation)来保证所有节点数据的一致性，使用两阶段提交机制（two-phasedcommit)保证所有节点都有相同的数据(如果任何一个写操作失败，则更新失败）。\n无共享的对等节点使得某台服务器上的更新操作在其他服务器上立即可见。传播更新使用一种复杂的通信机制，这一机制专用来提供跨网络的高吞吐量。\n通过多个MySQL服务器分配负载，从而最大程序地达到高性能，通过在不同位置存储数据保证高可用性和冗余。\nMysql分布式集群应用场景 解决海量存储问题，比如京东B2B就用的Mysql分布式集群。\n适用几十亿的PV对DB的访问。\nMysql分布式集群的优缺点 优点：\na) 高可用性\nb)快速的自动失效切换\nc)灵活的分布式体系结构，没有单点故障\nd)高吞吐量和低延迟\ne)可扩展性强，支持在线扩容\n缺点：\na)存在很多限制，比如：不支持外键\nb)部署、管理、配置很复杂\nc)占用磁盘空间大，内存大\nd)备份和恢复不方便\ne)重启的时候，数据节点将数据load到内存需要很长时间\nMysql分布式集群的持久化方案 负载均衡。\n管理节点备份。\n","permalink":"https://yemilice.com/posts/%E5%88%86%E5%B8%83%E5%BC%8F%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9%E5%87%A0%E7%A7%8D%E5%88%86%E5%B8%83%E5%BC%8F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BC%98%E7%BC%BA%E7%82%B9%E4%B8%80%E8%A7%88/","summary":"\u003ch1 id=\"分布式数据库如何选择几种分布式数据库优缺点一览\"\u003e分布式数据库如何选择？几种分布式数据库优缺点一览\u003c/h1\u003e\n\u003ch2 id=\"为什么选择分布式数据库\"\u003e为什么选择分布式数据库？\u003c/h2\u003e\n\u003cp\u003e优点如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e具有灵活的体系结构\u003c/li\u003e\n\u003cli\u003e适应分布式的管理和控制机构\u003c/li\u003e\n\u003cli\u003e经济性能优越\u003c/li\u003e\n\u003cli\u003e系统的可靠性高、可用性好\u003c/li\u003e\n\u003cli\u003e局部应用的响应速度快\u003c/li\u003e\n\u003cli\u003e可扩展性好，易于集成现有系统。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"相关的技术概念介绍\"\u003e相关的技术概念介绍\u003c/h2\u003e\n\u003ch3 id=\"什么是分布式数据库\"\u003e什么是分布式数据库？\u003c/h3\u003e\n\u003cp\u003e常见的分布式系统分为，\u003c/p\u003e","title":"分布式数据库如何选择，几种分布式数据库优缺点一览"},{"content":"开发中常用的Golang高级用法 前言 忙碌了两个月，这次开发终于要结束了，今天下午公司在重组集群机器，也没办法干活儿了，就写一些东西，相当于，留住一些东西，来纪念这辛苦的两个月吧。做一个纪念，也是为了方便以后自己去查看。在这次开发中，学习了不少Golang的高级特性，并且付诸于实现，也踩了不少坑，留下这篇文字，也是方便其他人能够查看，或者借鉴，如果帮到你，那么我也会很开心你。\n开发常遇到的问题 Golang判断一个元素在不在切片/列表当中 在Python中，我们可以直接用 in 的方式去判断，例如\nif \u0026#34;i\u0026#34; in lists 但是在Golang中，没有这种语法糖或者是关键字可以帮助我们处理这种问题，所以还是只能靠循环去处理这种问题，为此我封装了一个Golang函数，函数如下\n//FindType 循环对比，匹配到返回true，不匹配返回false func FindType(a string, typelist []string) bool { for _, b := range typelist { if b == a { return true } } return false } Golang获取文件的详细信息 Golang获取文件信息的方法相对来说容易一些，都已经有了对应的package，我这里只是把怎么用展示出来\n//timespecToTime 转换 func timespecToTime(ts syscall.Timespec) time.Time { return time.Unix(int64(ts.Sec), int64(ts.Nsec)) } //GetFileinfo 获取文件信息 func GetFileinfo(path string) { fileInfo, err := os.Stat(path) if err != nil { return 0 } //文件大小 filesize := fileInfo.Size() //文件创建时间 stat_ts := fileInfo.Sys().(*syscall.Stat_t) Ctime := timespecToTime(stat_ts.Ctim).Format(\u0026#34;2006/01/02\u0026#34;) //文件修改时间 Mtime := timespecToTime(stat_ts.Mtim).Format(\u0026#34;2006/01/02\u0026#34;) //文件访问时间 Attim := timespecToTime(stat_ts.Atim).Format(\u0026#34;2006/01/02\u0026#34;) //获取文件所有者 stat_ts := fileInfo.Sys().(*syscall.Stat_t) uid := strconv.Itoa(int(stat_ts.Uid)) usrs, err := user.LookupId(string(uid)) username := usrs.Username //获取文件名 filename := fileInfo.Name() } Golang比较两个list/切片的不同之处（差集） 在开发中，我需要比较两个list，然后取出他们之中不同的部分，这里Golang也没有合适的法子，一般的方法就是转map进行处理\n//difference 进行比对，输出不同的[]string func difference(slice1, slice2 []string) []string { m := make(map[string]int) nn := make([]string, 0) inter := intersect(slice1, slice2) for _, v := range inter { m[v]++ } for _, value := range slice1 { times, _ := m[value] if times == 0 { nn = append(nn, value) } } return nn } //intersect 把两个对比的列表进行map化处理 func intersect(slice1, slice2 []string) []string { m := make(map[string]int) nn := make([]string, 0) for _, v := range slice1 { m[v]++ } for _, v := range slice2 { times, _ := m[v] if times == 1 { nn = append(nn, v) } } return nn } func main() { infinode := []string{\u0026#34;10.0.9.1\u0026#34;,\u0026#34;10.0.9.2\u0026#34;} syncnodes := []string{\u0026#34;10.0.9.1\u0026#34;,\u0026#34;10.0.9.2\u0026#34;, \u0026#34;10.0.9.3\u0026#34;} ips := difference(infinode, syncnodes) } Golang格式化时间 golang 一般都可以通过Format的方法进行时间格式化，一般你可以直接调format，例如\nfmt.Println(time.Now().Format(\u0026#34;2006-01-02 15:04:05\u0026#34;)) 有些时候是他娘的时间戳\nfmt.Println(time.Unix(1389028339, 0).Format(\u0026#34;2006-01-02 15:04:05\u0026#34;)) 有些时候会让你搞成别的样子，比如\u0026quot;2006/01/02 15:04:05\u0026quot;\nfmt.Println(time.Now().Format(\u0026#34;2006/01/02 15:04:05\u0026#34;)) 有些时候会让你比较个时间大小，例如\n//比对时间，看看它在不在开始时间/结束时间的范围内 starttime := \u0026#34;2020-05-12\u0026#34; endtime := \u0026#34;2020-05-20\u0026#34; ctime := \u0026#34;2020-05-15\u0026#34; ft, err := time.Parse(\u0026#34;2006-01-02\u0026#34;, ctime) st, err := time.Parse(\u0026#34;2006-01-02\u0026#34;, starttime) et, err := time.Parse(\u0026#34;2006-01-02\u0026#34;, endtime) if ft.After(et) \u0026amp;\u0026amp; ft.Before(st) { fmt.Println(\u0026#34;在里面儿！\u0026#34;) } else { fmt.Println(\u0026#34;不在里面儿！\u0026#34;) } Golang并发循环的使用 我们处理循环的时候，在Python中，我举个例子\nlists = [1, 2, 3, 4] for i in lists: print i 这里打印的话是\n1 2 3 4 Golang有个黑科技是并发循环，简单说，就是能一下给你把这四个都给你打印出来，不是一条条循环，也不用等上一个循环的结果返回，也可以选择略过错误。我在这里简单实现一个\n直接并发循环（无需考虑错误） 这里不考虑error的情况，具体代码如下\nips := []string{\u0026#34;10.0.9.1\u0026#34;,\u0026#34;10.0.9.2\u0026#34;} ch := make(chan struct{}) //并发循环ip for _, ip := range ips { go func(ip string) { //模拟一个展示ip fmt.Println(ip) ch \u0026lt;- struct{}{} }(ip) } for range ips { \u0026lt;-ch } 输出错误的并发循环(考虑错误) ips := []string{\u0026#34;10.0.9.1\u0026#34;,\u0026#34;10.0.9.2\u0026#34;} //定义一个error errors := make(chan error) for _, ip := range ips { go func(ip string) { _, err := test(ip) errors \u0026lt;- err }(ip) } for range ips { if err := \u0026lt;-errors; err != nil { return err } } Golang做一个不退出的无限循环 有些时候我们希望一个服务/脚本/函数不断运行，或者每隔一段时间来一发（运行一次），这时候我们就需要定义无限循环\nfunc main() { for { //每隔1s打印一次start work time.Sleep(time.Second * 1) fmt.Println(\u0026#34;Start work......\u0026#34;) } } Golang实现一个遇到错误的重试机制 当我们最开发的时候，有些时候遇到错误，需要进行重试，这时候我们就需要进行错误捕获和函数重载\n//Retry 重试逻辑 //传入函数，如果捕获到错误，则重载函数，重新来一次执行，直到没错误为止 func Retry(fn func() error) error { if err := fn(); err != nil { if s, ok := err.(stop); ok { return s.error } return Retry(fn) } return nil } //TestWork 测试函数，无意义 func TestWork() error { //测试函数，这里是伪代码。 _, err := GetIpNode() if err != nil { return err } return nil } func main() { go Retry(TestWork) } Golang执行Linux命令 有些时候我们需要执行linux相关命令，Golang封装了相应的包，但是少部分时候我们需要控制一些很久不返回结果的命令，所以我加了一个超时时间，代码如下\nvar ( //这里我写死了，你可以自己定义，更灵活一些 Timeout = 60 * time.Second ) //Command 执行shell func Command(arg string) ([]byte, error) { ctxt, cancel := context.WithTimeout(context.Background(), Timeout) defer cancel() cmd := exec.CommandContext(ctxt, \u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, arg) var buf bytes.Buffer cmd.Stdout = \u0026amp;buf cmd.Stderr = \u0026amp;buf if err := cmd.Start(); err != nil { return buf.Bytes(), err } if err := cmd.Wait(); err != nil { return buf.Bytes(), err } return buf.Bytes(), nil } func main() { _, err := Command(\u0026#34;ls\u0026#34;) if err != nil { fmt.Println(err) } } Golang 简单的日志逻辑 这边贡献一个简单的日志实现代码，往文件里面写，可以自己定义格式，代码如下\nimport ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/op/go-logging\u0026#34; ) func Monlog() (logs *logging.Logger) { var log = logging.MustGetLogger(\u0026#34;monlog\u0026#34;) //定义格式 时间 go文件 行数 等级 错误信息 var format = logging.MustStringFormatter( `%{time:2006-01-02T15:04:05} %{shortfile} %{shortfunc} %{level} %{message}`) //文件的写入位置 追加模式/没有自动创建 logFile, err := os.OpenFile(\u0026#34;/var/log/monlog.log\u0026#34;, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) if err != nil { panic(fmt.Sprintf(\u0026#34;Open faile error!\u0026#34;)) } //头相关, 具体意思就是支持追加，然后格式化，且按照那个格式往里面写 backend1 := logging.NewLogBackend(logFile, \u0026#34;\u0026#34;, 0) backend2 := logging.NewLogBackend(os.Stderr, \u0026#34;\u0026#34;, 0) backend2Formatter := logging.NewBackendFormatter(backend2, format) backend1Formatter := logging.NewBackendFormatter(backend1, format) backend1Leveled := logging.AddModuleLevel(backend1Formatter) backend1Leveled.SetLevel(logging.INFO, \u0026#34;\u0026#34;) logging.SetBackend(backend1Leveled, backend2Formatter) return log } func main() { log := Monlog() log.Info(\u0026#34;info\u0026#34;) log.Notice(\u0026#34;notice\u0026#34;) log.Warning(\u0026#34;warning\u0026#34;) } Golang 使用 Aws-sdk 获取到指定bucket中全部的item 我前阵子写过一篇文章，是Golang 调用 s3对象存储的，使用指定的api可以获取其中的item，但是人家限定100条，估摸着怕撑爆内存，但是如果我们想要获取到所有的item，这个该怎么做呢？其实循环获取就好了，但是我还是徒手实现一下吧，大家抄抄得了。\nfunc GetRgwItem3(buckets string) ([]*s3.Object, error) { var items []*s3.Object sess, err := session.NewSession(\u0026amp;aws.Config{ Credentials: credentials.NewStaticCredentials(ak, sk, \u0026#34;\u0026#34;), Endpoint: aws.String(endpoint + \u0026#34;:7480\u0026#34;), Region: aws.String(\u0026#34;us-east-1\u0026#34;), DisableSSL: aws.Bool(true), S3ForcePathStyle: aws.Bool(false), //virtual-host style方式，不要修改 }) if err != nil { return nil, err } svc := s3.New(sess) err := svc.ListObjectsPages(\u0026amp;s3.ListObjectsInput{ Bucket: \u0026amp;buckets, }, func(p *s3.ListObjectsOutput, last bool) (shouldContinue bool) { for _, obj := range p.Contents { items = append(items, obj) } return false }) if err != nil { return items, err } return items, nil } 我这里没毛病啊，万一你有问题你就咨询我\n后记 暂时先更这么多，这也不少了，大部分代码我都给出了具体实现，要是还不会就留言给我或者发邮件。\n其实我特想对那些写几个爬虫在那里爬我博客的人说，你偷博客没所谓，复制也没所谓，不加名字说转载就很说不过去了，还TM标 榜你是原创，脸呢？cnblog我会逐渐弃掉，因为我有我自己的博客了，我也在逐渐搞迁移，把我原来写的博客都迁移到我自己的博客上去，未来我的cnblog博客将逐渐少更或者停更，就算更我也会用英语/日语双更，我让你TM抄我东西不说，fuck off bitch。\n","permalink":"https://yemilice.com/posts/%E5%BC%80%E5%8F%91%E4%B8%AD%E5%B8%B8%E7%94%A8%E7%9A%84golang%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/","summary":"\u003ch1 id=\"开发中常用的golang高级用法\"\u003e开发中常用的Golang高级用法\u003c/h1\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e忙碌了两个月，这次开发终于要结束了，今天下午公司在重组集群机器，也没办法干活儿了，就写一些东西，相当于，留住一些东西，来纪念这辛苦的两个月吧。做一个纪念，也是为了方便以后自己去查看。在这次开发中，学习了不少Golang的高级特性，并且付诸于实现，也踩了不少坑，留下这篇文字，也是方便其他人能够查看，或者借鉴，如果帮到你，那么我也会很开心你。\u003c/p\u003e","title":"开发中常用的Golang高级用法"},{"content":"前言（为什么要写这篇文章） 首先看过我博客的都应该知道，我去年发了一篇Python封装Elasticsearch的文章。但那是去年了，今年我将我的检索服务后端用Golang全部重写了一波，相当于用Go重构了以前的Python代码，不过我个人感觉Golang的效率还是高于Python的，而且我还加了一些异常判断和处理，这次的代码只会比以前更好更牛逼，为了纪念这一个多月的重构历程，我将关键功能记录下来，方便自己复习和各位兄弟姐妹查看。\n使用的Go包 我的Elasticsearch版本是6.3.2，6系列了，现在（2020-05-13）最新版本应该是7，不过新版本和旧版本应该就是少了Type，我的6版本代码，请各位自己自行斟酌使用。\n安装指定的Go包 olivere/elastic，现在有官方驱动的包了，但是我这篇文章用的包是 olivere/elastic，所以一切的代码都是以olivere为主。\ngo get github.com/olivere/elastic 基础的使用（Simple的使用） 接下来我举几个例子来说下这个golang 如何驱动 elasticsearch的\n连接Elasticserach package elasticdb import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/olivere/elastic\u0026#34; ) func main() { //连接127.0.0.1 client, err := elastic.NewClient(elastic.SetURL(\u0026#34;http://127.0.0.1:9200\u0026#34;)) if err != nil { fmt.Println(err) return } //检查健康的状况，ping指定ip，不通报错 _, _, err = client.Ping(ip).Do(context.Background()) if err != nil { fmt.Println(err) return } } 创建一个index索引 这里我默认你们都有一定的Es基础，其实你把index想成Mysql里面的表就可以了。\n//indexname 你可以想成表名 func CreateIndex(indexname string) { client, err := elastic.NewClient(elastic.SetURL(\u0026#34;http://127.0.0.1:9200\u0026#34;)) if err != nil { fmt.Println(err) return } client.CreateIndex(indexname).Do(context.Background()) } 删除一个index索引 想成删除一张表\n//indexname 你可以想成表名 func CreateIndex(indexname string) { client, err := elastic.NewClient(elastic.SetURL(\u0026#34;http://127.0.0.1:9200\u0026#34;)) if err != nil { fmt.Println(err) return } client.DeleteIndex(indexname).Do(context.Background()) } 往指定的index当中导一条数据 想成往一张表里面导入一条数据，在Golang中，我们可以导入json的字符串，我们也可以导入golang的struct类型，例如\n//结构体 type Task struct { Taskid string `json:\u0026#34;taskid\u0026#34;` Taskname string `json:\u0026#34;taskname\u0026#34;` } //字符串 jsonmsg := `{\u0026#34;taskid\u0026#34;:\u0026#34;123456\u0026#34;, \u0026#34;taskname\u0026#34;:\u0026#34;lwb\u0026#34;}` //导入数据，你需要index名，index的type，导入的数据 func PutData(index string, typ string, bodyJSON interface{}) bool { client, _ := elastic.NewClient(elastic.SetURL(\u0026#34;http://127.0.0.1:9200\u0026#34;)) _, err := client.Index(). Index(index). Type(typ). BodyJson(bodyJSON). Do(context.Background()) if err != nil { //验证是否导入成功 fmt.Sprintf(\u0026#34;\u0026lt;Put\u0026gt; some error occurred when put. err:%s\u0026#34;, err.Error()) return false } return true } func main() { //json字符串导入 jsonmsg = `{\u0026#34;taskid\u0026#34;:\u0026#34;123456\u0026#34;, \u0026#34;taskname\u0026#34;:\u0026#34;hahah\u0026#34;}` status := PutData(\u0026#34;test\u0026#34;, \u0026#34;doc\u0026#34;, jsonmsg) //struct结构体导入 task := Task{Taskid: \u0026#34;123\u0026#34;, Taskname: \u0026#34;hahah\u0026#34;} status := PutData(\u0026#34;test\u0026#34;, \u0026#34;doc\u0026#34;, task) } 删除一条数据 删除数据需要ID，这个ID是个啥玩意儿呢。。。就是咱们不是刚导了一条数据进去么，你可以设置这数据的唯一ID，也可以让Elasticsearch帮你自动生成一个，一般没事儿干谁自己设置啊，还容易重复，一重复就报错。。我在这里把这个删除的方法教给大家，记住这个ID一定是唯一的\nfunc DeleteData(index, typ, id string) { client, _ := elastic.NewClient(elastic.SetURL(\u0026#34;http://127.0.0.1:9200\u0026#34;)) _, err := client.Delete().Index(index).Type(typ).Id(id).Do(context.Background()) if err != nil { fmt.Println(err) return } } 高阶使用（条件查询／封装等） 简单讲了一下增删改，现在我们来讲一下高阶用法，高级增删改查吧，其实官方的文档讲的还算是比较清楚，不过我等大中华程序狗的姿势水平。。。至少我是图样图森破，看英文也算是废了九牛二虎之力，算是捋出来了一些高阶用法，顺手自己造了个轮子，现在我就来挑几点来说下吧。\n自动选择可用的Es节点 配合olivere的ping机制，可以做一个自动检测Es ip 是否可用的逻辑，这样可以增加我们put，updatge时候的稳定性\n自动检测节点Elasticseach的IP是否可用 olivere的Elasticserach sdk 限定了只能用一个ip，类似“http://127.0.0.1:9200”这样，我对原本的逻辑进行了一点改造，改成支持一个ip list，依次检测Es ip 是否可用\npackage elasticdb import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/olivere/elastic\u0026#34; ) //Elastic es的连接， type Elastic struct { Client *elastic.Client host string } //Connect 基础的连接代码 func Connect(ip string) (*Elastic, error) { //引入IP client, err := elastic.NewClient(elastic.SetURL(ip)) if err != nil { return nil, err } //Ping 的方式检查是否可用 _, _, err = client.Ping(ip).Do(context.Background()) if err != nil { return nil, err } //输出一个struct类型，可以被继承 es := \u0026amp;Elastic{ Client: client, host: ip, } return es, nil } //InitES 初始化Es连接 func InitES() (*Elastic, error) { //host是一个列表 host := []string{\u0026#34;http://10.0.6.245:9200\u0026#34;,\u0026#34;http://10.0.6.246:9200\u0026#34;,\u0026#34;http://10.0.6.247:9200\u0026#34;} //统计host的数量 Eslistsnum := len(host) //如果为零就不继续接下来的逻辑 if Eslistsnum == 0 { return nil, fmt.Errorf(\u0026#34;Cluster Not Es Node\u0026#34;) } //创建新的连接 for i, ip := range host { //判断是不是最后一个节点ip if (Eslistsnum - 1) != i { es, err := Connect(ip) //如果连接出错，则跳过 if err != nil { fmt.Println(err) continue } return es, nil //如果是最后一个节点 } else { es, err := Connect(ip) //输出错误 if err != nil { return nil, err } return es, nil } } return nil, nil } 后续我们可以采用继承的方法调用Es的client的连接，这个在后面我就不详细说了，聪明的你，看代码一定能整明白，再整不明白，你就直接上Git拷贝我的代码就得了。\n条件查询 以前我写过一个Python的Elasticsearch Sdk，那里面的查询基本都用了query，简单来说，就是你，给Es的api发一个query，es给你返回一个查询结果。这里我会举几个常用的条件查询例子，然后用golang封装一波。\n这里我先定义一下数据结构，假设我们的Elasticsearch中，有一个叫做Task的index(索引)，其中存储着很多task的运行日志，它们的数据格式如下:\n{ \u0026#34;taskid\u0026#34;: \u0026#34;081c255b-936c-11ea-8001-000000000000\u0026#34;, \u0026#34;starttime\u0026#34;: \u0026#34;2020/05/13 18:38:21\u0026#34;, \u0026#34;endtime\u0026#34;: \u0026#34;2020/05/13 18:38:47\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;cifs01\u0026#34;, \u0026#34;status\u0026#34;: 1, \u0026#34;count\u0026#34;: 365 } 我们现在要做的就是围绕task这个index和其中的数据做条件查找的例子，我说的很明白了吧？开工了！\n查询时间范围/年龄大小的条件查询方法 在业务需求中，我们经常会检索各种各样的数据，其中，范围查找应该是用的比较多的，所以我把它放到了最前面。\ntype Task struct { TaskID string `json:\u0026#34;taskid\u0026#34;` StartTime string `json:\u0026#34;starttime\u0026#34;` EndTime string `json:\u0026#34;endtime\u0026#34;` Name string `json:\u0026#34;name\u0026#34;` Status int `json:\u0026#34;status\u0026#34;` Count int `json:\u0026#34;count\u0026#34;` } //查找时间范围大于2020/05/13 18:38:21，并且小于2020/05/14 18:38:21的数据 func (Es *Elastic) FindTime() { var typ Task boolQ := elastic.NewBoolQuery() //生成查询语句，筛选starttime字段，查找大于2020/05/13 18:38:21，并且小于2020/05/14 18:38:21的数据 boolQ.Filter(elastic.NewRangeQuery(\u0026#34;starttime\u0026#34;).Gte(\u0026#34;2020/05/13 18:38:21\u0026#34;), elastic.NewRangeQuery(\u0026#34;starttime\u0026#34;).Lte(\u0026#34;2020/05/14 18:38:21\u0026#34;)) res, _ := Es.Client.Search(\u0026#34;task\u0026#34;).Type(\u0026#34;doc\u0026#34;).Query(boolQ).Do(context.Background()) //从搜索结果中取数据的方法 for _, item := range res.Each(reflect.TypeOf(typ)) { if t, ok := item.(Task); ok { fmt.Println(t) } } } 查询包含关键字的查询方法 我们经常遇到那种，搜那么一两个字，让你展示所有包含这一两个字的结果，就像百度，你搜个\u0026quot;开发\u0026quot;，就能搜出来例如\u0026quot;软件开发\u0026quot;,\u0026ldquo;硬件开发\u0026quot;等。接下来咱们也实现一个这个功能\n//查找包含\u0026#34;cifs\u0026#34;的所有数据 func (Es *Elastic) FindKeyword() { //因为不确定cifs如何出现，可能是cifs01，也可能是01cifs，所以采用这种方法 keyword := \u0026#34;cifs\u0026#34; keys := fmt.Sprintf(\u0026#34;name:*%s*\u0026#34;, keyword) boolQ.Filter(elastic.NewQueryStringQuery(keys)) res, _ := Es.Client.Search(\u0026#34;task\u0026#34;).Type(\u0026#34;doc\u0026#34;).Query(boolQ).Do(context.Background()) //从搜索结果中取数据的方法 for _, item := range res.Each(reflect.TypeOf(typ)) { if t, ok := item.(Task); ok { fmt.Println(t) } } } 多条件查询 如果说我们现在不仅仅需要找到符合时间的，也需要找到符合关键字的查询，那么就需要在查询条件上做文章。\nfunc (Es *Elastic) FindAll() { //因为不确定cifs如何出现，可能是cifs01，也可能是01cifs，所以采用这种方法 keyword := \u0026#34;cifs\u0026#34; keys := fmt.Sprintf(\u0026#34;name:*%s*\u0026#34;, keyword) boolQ.Filter(elastic.NewRangeQuery(\u0026#34;starttime\u0026#34;).Gte(\u0026#34;2020/05/13 18:38:21\u0026#34;), elastic.NewRangeQuery(\u0026#34;starttime\u0026#34;).Lte(\u0026#34;2020/05/14 18:38:21\u0026#34;), elastic.NewQueryStringQuery(keys)) res, err := Es.Client.Search(\u0026#34;task\u0026#34;).Type(\u0026#34;doc\u0026#34;).Query(boolQ).Do(context.Background()) //从搜索结果中取数据的方法 for _, item := range res.Each(reflect.TypeOf(typ)) { if t, ok := item.(Task); ok { fmt.Println(t) } } } 统计数量/多条件统计数量 有些时候我们需要去统计符合查询条件的结果数量，做统计用，这里也有直接可用的Sdk\nfunc (Es *Elastic) GetTaskLogCount() (int, error) { boolQ := elastic.NewBoolQuery() boolQ.Filter(elastic.NewRangeQuery(\u0026#34;starttime\u0026#34;).Gte(\u0026#34;2020/05/13 18:38:21\u0026#34;), elastic.NewRangeQuery(\u0026#34;starttime\u0026#34;).Lte(\u0026#34;2020/05/14 18:38:21\u0026#34;)) //统计count count, err := Es.Client.Count(\u0026#34;task\u0026#34;).Type(\u0026#34;doc\u0026#34;).Query(boolQ).Do(context.Background()) if err != nil { return 0, nil } return int(count), nil } 总结 我这边完成了几个查询/导入的基础功能，当然，我的代码大部分都放置在了github当中\n放置在: https://github.com/Alexanderklau/Go_poject/tree/master/Go-Elasticdb/Elasticsearch_sdk\n最近项目比较忙，我打算月中写一篇我开发的时候使用的一些Go特性，或者高级用法。如果喜欢的话麻烦Star我！最近压力颇大，想要换一个地方生活，所以也要准备离开了。如果大家有什么问题，可以直接给我提问，我看到了就会帮助大家的。\n","permalink":"https://yemilice.com/posts/golang%E5%B0%81%E8%A3%85elasticsearch%E5%B8%B8%E7%94%A8%E5%8A%9F%E8%83%BD/","summary":"\u003ch2 id=\"前言为什么要写这篇文章\"\u003e前言（为什么要写这篇文章）\u003c/h2\u003e\n\u003cp\u003e首先看过我博客的都应该知道，我去年发了一篇Python封装Elasticsearch的文章。但那是去年了，今年我将我的检索服务后端用Golang全部重写了一波，相当于用Go重构了以前的Python代码，不过我个人感觉Golang的效率还是高于Python的，而且我还加了一些异常判断和处理，这次的代码只会比以前更好更牛逼，为了纪念这一个多月的重构历程，我将关键功能记录下来，方便自己复习和各位兄弟姐妹查看。\u003c/p\u003e","title":"Golang封装Elasticsearch常用功能"},{"content":"写这篇文章的原因 按理说，你们看过我博客的人，都知道，我是一个相当乐观的人，我从来都不会怎么丧气一些事儿，特别是在工作上，我永远都是顶在最前面，有困难bug我来，有难题我来攻关，按理说我就像是一个长者，身经百战，见的多了！那些naive的问题，我都不放在心上，可今天，恩，一个绩效考评，算是让我有了一些感到心累或者说是心寒吧。\n发生了什么事儿 说起来也是很尴尬，大家都知道，因为疫情，我整个二月和三月都在家办公，我那电脑没带回家，我用我家旧电脑和我舅舅的电脑，算是拼了个单片机，没图形界面那种，这是前提啊！本来我这边在开发一个比较大的项目，这项目，怎么说呢，用许嵩那句话就是 “作词作曲都是我自己”，咱不说别的，从策划，到架构涉及包括一些有的没的，都是我一个人搞定，那家伙，就差说我是CTO了哈哈，再一个，我作为整个项目组排头兵，第一个吃螃蟹涉足Golang开发，从一无所有，到文档齐备，就用了1月底，到2月底，这一个月时间，在家开发，日夜不眠不休，真正996，头发都给我掉了多少哈啊哈。\n这转折来了嘿，3月初，我有天正干活儿呢，你们知道，那开发机，一个人分好几台，我在我那几台开发机上正挥汗如雨呢，结果突然黑屏了，那一瞬间，我还傻x的拍了一下我电脑的后盖，我一寻思，不对啊，这不是电视，黑屏那就得是出事儿！\n果不其然，我把记录一发群里，才知道原来测试那边儿，有人在清理集群！一不小心把开发集群给清了！\n我勒个大擦！我寻思你这是灭霸啊，人灭霸打个响指，害得摆个造型。你这不声不响的，按个enter，就给我一个多月努力干没了，你可真是现代爆破专家啊你。这TM叫删库，你丫想跑路了吧！\n我当时的想法 我当时脑子里先是一片空白，然后我脑子里一直弥漫着老八那句话，奥莉给！干他就完事儿了！\n是，我当时那个气啊，我就想给他丫干了，X的，小爷不说别的，以前也算是胡同口拍黑砖第一人啊！\n然后我的老大，紧急发来微信，问，你备份了吗？\n我说，没，那集群20多台机器呢，我在其中几台配了定时同步。\n我老大说，完了。\n我知道，我被删库了。\n删库的损失 不说别的，项目代码损失是最大的，2w行代码，一个月不眠不休的成果，完蛋了，等于这个月白干，是呗，是我倒霉么？人一倒霉，喝凉水都塞牙，给我气的。\n同时丢失的，还有Elasticsearch的work脚本，优化过后的ETCD封装代码，大部分文档，编译机的环境和包。\n为什么不备份？ 首先开发集群有20多台机器，其中我的机器就用将近8台，8台机器啊老铁们，你会想到它们一瞬间都被干死么？\n好吧，我承认，我那个黑不溜秋的单片机linux只能处理一些逻辑问题，30G硬盘，安个Golang，python，jre都够呛了，害得跑个带gopls的Vim，备份？我这不寻思我过两天回去了备份么！\n绩效的定义 写这个原因是，今儿（2020-4-13），考评下来，我老大找我谈话，说这次考评给了我C。也就是我绩效考核，3月份是C。\n我当时就疑惑了，删库的第一不是我，第二我不眠不休写的东西被人删了，我TM才是受害者！it‘s me！你给受害者考勤打C？我的项目都进测试部分了，你把开发机测试机一起干死了，当中是有我没备份的原因，但是你直接给我打C？？？我一打听，删库的人也是C，我勒个去，你这什么意思，删库的人和我都是C的评级？？那你是真的牛皮，感情我被删库我还成C了是吧！那你要追究责任，怎么不追究运维，为什么他会给删库的人那么大权限？让我背锅也太无耻了吧！\n结尾 这次我感觉我有点寒心，是真的，我平常都写技术，很少写这种令人难过的玩意儿，但是我今天真的感觉心很累，也很寒心。看起来总是要人背锅，也就只有我了呗。呵呵。\n晚安了啊，如果哪位技术大佬或者是HR看到这篇文章，请不要误会，这只是我对这件事的一种寒心，我还是会继续输出高质量文章的。\nover\n","permalink":"https://yemilice.com/posts/%E8%BF%99%E6%AC%A1%E7%BB%A9%E6%95%88%E8%80%83%E8%AF%84%E4%BC%9A%E6%88%90%E4%B8%BA%E5%8E%8B%E5%9E%AE%E6%88%91%E7%9A%84%E7%A8%BB%E8%8D%89%E4%B9%88/","summary":"\u003ch2 id=\"写这篇文章的原因\"\u003e写这篇文章的原因\u003c/h2\u003e\n\u003cp\u003e按理说，你们看过我博客的人，都知道，我是一个相当乐观的人，我从来都不会怎么丧气一些事儿，特别是在工作上，我永远都是顶在最前面，有困难bug我来，有难题我来攻关，按理说我就像是一个长者，身经百战，见的多了！那些naive的问题，我都不放在心上，可今天，恩，一个绩效考评，算是让我有了一些感到心累或者说是心寒吧。\u003c/p\u003e","title":"这次绩效考评会成为压垮我的稻草么？"},{"content":"前言 介绍Rabbimq Rabbitmq消息队列是干嘛的？ 简单的说，消息队列，引申一下就是传递消息用的队列，也可以称为传递消息的通信方法。用争抢订单的快车举个例子，假如，A用户发送了一个用车的消息，那么消息队列要做的就是把A用户用车的这个消息广而告之，发送到一个公用队列当中，司机只管取到消息，而不管是谁发布的，这就是一个简单的消息队列例子，Rabbitmq其实就是消息队列的一种，用的比较多的还可能有Redis，kafka，ActiceMq等等，这个后面的博文里面我会说，这次我们只说Rabbimq消息队列\nRabbitmq消息队列的好处是什么？为什么我们要用他？ 这个网上有很多类似的玩意，我不说太多，就只说我在使用中感觉比较好的地方。\n分布式，多节点部署。一个集群，保证消息的持久化和高可用，某节点挂了，其他节点可以结力。\n路由Exchange，这个已经提供了内部的几种实现方法，可以指定路由，也就是指定传递的地址。\n多语言支持，我以前干活儿用Python，现在用Go和java，人家无缝对接，多牛逼！\nAck的消息确认机制，这样就保证了，任务下发时候的稳定性，ack消息确认可以手动，也可以自动，这样就保证了任务下发时候的可控和监控。\n初步开始 简单的生产者和消费者的模型 讲那么多废话理论，还不如直接开始写代码更直观是吧，所以，奥莉给，干了兄弟们！我们实现一个简答的生产者，消费者模型。这个不用我多解释吧，基础的流程就是，我们定义一个生产者，生产信息到Rabbitmq中，然后再定义一个消费者，把数据从Rabbitmq中取出来，就这么简单，下面咱们就干了，先讲几个基础。\nRabbitmq的基础知识 发送 Publish 发送，你可以理解为上传，意思就是，上传一个消息到Rabbitmq当中。它这块的基础代码比较简单\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;github.com/streadway/amqp\u0026#34; ) func main() { //初始化一个Rabbimtq连接，后跟ip，user，password conn, err := amqp.Dial(\u0026#34;amqp://guest:guest@localhost:5672/\u0026#34;) if err != nil { return } defer conn.Close() //创建一个channel的套接字连接 ch, _ := conn.Channel() //创建一个指定的队列 q, _ := ch.QueueDeclare( \u0026#34;work\u0026#34;, // 队列名 false, // durable false, // 不使用删除？ false, // exclusive false, // 不必等待 nil, // arguments ) //定义上传的消息 body := \u0026#34;work message\u0026#34; //调用Publish上传消息1到指定的work队列当中 err = ch.Publish( \u0026#34;\u0026#34;, // exchange \u0026#34;work\u0026#34;, // 队列名 false, // mandatory false, // immediate amqp.Publishing { ContentType: \u0026#34;text/plain\u0026#34;, //[]byte化body Body: []byte(body), }) } 这样就完成了上传消息到work队列当中。\n接收 Consume 接收，顾名思义，就是接收到指定队列中的信息，信息存在队列当中，总要被拿出来用吧，放那里又不能下崽儿，所以，拿出来感觉用了才是最重要的。这块的基础代码如下\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;github.com/streadway/amqp\u0026#34; ) func main() { //初始化一个Rabbimtq连接，后跟ip，user，password conn, err := amqp.Dial(\u0026#34;amqp://guest:guest@localhost:5672/\u0026#34;) if err != nil { return } defer conn.Close() //创建一个channel的套接字连接 ch, _ := conn.Channel() msgs, err := ch.Consume( \u0026#34;work\u0026#34; // 队列名 \u0026#34;\u0026#34;, // consumer true, // auto-ack false, // exclusive false, // no-local false, // 不等待 nil, // args ) //定义一个forever，让他驻留在后台，等待消息，来了就消费 forever := make(chan bool) //执行一个go func 完成任务消费 go func() { for d := range msgs { //打印body log.Printf(\u0026#34;message %s\u0026#34;, d.Body) } }() \u0026lt;-forever } 生产者／消费者模型 上面简单说了一下rabbimq的发送和接收，这下咱们就要实现一个生产者消费者模型了，这个模型的主要逻辑，就是生产者发送任务到指定的队列，有一个，或者多个消费者，会在此留守，一有任务，就争抢并且消费。\n生产者逻辑 其实生产者逻辑和上面的发送逻辑差不多，这里给出写法。\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;github.com/streadway/amqp\u0026#34; ) func main() { //初始化一个Rabbimtq连接，后跟ip，user，password conn, err := amqp.Dial(\u0026#34;amqp://guest:guest@localhost:5672/\u0026#34;) if err != nil { return } defer conn.Close() //创建一个channel的套接字连接 ch, _ := conn.Channel() //创建一个指定的队列 q, _ := ch.QueueDeclare( \u0026#34;work\u0026#34;, // 队列名 false, // durable false, // 不使用删除？ false, // exclusive false, // 不必等待 nil, // arguments ) //定义上传的消息 body := \u0026#34;work message\u0026#34; //调用Publish上传消息1到指定的work队列当中 err = ch.Publish( \u0026#34;\u0026#34;, // exchange \u0026#34;work\u0026#34;, // 队列名 false, // mandatory false, // immediate amqp.Publishing { ContentType: \u0026#34;text/plain\u0026#34;, //[]byte化body Body: []byte(body), }) } 消费者逻辑 消费者逻辑这边，主要是加了一个qos控制和手动ack，代码如下\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;github.com/streadway/amqp\u0026#34; ) func main() { //初始化一个Rabbimtq连接，后跟ip，user，password conn, err := amqp.Dial(\u0026#34;amqp://guest:guest@localhost:5672/\u0026#34;) if err != nil { return } defer conn.Close() //创建一个channel的套接字连接 ch, _ := conn.Channel() //创建一个qos控制 err = ch.Qos( 3, // 同时最大消费数量（意思就是最多能消费几个任务） 0, // prefetch size false, // 全局设定？ ) if err != nil { return err } msgs, err := ch.Consume( \u0026#34;work\u0026#34; // 队列名 \u0026#34;\u0026#34;, // consumer true, // auto-ack false, // exclusive false, // no-local false, // 不等待 nil, // args ) //定义一个forever，让他驻留在后台，等待消息，来了就消费 forever := make(chan bool) //执行一个go func 完成任务消费 go func() { for d := range msgs { //打印body log.Printf(\u0026#34;message %s\u0026#34;, string(d.Body)) //手动ack，不管是否发送完毕。 d.Ack(false) } }() \u0026lt;-forever } Golang封装Rabbitmq的基础接口 Rabbitmq会用了吧，上面那个估计比较简单，但是估摸着你们还想要别的功能，好，那我就惯大家一次，干了兄弟们，奥莉给！\n初始化Rabbitmq连接 为了避免每次重复调用Rabbitmq连接，我这里提供一个简单写法。\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/streadway/amqp\u0026#34; ) //Rabbitmq 初始化rabbitmq连接 type Rabbitmq struct { conn *amqp.Connection err error } func New(ip string) (*Rabbitmq, error) { amqps := fmt.Sprintf(\u0026#34;amqp://guest:guest@%s:5672/\u0026#34;, ip) conn, err := amqp.Dial(amqps) if err != nil { return nil, err } rabbitmq := \u0026amp;Rabbitmq{ conn: conn, } return rabbitmq, nil } 创建一个Queue队列 func (rabbitmq *Rabbitmq) CreateQueue(id string) error { ch, err := rabbitmq.conn.Channel() defer ch.Close() if err != nil { return err } _, err = ch.QueueDeclare( id, // name true, // durable false, // delete when unused false, // exclusive false, // no-wait nil, // arguments ) if err != nil { return err } return nil } 上传消息到指定的queue中 func (rabbitmq *Rabbitmq) PublishQueue(id string, body string) error { ch, err := rabbitmq.conn.Channel() defer ch.Close() if err != nil { return err } err = ch.Publish( \u0026#34;\u0026#34;, // exchange id, // routing key false, // mandatory false, amqp.Publishing{ DeliveryMode: amqp.Persistent, ContentType: \u0026#34;text/plain\u0026#34;, Body: []byte(body), }) if err != nil { return err } return nil } 从队列中取出消息并且消费 func (rabbitmq *Rabbitmq) PublishQueue(id string, body string) error { ch, err := rabbitmq.conn.Channel() defer ch.Close() if err != nil { return err } err = ch.Publish( \u0026#34;\u0026#34;, // exchange id, // routing key false, // mandatory false, amqp.Publishing{ DeliveryMode: amqp.Persistent, ContentType: \u0026#34;text/plain\u0026#34;, Body: []byte(body), }) if err != nil { return err } return nil } 统计队列中预备消费的数据 func (rabbitmq *Rabbitmq) GetReadyCount(id string) (int, error) { count := 0 ch, err := rabbitmq.conn.Channel() defer ch.Close() if err != nil { return count, err } state, err := ch.QueueInspect(id) if err != nil { return count, err } return state.Messages, nil } 统计消费者／正在消费的数据 func (rabbitmq *Rabbitmq) GetConsumCount(id string) (int, error) { count := 0 ch, err := rabbitmq.conn.Channel() defer ch.Close() if err != nil { return count, err } state, err := ch.QueueInspect(id) if err != nil { return count, err } return state.Consumers, nil } 清理队列 func (rabbitmq *Rabbitmq) ClearQueue(id string) (string, error) { ch, err := rabbitmq.conn.Channel() defer ch.Close() if err != nil { return \u0026#34;\u0026#34;, err } _, err = ch.QueuePurge(id, false) if err != nil { return \u0026#34;\u0026#34;, err } return \u0026#34;Clear queue success\u0026#34;, nil } 总结 简单讲了一下Rabbimtq是啥，怎么用，我是怎么用的。\n完整代码请访问我的Github： https://github.com/Alexanderklau/Go_poject/blob/master/Go-Rabbitmq/rabbitmq.go\n如果有不懂的欢迎留言！如果能帮大家的我一定会帮！也希望你们指出我的错误！一起进步！\n","permalink":"https://yemilice.com/posts/golang%E8%B0%83%E7%94%A8rabbitmq%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%92%8C%E5%B0%81%E8%A3%85/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003ch3 id=\"介绍rabbimq\"\u003e介绍Rabbimq\u003c/h3\u003e\n\u003ch4 id=\"rabbitmq消息队列是干嘛的\"\u003eRabbitmq消息队列是干嘛的？\u003c/h4\u003e\n\u003cp\u003e简单的说，消息队列，引申一下就是传递消息用的队列，也可以称为传递消息的通信方法。用争抢订单的快车举个例子，假如，A用户发送了一个用车的消息，那么消息队列要做的就是把A用户用车的这个消息广而告之，发送到一个公用队列当中，司机只管取到消息，而不管是谁发布的，这就是一个简单的消息队列例子，Rabbitmq其实就是消息队列的一种，用的比较多的还可能有Redis，kafka，ActiceMq等等，这个后面的博文里面我会说，这次我们只说Rabbimq消息队列\u003c/p\u003e","title":"Golang调用Rabbitmq消息队列和封装"},{"content":"前言 上篇文章，大概讲了一下robfig/cron 包的使用，怎么开始一个定时任务，那个东西比较简单，也就是调用函数而已，人家都给你把包都封装好了。鉴于上一章我没提到cron相关，这一章专门我写个cron相关，讲讲怎么cron语法，然后再实现一个自动生成cron语句的逻辑。\n需求分析 cron的基础科普 根据时间自动生成可用的cron语句 Cron表达式的基础 Go的Cron和linux的Cron的区别就是，linux只到分钟，但是Go的Cron可以通过我上一节描述的代码设置精确到秒。所以一般的Cron表达式就是\n* * * * * * command 可以看出来，这是一个时间集合，但是其中每个 * 代表什么含义呢？下面给出Golang的cron设置表\n字段 需要的值 字符表示 秒 0-59 * / , - 分 0-59 * / , - 时 0-23 * / , - 日 1-31 * / , - 月 1-12 * / , - 星期 0-6 * / , - 下面举几个cron的具体例子 每秒执行一次任务\n* * * * * * Command 每分钟执行一次任务\n* *／1 * * * * Command 每天12点执行一次任务\n* 0 12 * * * Command 每个月1号12点执行一次任务\n* 0 12 1 * * Command 2月14号12点执行一次任务（执行一次）\n0 0 12 14 2 * Command 每周二12点执行一次任务\n* 0 12 * * 1 Command Golang 实现一个Cron表达式自动生成器 Cron这个东西，其实没那么难，但是你每次让我们徒手撸，还是会有点烦，特别是现在网上基本没有在线自动生成Cron语法的网站了，所以我们还是站撸一个Cron自动生成器，首先咱们要明确一个重要东西,任务可能是循环的，也可能是只执行一次的，看到了么，这下我们就要针对不同的任务类型，输出不同的任务表达式。\n规定输入的时间格式 首先输入的时间有多种多样，我们没办法控制输入的时间表达，所以我在这里先行规定，我的代码也是按照这个规定来的，前提在此。\n循环执行的任务 对于循环执行的任务，可能有每月，每周，每日，每时等等，所以我在这里举例\n每月3号12点执行\nm,03,12:00 每周三的12点执行\nw,3,12:00 每天的12点执行\nd,12:00 观察一下，聪明的你应该知道我要做什么，拿每天循环执行来举例\ntimelists := strings.Split(times, \u0026#34;,\u0026#34;) hours := strings.Split(timelists[1], \u0026#34;:\u0026#34;)[0] minutes := strings.Split(timelists[1], \u0026#34;:\u0026#34;)[1] crontab := fmt.Sprintf(\u0026#34;* %s %s * * *\u0026#34;, minutes, hours) fmt.Println(crontab) 结合其他部分\ntimelists := strings.Split(times, \u0026#34;,\u0026#34;) // 在这里判断类型，天，月，周 if timelists[0] == \u0026#34;d\u0026#34; { hours := strings.Split(timelists[1], \u0026#34;:\u0026#34;)[0] minutes := strings.Split(timelists[1], \u0026#34;:\u0026#34;)[1] crontab := fmt.Sprintf(\u0026#34;* %s %s * * *\u0026#34;, minutes, hours) return crontab } else if timelists[0] == \u0026#34;w\u0026#34; { days := strings.Split(timelists[1], \u0026#34;,\u0026#34;)[0] hours := strings.Split(strings.Split(timelists[2], \u0026#34;,\u0026#34;)[0], \u0026#34;:\u0026#34;)[0] minutes := strings.Split(strings.Split(timelists[2], \u0026#34;,\u0026#34;)[0], \u0026#34;:\u0026#34;)[1] crontab := fmt.Sprintf(\u0026#34;* %s %s * * %s\u0026#34;, minutes, hours, days) return crontab } else if timelists[0] == \u0026#34;m\u0026#34; { days := strings.Split(timelists[1], \u0026#34;,\u0026#34;)[0] hours := strings.Split(strings.Split(timelists[2], \u0026#34;,\u0026#34;)[0], \u0026#34;:\u0026#34;)[0] minutes := strings.Split(strings.Split(timelists[2], \u0026#34;,\u0026#34;)[0], \u0026#34;:\u0026#34;)[1] crontab := fmt.Sprintf(\u0026#34;* %s %s %s * *\u0026#34;, minutes, hours, days) return crontab } else { crontab := \u0026#34;* * * * * *\u0026#34; return crontab } 执行一发看看,生成个每月的cron表达式\n* 0 12 03 * * Command 诶，怎么多了个03。。。看起来咱们需要格式化一下，把它转换一下成可用的。\nfunc FkZero(times string) (fmttime string) { // 如果第一个值不为0，直接认为是正常的 if string(times[0]) != \u0026#34;0\u0026#34; { return times // 判断00的情况 } else if strings.Split(times, \u0026#34;0\u0026#34;)[1] == \u0026#34;\u0026#34; { fkzero := \u0026#34;0\u0026#34; return fkzero // 清理03，为 3 } else { fkzero := strings.Split(times, \u0026#34;0\u0026#34;)[1] return fkzero } } 执行一次的任务 执行一次的任务表达方式 2020-03-20 12:00\n处理代码如下\ntimelists := strings.Split(times, \u0026#34; \u0026#34;)[0] month := strings.Split(timelists, \u0026#34;-\u0026#34;)[1] day := strings.Split(timelists, \u0026#34;-\u0026#34;)[2] timework := strings.Split(times, \u0026#34; \u0026#34;)[1] hours := strings.Split(timework, \u0026#34;:\u0026#34;)[0] minutes := strings.Split(timework, \u0026#34;:\u0026#34;)[1] crontab := fmt.Sprintf(\u0026#34;0 %s %s %s %s *\u0026#34;, minutes, hours, day, month) return crontab 总结 其实这个代码主要就是一个strings的split切分，但是涉及到了crontab语言的输出，其实没那么难，也就是麻烦，我把它传到github上了，有需要可以自己get下来。\nhttps://github.com/Alexanderklau/Go_poject/tree/master/Go-Script/crontab\n","permalink":"https://yemilice.com/posts/golang-%E5%AE%8C%E6%88%90%E4%B8%80%E4%B8%AA-crontab%E5%AE%9A%E6%97%B6%E5%99%A82/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e上篇文章，大概讲了一下robfig/cron 包的使用，怎么开始一个定时任务，那个东西比较简单，也就是调用函数而已，人家都给你把包都封装好了。鉴于上一章我没提到cron相关，这一章专门我写个cron相关，讲讲怎么cron语法，然后再实现一个自动生成cron语句的逻辑。\u003c/p\u003e","title":"Golang 完成一个 Crontab定时器（2）"},{"content":"前言 Linux的Crontab定时器似乎已经足够强大，但是我认为还是没有办法满足我们所有的需求，例如定时器某一瞬间需要动态添加／删除任务的功能，例如定时器只能在指定的节点上启动（主节点），其他节点不需要定时服务，这种情况Linux自带的Crontab就不能够满足我们的需求了，所以这次要徒手定义一个Crontab定时器，作为自己的备用。\n需求分析 看我博客的基本也都知道，做任何事，都要进行一个需求分析。既然是一个定时器，那么应该支持的功能如下\n定时启动任务（废话） 支持基础的Crontab语法 支持将时间转换为Crontab语法 支持Crontab语法校验 记录日志的功能 支持crontab任务到秒级 综合上面的需求，不难看出，其实最重要的功能就是实现一个定时启动任务的玩意儿，在Go开发当中，也就是在某个时间点执行某个Go函数。说的简单点，就是，定时跑Go函数。也就是定时go func而已。\nrobfig/cron 包的使用 安装robfig/cron包 我这里默认你们都有go 基础,先安装个包，这玩意儿是驱动Golang 驱动 Crontab的重要框架。\ngo get github.com/robfig/cron robfig/cron的使用举例 启动一个定时任务其实很简单\njob := cron.New() job.AddFunc(\u0026#34;* * * * *\u0026#34;, func() {fmt.Println(\u0026#34;Start job....\u0026#34;)}) job.Start() 一个定时任务就这样被写好了，其实仔细琢磨一下，添加任务的方式就是\n// 新任务 job := cron.New() ///任务添加 job.AddFunc(\u0026#34;Cronta 语句\u0026#34;, func() {执行函数（）}) //任务开始 job.Start() 这样就完成了一个定时任务的添加\n支持crontab任务到秒级 估计你们看到这里一脸懵逼，为啥你个单独到秒级的也要整出来呢？其实我也表示，我也不想啊！奈何一点，robfig/cron这玩意有个很奇葩的一点，它只支持的分钟级别的任务，不支持到秒级别！！！！，它只支持的分钟级别的任务，不支持到秒级别！！！！，它只支持的分钟级别的任务，不支持到秒级别！！！！，重要的事儿说三遍！这里需要你自己定义秒级别的任务，在翻了一下它的test源码之后，拉到最底下，看到这么一行代码。\n// newWithSeconds returns a Cron with the seconds field enabled. func newWithSeconds() *Cron { return New(WithParser(secondParser), WithChain()) } 这行代码啥意思呢，意思就是启用返回seconds字段的任务，说白了就是，你要加这个，才能开启秒级别的任务\n网上好多博客写的，都是你抄我，我抄你，抄来抄去，没一个代码能用的，大家如果发现抄网上那帮人的代码，跑不起来，那绝对就是这个原因！人家只支持到分钟，网上给出的例子全都是秒级别的，并且没打开秒级别任务定义，还能跑起来？我都怀疑你们怎么写的代码。\n定义秒级别任务代码\n这段代码主要的意思就是，开放到秒级别的任务支持，看到了second么，这段代码在源码包的test下有，你们可以自己去看看。\nfunc newWithSeconds() *cron.Cron { secondParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor) return cron.New(cron.WithParser(secondParser), cron.WithChain()) } func main() { // 使用秒级别任务 job := newWithSeconds() // 任务定义,3s输出一个j1 start job,不停循环 job.AddFunc(\u0026#34;0/3 * * * * ? \u0026#34;, func() { fmt.Println(\u0026#34;j1 start job....\u0026#34;, time.Now().Format(\u0026#34;2020-03-20 15:04:05\u0026#34;)) }) // 任务定义，每分钟的第三秒执行任务 job.AddFunc(\u0026#34;3 * * * * ? \u0026#34;, func() { fmt.Println(\u0026#34;j2 start job....\u0026#34;, time.Now().Format(\u0026#34;2020-03-20 15:04:05\u0026#34;)) }) //开始任务 job.Start() select {} } 输出的结果\nj1 start job.... 2020-03-21 17:09:36 j1 start job.... 2020-03-21 17:09:39 j1 start job.... 2020-03-21 17:09:42 j1 start job.... 2020-03-21 17:09:45 总结 初步完成了crontab的基础功能，这篇文章默认，你们都比较懂crontab语法和go开发了，如果不懂就请期待下一篇，实现crontab语法自动生成和自动加载任务吧。\n","permalink":"https://yemilice.com/posts/golang-%E5%AE%8C%E6%88%90%E4%B8%80%E4%B8%AA-crontab%E5%AE%9A%E6%97%B6%E5%99%A81/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003eLinux的Crontab定时器似乎已经足够强大，但是我认为还是没有办法满足我们所有的需求，例如定时器某一瞬间需要动态添加／删除任务的功能，例如定时器只能在指定的节点上启动（主节点），其他节点不需要定时服务，这种情况Linux自带的Crontab就不能够满足我们的需求了，所以这次要徒手定义一个Crontab定时器，作为自己的备用。\u003c/p\u003e","title":"Golang 完成一个 Crontab定时器（1）"},{"content":"Golang利用context实现一个任务并发框架 消失这么久的原因 疫情太严重，哥们本来打算在新疆滑雪+吃烤肉度过一个美好的假期，结果没成想给困那里了，这不就尴尬了么，这不，博客没更新，现在我又回来了，哈哈哈哈！\n我要实现个什么玩意儿 有一个需求，简单的说就是我要写一个任务管理框架，主要功能有任务开启，任务关闭，任务监控等等。说的抽象点，就是我要用Golang写一个任务管理的功能，任务很可能有多个，并且我想停任务就停任务，想开始任务我就开始任务！我不要你觉得，我要我觉得！\n为什么我要用Golang 众所周知，golang这东西，有个黑科技，叫goroutine，这东西很牛逼，牛逼在哪儿呢？简单的说，快，小，短！协程切换快，占用资源少，并且，异步的，可以开多个，直接一个go 关键字就给人家打开了，多棒！\n实际需求分析 结合我们上面的任务需求，实现一个任务开始，任务停止的逻辑，这就说明，任务肯定不只有一个，并且任务都在后台，我们该怎么去监控，或者去管理这个go任务，或者go函数呢，golang提供了很多解决办法，例如WaitGroup，context等方法。\n思考一下，我们的这个需求，任务都是跑在后台的异步并发逻辑，这就说明不只一个任务会被启动和停止，这样对我们的任务管理是一个很大的挑战，因为任务都是在后台隐秘执行的，如果是一般逻辑，我们要停止任务，首先要找到任务的pid，然后kill任务进程，这是一个完整的结束任务的流程。\n回到我们这个需求，基本的流程就是：\n发送一个任务请求(开启任务/停止任务) -\u0026gt; 接收到任务请求 -\u0026gt; 执行任务请求\n思考一下，如果，我们开启任务之后，任务进入后台，那么，我们在停止任务的时候，怎么保证，能够找到这个任务，精准的打击（停止）它呢？看了题目你应该就知道了，用context就好了。下面我就来介绍一下它吧。\n主角context介绍 网上很多博客介绍它，我粗粗看了一眼，非常抽象，很多人再一描述，就更麻烦更抽象了。我这里不说的太麻烦，简单描述一下，这个东西context，是干嘛呢，你们理解一下株连，连坐这两个词汇，这个东西相当于就是锁链，铁锁连舟，不进则退，说明白点，就是一个串连上下文的类似信号传递的玩意儿。每个调用链上的函数都要以它作为函数进行传递，举个例子,\u0026ldquo;株九族\u0026quot;这个词，是因为一个人犯罪，结果家人都因为他被砍了头，这个犯罪的人，就是父context，其他因为他被杀的人，就是子context，还可能有孙context，他被砍头了，其他人也得跟着一起死，用代码表示一波\n// 这是儿子函数 func gen(ctx context.Context) \u0026lt;-chan int { dst := make(chan int) n := 1 go func() { for { select { // 接收到爹挂了的消息 case \u0026lt;-ctx.Done(): fmt.Println(\u0026#34;儿子被砍头了。\u0026#34;) // 退出任务 return case dst \u0026lt;- n: n++ time.Sleep(time.Second * 1) } } }() return dst } // 这是爹函数 fun test() { ctx, cancel := context.WithCancel(context.Background()) // 让我造个儿子，给我儿子传个ctx intChan := gen(ctx) // 我被干了，cancel是结束 defer Cancel() } 这下说的明白了么？其实context还有很多别的，例如timeout之类的，但是那个是我后面准备写的，这一节就不写这些了。\n实现我们的需求 我们的武器context已经准备好了，大概的使用逻辑我们也明白了，现在你们可以看到，我们只要拿到主函数（爹函数）的ctx和cancel，我们就可以控制子函数（儿子函数）的死活，我们在开发当中，任务的状态是不断在变化的，一个爹对应一个儿子，但是可能有多个任务，多个任务我们该怎么管理它？\n在一般的开发任务中，我们习惯将任务记录到数据库当中，然后在开发当中不停的遍历数据库，去判断任务的状态到底是开启还是停止，这里我们要考虑到，频繁遍历数据库，会不会带来大量的访问堆积？还是否有别的解决办法？\n我的解决方案 这次开发中，我选择定义一个全局变量的主map，并且定义一个任务的struct类型，代码如下\n// 全局map var jobmap = make(map[string]interface{}) //Job 任务 type Jobs struct { ID string Status int Ctx context.Context Cancel context.CancelFunc } 然后我选择在任务开始的时候（创造儿子的时候），将信息填充，修改test代码如下\nfunc test() { var jobs Jobs ctx, cancel := context.WithCancel(context.Background()) // 造一个儿子 intChan := gen(ctx) // 任务开始了 fmt.Println(\u0026#34;start job\u0026#34;) // 重要的东西传进去 jobs.Status = 1 jobs.Cancel = cancel jobs.Ctx = ctx // 定义一个任务id，这个可以用uuid，或者随便整个别的 jobs.ID = \u0026#34;sdads\u0026#34; m1[\u0026#34;sdads\u0026#34;] = jobs // 阻塞任务，假装任务执行很久 for n := range intChan { fmt.Println(n) if n == 1000 { break } } 再然后，我选择写一个停止函数（砍头函数）\nfunc stopGetmi(id string) { //把任务停掉 fmt.Println(\u0026#34;stop jobs\u0026#34;) jobss := m1[id] //interface 转 struct op, ok := jobss.(Jobs) // 调用砍头函数cancel defer op.Cancel() } 进行测试\nfunc main() { go test() go stopGetmi(\u0026#34;sdads\u0026#34;) time.Sleep(time.Second * 200) } 发现任务执行结果\n这样你就完成了干掉老爹，也干掉儿子的素质操作。\n总结 主要是context的基础和说明，其实context我还是推荐大家去看看原版，我这里写的太过于简单，不过这篇博客，也是我记录一下自己开发中遇到的难题，当时看网上没有类似的说明，于是写了这篇博客，希望大家多多包涵。\n祝大家都能躲过瘟疫，我们终究会在春花花开的地方相见。\n","permalink":"https://yemilice.com/posts/golang%E5%88%A9%E7%94%A8context%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E4%BB%BB%E5%8A%A1%E5%B9%B6%E5%8F%91%E6%A1%86%E6%9E%B6/","summary":"\u003ch1 id=\"golang利用context实现一个任务并发框架\"\u003eGolang利用context实现一个任务并发框架\u003c/h1\u003e\n\u003ch2 id=\"消失这么久的原因\"\u003e消失这么久的原因\u003c/h2\u003e\n\u003cp\u003e疫情太严重，哥们本来打算在新疆滑雪+吃烤肉度过一个美好的假期，结果没成想给困那里了，这不就尴尬了么，这不，博客没更新，现在我又回来了，哈哈哈哈！\u003c/p\u003e","title":"Golang利用context实现一个任务并发框架"},{"content":"ETCD分布式锁实现选主机制(Golang) 为什么要写这篇文章 做架构的时候，涉及到系统的一个功能，有一个服务必须在指定的节点执行，并且需要有个节点来做任务分发，想了半天，那就搞个主节点做这事呗，所以就有了这篇文章的诞生，我把踩的坑和收获记录下来，方便未来查看和各位兄弟们参考。\n选主机制是什么 举个例子，分布式系统内，好几台机器，总得分个三六九等，发号施令的时候总得有个带头大哥站出来，告诉其他小弟我们今天要干嘛干嘛之类的，这个大哥就是master节点，master节点一般都是做信息处理分发，或者重要服务运行之类的。所以，选主机制就是，选一个master出来，这个master可用，并且可以顺利发消息给其他小弟，其他小弟也认为你是master，就可以了。\nETCD的分布式锁是什么 首先认为一点，它是唯一的，全局的，一个key值\n为什么一定要强调这个唯一和全局呢，因为分布式锁就是指定只能让一个客户端访问这个key值，其他的没法访问，这样才能保证它的唯一性。\n再一个，认为分布式锁是一个限时的，会过期的的key值\n你创建了一个key，要保证访问它的客户端时刻online，类似一个“心跳”的机制，如果持有锁的客户端崩溃了，那么key值在过期后会被删除，其他的客户端也可以继续抢key，继续接力，实现高可用。\n选主机制怎么设计 其实主要的逻辑前面都说清楚了，我在这里叙述下我该怎么做。\n我们假设有三个节点，node1,node2,node3\n三个节点都去创建一个全局的唯一key /dev/lock 谁先创建成功谁就是master主节点 其他节点持续待命继续获取，主节点继续续租key值（key值会过期） 持有key的节点down机，key值过期被删，其他节点创key成功，继续接力。 ETCD分布式锁简单实现 看一下ETCD的golang代码，还是给出了如何去实现一个分布式锁，这个比较简单，我先写一个简单的Demo说下几个接口的功能\n创建锁 kv = clientv3.NewKV(client) txn = kv.Txn(context.TODO()) txn.If(clientv3.Compare(clientv3.CreateRevision(\u0026#34;/dev/lock\u0026#34;),\u0026#34;=\u0026#34;,0)).Then( clientv3.OpPut(\u0026#34;/dev/lock\u0026#34;,\u0026#34;占用\u0026#34;,clientv3.WithLease(leaseId))).Else(clientv3.OpGet(\u0026#34;/dev/lock\u0026#34;)) txnResponse,err = txn.Commit() if err !=nil{ fmt.Println(err) return } 判断是否抢到锁 if txnResponse.Succeeded { fmt.Println(\u0026#34;抢到锁了\u0026#34;) } else { fmt.Println(\u0026#34;没抢到锁\u0026#34;,txnResponse.Responses[0].GetResponseRange().Kvs[0].Value) } 续租逻辑 for { select { case leaseKeepAliveResponse = \u0026lt;-leaseKeepAliveChan: if leaseKeepAliveResponse != nil{ fmt.Println(\u0026#34;续租成功,leaseID :\u0026#34;,leaseKeepAliveResponse.ID) }else { fmt.Println(\u0026#34;续租失败\u0026#34;) } } time.Sleep(time.Second*1) } 我的实现逻辑 首先我的逻辑就是，大家一起抢，谁抢到谁就一直续，要是不续了就另外的老哥上，能者居之嘛！我上一下我的实现代码\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;context\u0026#34; \u0026#34;time\u0026#34; //\u0026#34;reflect\u0026#34; \u0026#34;go.etcd.io/etcd/clientv3\u0026#34; ) var ( lease clientv3.Lease ctx context.Context cancelFunc context.CancelFunc leaseId clientv3.LeaseID leaseGrantResponse *clientv3.LeaseGrantResponse leaseKeepAliveChan \u0026lt;-chan *clientv3.LeaseKeepAliveResponse leaseKeepAliveResponse *clientv3.LeaseKeepAliveResponse txn clientv3.Txn txnResponse *clientv3.TxnResponse kv clientv3.KV ) type ETCD struct { client *clientv3.Client cfg clientv3.Config err error } // 创建ETCD连接服务 func New(endpoints ...string) (*ETCD, error) { cfg := clientv3.Config{ Endpoints: endpoints, DialTimeout: time.Second * 5, } client, err := clientv3.New(cfg) if err != nil { fmt.Println(\u0026#34;连接ETCD失败\u0026#34;) return nil, err } etcd := \u0026amp;ETCD{ cfg: cfg, client: client, } fmt.Println(\u0026#34;连接ETCD成功\u0026#34;) return etcd, nil } // 抢锁逻辑 func (etcd *ETCD) Newleases_lock(ip string) (error) { lease := clientv3.NewLease(etcd.client) leaseGrantResponse, err := lease.Grant(context.TODO(), 5) if err != nil { fmt.Println(err) return err } leaseId := leaseGrantResponse.ID ctx, cancelFunc := context.WithCancel(context.TODO()) defer cancelFunc() defer lease.Revoke(context.TODO(), leaseId) leaseKeepAliveChan, err := lease.KeepAlive(ctx, leaseId) if err != nil { fmt.Println(err) return err } // 初始化锁 kv := clientv3.NewKV(etcd.client) txn := kv.Txn(context.TODO()) txn.If(clientv3.Compare(clientv3.CreateRevision(\u0026#34;/dev/lock\u0026#34;), \u0026#34;=\u0026#34;, 0)).Then( clientv3.OpPut(\u0026#34;/dev/lock\u0026#34;, ip, clientv3.WithLease(leaseId))).Else( clientv3.OpGet(\u0026#34;/dev/lock\u0026#34;)) txnResponse, err := txn.Commit() if err != nil { fmt.Println(err) return err } // 判断是否抢锁成功 if txnResponse.Succeeded { fmt.Println(\u0026#34;抢到锁了\u0026#34;) fmt.Println(\u0026#34;选定主节点\u0026#34;, ip) // 续租节点 for { select { case leaseKeepAliveResponse = \u0026lt;-leaseKeepAliveChan: if leaseKeepAliveResponse != nil { fmt.Println(\u0026#34;续租成功,leaseID :\u0026#34;, leaseKeepAliveResponse.ID) } else { fmt.Println(\u0026#34;续租失败\u0026#34;) } } } } else { // 继续回头去抢，不停请求 fmt.Println(\u0026#34;没抢到锁\u0026#34;, txnResponse.Responses[0].GetResponseRange().Kvs[0].Value) fmt.Println(\u0026#34;继续抢\u0026#34;) time.Sleep(time.Second * 1) } return nil } func main(){ // 连接ETCD etcd, err := New(\u0026#34;xxxxxxxx:2379\u0026#34;) if err != nil { fmt.Println(err) } // 设定无限循环 for { etcd.Newleases_lock(\u0026#34;node1\u0026#34;) } } 总结 相关代码写入到github当中，其中的地址是\nhttps://github.com/Alexanderklau/Go_poject/tree/master/Go-Etcd/lock_work\n实现这个功能废了不少功夫，好久没写go了，自己太菜了，如果有老哥发现问题请联系我，好改正。\n","permalink":"https://yemilice.com/posts/etcd%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%AE%9E%E7%8E%B0%E9%80%89%E4%B8%BB%E6%9C%BA%E5%88%B6-golang/","summary":"\u003ch1 id=\"etcd分布式锁实现选主机制golang\"\u003eETCD分布式锁实现选主机制(Golang)\u003c/h1\u003e\n\u003ch2 id=\"为什么要写这篇文章\"\u003e为什么要写这篇文章\u003c/h2\u003e\n\u003cp\u003e做架构的时候，涉及到系统的一个功能，有一个服务必须在指定的节点执行，并且需要有个节点来做任务分发，想了半天，那就搞个主节点做这事呗，所以就有了这篇文章的诞生，我把踩的坑和收获记录下来，方便未来查看和各位兄弟们参考。\u003c/p\u003e","title":"ETCD分布式锁实现选主机制(Golang)"},{"content":"算法笔记-入门-数据结构篇 从大学毕业之后就没研究过算法，都快忘光了，现在开个新坑，从头学起算法，哈哈，希望自己能够坚持住，不过我一定可以坚持住的，我就像易筋洗髓一样，将自己全身打断，重塑自己的一切，回归初心，以一个听者的名义对待一切，因为我做的都是我自己喜欢的事儿。\n基本的数据结构类型 什么是数据结构 说白了很简单，数据存计算机里，总得有个存放规律，不能乱来，就像你查字典，你可以一页一页翻着找字儿，你也可以直接按拼音跳转找字儿，这就是查字典的数据结构，数据结构就是决定数据顺序和位置的关系。\n数据结构的子类-链表 链表，这玩意儿理解起来会抽象一些，大学课本上表示它的数据是一个线性排列的，要我说不用这么麻烦，链表其实就是一列火车，举例来说，现在有4节车厢，你必须通过一节车厢才能到下一节去，也就是说，车厢（链表）都有一个指示牌（指针），你必须一个个往下，到达下面的车厢（指向下一个地址）。\n链表这玩意儿吧，慢，查东西你得一个个往下，添加，删除数据都先要改变指针。\n查一个东西，拿大O表示法，它的复杂度是O(n)，算是相当慢的一种算法了。\n数据结构的子类-数组 数组，也是线性排列的数据结构，还记得链表么，链表是靠指针指向，告诉你我下一个老哥是谁，但是数组不一样，它是靠一个叫数组下标的东西来告诉你，我是第几个，在Python中，这玩意儿被运用在列表里，就像a = [1,2,3,4,5] 这种形式，a[0] = 1, a[1] = 2\u0026hellip;\u0026hellip;.\n其实你想查一个数组里面的东西，一般都是随机访问，可以直接去访问数组下标，这东西就相当于你吃饭取的号儿，到你了，人家就喊“XXX号用餐了！”数组下标就这个功能。\n数组里面，你要添加或者删除一个元素，那可有点麻烦，你要先在数组尾部，加一个多的存储空间，总不可能让新元素没地方去吧，然后你要让旧元素给新朋友腾个位置，然后把旧朋友往后面赶，然后新朋友才能顺利插队。。。\n数据结构的子类-栈 栈这位老哥会理解麻烦一点，这么想，你随时随地都能吃到最新鲜的水果，每天都有新的水果，你总能拿到新水果，旧水果就只有在下面，所以你如果想吃旧水果，你就要把水果箱子一点点拿出来，被称为出栈，把水果放回去，叫进栈，这个东西就是你只能拿最新的，后进先出，LIFO结构，这玩意儿还是挺不方便的。\n不过在业务中，如果你需要时刻保持最新数据在前面，例如，时事热点，附近的人等等，拿这玩意儿就好用多了。\n数据结构的子类-队列 和上面的栈老哥相反，队列的意思就是，你先进来的啊，你边儿待着，该需要的时候要我旧数据先上，拿数据从最老的数据拿，新数据一边玩去。想拿新数据？不好意思，一个个出来吧你，直到该你出去为止。\n数据结构的子类-哈希表 这个在这说有点那啥，但是啊，你们写过Python的应该知到，Python里面有个dict（字典），字典这玩意儿就是一个key 对应 一个value，例如\n{ \u0026#34;蔡徐坤\u0026#34; ：\u0026#34;篮球\u0026#34;， \u0026#34;吴亦凡\u0026#34; ：\u0026#34;说唱\u0026#34; } 这就实现了一个字典，你会问，这和TM哈希表有啥关系，哈希表这玩意儿，是存dict的东西，它是个类数组的东西，但是存的东西很变态，一般我们会用hash函数，计算\u0026quot;蔡徐坤\u0026quot;的键,也就是\u0026quot;蔡徐坤\u0026quot;的哈希值，然后我们将得到的哈希值除以数组长度（哈希表长）取余数，计算出\u0026quot;蔡徐坤\u0026quot;在数组中的位置，然后把它放进去。这里比较抽象，后面会专门讲一下哈希这个算法。\n那么怎么查呢，首先我们拿到\u0026quot;蔡徐坤\u0026quot;的哈希值，然后通过刚才的计算就能找到\u0026quot;蔡徐坤\u0026quot;在哈希表中的位置了。\n数据结构的子类-堆 重头戏来了，这玩意儿是一个树形结构，树形，顾名思义，是分叉的，想象一下，一棵树的样子，这玩意儿是拿来搞优先队列用的，堆里面有个老大，叫结点，所有的数据都是结点的小弟，受他罩着，其中他有多个手下，叫子结点，子结点一般比父结点大，最小的值一般都在堆的顶点。\n堆中最顶端的数据始终最小，所以无论数据量有多少，取出最小值的时间复杂度都 为 O(1)。 另外，因为取出数据后需要将最后的数据移到最顶端，然后一边比较它与子结点数据 的大小，一边往下移动，所以取出数据需要的运行时间和树的高度成正比。假设数据量为 n，根据堆的形状特点可知树的高度为 log2n ，那么重构树的时间复杂度便为 O(logn)。 添加数据也一样。在堆的最后添加数据后，数据会一边比较它与父结点数据的大 小，一边往上移动，直到满足堆的条件为止，所以添加数据需要的运行时间与树的高度 成正比，也是 O(logn）\n数据结构的子类-二叉查找树 和楼上那位一样，也是个树形结构，不一样的是，二叉树每个结点的值大于左子树上任意一个结点的值，比较抽象对吧，来看个图\n意思就是，无论怎么样，左边老哥总会比我小。\n第二个分歧就是，右边老哥总比我大，继续看图\n这就是一部分基础，做了粗略写作，写得不好，见谅，over！\n","permalink":"https://yemilice.com/posts/%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0-%E5%85%A5%E9%97%A8-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E7%AF%87/","summary":"\u003cp\u003e算法笔记-入门-数据结构篇\n从大学毕业之后就没研究过算法，都快忘光了，现在开个新坑，从头学起算法，哈哈，希望自己能够坚持住，不过我一定可以坚持住的，我就像易筋洗髓一样，将自己全身打断，重塑自己的一切，回归初心，以一个听者的名义对待一切，因为我做的都是我自己喜欢的事儿。\u003c/p\u003e","title":"算法笔记-入门-数据结构篇"},{"content":"前言 为什么要写这篇文章呢。。。主要还是业务中有个需求，遍历一个将近200w数据的文件夹，大部分还都是视频文件那种，但是这玩意用的次数还不多，做文件夹index也不是很ok，所以写了一个脚本来处理这个问题，从而发现了自己的一些薄弱点，将其记录下来，方便自己，也方便未来其他的兄弟使用\n基本需求 把文件夹中的重复文件找出来 找出来之后用csv输出，左边是源文件，右边是重复文件 效率不能差，不能直接撑爆内存，不能占用过多资源 检测的文件夹和存放csv的地方可以自己定义，加上终端交互 重复文件筛选支持md5，大小等方式 需求分析 首先要分析一点，就是我们该如何去做重复文件的对比，并且效率还要高，首先网上过多的递归，os.walk的方法不可用，因为他们都会把遍历到的内容直接做成一个大列表，塞到内存里面，数据量大很容易爆掉，并且还要进行MD5，或者是大小比对，这个就非常难缠了。\n基础想法 其实说白了，拿到所有文件列表file_list，把文件依次对比，这里我们可以用dict，分两种情况\n按照文件名和大小 设定两个dict，例如record和dup，遍历file_list,生成一个数组，比对其中的文件名和大小\n按照大小和MD5值 设定两个dict，例如record和dup，遍历file_list,生成一个数组，比对其中的md5值和大小\n具体代码 闲话休提，我们开始写代码吧\n定义遍历函数代码 首先定义遍历文件夹的部分diskwalk.py\n# coding: utf-8 __author__ = \u0026#34;lau.wenbo\u0026#34; import os,sys class diskwalk(object): def __init__(self, path): self.path = path def paths(self): path = self.path # 这里用了一个迭代器逻辑，防止所有数据塞内存爆掉 path_collection = (os.path.join(root,fn) for root,dirs,files in os.walk(path) for fn in files) return path_collection 定义检查md5值代码 接着我们定义检查md5值的一个逻辑checksum.py\n# coding: utf-8 __author__ = \u0026#34;lau.wenbo\u0026#34; import hashlib,sys # 分块读MD，速度快 def create_checksum(path): fp = open(path) checksum = hashlib.md5() while True: buffer = fp.read(8192) if not buffer: break checksum.update(buffer) fp.close() checksum = checksum.digest() return checksum 定义主函数代码 # coding: utf-8 __author__ = \u0026#34;lau.wenbo\u0026#34; from checksum import create_checksum from diskwalk import diskwalk from os.path import getsize import csv import os import sys reload(sys) sys.setdefaultencoding(\u0026#39;utf8\u0026#39;) def findDupes(path): record = {} dup = {} d = diskwalk(path) files = d.paths() for file in files: try: # 这里使用了大小，文件名的对比方式，如果你需要MD5值的对比方式，可以打开下面的注释 #compound_key = (getsize(file),create_checksum(file)) compound_key = (getsize(file), file.split(\u0026#34;/\u0026#34;)[-1]) if compound_key in record: dup[file] = record[compound_key] else: record[compound_key]=file except: continue return dup if __name__ == \u0026#39;__main__\u0026#39;: path = sys.argv[1] csv_path = sys.argv[2] if not os.path.isdir(path) or not os.path.isdir(csv_path) or csv_path[-1] != \u0026#34;/\u0026#34;: print u\u0026#34;参数不是一个有效的文件夹！\u0026#34; exit() else: path = path.decode(\u0026#34;utf-8\u0026#34;) print u\u0026#34;待检测的文件夹为{path}\u0026#34;.format(path=path) with open(u\u0026#34;{csv_path}重复文件.csv\u0026#34;.format(csv_path=csv_path),\u0026#34;w+\u0026#34;) as csvfile: # 源文件 重复文件 header = [\u0026#34;Source\u0026#34;, \u0026#34;Duplicate\u0026#34;] writer = csv.DictWriter(csvfile, fieldnames=header) writer.writeheader() print u\u0026#34;开始遍历文件夹，寻找重复文件，请等待.........\u0026#34; print u\u0026#34;开始写入CSV文件，请等待........\u0026#34; for file in findDupes(path).items(): writer.writerow({\u0026#34;Source\u0026#34;:file[1],\u0026#34;Duplicate\u0026#34;:file[0]}) 结语 实现了哪些功能呢，哈哈，结尾来说一下，其实核心就是我用了一个列表生成器，加了一个迭代器，迭代器可是好东西，不会撑内存，不错了，效率也还可以，200w数据判定也就20多分钟，支持大数据量，如果有什么不懂的，可以邮件联系我或者等待我的评论系统搞完，over\ngithub地址在这: https://github.com/Alexanderklau/Amusing_python/tree/master/File_operation/repeat\n","permalink":"https://yemilice.com/posts/python%E9%AB%98%E6%95%88%E7%8E%87%E9%81%8D%E5%8E%86%E6%96%87%E4%BB%B6%E5%A4%B9%E5%AF%BB%E6%89%BE%E9%87%8D%E5%A4%8D%E6%96%87%E4%BB%B6/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e为什么要写这篇文章呢。。。主要还是业务中有个需求，遍历一个将近200w数据的文件夹，大部分还都是视频文件那种，但是这玩意用的次数还不多，做文件夹index也不是很ok，所以写了一个脚本来处理这个问题，从而发现了自己的一些薄弱点，将其记录下来，方便自己，也方便未来其他的兄弟使用\u003c/p\u003e","title":"Python高效率遍历文件夹寻找重复文件"},{"content":"Elasticsearch for python API模块化封装 模块的具体功能 检测Elasticsearch节点是否畅通 查询Elasticsearch节点健康状态 查询包含的关键字的日志（展示前10条） 查询指定的索引下的数据，并且分页 输出所有日志(输出全部) 输出去重后的日志(分页，带关键字） 删除指定索引的值 往索引中添加数据 获取指定index、type、id对应的数据 更新指定index、type、id所对应的数据 批量插入数据 使用方法 一般作为独立的包进行导入，并且对其进行了大数据预览的优化和处理\n作为一个独立Python模块进行导入，并且调取接口使用。\n调用方法\nimport elasticdb.es_sysdb as es esdb = es.Es() 使用举例 打印出索引（表）内的所有数据：\n需要index名，也就是指定索引名，在这里，假设我要查所有的monlog数据，那么查询语句如:\na = esdb.search_all(client=esdb.conn, index=monlog, type=\u0026#34;doc\u0026#34;) for i in a: c.append(i[\u0026#34;_source\u0026#34;][\u0026#34;message\u0026#34;]) 接口详情 接口参数说明\n参数 必选 类型 说明 index ture str 索引名 ，可认为是数据库 type true str 索引类型，可认为是表名 keywords ture str 关键字 page ture str 页数，分页逻辑 size ture str 每页展示条数，分页逻辑使用 查询包含的关键字的日志（展示前10条） a = esdb.search_searchdoc(index=monlog, type=\u0026#34;doc\u0026#34;, keywords=\u0026#34;cpu\u0026#34;) for i in a: print i[\u0026#34;_source\u0026#34;][\u0026#34;message\u0026#34;] 查询指定的索引下的数据，并且分页 示例：查询index为”oplog-2018-08,oplog-2018-12”，并且每页展示（size）5条，输出第二页（page）\nfor i in esdb.serch_by_index(index=\u0026#34;oplog-2018-08,oplog-2018-12\u0026#34;, page=2, size=5)[\u0026#34;hits\u0026#34;][\u0026#34;hits\u0026#34;]: print(i[\u0026#34;_source\u0026#34;][\u0026#34;message\u0026#34;]) 输出所有日志(输出全部) for i in esdb.search_all(client=esdb.conn, index=\u0026#34;monlog-*\u0026#34;, type=\u0026#34;doc\u0026#34;): print i 输出去重后的日志(分页，带关键字） 示例：关键字为空，搜索monlog的所有数据，展示第一页，并且每页展示10条\nfor i in esdb.serch_es_count(keywords = \u0026#34;\u0026#34;, index=\u0026#34;monlog-*\u0026#34;, type=\u0026#34;doc\u0026#34;,page=1, size=10): print i 删除指定索引的值 示例：删除monlog的所有值\nesdb.delete_all_index(index=\u0026#34;monlog-*\u0026#34;, type=\u0026#34;doc\u0026#34;) 查询集群健康状态 esdb.check_health() 往索引中添加数据 body = {\u0026#34;name\u0026#34;: \u0026#39;lucy2\u0026#39;, \u0026#39;sex\u0026#39;: \u0026#39;female\u0026#39;, \u0026#39;age\u0026#39;: 10} print esdb.insertDocument(index=\u0026#39;demo\u0026#39;, type=\u0026#39;test\u0026#39;, body=body) 获取指定index、type、id对应的数据 print esdb.getDocById(index=\u0026#39;demo\u0026#39;, type=\u0026#39;test\u0026#39;, id=\u0026#39;6gsqT2ABSm0tVgi2UWls\u0026#39;) 更新指定index、type、id所对应的数据 body = {\u0026#34;doc\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#39;jackaaa\u0026#39;}}#修改部分字段 print esdb.updateDocById(\u0026#39;demo\u0026#39;, \u0026#39;test\u0026#39;, \u0026#39;z\u0026#39;, body) 批量插入数据 _index = \u0026#39;demo\u0026#39; _type = \u0026#39;test_df\u0026#39; import pandas as pd frame = pd.DataFrame({\u0026#39;name\u0026#39;: [\u0026#39;tomaaa\u0026#39;, \u0026#39;tombbb\u0026#39;, \u0026#39;tomccc\u0026#39;], \u0026#39;sex\u0026#39;: [\u0026#39;male\u0026#39;, \u0026#39;famale\u0026#39;, \u0026#39;famale\u0026#39;], \u0026#39;age\u0026#39;: [3, 6, 9], \u0026#39;address\u0026#39;: [u\u0026#39;合肥\u0026#39;, u\u0026#39;芜湖\u0026#39;, u\u0026#39;安徽\u0026#39;]}) print esAction.insertDataFrame(_index, _type, frame) 代码示例 from elasticsearch import Elasticsearch from elasticsearch import helpers class Es: def __init__(self): self.hosts = \u0026#34;127.0.0.1\u0026#34; self.conn = Elasticsearch(hosts=self.hosts, port=9200) def check(self): \u0026#39;\u0026#39;\u0026#39; 输出当前系统的ES信息 \u0026#39;\u0026#39;\u0026#39; return self.conn.info() def ping(self): return self.conn.ping() def check_health(self): \u0026#39;\u0026#39;\u0026#39; 检查集群的健康状态 :return: \u0026#39;\u0026#39;\u0026#39; status = self.conn.transport.perform_request(\u0026#39;GET\u0026#39;, \u0026#39;/_cluster/health\u0026#39;, params=None)[\u0026#34;status\u0026#34;] return statuu def get_index(self): return self.conn.indices.get_alias(\u0026#34;*\u0026#34;) def search_specify(self, index=None, type=None, keywords=None, page=None, size=None): # 查询包含的关键字的日志 query = { \u0026#39;query\u0026#39;: { \u0026#39;match\u0026#39;: { \u0026#39;message\u0026#39;: keywords } }, \u0026#39;from\u0026#39;:page * size, \u0026#39;size\u0026#39;:size } message = self.searchDoc(index, type, query) return message 完整的代码地址：https://github.com/Alexanderklau/elasticdb\n","permalink":"https://yemilice.com/posts/elasticsearch-for-python-api%E6%A8%A1%E5%9D%97%E5%8C%96%E5%B0%81%E8%A3%85/","summary":"\u003ch1 id=\"elasticsearch-for-python-api模块化封装\"\u003eElasticsearch for python API模块化封装\u003c/h1\u003e\n\u003ch2 id=\"模块的具体功能\"\u003e模块的具体功能\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e检测Elasticsearch节点是否畅通\u003c/li\u003e\n\u003cli\u003e查询Elasticsearch节点健康状态\u003c/li\u003e\n\u003cli\u003e查询包含的关键字的日志（展示前10条）\u003c/li\u003e\n\u003cli\u003e查询指定的索引下的数据，并且分页\u003c/li\u003e\n\u003cli\u003e输出所有日志(输出全部)\u003c/li\u003e\n\u003cli\u003e输出去重后的日志(分页，带关键字）\u003c/li\u003e\n\u003cli\u003e删除指定索引的值\u003c/li\u003e\n\u003cli\u003e往索引中添加数据\u003c/li\u003e\n\u003cli\u003e获取指定index、type、id对应的数据\u003c/li\u003e\n\u003cli\u003e更新指定index、type、id所对应的数据\u003c/li\u003e\n\u003cli\u003e批量插入数据\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"使用方法\"\u003e使用方法\u003c/h2\u003e\n\u003cp\u003e一般作为独立的包进行导入，并且对其进行了大数据预览的优化和处理\u003cbr\u003e\n作为一个独立Python模块进行导入，并且调取接口使用。\u003cbr\u003e\n调用方法\u003c/p\u003e","title":"Elasticsearch for python API模块化封装"},{"content":"Golang 调用 aws-sdk 操作 S3对象存储 前言 因为业务问题，要写一个S3对象存储管理代码，由于一直写Go，所以这次采用了Go，Go嘛，快，自带多线程，这种好处就不用多说了吧。\n基础的功能 查看S3中包含的bucket bucket中的文件/文件夹 bucket的删除 bucket的创建 bucket的文件上传 bucket的文件下载 bucket的文件删除 aws-sdk 的安装 玩Golang你还能不会那啥？对吧，那啥？那飞机！那飞机场，安上~\ngo get github.com/aws/aws-sdk-go aws-sdk-go 的基础使用 构建基础的S3连接 访问S3的时候，咱们需要access_key，secret_key，对象存储访问IP这三个参数，我们首先要创建一个aws的config，说白了，我们需要定义aws的配置，这样它才知道要怎么访问，去哪里访问等问题。\n构建一个S3连接代码如下\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/credentials\u0026#34; _ \u0026#34;github.com/aws/aws-sdk-go/service/s3/s3manager\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/session\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/service/s3\u0026#34; ) func main() { access_key := \u0026#34;xxxxxxxxxxxxx\u0026#34; secret_key := \u0026#34;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u0026#34; end_point := \u0026#34;http://xx.xx.xx.xx:7480\u0026#34; //endpoint设置，不要动 sess, err := session.NewSession(\u0026amp;aws.Config{ Credentials: credentials.NewStaticCredentials(access_key, secret_key, \u0026#34;\u0026#34;), Endpoint: aws.String(end_point), Region: aws.String(\u0026#34;us-east-1\u0026#34;), DisableSSL: aws.Bool(true), S3ForcePathStyle: aws.Bool(false), //virtual-host style方式，不要修改 }) } 这时候需要你自己去定义一下access_key，secret_key，end_point这三个参数\n接下来所有的代码，都是以这个连接模板，为核心，后面我就用同上代替配置，请注意！\n所有的代码都传到GIT上了，到时候会给出地址，不懂得copy下来吧！\n查看S3中包含的bucket 查看所有的bucket\npackage main import ( 导入包同上 ) func exitErrorf(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, msg+\u0026#34;\\n\u0026#34;, args...) os.Exit(1) } func main() { 配置同上 svc := s3.New(sess) result, err := svc.ListBuckets(nil) if err != nil { exitErrorf(\u0026#34;Unable to list buckets, %v\u0026#34;, err) } fmt.Println(\u0026#34;Buckets:\u0026#34;) for _, b := range result.Buckets { fmt.Printf(\u0026#34;* %s created on %s\\n\u0026#34;, aws.StringValue(b.Name), aws.TimeValue(b.CreationDate)) } for _, b := range result.Buckets { fmt.Printf(\u0026#34;%s\\n\u0026#34;, aws.StringValue(b.Name)) } } 列出bucket中的文件/文件夹 查看某个bucket中包含的文件/文件夹\npackage main import ( \u0026#34;github.com/aws/aws-sdk-go/aws\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/session\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/credentials\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/service/s3\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func exitErrorf(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, msg+\u0026#34;\\n\u0026#34;, args...) os.Exit(1) } func main() { 配置同上 // bucket后跟，go run ....go bucketname bucket := os.Args[1] fmt.Printf(bucket) fmt.Printf(\u0026#34;\\n\u0026#34;) svc := s3.New(sess) params := \u0026amp;s3.ListObjectsInput{ Bucket: aws.String(bucket), } resp, err := svc.ListObjects(params) if err != nil { exitErrorf(\u0026#34;Unable to list items in bucket %q, %v\u0026#34;, bucket, err) } for _, item := range resp.Contents { fmt.Println(\u0026#34;Name: \u0026#34;, *item.Key) fmt.Println(\u0026#34;Last modified:\u0026#34;, *item.LastModified) fmt.Println(\u0026#34;Size: \u0026#34;, *item.Size) fmt.Println(\u0026#34;Storage class:\u0026#34;, *item.StorageClass) fmt.Println(\u0026#34;\u0026#34;) } } bucket的创建 创建bucket\npackage main import ( 导包同上 ) func exitErrorf(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, msg+\u0026#34;\\n\u0026#34;, args...) os.Exit(1) } func main() { 配置同上 bucket := os.Args[1] if len(os.Args) != 2 { exitErrorf(\u0026#34;Bucket name required\\nUsage: %s bucket_name\u0026#34;, os.Args[0]) } // Create S3 service client svc := s3.New(sess) params := \u0026amp;s3.CreateBucketInput{ Bucket: aws.String(bucket), } _, err = svc.CreateBucket(params) if err != nil { exitErrorf(\u0026#34;Unable to create bucket %q, %v\u0026#34;, bucket, err) } // Wait until bucket is created before finishing fmt.Printf(\u0026#34;Waiting for bucket %q to be created...\\n\u0026#34;, bucket) err = svc.WaitUntilBucketExists(\u0026amp;s3.HeadBucketInput{ Bucket: aws.String(bucket), }) if err != nil { exitErrorf(\u0026#34;Error occurred while waiting for bucket to be created, %v\u0026#34;, bucket) } fmt.Printf(\u0026#34;Bucket %q successfully created\\n\u0026#34;, bucket) } bucket的文件上传 往某个固定的bucket里传文件\npackage main import ( \u0026#34;github.com/aws/aws-sdk-go/aws\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/session\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/credentials\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/service/s3/s3manager\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func exitErrorf(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, msg+\u0026#34;\\n\u0026#34;, args...) os.Exit(1) } func main() { 配置同上 if len(os.Args) != 3 { exitErrorf(\u0026#34;bucket and file name required\\nUsage: %s bucket_name filename\u0026#34;, os.Args[0]) } bucket := os.Args[1] filename := os.Args[2] file, err := os.Open(filename) if err != nil { exitErrorf(\u0026#34;Unable to open file %q, %v\u0026#34;, err) } defer file.Close() uploader := s3manager.NewUploader(sess) _, err = uploader.Upload(\u0026amp;s3manager.UploadInput{ Bucket: aws.String(bucket), Key: aws.String(filename), Body: file, }) if err != nil { // Print the error and exit. exitErrorf(\u0026#34;Unable to upload %q to %q, %v\u0026#34;, filename, bucket, err) } fmt.Printf(\u0026#34;Successfully uploaded %q to %q\\n\u0026#34;, filename, bucket) } bucket的文件下载 下载某个bucket中的某个文件\npackage main import ( \u0026#34;github.com/aws/aws-sdk-go/aws\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/session\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/credentials\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/service/s3\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/service/s3/s3manager\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func exitErrorf(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, msg+\u0026#34;\\n\u0026#34;, args...) os.Exit(1) } func main() { 配置同上 if len(os.Args) != 3 { exitErrorf(\u0026#34;Bucket and item names required\\nUsage: %s bucket_name item_name\u0026#34;, os.Args[0]) } bucket := os.Args[1] item := os.Args[2] file, err := os.Create(item) if err != nil { exitErrorf(\u0026#34;Unable to open file %q, %v\u0026#34;, err) } defer file.Close() downloader := s3manager.NewDownloader(sess) numBytes, err := downloader.Download(file, \u0026amp;s3.GetObjectInput{ Bucket: aws.String(bucket), Key: aws.String(item), }) if err != nil { exitErrorf(\u0026#34;Unable to download item %q, %v\u0026#34;, item, err) } fmt.Println(\u0026#34;Downloaded\u0026#34;, file.Name(), numBytes, \u0026#34;bytes\u0026#34;) }\tbucket的文件删除 删除某个bucket里面的某个文件\npackage main import ( \u0026#34;github.com/aws/aws-sdk-go/aws\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/session\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/aws/credentials\u0026#34; \u0026#34;github.com/aws/aws-sdk-go/service/s3\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func exitErrorf(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, msg+\u0026#34;\\n\u0026#34;, args...) os.Exit(1) } func main() { 配置同上 if len(os.Args) != 3 { exitErrorf(\u0026#34;Bucket and object name required\\nUsage: %s bucket_name object_name\u0026#34;, os.Args[0]) } bucket := os.Args[1] obj := os.Args[2] svc := s3.New(sess) _, err = svc.DeleteObject(\u0026amp;s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(obj)}) if err != nil { exitErrorf(\u0026#34;Unable to delete object %q from bucket %q, %v\u0026#34;, obj, bucket, err) } err = svc.WaitUntilObjectNotExists(\u0026amp;s3.HeadObjectInput{ Bucket: aws.String(bucket), Key: aws.String(obj), }) fmt.Printf(\u0026#34;Object %q successfully deleted\\n\u0026#34;, obj) } 代码所在地 https://github.com/Alexanderklau/Go_poject/tree/master/Go-Storage\n","permalink":"https://yemilice.com/posts/golang-%E8%B0%83%E7%94%A8-aws-sdk-%E6%93%8D%E4%BD%9C-s3%E5%AF%B9%E8%B1%A1%E5%AD%98%E5%82%A8/","summary":"\u003ch1 id=\"golang-调用-aws-sdk-操作-s3对象存储\"\u003eGolang 调用 aws-sdk 操作 S3对象存储\u003c/h1\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e因为业务问题，要写一个S3对象存储管理代码，由于一直写Go，所以这次采用了Go，Go嘛，快，自带多线程，这种好处就不用多说了吧。\u003c/p\u003e","title":"Golang 调用 aws-sdk 操作 S3对象存储"},{"content":"程序员如何锻炼自己的产品思维 写作目的 源于一次需求会议被怼，大老板总是说我以技术思维决定一切。\n后来我一思考，卧槽，果然是这样，每次我都是非常纠结于技术实现和技术细节，总是纠缠在业务实现里面，所以渐渐就养成了那个习惯。思考了一下，有些地方也的确是应该做出一点点改变了，老话说，种一棵树，最早是十年前，其次就是现在，那么我们就开始种树吧。趁着放假读几本产品思维的书，有一点点感悟，所以将此文写下，方便自己，也方便急于转型的各位程序员老哥。\n个人技术背景 1.菜逼一个，看博客就知道了。\n2.掌握技术：\n后端： Python，Golang，Java\n移动端：OC（大学时候兼职IOS开发）\n前端：基础的React和Vue框架\n3.算法技术：渣渣\n4.没有任职过任何产品岗位\n个人分析 优点 技术涉及面广，做项目多，一直在项目第一线，熟悉项目业务，思维活跃，善于解决和发现问题。\n缺点 算法能力差，前端能力差，抽象思维较弱，容易钻牛角尖，对项目整体了解不够透彻，只了解某些模块和部分。\n什么是产品思维？ 理论上的产品思维 1.把握关键点的能力\n2.出方案，协调资源，说服团队把资源倾斜到关键点上的能力\n3.评估关键点进展程度的能力\n大白话解释 1.首先用户就是一切，一切为了用户爽\n2.反向思维，逆推解决问题\n3.换位思考啊大哥，你不仅仅是产品人员，你还得是老板，用户，程序员balabala\n4.脑子里对业务and产品都算是有了解（当然不能说像程序员一样）\n其他来源对产品思维的解释 1.从人性本质挖掘需求\n说白了就是你要从人去思考问题，也就是说，你要人的表面挖掘到人的内心，类似挠痒痒，不能挠痒痒之后把皮整破了，这就是你满足了表面需求，但是破坏了底层需求 2.从赚钱的角度思考\n说白了就是追逐利益，想法儿怎么搞钱，例如扫码送东西，例如扫码给红旗，例如十一的时候给微信加国旗，这都是从逐利的思想去发觉需求 3.沟通能力\n我缺乏哪些技能？ 粗略看一下，其实缺乏的东西看起来很简单\n对整体架构了解不多 逆向思维较差，不能从用户需求去理解问题，只单纯纠结能不能实现功能 评估项目和关键点能力不足 不能够和其他程序员有很好的沟通 平常和业务纠缠太多了，我这种Code monkey每天都去思考这个功能怎么实现，用什么技术更牛逼，怎么优化之类的，纠结技术，功能，细节等等。举个例子，\n我作为一个代码工程师\n工程师思维关注技术至上，技术水平代表实力，向于在产品中使用先进、流行的技术，因为掌握先进主流的技术可以提高他的身价。\n产品思维关注的是，这技术能给用户带来什么价值？有什么商业价值？\n所以我需要跳出这个怪圈，学会用产品的思维去思考问题，这样也能够开拓自己的眼界，无论是技术还是其他的路，都可以走的更远。\n我该如何去补强这些技能？ 我理解的产品思维 每一个项目都是产品。\n我们可以把工作当中的任何一个输出成果当做产品，用产品思维来完成这个成果。\n比如，我现在正在开发一个分布式的同步备份工程，将之称为产品。\n按照产品思维来策划这个工程，你要思考：\n我为什么要做这个产品？希望得到什么？\n用户是谁？谁在用这个？他们希望怎么去用？\n干系人有哪些？他们的期待是？\n使用场景：\n现有的web？还是独立开发APP？或者是普通的云计算服务？或者是普通存储服务？或者是类似同步服务？ 用户的关注点：\n怎么用？好操控么？用着舒服么？界面看着开心么？\n思考一下，产品思维的确和工程师思维不太一样，我也不能总是在工程师思维这个怪圈中徘徊\n理论上的补强手段 保持自己对于不同产品、不同领域的好奇心和敏感度 很多时候我都忙于自己当下的工作，很难有机会接触到不同领域不同产品。很可能渐渐地就失去了对于产品的好奇心和敏感度，所以必须要让自己走出去，多去接触，或者看一看别人的产品or项目如何设计，思考他们是怎么做产品的？他们为什么这么做 ？如果我来做能怎样做？通过这样的思考和练习，来保持自己对产品的好奇心和敏感度\n向上拓展自己的能力，不能停留于技术人员or产品经理 一直纠缠与技术实现细节，总归是只有一层，如果满足这一层，也就是写代码的工具，或者是模块添加人员，也就没有办法建立起来自己的核心竞争力，笑傲江湖里面，剑宗气宗之争也是这一点，剑气双休才是最重要的。所以不仅仅是技术要抓，思想也要抓。\n强化自己逻辑思维分析能力 在逻辑思维方面，我想没有谁比得过程序员，程序员本来就是逻辑性很强的工作，这一点其实我认为更重要的是换位分析，易地而处的一种状态。我们往往分析自己的工作比较容易，但是涉及大局分析，就有些力不从心，这个我认为还是要针对性训练。\n分解问题的能力 其实这个在写代码的时候也经常预见到，不是么，一个大问题细化为好几个小问题，换算成产品思维也即是：\n1.产品有哪些功能？\n2.这些功能下面又分哪些模块？\n3.具体的应用场景在哪里？\n4.产品模块之间相互的联系是什么？\n5.谁在用这些产品？\n6.业务部门之间的需求是否互相耦合？是否已经存在重复需求？\n这里只是举个例子，具体问题具体分析，将自己想象成产品经理，先不要思考问题怎么解决，看看产品是怎么做的，再去对比思路思考解决问题。\n用户行为分析能力 什么是用户行为？关键就是用户用着你这个产品产生的行为，再去产生其他行为，这是用户增长和用产品化的重要组成部分。\n首先我们需要找出，关键用户行为，也就是，用户在使用产品时，是奔着你产品的什么方面来的，拿我正在做的项目举个例子，同步备份模块，干嘛的，同步备份文件的，特点呢？分布式，速度快，那不就完了！关键行为就是同步备份，这才是用户的关键行为。\n首先我们要考虑为什么会产生关键行为，也就输确定产品的价值，产品的价值就是用户愿不愿意给这个产品花钱，愿不愿意花钱去买我们的东西，解决痛点是第一位，但是在这之前，有没有类似的产品做了？人家做的好不好？谁的效率高？谁更牛逼？牛逼在哪？这才是要去分析的地方。\n场景分析能力 说白了，角色扮演，你把自己想象成一个用户，现在我想要一个产品，思考一下\n1.产品包含哪些场景\n2.产品涉及哪些角色\n3.场景会被第三方影响么？如果会，该如何去降低它？\n举个例子，同步备份的产品用在普通用户手中，普通用户的网很慢，同步时断时续，这就是第三方的缘故，但是我们的产品是单节点，也就是说只能一点点下，不能分布式，这就坑了，用户会觉得，你这怎么那么卡，你看看人迅雷，都能断点，都能分布式，你这个，get out，这就是第三方影响使用场景。\n再举个例子哈：\n朱啸虎先生在杭州的一次演讲中提到了维诺城。维诺城是在地铁口放置终端，用户出了地铁口之后可以在上面打印周围商家的优惠券。维诺城最初的生意非常好，因为地铁人流大，又是优惠券提供，在大众点评美团还没崛起的时候，它确实是很方便的产品。然而现在一方面因为美团这些APP的强势崛起，另一方面因为地铁提高了租金，更多的商家进场，甚至地铁公司本身都要来抢这个生意，维诺城的生意就下坡路了。\n维诺城的例子说明什么？说明如果产品的主要场景容易受限于特殊的场地和时间特性，而这个场景进入的门槛比较低或是由第三方来控制，那么这个产品从场景上来说是有很高风险的；作为产品经理就要尝试思考有没有办法去降低这些影响，或是去发现自己产品不过度依赖这个场景的核心竞争力？\n数据分析能力 数据这玩意，永远是支撑一个产品，或者是一个理论的重要依据。如何在通过数据去引导自己的产品思维\n1、明确数据指标的定义、口径和使用场景。\n要能清楚地和开发人员描述数据指标到底是什么，有哪些维度，在哪个页面或哪个场景之下发生；\n2、层层剥离，穷举指标\n产品经理为了保证数据的准确性，要尽可能地将指标拆解，拆解到不能拆解为止。同时也要分清哪些是核心指标，哪些是主要指标，哪些是次要指标；\n3、数据指标和用户结合\n新用户做了什么？老用户做了什么？付费用户做了什么？非付费用户又做了什么？流失用户在流失之前做了什么？要回答这些问题就要将数据指标和不同的用户结合起来分析\n","permalink":"https://yemilice.com/posts/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%A6%82%E4%BD%95%E9%94%BB%E7%82%BC%E8%87%AA%E5%B7%B1%E7%9A%84%E4%BA%A7%E5%93%81%E6%80%9D%E7%BB%B4/","summary":"\u003ch1 id=\"程序员如何锻炼自己的产品思维\"\u003e程序员如何锻炼自己的产品思维\u003c/h1\u003e\n\u003ch2 id=\"写作目的\"\u003e写作目的\u003c/h2\u003e\n\u003cp\u003e源于一次需求会议被怼，大老板总是说我以技术思维决定一切。\u003cbr\u003e\n后来我一思考，卧槽，果然是这样，每次我都是非常纠结于技术实现和技术细节，总是纠缠在业务实现里面，所以渐渐就养成了那个习惯。思考了一下，有些地方也的确是应该做出一点点改变了，老话说，种一棵树，最早是十年前，其次就是现在，那么我们就开始种树吧。趁着放假读几本产品思维的书，有一点点感悟，所以将此文写下，方便自己，也方便急于转型的各位程序员老哥。\u003c/p\u003e","title":"程序员如何锻炼自己的产品思维"},{"content":"算是日记吧 其实自己一直在cnblog上更新自己的博客，从16年3月入行到现在，已经过去了三个春秋，发觉自己的技术还是个渣渣，最近生活真的忙成一团，工作也很忙，生活也很忙，有时候饭都不容易吃上，感觉自己学习真的学不动了，不知道自己是不是懈怠了，运动，音乐什么的，也停下来了，感觉真的缺少了一点点乐趣。\n不过说真的，自己也真的该动起来了，因为时间不等人啊，已经入行这么久，跌跌撞撞像个摇摆人，所以未来我还是要多学习，多更新我的博客，将自己的技术或者是一些感悟，哪怕是一些灵光一闪的理想，都记录下来，哈哈，这样会不会好一些呢？会不会明天就是更好的那一天呢？\n算是自己我介绍吧 渣渣一个，技术又全又杂，精通的少，各种都会一点，但是真正就是个渣渣，哈哈，现在孑然一生，18年年底被踹，重新出发，想想自己当时为了感情选择留在这个陌生的城市，现在已经爱上了这座城市，这座美丽的西南大都会。我想我会留下来的吧，我会留下来的？我也不确定吧，现在要慢慢出发，重新出发，我想我会越来越好的。\n算是结尾吧 可能你因为一些别的原因走到我博客来，其实欢迎你，欢迎你听一个24岁的家伙碎碎念那么久，未来大部分应该会更新技术，或者是我的一些奇特爱好，哈哈，希望你们会喜欢，我爱你们。\n","permalink":"https://yemilice.com/posts/%E6%96%B0%E6%97%A7%E5%8D%9A%E5%AE%A2%E8%BF%81%E7%A7%BB%E7%9A%84%E4%B8%80%E7%82%B9%E6%84%9F%E6%82%9F/","summary":"\u003ch2 id=\"算是日记吧\"\u003e算是日记吧\u003c/h2\u003e\n\u003cp\u003e其实自己一直在cnblog上更新自己的博客，从16年3月入行到现在，已经过去了三个春秋，发觉自己的技术还是个渣渣，最近生活真的忙成一团，工作也很忙，生活也很忙，有时候饭都不容易吃上，感觉自己学习真的学不动了，不知道自己是不是懈怠了，运动，音乐什么的，也停下来了，感觉真的缺少了一点点乐趣。\u003cbr\u003e\n不过说真的，自己也真的该动起来了，因为时间不等人啊，已经入行这么久，跌跌撞撞像个摇摆人，所以未来我还是要多学习，多更新我的博客，将自己的技术或者是一些感悟，哪怕是一些灵光一闪的理想，都记录下来，哈哈，这样会不会好一些呢？会不会明天就是更好的那一天呢？\u003c/p\u003e","title":"新旧博客迁移的一点感悟"}]