用 Ruby 讲从创业到 996 公司的故事(戏说 master-worker 模式)

讨论 未结 0 54
Mark24
Mark24 会员 2022年7月23日 13:59 发表
<h1>前言</h1> <p>阅读大概需要 20 分钟。</p> <p>假设你希望了解 线程、线程池、集群模式 /Master-Worker 模式、调度器。</p> <p>需要了解 Ruby 基本的用法和面向对象思想。</p> <p>本文戏说,无须严肃对待。勿对号入座。个人也没有严肃观点。个人观点和所有人没有关系。</p> <p><a href="https://mark24code.github.io/ruby/2022/07/23/%E7%94%A8ruby%E6%9D%A5%E5%86%99%E4%B8%80%E4%B8%AA996%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F(master-worker).html" rel="nofollow">本文博客地址</a></p> <h2>完整代码示例</h2> <p><a href="https://github.com/Mark24Code/rb-master-worker-demo" rel="nofollow">github:rb-master-worker-demo</a></p> <h1>Master Worker 模式</h1> <p>MasterWorker 模式,也有翻译成作集群模式、也叫 Master-Slave 模式。</p> <blockquote> <p>Git 不许使用 master 了,换成了 main ,Master/Slave 具有政治不正确的歧视色彩。不过这不重要了。其实这个名字很能表达这个模式的特点。</p> </blockquote> <p>主要思想就是由一个 Master 抽象对象来调度 Worker 对象来工作。</p> <h2>Ruby 文学编程,用代码讲故事</h2> <p>其实这也非常像现实中的工作模型。Ruby 天生面向对象,表达的文学性,我们可以很方便的来使用代码模拟这种现实情况。 我们来用 Ruby 模拟下现实中这种情况,顺便学下如何实现这个模式。</p> <h2>约定</h2> <p>会出现几个类:</p> <ul> <li>Master 代表 “领导”,不干活,主要工作任务是分配任务,这是 Master 类的特征。</li> <li>Worker 代表 “打工人”,工作和创造价值的主体。主要任务就是干活。</li> <li>Workshop 代表 “公司”,主要是负责接单。</li> </ul> <p>故事的思路:</p> <p>我们自己是客户,把“任务”订单交给“公司”,这些任务会转交给“领导”手中,然后“领导”会排期,把工作布置给“打工人”。最终“打工人”乐此不疲的完成任务。</p> <h2>实现 打工人 Worker 类</h2> <h3>step1 给员工工号</h3> <p>首先我们建立一个 Worker 类,我们给他一个名字属性。<code>attr</code> 暴露出 <code>name</code> 属性。</p> <pre><code class="language-ruby"># Workshop.rb class Worker attr :name def initialize(name) @name = "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="51263e233a342311">[email&nbsp;protected]</a>#{name}" end end </code></pre> <p>我们采用 TDD 方式来逐步实现我们的想法:</p> <pre><code class="language-ruby">#Workshop_test.rb require 'minitest/autorun' require_relative '../lib/Workshop' describe Worker do it "check worker name" do w = Worker.new("ruby01") assert_equal w.name, "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ccbba3bea7a9be8cbeb9aeb5fcfd">[email&nbsp;protected]</a>" end end </code></pre> <p>很快,我们知道这名打工人他叫 “ruby01” 员工。</p> <h2>step2 给员工 KPI/OKR</h2> <p>我们不希望打工人每次只能做一件事,你必须得推着他才能工作。他最好学会“成长”会自己努力的工作。 其实就是一堆任务,我们希望他们一直忙。给他 N 件事情,他一个一个自己做。 我们要给他一个目标,也就是 KPI 或者 OKR 随便吧,实际上这是一个队列对吧。我们用队列实现。</p> <pre><code class="language-ruby">require 'thread' class Worker attr :name def initialize(name) @name = "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c3b4acb1a8a6b183">[email&nbsp;protected]</a>#{name}" @queue = Queue.new @thr = Thread.new { perfom } end def &lt;&lt;(job) @queue.push(job) end def join @thr.join end def perfom while (job = @queue.deq) break if job == :done puts "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b8cfd7cad3ddcaf8">[email&nbsp;protected]</a>#{name}: job:#{job}" job.call end end def size @queue.size end end </code></pre> <p>现在打工人变得充实了许多,他自从来了公司培训之后,就拥有了很多属性和方法。</p> <ul> <li>属性说明:</li> </ul> <p><code>@queue</code> 就是他的 OKR 清单,他必须完成所有的工作任务。</p> <p><code>@thr</code> 意思是 thread 缩写,这里是会使用一个线程来调用 <code>perform</code> 我们在用线程模拟打工人干活这件事。可以理解为 <code>@thr</code> 就是打工人的灵魂。</p> <ul> <li>方法说明:</li> </ul> <p><code>&lt;&lt;</code> 是一个 push 方法的语法糖,就给给自己的 OKR 里添加任务。</p> <p><code>perform</code> 可能要说下 perform 方法, 这里是 “运行”的意思哈,不是“表演” :P 。 打工人怎么干活呢?这得说道说道。我们得指导他如何“成长”。</p> <p>我们前面说了 <code>@queue</code> 就是他的 OKR, 他必须从自己的 OKR 中取出任务然后执行。这里我用了 <code>job.call</code>。 暗示,这必须是一个 callable 对象,在 ruby 里也就是拥有 <code>call</code> 方法的对象。可以是 lambda 、或者实现 call 的。 这也很合理,需求必须能做才会做。没法做的需求,做不了就是做不了。</p> <p>但是如果给了一个 <code>:done</code> 另说。循环会结束,这个线程会消失。(裁员了 :P)</p> <pre><code class="language-ruby"> def perfom while (job = @queue.deq) break if job == :done puts "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e0978f928b8592a0">[email&nbsp;protected]</a>#{name}: job:#{job}" job.call end end </code></pre> <blockquote> <p>其实 Queue 这个对象很有意思,Ruby 做了一些工作。Queue 在空的时候,虚拟机会让线程进入睡眠等待。如果队列里有任务,就会继续工作。Ruby 很贴心,果然是程序员的好朋友啊。 其实我不知道其他语言什么样,懒得查了。</p> </blockquote> <p><code>join</code> 方法是一个 Thread 的线程方法,主要的作用是告诉主线程你要等待每一个子线程(自己)的完成。如果不写这句,主线程如果比所有子线程提前结束。那么子线程会被全部关闭。简而言之 <code>join</code> 就是同步等待线程结果。</p> <p>让我们来看看 TDD:</p> <p>我们可以加一段验证工号 ruby02 的打工人是不是如期的完成了工作。</p> <pre><code class="language-ruby"># .... it "check worekr do sth job" do w = Worker.new("ruby02") finished = [] w &lt;&lt; lambda { puts "do job 1"; finished.push "job1"} w &lt;&lt; lambda { puts "do job 2"; finished.push "job2"} w &lt;&lt; :done w.join assert_equal finished, ["job1","job2"] end # .... </code></pre> <p>其实到这里,一个合格的打工人就打造完毕了。打工人很简单,只要吃苦耐劳,一切都 OK 。 下面我们要实现下 Workshop 公司类。</p> <h2>实现 公司 Workshop 类</h2> <h3>在此之前,我们先实现:创业公司 MiniWorkshop 类</h3> <p>其实我打算过渡下,首先实现一个 “创业公司” <code>MiniWorkshop</code>。 创业公司刚起步,一般是只有“打工人”,没有真正意义上的中层出现。 这一时期非常简单,伊甸园时期。有活大家一起干,大家都是兄弟。</p> <pre><code class="language-ruby">class MiniWorkshop def initialize(count) @worker_count = count # 打工人数量 @workers = @worker_count.times.map do |i| # 根据数量生成(招聘)打工人 Worker.new(i) # 给个工号 end end # 初创公司分配任务 def &lt;&lt;(job) if job == :done @workers.map {|m| m &lt;&lt; job} else # 随机选择一个打工人,接活 @workers.sample &lt;&lt; job end end def join @workers.map {|m| m.join} end end </code></pre> <p>这里可能说下</p> <pre><code class="language-ruby"> def &lt;&lt;(job) if job == :done @workers.map {|m| m &lt;&lt; job} else # 随机选择一个打工人,接活 @workers.sample &lt;&lt; job end end </code></pre> <p>这里干活的模式可能不好,因为我们竟然 <code>Array#sample</code> 方式。这是一个随机方法。随机选择一个。 看似不合理,实际上也合情合理。</p> <p>创业公司初期虽然是草根,可是大家哪个不是大佬。所以活来了谁都行,问题不大。</p> <p>没事我们后面再改进好了。</p> <p>TDD:</p> <p>我们的单元测试其实描述了一个故事。一家创业公司,只有 2 个人。接到了一个订单是 4 个工作内容。</p> <pre><code class="language-ruby"># ... it "check MiniWorkshop work" do ws = MiniWorkshop.new(2) finished = [] ws &lt;&lt; lambda { puts "job1"; finished.push "job1"} ws &lt;&lt; lambda { puts "job2"; finished.push "job2"} ws &lt;&lt; lambda { puts "job3"; finished.push "job3"} ws &lt;&lt; lambda { puts "job4"; finished.push "job4"} ws &lt;&lt; :done ws.join assert_equal finished.size, 4 end # ... </code></pre> <p>我们回过头再看 <code>MiniWorkshop</code> 类,初始化的时候创建了两个员工。任务来了就随机分配给一个员工。 很符合小作坊的模式。</p> <h2>实现上市公司</h2> <p>公司变大了,就不止 2 个员工了。可能四五百号,随机交给一个员工,不现实。中层管理出现。中层出现意味着我们公司的类也要进行改变,公司需要改革。</p> <p>我们先实现一个改革之后的 Workshop 公司类。</p> <pre><code class="language-ruby">class Workshop def initialize(count, master_name) @worker_count = count @workers = @worker_count.times.map do |i| Worker.new(i) end @master = Master.new(@workers) # 新增角色 end def &lt;&lt;(job) if job == :done @workers.map {|m| m &lt;&lt; job} else @master.assign(job) # master 分配任务 end end def join @workers.map {|m| m.join} end end </code></pre> <p>可以看到,我们在初始化函数里新增了 <code>@master</code> 他接受 <code>@workers</code> 作为参数。毕竟领导要点兵啊。</p> <p><code>&lt;&lt;</code>方法也进行了改进,由以前的 直接让 @workers 接收任务,变成 <code>@master.assign</code> 分配任务。</p> <p>让我们来看下 Master 类</p> <pre><code class="language-ruby">class Master def initialize(workers) @workers = workers end def assign(job) @workers.sort{|a,b| a.size &lt;=&gt; b.size}.first &lt;&lt; job end end </code></pre> <p>其实也不复杂。我们保持了 @workers 的指针, <code>assign</code> 方法更像是把以前分配的逻辑接过来实现了一遍。</p> <p>这次我们改了分配任务的方式,我们要根据 <code>Worker#size</code> 忙碌程度来分配任务。</p> <p>毕竟嘛,领导有个方法论,会比小作坊高级很多。</p> <h2>多重领导</h2> <p>一个领导就足够了么?不。</p> <p>现实中我们见过形形色色的领导,有的是自己培养,有的是留过洋,有的是大厂空降。他们拥有不同的“方法论”,也就是 <code>Master#assign</code> 的方式可能不同。</p> <p>我们给公司再加两个领导。</p> <h3>无限方法论</h3> <p>996ICU 领导:</p> <p>我们使用了 <code>Array#cycle</code> 的方式,这是一个迭代器。比如 <code>[1,2,3].cycle</code> 每次 <code>.next</code> 会产生 <code>1 、2 、3 、1 、2 、3 、1 、2 、3 .....</code> 无限轮训。</p> <p>这个方法论就是 996 方法论,只要干不死就往死里干。人海战术,把人轮番填上。</p> <pre><code class="language-ruby">class ICU996Master def initialize(workers) @current_worker = workers.cycle # 迭代器 end def assign(job) @current_worker.next &lt;&lt; job end end </code></pre> <h3>分组任务方法论</h3> <p>等我们的公司变大了,我们的业务也会变得丰富,任务不是那么单一。很多工作要添加上组别 group_id ,分门别类的交给不同工种的打工人,比如 开发、产品、测试、设计、运营。</p> <pre><code class="language-ruby"> class GroupMaster GROUPS = [:group1, :group2, :group3] def initialize(workers) @workers = {} workers_per_group = workers.length / GROUPS.size workers.each_slice(workers_per_group).each_with_index do |slice, index| group_id = GROUPS[index] @workers[group_id] = slice end end def assign(job) worker = @workers[job.group].sort_by(&amp;:size).first worker &lt;&lt; job end end </code></pre> <p>然后我们可以把不同风格的领导班子集中起来</p> <pre><code class="language-ruby">Masters = { normal: NormalMaster, ICU996: ICU996Master, group: GroupMaster } </code></pre> <p>我们改造下 <code>Workshop</code> 毕竟这个词是一个 工作室的意思,其实是个小部门。</p> <p>我们改造之后,我们的小部门可以按照风格不同的领导进行分派工作。</p> <pre><code class="language-ruby">class Workshop def initialize(count, master_name) # 新增 master_name 指定 @worker_count = count @workers = @worker_count.times.map do |i| Worker.new(i) end # 匹配 master @master = Masters[master_name].new(@workers) end def &lt;&lt;(job) if job == :done @workers.map {|m| m &lt;&lt; job} else @master.assign(job) end end def join @workers.map {|m| m.join} end end </code></pre> <p>我们来看看不同部门的 TDD</p> <pre><code class="language-ruby"> it "check <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="590e362b322a31362919">[email&nbsp;protected]</a> normal master" do ws = Workshop.new(4, :normal) finished = [] ws &lt;&lt; lambda { puts "job1"; finished.push "job1"} ws &lt;&lt; lambda { puts "job2"; finished.push "job2"} ws &lt;&lt; lambda { puts "job3"; finished.push "job3"} ws &lt;&lt; lambda { puts "job4"; finished.push "job4"} ws &lt;&lt; :done ws.join assert_equal finished.size, 4 end it "check <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="34635b465f475c5b4474">[email&nbsp;protected]</a> ICU996 master" do ws = Workshop.new(4, :ICU996) finished = [] ws &lt;&lt; lambda { puts "job1"; finished.push "job1"} ws &lt;&lt; lambda { puts "job2"; finished.push "job2"} ws &lt;&lt; lambda { puts "job3"; finished.push "job3"} ws &lt;&lt; lambda { puts "job4"; finished.push "job4"} ws &lt;&lt; :done ws.join assert_equal finished.size, 4 end it "check <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f5a29a879e869d9a85b5">[email&nbsp;protected]</a> group master" do ws = Workshop.new(4, :group) class GroupJob def initialize(group_id, &amp;b) @group_id = group_id @blk = b end # 任务分组 def group "group#{@group_id}".to_sym end def call @blk.call(@group_id) end end finished = [] ws &lt;&lt; GroupJob.new(1) { |group_id| finished.push(group_id)} ws &lt;&lt; GroupJob.new(2) { |group_id| finished.push(group_id)} ws &lt;&lt; GroupJob.new(3) { |group_id| finished.push(group_id)} ws &lt;&lt; GroupJob.new(1) { |group_id| finished.push(group_id)} ws &lt;&lt; :done ws.join assert_equal finished.size, 4 end </code></pre> <h1>总结 Master-Worker 模式</h1> <p>好吧,戏说不是胡说,改编不是乱编。</p> <p>我们从现实的故事中走出来。</p> <ul> <li>调度器(Scheduler)</li> </ul> <p>其实在这里 Master 类,可能会被叫做 <code>Scheduler</code> 即调度器。内部的方法主要是使用不同的策略来分配任务。</p> <p>而不同的 Master 实现的 assign 方法就是 调度策略。</p> <ul> <li>线程池(Thread Pool)</li> </ul> <p>Workshop 其实 持有 <code>@workers</code>,也就是说汇聚了实际工作线程的对象。他们可能会有另一个名字 —— 线程池( Thread Pool)</p> <p>故事讲完了,你有没有学会呢? :D</p> <h1>示例代码:</h1> <ul> <li><a href="https://github.com/Mark24Code/rb-master-worker-demo" rel="nofollow">rb-master-worker-demo</a></li> </ul> <h1>参考资料:</h1> <ul> <li><a href="https://devdocs.io/ruby%7E3.1/thread" rel="nofollow">Ruby3 Doc: Thread</a></li> <li><a href="https://devdocs.io/ruby%7E3.1/thread/queue" rel="nofollow">Ruby3 Doc: Thread#Queue</a></li> <li><a href="https://hspazio.github.io/2017/worker-pool/" rel="nofollow">worker-pool</a></li> </ul>
收藏(0)  分享
相关标签: 灌水交流
注意:本文归作者所有,未经作者允许,不得转载
0个回复
  • 消灭零回复