lowkiq 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.ru.md ADDED
@@ -0,0 +1,645 @@
1
+ [![Gem Version](https://badge.fury.io/rb/lowkiq.svg)](https://badge.fury.io/rb/lowkiq)
2
+
3
+ # Lowkiq
4
+
5
+ Упорядоченная обработка фоновых задач.
6
+
7
+ ![dashboard](doc/dashboard.png)
8
+
9
+ * [Rationale](#rationale)
10
+ * [Description](#description)
11
+ * [Sidekiq](#sidekiq)
12
+ * [Очередь](#%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C)
13
+ + [Алгоритм расчета retry_count и perform_in](#%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC-%D1%80%D0%B0%D1%81%D1%87%D0%B5%D1%82%D0%B0-retry_count-%D0%B8-perform_in)
14
+ + [Правило слияния задач](#%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D0%BE-%D1%81%D0%BB%D0%B8%D1%8F%D0%BD%D0%B8%D1%8F-%D0%B7%D0%B0%D0%B4%D0%B0%D1%87)
15
+ * [Install](#install)
16
+ * [Api](#api)
17
+ * [Ring app](#ring-app)
18
+ * [Настройка](#%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0)
19
+ * [Запуск](#%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA)
20
+ * [Остановка](#%D0%BE%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0)
21
+ * [Debug](#debug)
22
+ * [Development](#development)
23
+ * [Исключения](#%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D1%8F)
24
+ * [Rails integration](#rails-integration)
25
+ * [Splitter](#splitter)
26
+ * [Scheduler](#scheduler)
27
+ * [Рекомендации по настройке](#%D1%80%D0%B5%D0%BA%D0%BE%D0%BC%D0%B5%D0%BD%D0%B4%D0%B0%D1%86%D0%B8%D0%B8-%D0%BF%D0%BE-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B5)
28
+ + [`SomeWorker.shards_count`](#someworkershards_count)
29
+ + [`SomeWorker.max_retry_count`](#someworkermax_retry_count)
30
+ * [Изменение количества шардов воркера](#%D0%B8%D0%B7%D0%BC%D0%B5%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BA%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%B0-%D1%88%D0%B0%D1%80%D0%B4%D0%BE%D0%B2-%D0%B2%D0%BE%D1%80%D0%BA%D0%B5%D1%80%D0%B0)
31
+
32
+ ## Rationale
33
+
34
+ При использовании Sidekiq мы столкнулись с проблемами при обработке сообщений от сторонней системы.
35
+
36
+ Скажем, сообщение представляет собой данные заказа в определенный момент времени.
37
+ При изменении атрибутов или статуса отправляется новое сообщение сторонней системой.
38
+ Заказы обновляются часто и в очереди рядом находятся сообщения, касающиеся одного и того же заказа.
39
+
40
+ Sidekiq не гарантирует строгого порядка сообщений, т.к. очередь обрабатывается в несколько потоков.
41
+ Например, пришло 2 сообщения: M1 и M2.
42
+ Sidekiq обработчики начинают обрабатывать их параллельно,
43
+ при этом M2 может обработаться раньше M1.
44
+
45
+ Параллельная обработка данных одного заказа приводит к:
46
+
47
+ + dead locks
48
+ + затиранию новых данных старыми
49
+
50
+ Lowkiq призван устранить эти проблемы, исключая параллельность обработки сообщений в рамках одной сущности.
51
+
52
+ ## Description
53
+
54
+ Очереди надежны. Lowkiq сохраняет данные об обрабатываемой задаче и при запуске переносит
55
+ незавершенные задачи обратно в очередь.
56
+
57
+ Задачи в очереди отсортированы по заданному времени исполнения, т.е. это не FIFO очереди.
58
+
59
+ Каждая задача имеет идентификатор. Очереди гарантируют, что не может быть ситуации,
60
+ когда несколько потоков обрабатывают задачи с одинаковыми идентификаторами.
61
+
62
+ Каждая очередь разбивается на постоянный набор шардов.
63
+ На основе идентификатора задачи выбирается шард, в который попадет задача.
64
+ Таким образом задачи с одним идентификатором всегда попадают в один и тот же шард.
65
+ Задачи шарда всегда обрабатываются одним и тем же потоком.
66
+ Это гарантирует порядок обработки задач с одинаковым идентификатором и исключает возможность блокировок.
67
+
68
+ Кроме идентификатора задача имеет полезную нагрузку или данные задачи (payload).
69
+ Для задач с одинаковым идентификаторм происходит слияние полезных нагрузок.
70
+ Таким образом одновременно в обработку попадают все накопленные полезные нагрузки задачи.
71
+ Это полезно, если нужно обработать только последнее сообщение и отбросить все предыдущие.
72
+
73
+ Каждой очереди соответствует воркер, содержащий логику обработки задачи.
74
+
75
+ Для обработки всех задач всех очередей используется фиксированное количество тредов.
76
+ Добавление или удаление очередей или их шардов не приводит к изменению числа тредов.
77
+
78
+ ## Sidekiq
79
+
80
+ Если для ваших задач подходит Sidekiq - используйте его.
81
+
82
+ Если вы используете плагины вроде
83
+ [sidekiq-grouping](https://github.com/gzigzigzeo/sidekiq-grouping),
84
+ [sidekiq-unique-jobs](https://github.com/mhenrixon/sidekiq-unique-jobs),
85
+ [sidekiq-merger](https://github.com/dtaniwaki/sidekiq-merger)
86
+ или реализуете собственный механизм блокировок, то стоит рассмотреть Lowkiq.
87
+
88
+ Например, sidekiq-grouping предварительно накапливает пачку задач, ставит ее в очередь и начинает накапливать следующую.
89
+ При таком подходе случается ситуация, когда в очереди находятся 2 пачки с данными одного заказа.
90
+ Эти пачки начинают обрабатываться одновременно разными тредами, что приводит к изначальной проблеме.
91
+
92
+ Lowkiq изначально проектировался так, чтобы не использовать любые блокировки.
93
+
94
+ Кроме того, в Lowkiq очереди изначально надежны. Только Sidekiq Pro или плагины добавляют такую функциональность.
95
+
96
+ Этот [бенчмарк](examples/benchmark) показывает накладные расходы на взаимодействие с redis.
97
+ Для 5 threads, 100'000 blank jobs получились результаты:
98
+
99
+ + lowkiq: 214 sec или 2,14 мс на задачу
100
+ + sidekiq: 29 sec или 0,29 мс на задачу
101
+
102
+ Эта разница связана с принципиально различным устройством очередей.
103
+ Sidekiq использует один список для всех воркеров и извлекает задачу целиком за O(1).
104
+ Lowkiq использует несколько типов данных, включая сортированные множества для хранения идентификаторов задач.
105
+ Таким образом только получение идентификатора задачи занимает O(log(N)).
106
+
107
+ ## Очередь
108
+
109
+ Каждая задача в очереди имеет аттрибуты:
110
+
111
+ + `id` - идентификатор задачи (строка)
112
+ + `payloads` - сортированное множество payload'ов (объекты) по их score (вещественное число)
113
+ + `perform_in` - запланированное время начала иполнения задачи (unix timestamp, вещественное число)
114
+ + `retry_count` - количество совершённых повторов задачи (вещественное число)
115
+
116
+ `id` может быть, например, идентификатором реплицируемой сущности
117
+ `payloads` - множество,
118
+ получаемое в результате группировки полезной нагрузки задачи по `id` и отсортированное по ее `score`.
119
+ `payload` может быть ruby объектом, т.к. сериализуется с помощью `Marshal.dump`.
120
+ `score` может быть датой (unix timestamp) создания `payload`
121
+ или ее монотонно увеличивающимся номером версии.
122
+ По умолчанию - текущий unix timestamp.
123
+ По умолчанию `perform_in` - текущий unix timestamp.
124
+ `retry_count` для новой необработанной задачи равен `-1`, для упавшей один раз - `0`,
125
+ т.е. считаются не совершённые, а запланированные повторы.
126
+
127
+ Выполнение задачи может закончиться неудачей.
128
+ В этом случае ее `retry_count` инкрементируется и по заданной формуле вычисляется новый `perform_in`,
129
+ и она ставится обратно в очередь.
130
+
131
+ В случае, когда `retry_count` становится `>=` `max_retry_count`
132
+ элемент payloads с наименьшим(старейшим) score перемещается в морг,
133
+ а оставшиеся элементы помещаются обратно в очередь, при этом
134
+ `retry_count` и `perform_in` сбрасываются в `-1` и `now()` соответственно.
135
+
136
+ ### Алгоритм расчета retry_count и perform_in
137
+
138
+ 0. задача выполнилась и упала
139
+ 1. `retry_count++`
140
+ 2. `perform_in = now + retry_in(try_count)`
141
+ 3. `if retry_count >= max_retry_count` задача перемещается в морг
142
+
143
+ | тип | `retry_count` | `perform_in` |
144
+ | --- | --- | --- |
145
+ | новая не выполнялась | -1 | задан или `now()` |
146
+ | новая упала | 0 | `now() + retry_in(0)` |
147
+ | повтор упал | 1 | `now() + retry_in(1)` |
148
+
149
+ Если `max_retry_count = 1`, то попытки прекращаются.
150
+
151
+ ### Правило слияния задач
152
+
153
+ Когда применяется:
154
+
155
+ + если в очереди была задача и добавляется еще одна с тем же id
156
+ + если при обработке возникла ошибка, а в очередь успели добавили задачу с тем же id
157
+ + если задачу из морга поставили в очередь, а в очереди уже есть задача с тем же id
158
+
159
+ Алгоритм:
160
+
161
+ + payloads объединяются, при этом выбирается минимальный score,
162
+ т.е. для одинаковых payload выигрывает самая старая
163
+ + если объединяется новая и задача из очереди,
164
+ то `perform_in` и `retry_count` берутся из задачи из очереди
165
+ + если объединяется упавшая задача и задача из очереди,
166
+ то `perform_in` и `retry_count` берутся из упавшей
167
+ + если объединяется задача из морга и задача из очереди,
168
+ то `perform_in = now()`, `retry_count = -1`
169
+
170
+ Пример:
171
+
172
+ ```
173
+ # v1 - первая версия, v2 - вторая
174
+ # #{"v1": 1} - сортированное множество одного элемента, payload - "v1", score - 1
175
+
176
+ # задача в очереди
177
+ { id: "1", payloads: #{"v1": 1, "v2": 2}, retry_count: 0, perform_in: 1536323288 }
178
+ # добавляемая задача
179
+ { id: "1", payloads: #{"v2": 3, "v3": 4}, retry_count: -1, perform_in: 1536323290 }
180
+
181
+ # результат
182
+ { id: "1", payloads: #{"v1": 1, "v2": 3, "v3": 4}, retry_count: 0, perform_in: 1536323288 }
183
+ ```
184
+
185
+ Морг - часть очереди. Задачи в морге не обрабатываются.
186
+ Задача в морге имеет следующие атрибуты:
187
+
188
+ + id - идентификатор задачи
189
+ + payloads
190
+
191
+ Задачи в морге можно отсортировать по дате изменения или id.
192
+
193
+ Задачу из морга можно переместить в очередь. При этом для нее `retry_count = 0`, `perform_in = now()`.
194
+
195
+ ## Install
196
+
197
+ ```
198
+ # Gemfile
199
+
200
+ gem 'lowkiq'
201
+ ```
202
+
203
+ Redis версии >= 3.2.
204
+
205
+ ## Api
206
+
207
+ ```ruby
208
+ module ATestWorker
209
+ extend Lowkiq::Worker
210
+
211
+ self.shards_count = 24
212
+ self.batch_size = 10
213
+ self.max_retry_count = 5
214
+
215
+ def self.retry_in(count)
216
+ 10 * (count + 1) # (i.e. 10, 20, 30, 40, 50)
217
+ end
218
+
219
+ def self.perform(payloads_by_id)
220
+ # payloads_by_id - хеш
221
+ payloads_by_id.each do |id, payloads|
222
+ # id - идентификатор задачи
223
+ # payloads отсортированы по score, от старых к новым (от минимальных к максимальным)
224
+ payloads.each do |payload|
225
+ do_some_work(id, payload)
226
+ end
227
+ end
228
+ end
229
+ end
230
+ ```
231
+
232
+ Значения по умолчанию:
233
+
234
+ ```ruby
235
+ self.shards_count = 5
236
+ self.batch_size = 1
237
+ self.max_retry_count = 25
238
+ self.queue_name = self.name
239
+
240
+ # i.e. 15, 16, 31, 96, 271, ... seconds + a random amount of time
241
+ def retry_in(retry_count)
242
+ (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))
243
+ end
244
+ ```
245
+
246
+ ```ruby
247
+ ATestWorker.perform_async [
248
+ { id: 0 },
249
+ { id: 1, payload: { attr: 'v1' } },
250
+ { id: 2, payload: { attr: 'v1' }, score: Time.now.to_i, perform_in: Time.now.to_i },
251
+ ]
252
+ # payload по умолчанию равен ""
253
+ # score и perform_in по умолчанию равны Time.now.to_i
254
+ ```
255
+
256
+ Вы можете переопределить `perform_async` и вычислять `id`, `score` и `perform_in` в воркере:
257
+
258
+ ```ruby
259
+ module ATestWorker
260
+ extend Lowkiq::Worker
261
+
262
+ def self.perform_async(jobs)
263
+ jobs.each do |job|
264
+ job.merge! id: job[:payload][:id]
265
+ end
266
+ super
267
+ end
268
+
269
+ def self.perform(payloads_by_id)
270
+ #...
271
+ end
272
+ end
273
+
274
+ ATestWorker.perform_async 1000.times.map { |id| { payload: {id: id} } }
275
+ ```
276
+
277
+ ## Ring app
278
+
279
+ `Lowkiq::Web` - ring app.
280
+
281
+ + `/` - dashboard
282
+ + `/api/v1/stats` - длина очереди, длина морга, лаг для каждого воркера и суммарно
283
+
284
+ ## Настройка
285
+
286
+ Опции и значения по умолчанию:
287
+
288
+ + `Lowkiq.poll_interval = 1` - задержка в секундах между опросами очереди на предмет новых задач.
289
+ Используется только если на предыдущей итерации очередь оказалась пуста или случилась ошибка.
290
+ + `Lowkiq.threads_per_node = 5` - кол-во тредов для каждой ноды.
291
+ + `Lowkiq.redis = ->() { Redis.new url: ENV.fetch('REDIS_URL') }` - настройка redis.
292
+ + `Lowkiq.client_pool_size = 5` - размер пула редиса для постановки задач в очередь.
293
+ + `Lowkiq.pool_timeout = 5` - таймаут клиентского и серверного пула редиса
294
+ + `Lowkiq.server_middlewares = []` - список middleware, оборачивающих воркер.
295
+ + `Lowkiq.on_server_init = ->() {}` - выполнения кода при инициализации сервера.
296
+ + `Lowkiq.build_scheduler = ->() { Lowkiq.build_lag_scheduler }` - планировщик.
297
+ + `Lowkiq.build_splitter = ->() { Lowkiq.build_default_splitter }` - сплиттер.
298
+ + `Lowkiq.last_words = ->(ex) {}` - обработчик исключений, потомков `StandardError`, вызвавших остановку процесса.
299
+
300
+ ```ruby
301
+ $logger = Logger.new(STDOUT)
302
+
303
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
304
+ $logger.info "Started job for #{worker} #{batch}"
305
+ block.call
306
+ $logger.info "Finished job for #{worker} #{batch}"
307
+ end
308
+
309
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
310
+ begin
311
+ block.call
312
+ rescue => e
313
+ $logger.error "#{e.message} #{worker} #{batch}"
314
+ raise e
315
+ end
316
+ end
317
+ ```
318
+
319
+ ## Запуск
320
+
321
+ `lowkiq -r ./path_to_app`
322
+
323
+ `path_to_app.rb` должен загрузить приложение. [Пример](examples/dummy/lib/app.rb).
324
+
325
+ Ленивая загрузка модулей воркеров недопустима.
326
+ Используйте для предварительной загрузки модулей
327
+ `require` или [`require_dependency`](https://api.rubyonrails.org/classes/ActiveSupport/Dependencies/Loadable.html#method-i-require_dependency)
328
+ для Ruby on Rails.
329
+
330
+ ## Остановка
331
+
332
+ Послать процессу TERM или INT (Ctrl-C).
333
+ Процесс будет ждать завершения всех задач.
334
+
335
+ Обратите внимание, если очередь пуста, процесс спит `poll_interval` секунд.
336
+ Таким образом завершится не позднее чем через `poll_interval` секунд.
337
+
338
+ ## Debug
339
+
340
+ Получить trace всех тредов приложения:
341
+
342
+ ```
343
+ kill -TTIN <pid>
344
+ cat /tmp/lowkiq_ttin.txt
345
+ ```
346
+
347
+ ## Development
348
+
349
+ ```
350
+ docker-compose run --rm --service-port app bash
351
+ bundle
352
+ rspec
353
+ cd examples/dummy ; bundle exec ../../exe/lowkiq -r ./lib/app.rb
354
+ ```
355
+
356
+ ## Исключения
357
+
358
+ `StandardError` выброшенные воркером обрабатываются с помощью middleware.
359
+ Такие исключения не приводят к остановке процесса.
360
+
361
+ Все прочие исключения приводят к остановке процесса.
362
+ При этом Lowkiq дожидается выполнения задач другими тредами.
363
+
364
+ `StandardError` выброшенные вне воркера передаются в `Lowkiq.last_words`.
365
+ Например это происходит при потере соединения к Redis или при ошибке в коде Lowkiq.
366
+
367
+ ## Rails integration
368
+
369
+ ```ruby
370
+ # config/routes.rb
371
+
372
+ Rails.application.routes.draw do
373
+ # ...
374
+ mount Lowkiq::Web => '/lowkiq'
375
+ # ...
376
+ end
377
+ ```
378
+
379
+ ```ruby
380
+ # config/initializers/lowkiq.rb
381
+
382
+ # загружаем все lowkiq воркеры
383
+ Dir["#{Rails.root}/app/lowkiq_workers/**/*.rb"].each { |file| require_dependency file }
384
+
385
+ # конфигурация:
386
+ # Lowkiq.redis = -> { Redis.new url: ENV.fetch('LOWKIQ_REDIS_URL') }
387
+ # Lowkiq.threads_per_node = ENV.fetch('LOWKIQ_THREADS_PER_NODE').to_i
388
+ # Lowkiq.client_pool_size = ENV.fetch('LOWKIQ_CLIENT_POOL_SIZE').to_i
389
+ # ...
390
+
391
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
392
+ logger = Rails.logger
393
+ tag = "#{worker}-#{Thread.current.object_id}"
394
+
395
+ logger.tagged(tag) do
396
+ time_start = Time.now
397
+ logger.info "#{time_start} Started job, batch: #{batch}"
398
+ begin
399
+ block.call
400
+ rescue => e
401
+ logger.error e.message
402
+ raise e
403
+ ensure
404
+ time_end = Time.now
405
+ logger.info "#{time_end} Finished job, duration: #{time_end - time_start} sec"
406
+ end
407
+ end
408
+ end
409
+
410
+ # Sentry integration
411
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
412
+ opts = {
413
+ extra: {
414
+ lowkiq: {
415
+ worker: worker.name,
416
+ batch: batch,
417
+ }
418
+ }
419
+ }
420
+
421
+ Raven.capture opts do
422
+ block.call
423
+ end
424
+ end
425
+
426
+ # NewRelic integration
427
+ if defined? NewRelic
428
+ class NewRelicLowkiqMiddleware
429
+ include NewRelic::Agent::Instrumentation::ControllerInstrumentation
430
+
431
+ def call(worker, batch, &block)
432
+ opts = {
433
+ category: 'OtherTransaction/LowkiqJob',
434
+ class_name: worker.name,
435
+ name: :perform,
436
+ }
437
+
438
+ perform_action_with_newrelic_trace opts do
439
+ block.call
440
+ end
441
+ end
442
+ end
443
+
444
+ Lowkiq.server_middlewares << NewRelicLowkiqMiddleware.new
445
+ end
446
+
447
+ # Rails reloader, в том числе отвечает за высвобождение ActiveRecord коннектов
448
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
449
+ Rails.application.reloader.wrap do
450
+ block.call
451
+ end
452
+ end
453
+
454
+ Lowkiq.on_server_init = ->() do
455
+ [[ActiveRecord::Base, ActiveRecord::Base.configurations[Rails.env]]].each do |(klass, init_config)|
456
+ klass.connection_pool.disconnect!
457
+ config = init_config.merge 'pool' => Lowkiq.threads_per_node
458
+ klass.establish_connection(config)
459
+ end
460
+ end
461
+ ```
462
+
463
+ Запуск: `bundle exec lowkiq -r ./config/environment.rb`
464
+
465
+ ## Splitter
466
+
467
+ У каждого воркера есть несколько шардов:
468
+
469
+ ```
470
+ # worker: shard ids
471
+ worker A: 0, 1, 2
472
+ worker B: 0, 1, 2, 3
473
+ worker C: 0
474
+ worker D: 0, 1
475
+ ```
476
+
477
+ Lowkiq использует фиксированное кол-во тредов для обработки задач, следовательно нужно распределить шарды
478
+ между тредами. Этим занимается Splitter.
479
+
480
+ Чтобы определить набор шардов, которые будет обрабатывать тред, поместим их в один список:
481
+
482
+ ```
483
+ A0, A1, A2, B0, B1, B2, B3, C0, D0, D1
484
+ ```
485
+
486
+ Рассмотрим Default splitter, который равномерно распределяет шарды по тредам единственной ноды.
487
+
488
+ Если `threads_per_node` установлено в 3, то распределение будет таким:
489
+
490
+ ```
491
+ # thread id: shards
492
+ t0: A0, B0, B3, D1
493
+ t1: A1, B1, C0
494
+ t2: A2, B2, D0
495
+ ```
496
+
497
+ Помимо Default есть ByNode splitter. Он позволяет распределить нагрузку по нескольким процессам (нодам).
498
+
499
+ ```
500
+ Lowkiq.build_splitter = -> () do
501
+ Lowkiq.build_by_node_splitter(
502
+ ENV.fetch('LOWKIQ_NUMBER_OF_NODES').to_i,
503
+ ENV.fetch('LOWKIQ_NODE_NUMBER').to_i
504
+ )
505
+ end
506
+ ```
507
+
508
+ Таким образом, вместо одного процесса нужно запустить несколько и указать переменные окружения:
509
+
510
+ ```
511
+ # process 0
512
+ LOWKIQ_NUMBER_OF_NODES=2 LOWKIQ_NODE_NUMBER=0 bundle exec lowkiq -r ./lib/app.rb
513
+
514
+ # process 1
515
+ LOWKIQ_NUMBER_OF_NODES=2 LOWKIQ_NODE_NUMBER=1 bundle exec lowkiq -r ./lib/app.rb
516
+ ```
517
+
518
+ Отмечу, что общее количество тредов будет равно произведению `ENV.fetch('LOWKIQ_NUMBER_OF_NODES')` и `Lowkiq.threads_per_node`.
519
+
520
+ Вы можете написать свой сплиттер, если ваше приложение требует особого распределения шардов между тредами или нодами.
521
+
522
+ ## Scheduler
523
+
524
+ Каждый тред обрабатывает набор шардов. За выбор шарда для обработки отвечает планировщик.
525
+ Каждый поток имеет свой собственный экземпляр планировщика.
526
+
527
+ Lowkiq имеет 2 планировщика на выбор.
528
+ Первый, `Seq` - последовательно перебирает шарды.
529
+ Второй, `Lag` - выбирает шард с самой старой задачей, т.е. стремится минимизировать лаг.
530
+ Используется по умолчанию.
531
+
532
+ Планировщик задается через настройки:
533
+
534
+ ```
535
+ Lowkiq.build_scheduler = ->() { Lowkiq.build_seq_scheduler }
536
+ # или
537
+ Lowkiq.build_scheduler = ->() { Lowkiq.build_lag_scheduler }
538
+ ```
539
+
540
+ ## Рекомендации по настройке
541
+
542
+ ### `SomeWorker.shards_count`
543
+
544
+ Сумма `shards_count` всех воркеров не должна быть меньше `Lowkiq.threads_per_node`
545
+ иначе треды будут простаивать.
546
+
547
+ Сумма `shards_count` всех воркеров может быть равна `Lowkiq.threads_per_node`.
548
+ В этом случае тред обрабатывает единственный шард. Это имеет смысл только при равномерной нагрузке на очереди.
549
+
550
+ Сумма `shards_count` всех воркеров может быть больше `Lowkiq.threads_per_node`.
551
+ В этом случае `shards_count` можно рассматривать в качестве приоритета.
552
+ Чем он выше, тем чаще задачи этой очереди будут обрабатываться.
553
+
554
+ Нет смысла устанавливать `shards_count` одного воркера больше чем `Lowkiq.threads_per_node`,
555
+ т.к. каждый тред будет обрабатывать более одного шарда этой очереди, что увеличит накладные расходы.
556
+
557
+ ### `SomeWorker.max_retry_count`
558
+
559
+ Исходя из `retry_in` и `max_retry_count`,
560
+ можно вычислить примерное время, которая задача проведет в очереди.
561
+ Под задачей тут понимается payload задачи.
562
+ После достижения `max_retry_count` в морг переносится только payload с минимальным score.
563
+
564
+ Для `retry_in`, заданного по умолчанию получается следующая таблица:
565
+
566
+ ```ruby
567
+ def retry_in(retry_count)
568
+ (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))
569
+ end
570
+ ```
571
+
572
+ | `max_retry_count` | кол-во дней жизни задачи |
573
+ | --- | --- |
574
+ | 14 | 1 |
575
+ | 16 | 2 |
576
+ | 18 | 3 |
577
+ | 19 | 5 |
578
+ | 20 | 6 |
579
+ | 21 | 8 |
580
+ | 22 | 10 |
581
+ | 23 | 13 |
582
+ | 24 | 16 |
583
+ | 25 | 20 |
584
+
585
+ `(0...25).map{ |c| retry_in c }.sum / 60 / 60 / 24`
586
+
587
+
588
+ ## Изменение количества шардов воркера
589
+
590
+ Старайтесь сразу расчитать количество шардов и не именять их количество в будущем.
591
+
592
+ Если вы можете отключить добавление новых заданий,
593
+ то дождитесь опустошения очередей и выкатите новую версию кода с измененным количеством шардов.
594
+
595
+ Если такой возможности нет, воспользуйтесь следующим сценарием.
596
+
597
+ Например, есть воркер:
598
+
599
+ ```ruby
600
+ module ATestWorker
601
+ extend Lowkiq::Worker
602
+
603
+ self.shards_count = 5
604
+
605
+ def self.perform(payloads_by_id)
606
+ some_code
607
+ end
608
+ end
609
+ ```
610
+
611
+ Теперь нужно указать новое кол-во шардов и задать новое имя очереди:
612
+
613
+ ```ruby
614
+ module ATestWorker
615
+ extend Lowkiq::Worker
616
+
617
+ self.shards_count = 10
618
+ self.queue_name = "#{self.name}_V2"
619
+
620
+ def self.perform(payloads_by_id)
621
+ some_code
622
+ end
623
+ end
624
+ ```
625
+
626
+ И добавить воркер, перекладывающий задачи из старой очереди в новую:
627
+
628
+ ```ruby
629
+ module ATestMigrationWorker
630
+ extend Lowkiq::Worker
631
+
632
+ self.shards_count = 5
633
+ self.queue_name = "ATestWorker"
634
+
635
+ def self.perform(payloads_by_id)
636
+ jobs = payloads_by_id.each_with_object([]) do |(id, payloads), acc|
637
+ payloads.each do |payload|
638
+ acc << { id: id, payload: payload }
639
+ end
640
+ end
641
+
642
+ ATestWorker.perform_async jobs
643
+ end
644
+ end
645
+ ```