Коварный backtick или как определить размер используемой памяти

August 16, 2016

В одном жирненьком руби-воркере мы логировали размер памяти, которую расходует процесс, с помощью команды `ps -o rss -p #{$$}`. Такой способ легко найти в интернете и он обычно работает.

Однако сам Kernel#` сделан так, что он форкает руби процесс, соответственно на доли секунды резервируя еще столько же памяти, сколько процесс занимает уже. Наш воркер в ходе работы раздувался до 2.5-3GB и начал падать на 4GB оперативной памяти.

Я подсмотрел как сделано в ньюрелике (файл new_relic/agent/samplers/memory_sampler.rb):

proc_status_file = "/proc/#{$$}/status"
proc_status = File.open(proc_status_file, "r") {|f| f.read_nonblock(4096).strip }
proc_status =~ /RSS:\s*(\d+) kB/i
rss = $1.to_f 

На линуксе вместо запуска ps лучше использовать информацию из раздела /proc. Такой способ не требует избыточной памяти и работает у нас по сей день.

Когда все слишком хорошо

August 15, 2016

Давно заметил такой факт, что если выкатка сложная, то я сразу же открываю все инструменты мониторинга и жду ошибок. Одна, две, три проблемы - фикс и я спокоен. Но если проблем нет, то становится очень страшно.

Может быть сломалось вообще все?!?!

Большие изменения означают, что у задачи очень много факторов, все комбинации которых невозможно отследить самым тщательным тестированием и код ревью. Я уверен, что при любом сложном деплое что-то должно сломаться. По мелочи. Или по крупному. Обязательно должно.

И когда действительно сломалось, то как бы не наивно это звучало, я становлюсь сильно спокойнее. Молния же не попадает в одно место дважды? :smile:

···

Расскажу пару историй из жизни.

Мы оптимизировали тормозной кусок кода, который сильно нагружал базу. Выкатили и сразу смотрим заббикс. Немного ожидания и... Красота! База работает великолепно, CPU уменьшилось в 10 раз, все ровненько, гладенько. Чудесно.

Захожу на сайт - ошибка 500 в кабинете ученика на всех страницах. Естественно нагрузка тут же снизилась на порядок, база отдыхает. Хотфиксим, катим, ждем немного пока трафик вернется и заббикс отрисует новые данные. Вот теперь ок. База нагружена, но чтение с диска уменьшилось раза в полтора, как мы и планировали.

Коллега проводит штатное нагрузочное тестирование, получает великолепные результаты. Четыре сервера выдают столько rps, сколько в прошлый раз выдавали двенадцать. Коллега радуется как хорошо настроил ферму. Подозрительно.

Через неделю проводим тестирование заново и выясняем, что в прошлом запуске сломалась авторизация. Нагрузка тестировала как здорово NGINX отдает 302 на if current_user.nil?. Чиним, и совсем другая история - пора бежать докупать сервера.

Поэтому я давно решил, если все слишком хорошо, это очень и очень плохо.

Как можно документировать схему базу данных

August 12, 2016

Когда проект начал расти я почувствовал, что очень сильно не хватает комментариев к схеме базы данных. В живом приложении появляются таинcтвенные таблицы, какие-то флаги, json-ны и прочие неведомые вещи, иногда хочется узнать, что они означают.

Конечно некоторые поля в моделях очевидны, в Student#name в принципе очевидно что хранится, но какое-нибудь Group#migrated_from_old_db2 (название вымышленое) нуждается в объяснении - кто, когда, зачем это мигрировал и что такое старая база (вторая).

Я завел файл doc/db.md, в котором описал все таблицы и колонки, которые были на тот момент и попросил коллег поддерживать его. Конечно никто поддерживать не стал. И не из вредности, а потому-что при программировании приходится и так помнить тысячу вещей. Документирование изменений схемы не входило в обязательные рутины работы.

···

Решил проблему просто - написал тест, который падает, если в файле doc/db.md нет описания для какой-нибудь колонки. Формат файла очень простой, он легко парсится и красиво выглядит в интерфейсе гитхаба:

# uchi.ru db schema

## schools

Школы.

| column | description |
|--:|--:|
| name | Название |
...

Все таблицы и колонки легко вытянуть с помощью AR::Base.connection.tables и AR::Base.connection.columns. Вуаля! Мержить пул реквест на красных тестах нельзя. Но тесты падают, если схема обновилась, а doc/db.md нет!

···

Теперь все базы данных у нас документированы, а файл doc/db.md в каждом репозитарии один из ключевых при изучении приложения с которым работаешь первый раз или давно не работал. Поддерживать такую документацию легко и естественно, в итоге она гораздо информативнее стандартного файла db/schema.rb.

%r{егекспы}

August 11, 2016

Недавно в канал коллега скинул ссылку 5 Tips for Writing a Legible Regexp.

Лично мне кажется, что регекспы созданы для того, чтобы их только писать, читать их невозможно, как ни старайся. Однако совет про %r{http://...} понравился, я использовал его в тот день для парсинга ссылок в тексте.

Зачем мне понадобилось парсить ссылки? О, это вообще другая история. У нас несколько десятков репозитариев, я слежу, чтобы они все были перечислены на главной странице в вики, сгруппированные по проектам, чтобы в случае чего было понятно к кому обратиться с вопросом. Простой скрипт на руби сравнивает список ссылок на странице с репозитариями в гитхабе и выдает разницу если она есть.

Однако %r{} не единственная хитрость, которую стоит попробовать. В руби вообще на мой вкус самый дружелюбный синтаксис, среди всех языков, которые встречал. %w[word1 word2], system %(ssh alex@host 'grep "foo" /var/log/bar.log'), return unless ... и кучу других маленьких удобных приемов я использую каждый день.

И снова здравствуйте

August 10, 2016

Я много раз начинал писать блог и много раз бросал. Но природу не обманешь, нравится мне это дело, попробую еще раз.

Итак, представлюсь. Меня зовут Алексей Вахов. 7 лет я был C++ разработчиком разной степени суровости. С Руби он Рейлс меня познакомил дорогой друг Иван Евтухович в 2006 году. Увлекаюсь Рейлс почти с самых первых версий и даже попал в топ-100 контрибьюторов. Однако профессионально занимаюсь веб-разработкой около 6 лет.

Сейчас работаю техническим директором в компании Учи.ру. Мы занимаемся интересным и важным делом - учим детей математике и даже уже чуть-чуть русскому языку. Четыре года назад проект начинался с нуля, первых пользователей мы нашли в школе, которая находится рядом с офисом. Сегодня у нас 900 тысяч учеников, много продакшенов, серверов, задач, вопросов и планов.

До сих пор очень люблю Рейлс, хотя заниматься им стал конечно гораздо меньше. В настоящее время больше интересуюсь всем, что связано с разработкой ПО в целом: как создать комфортную среду, наладить процессы и взаимодействие внутри команды и вне, обеспечить стабильность приложений и их дальнейшее развитие, и так далее.

В блоге планирую писать про разные штуки, связанные с разработкой, работой, а также свои мысли, идеи и все такое. Надеюсь что-то покажется интересным и возможно даже поможет. Писать планирую регулярно и достаточно долгий срок. Как говаривал Будда в жизни есть 2 сложные вещи: начать что-то делать и продолжить это делать.

На сегодня все, спасибо за внимание и до новых встреч!

Алексей Вахов

Ember - 3. REST

February 17, 2015

Давно не брал я в руки вима, вот беру и снова беру. Недавно узнал, что есть устойчивая аббревиатура MEAN для стека из монго, експресса, ангуляра и ноды. У нас ембер вместо ангуляра, меняем ноду на iojs, работаем в стеке MEEIO.

В прошлом выпуске мы сделали буковку R из CRUD для постов, сегодня доделаем все остальные буквы. Обновляем рауты:

Router.map(function() {
  this.route('posts/index', {path: '/'});
  this.route('posts/new', {path: '/posts/new'});
  this.route('posts/show', {path: '/posts/:id'});
  this.route('posts/edit', {path: '/posts/:id/edit'});
});

На главной странице добавляем ссылку на создание нового поста (app/templates/posts/index.hbs):

<p>{{#link-to "posts/new"}}New Post{{/link-to}}</p>

Делаем раут для new (app/routes/posts/new.js):

export default Ember.Route.extend({
  model: function() {
    return this.store.createRecord('post');
  },

  deactivate: function() {
    if (this.controller.get("model.isNew")) {
      this.controller.get("model").rollback();
    }
  }
});

Обратите внимание на метод deactivate. Я не знаю насколько это стандартный прием, придумал его из головы. В прошлом выпуске мы обсуждали, что ембер-store - это некоторая сущность, которая должна синхронизироваться с сервером. createRecord не синхронизирует запись с сервером, однако создает его в сторе. И если вы откажитесь от сохранения записи и выйдите через меню на главную страницу, то пустая запись будет в вашем списке. Чтобы этого не произошло и добавил ролбек на не сохрненную модель.

В форме вешаем на кнопку экшен (app/templates/posts/_form.hbs):

  <button type="submit" class="btn btn-default" {{action "save"}}>Save</button>

Который обрабатываем в контроллере (app/controllers/posts/new.js):

import Ember from 'ember';

export default Ember.ObjectController.extend({
  actions: {
    save: function() {
      var me = this;
      var model = this.model;
      model.save().then(function() {
        me.transitionToRoute("posts/show", model);
      });
    }
  }
});

Таким образом реализовали C. Остальные буквы легко сделать по аналогии.

Tsort

February 2, 2015

В исходниках Рейлс я как-то увидел ссылку на модуль tsort . "Tsort, WTF?" - подумал я (в переводе с английского это означает - что за неведомый, удивительный метод tsort есть в нашем любимом руби).

Документация говорит : "TSort implements topological sorting using Tarjan's algorithm for strongly connected components". Википедия поддерживает : "In the mathematical theory of directed graphs, a graph is said to be strongly connected if every vertex is reachable from every other vertex".

Обычно такие объяснения выглядят для меня примерно так: "麺類 緑茶 algorithm 車青出", но в этот раз я даже разобрался. Эта штука помогает найти циклические зависимости в графе, что очень хорошо, например, для подсчета зависимостей:

require 'tsort'

class BuildSystem < Hash
  include TSort
  alias tsort_each_node each_key
  def tsort_each_child(node, &block)
    self[node].each(&block)
  end

  def set(component, dependencies = [])
    self[component] = dependencies
  end

  alias build_order tsort

  def valid?
    strongly_connected_components.each do |component|
      if component.length > 1
        puts "Cyclic deps: #{component.inspect}"
        return false
      end
    end
    true
  end
end

sys = BuildSystem.new
sys.set("rack", %w[])
sys.set("rails", %w[activerecord activesupport actionmailer])
sys.set("actionmailer", %w[rack])
sys.set("activesupport", %w[rack])
sys.set("activerecord")

p sys.valid?      # => true
p sys.build_order
# => ["rack", "activerecord", "activesupport", "actionmailer", "rails"]

sys.set("rack", %w[rails])
p sys.valid?
# Cyclic deps: ["rack", "rails", "activesupport", "actionmailer"]
# => false

Вот такой вот дивный компонент.

Ember.js - 2. Index & Show

January 29, 2015

Сколько раз в жизни вам нужно было сделать одностраничное приложение, которое хранит данные в локал сторадже наподобие TodoMVC из официального гайда Эмбера ? Мне ни разу. Поэтому, как только я начинал делать что-то полезное, то сразу же возникало сотни практических вопросов, на которые гайды не отвечали.

Если подумать, то что представляют из себя 99% современных сайтов? Это CRUD-работа с базой. Предположим людям не нужен дизайн и всякая анимация, что лучше всего подходит для создания CRUD-интерфейсов? Конечно же Рейлс приложение на бутстрапе! Предлагаю делать бутстрап-CRUD на Эмбере, это будет почти также круто, как Рейлс :trollface:.

Сервер

Я решил, если клиент на javascript, то и сервер должен быть на javascript, а база должна быть Монго. С меня сошло семь потов пока я написал простенькое приложение на ноде, обрабатывающее запросы от REST-адаптера Эмбера. Если кратко, то запросы принимаются в виде {record: {field1: "value1"}, ...} и возвращаются в том же виде. Более подробно можно прочитать в официальной документации :

// INDEX
app.get("/api/posts", function(req, res) {
  db.collection("posts").find().toArray(function(err, result) {
    // ...
  });
});

// CREATE
app.post("/api/posts", function(req, res) {
  //..
});

// SHOW
app.get("/api/posts/:id", function(req, res) {
  //..
});

// ...

Вообщем такой, обычный REST. Код сервера здесь . Я не добавил обработку ошибок и, уверен, нарушил кучу принципов построения ноде-приложений, так как понятия не имею как это правильно делать, но для демонстрации работы Эмбера нам будет достаточно и такого сервера.

Томстер, зажигай

В принципе Эмбер действительно похож на Рейлс. Каждой странице соответствует свой уникальный урл. Определением кто будет рисовать страницу заведует раутер:

// app/router.js
Router.map(function() {
  this.route('posts/index', {path: '/'});
  this.route('posts/new', {path: '/posts/new'});
  this.route('posts/show', {path: '/posts/:id'});
  this.route('posts/edit', {path: '/posts/:id/edit'});
});

Рауты я использовал немного не канонические, но таким образом все файлы очень здорово расскладываются по папочкам.

Когда мы заходим на главную страницу, то вызывается раут posts/index. В Рейлс мы можем добавить вьюху, а экшен в контроллере сгенерируется автоматически (хотя я так обычно не люблю делать и всегда добавляю пустой метод), в Эмбере принцип автоматической генерации доведен до абсолюта. Любой компонент в цепочке вызова после раута можно пропустить и он сгенерируется автоматически.

Для posts/index мы определим раут явно:

// app/routes/posts/index.js
import Ember from 'ember';

export default Ember.Route.extend({
  model: function(params) {
    return this.store.find('post');
  }
});

Обратите внимания используется магия модулей ES6, запоминать синтаксис не нужно, заготовки файлов удобно создаются с помощью ember-cli генераторов (в консоли ember g route posts/index). Задача раута в большинстве случаев сохранить модель или коллекцию моделей. На первом этапе нужно придерживаться правила один урл = один раут = одна модель/коллекция моделей. Такое соглашение обедняет возможный функционал, но в дальнейшем мы научимся обходить это ограничение несколькими способами. Метод model должен вернуть модель или промис (мы же находимся в асинхронном енвайроменте).

store - это "база данных" нашего клиентского приложения. Работой с моделями занимается специальный модуль Ember Data, который никак не может выйти из статуса беты. Стор частично зеркалирует данные, содержащиеся на сервере. То есть, если мы запросим все посты с сервера, они будут лежат в сторе, пока мы не запросим их заново. Мы можем добавить в стор запись и позднее ее сохранить на сервере. Таким образом если сильно постараться можно добиться приятной отзывчивости своего приложения.

store.find возвращает промис на коллекцию моделей post, которые Эмбер запрашивает с сервера, используя REST-адаптер. Чтобы система заработала необходимо определить собственно сам адаптер:

// app/adapters/application.js
import DS from 'ember-data';
import config from '../config/environment';

export default DS.RESTAdapter.extend({
  host: config.baseHost,
  namespace: 'api'
});

Хост указан, чтобы при разработке мы запускали ember-cli и ноде-сервер на разных портах, нейспейс помогает разделить запросы к API, с доставкой приложения (Эмбер хоть и одностраничное приложение, при использовании history api он занимает все урлы).

И также описываем схему самой модели:

// app/models/post.js
import DS from 'ember-data';

export default DS.Model.extend({
  title: DS.attr('string'),
  body: DS.attr('string')
});

Многообразием типов Эмбер в отличие от Рейлс не балует (да и в принципе он не стремиться нас в чем-то особо баловать), однако строки, числа, даты и даже связи между моделями есть.

Следующим компонентом в иерархии обработки запроса идет контроллер. Задача контроллера - хранить стейт страницы и пробрасывать модель в темплейт. В данном случае мы оставим автосгенеренный контроллер. Контроллеры нам понадобятся при реализации других раутов.

И наконец темплейт - это реактивный хенлебар-шаблон с расширениями и разными наворотами (в прошлой записе я говорил, что все есть Ember.Object с свойствами через set/get и computed-свойствами, через магию Эмбера все что вы поменяется в модели, контроллере и тд автоматически отобразится во вьюхе наподобие React.js):

<ol class="breadcrumb">
  <li class="active">Posts</li>
</ol>

<p>{{#link-to "posts/new" class="btn btn-default"}}New Post{{/link-to}}

{{#each post in model}}
  <h2>{{#link-to "posts/show" post}}{{post.title}}{{/link-to}}</h2>
  {{post.body}}
{{/each}}

Тут все просто: бежим по model (который на самом деле в данном случае коллекция постов) и отрисовываем. link-to - это хэлпер, которые делает известно что. Кстати особенностью создания ссылки в таком виде является то, что при нажатии на эту ссылку, хук model в рауте post/show вызван не будет, то есть не будет запроса к серверу и с главной страницы вы мгновенно попадете на страницу поста. Это хорошо с одной стороны, но с другой стороны вызывает такие нежданные баги, которым позавидует любая другая система.

Аналогично создаем раут для posts/show.

// app/routes/posts/show.js
import Ember from 'ember';

export default Ember.Route.extend({
  model: function(params) {
    return this.store.find('post', params.id);
  }
});

Здесь мы уже используем динамический раут с айдишником записи, прямо как в рейлс. Темплейт тоже стандартный:

<ol class="breadcrumb">
  <li>{{#link-to 'posts/index'}}Posts{{/link-to}}</li>
  <li class="active">{{title}}</li>
</ol>

<h2>{{title}}</h2>
{{body}}

Хотя здесь есть один нюанс, мы используем {{title}} вместо {{model.title}}. Так можно делать. Здесь включается очередная магия Эмбера, которую пока можно принять просто как факт.

Пару мелочей

Бутстрап подключаем в Brocfile.js (это штука для сборщика ассетов в ember-cli):

// Brocfile.js

var EmberApp = require('ember-cli/lib/broccoli/ember-app');

var app = new EmberApp();

// Bootstrap
app.import("vendor/bootstrap-3.3.1/css/bootstrap.css");
app.import("vendor/bootstrap-3.3.1/js/bootstrap.js");
var fonts = [
  "vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.eot",
  "vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.svg",
  "vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.ttf",
  "vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.woff"
];
fonts.forEach(function(font) {
  app.import(font, {destDir: "fonts"});
});

module.exports = app.toTree();

Создаем в базе тестовые записи:

rs-ds053668:PRIMARY> db.posts.find()
{ "_id" : ObjectId("54c9702fce1c75040021bb37"), "title" : "First Post", "body" : "Lorem
ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." }
{ "_id" : ObjectId("54c97fc5ce1c75040021bb39"), "title" : "Second Post", "body" : "Lorem
ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." }

ember build - компилирует код клиентского приложения, это можно повесить в post-install, но для простоты я скомпилировал ассеты и положил их в репозитарий:

cd client && ember build --environment=production --output-path=../server/public

Ахалай Махалай! Киргуду Бургуду!

Результат

Ура!

Фух. Спасибо, что домотали до конца. Я выкатил приложение на хероку, исходники на гитхабе. Сегодня нам получилось реализовать только буковку R. Буковки С, U и D реализуем в следующий раз. Если ничего не понятно - это нормально. Эмбер и не должен быть понятным. Это же Эмбер! Спасибо за внимание и до новых встреч!

Enumerable#flat_map

January 26, 2015

В Руби 1.9.2 появился класный метод flat_map , который я незаслуженно обходил стороной.

Он делает все тоже самое, что обычный map, только объединяет возвращаемые значения не в массив, а друг с другом. Официальная документация описывает это так:

[1, 2, 3, 4].flat_map { |e| [e, -e] } #=> [1, -1, 2, -2, 3, -3, 4, -4]
[[1, 2], [3, 4]].flat_map { |e| e + [100] } #=> [1, 2, 100, 3, 4, 100]

В Рейлс же этот метод можно использовать вот так:

class Planet < ActiveRecord::Base
  has_many :countries
end

class Country < ActiveRecord::Base
  has_many :cities
end

class City < ActiveRecord::Base
end

Planet.
  includes(:countries => :cities).
  flat_map(&:countries).flat_map(&:cities)

Руби - гениальный язык!

Ember.js - 1. Введение

January 23, 2015

Говорят, что у Ember.js кривая обучения крутовата. Категорически с этим не соглашусь.

На самом деле обучение Ember.js - это глубокий темный колодец, в котором сидишь на дне и воешь от отчаяния. Но когда я немного разобрался, Эмбер начал мне нравится, даже сказал бы, что я его полюбил. Это действительно очень достойная и перспективная разработка.

Я хочу посвятить несколько выпусков работе с этим замечательным фреймворком. Не знаю, что в итоге получится, пока планирую, что мы зайдем немного с другой стороны, чем предлагается в официальных гайдах. Выпуски про Эмбер будут перемежаться с другими, более привычными темами про Руби, Рейлс и все такое, чтобы было веселее. Для успешной работы нам понадобится Node.js (я поставил через ndenv по аналогии с моим любимым rbenv) и Ember-cli - это тру-способ организации проекта.

Для начала хочу отметить, что в Эмбере все наследуется от Ember.Object, основная фишка которого, это возможность установить свойство через set и взять его через get. Всегда пользуйтесь этими методами. В качестве бонуса за непривычный синтаксис вы получите computed-свойста, вычисляемые реактивно, обзерверы, следящие за измененим свойств и всякие другие любопытные штуки. Создавать объект следует через create:

var Person = Ember.Object.extend({
  fullName: function() {
    return this.get("name") + " " + this.get("surName");
  }.property("name", "surName")
});

var alex = Person.create(name: "Alexey");
console.log(alex.get("fullName")); // => "Alexey "

alex.set("surName", "Vakhov");
console.log(alex.get("fullName")); // => "Alexey Vakhov"

Подробнее можно прочитать в разделе The Object Model официальной документации.

На сегодня все, до новых встреч!