Перейти к основному содержимому

19 записей с тегом "backend"

Посмотреть все теги

Путь к Федеративному GraphQL

· 14 мин. чтения

Картинка с dgraph.io

Программисты любят хорошие истории, поэтому надеюсь что пятилетний путь к композитному API с помощью GraphQL в боевой среде (на пике выдающей 110 запросов в секунду при 100мс задержке) будет интересен.

[Если вы спешите, проскрольте ниже к урокам и гляньте на открытый код graphql-schema-registry]

schema registry с примером тестовой схемы

Задача

Годами, Pipedrive (которому в начале 2020 стукнуло уже 10 лет), давал публичный REST API где были и недокументированные пути для нашего веб приложения. Один из них /users/self, который изначально задумывался для загрузки информации о пользователе, нормально с течением времени стал грузить все что надо для первой загрузки страницы, доходя до 30 разных типов сущностей. Сам он выдавался PHP монолитом, который по натуре синхронный. Мы пытались его распараллелить, но неудачно. /users/self распределение задержки для оставшегося траффика

С точки зрения поддержки, каждое новое изменение все больше усложняло код и никто не хотел владеть этой огромной функцией.

Прототип с прямым доступом к БД

Давайте вернемся назад в прошлое, когда наши разработчики стали экспериментировать с graphql.

Года 3-4 назад, в команде разработки рынка приложений, я услышал от Паши, нашего фулл-стек разработчика новенькие для меня термины - elixir и graphql. Он участвовал в тестовом проекте в котором напрямую работал с MySQL и выдавал основные сущности Pipedrive по /graphql.

На локальной машине работало прекрасно, но это не масштабировалось на весь код, потому что функционал - не обычный CRUD, а весь монолит переписывать никто не хотел.

Прототип со склейкой

Переносимся в 2019, когда я заметил еще один тестовый репозиторий от другого коллеги, который стал использовать GraphQL stitching с определением сущностей с помощью graphql-compose и резолверами которые делали запрос уже к нашему REST API. Как можете представить, это уже было значительным улучшением, потому что нам не надо было переопределять всю бизнес-логику и graphql становился просто приятной оберткой.

Из недостатков этого решения:

  • Производительность. Небыло dataloader'а, поэтому был риск N+1 сетевых запросов. Небыло ограничения сложности запроса и какого-либо промежуточного кеширования.

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

Подготовка

В Октябре 2019, я стал готовиться к миссии, которая превратила бы прошлое решение в рабочее с помощью Apollo федерации, которая вышла чуть ранее в этом же году. Кроме того, результат приземлился бы в команде Core, которая стала бы поддерживать его в долгосрочной перспективе.

Сбор ожиданий разработчиков

В компании некоторые разработчики были довольно скептичны и предлагали построить свое решение просто склеивая запросы воедино, отправляя по POST и надеясь на свой gateway который будет распараллеливать обработку.

Другие считали что graphql еще слишком сырой что-бы использовать в жизни и лучше подождать. Третьи предлагали рассмотреть альтернативы, типа Protobuf и Thrift, а в качестве транспорта рассмотреть GRPCOData.

Четвертые напротив, на полном ходу писали graphql под одиночные сервисы (insights, teams) и выкатывали в лайв, однако не могли повторно использовать сущности (например User). В Праге ребята написали все на typescript + relay на фронтенде, который нам еще предстоит перенести в федерацию.

Изучать новую технологию было круто.

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

Впрочем, я знал что нам понадобится управлять схемой как-то динамически, что-бы не полагаться на захардкоженые значения и видеть что происходит. Нечно типа Confluent’s schema-registry или Atlassian’s Braid, но они либо под Кафку делались, либо на Java, которую мы не хотели поддерживать.

План

Я выступил с инженерной миссией у которой было 3 цели:

  • Уменьшение первичной загрузки страницы (воронка продаж) на 15%. Достижимо если заменить некоторые REST запросы в один /graphql

  • Уменьшение траффика на 30%. Достижимо если перенести загрузку всех сделок для воронки продаж в graphql и спрашивать меньше свойств.

  • Использовать строгую типизацию (что-бы фронтендерам надо было писать меньше кода в защитном стиля)

Мне повезло, трое разработчиков присоединились к миссии, в т.ч. автор прототипа.

Сетевые запросы по мере загрузки веб-приложения

Изначальный план по сервисам выглядел так:

Сервисы над которыми предстояло бы работать

Тут schema-registry был бы общим сервисом, который мог бы хранить схему любого типа, получая на входе что вы в него кинете (swagger, typescript, graphql, avro, proto). На выходе он мог бы так же выдавать что вы хотите.

Gateway периодически опрашивал бы schema-registry и вызывал бы уже сервисы которые владеют данными согласно запросу и схеме. В свою очередь frontend компоненты могли бы скачивать актуальную схему для autocomplete и самих запросов.

На деле впрочем, мы сделали только поддержку graphql формата, потому что для федерации его было достаточно, а время как обычно быстро закончилось.

Итоги

Главную цель по замене /users/self в веб-приложении мы сделали в течение первых двух недель ? (ничоси!). А вот полировка всего решения что-бы оно быстро и надежно работало, заняло все оставшееся время.

К концу миссии (в феврале 2020), мы каким-то чудом достигли 13% ускорения первой загрузки страницы и 25% ускорения при повторной загрузке (из-за добавленного кеширования), согласно синтетическому UI тесту который мы гоняли на Datadog.

Уменьшить трафик нам не удалось, потому что мы не достигли рефакторинга вида воронки продаж - там все еще REST.

Что-бы ускорить добавление сервисов в федерацию другими командами (у нас 600+ человек), мы записали обучающие видео что-бы все были в курсе как оно все работает вместе. После окончания миссии IOS- и Android- клиенты тоже мигрировали на graphql и в целом были рады.

Выученные уроки

Глядя на дневник миссии из этих 60 дней, могу выделить наибольшие трудности что-бы вам было бы попроще

Управляйте своей схемой

_Могли бы мы построить это сами? Возможно, но это не было бы так отполировано.
_Mark Stuart, Paypal Engineering

В течение первых пары дней я попробовал Apollo studio и их инструментарий для терминала по проверке схемы. Сервис прекрасный и работает из коробки с минимальными настройками gateway.

Написанный нами инструментарий для валидации схемы

Написанный нами инструментарий для валидации схемы

Однако несмотря на все фишки этого облачного сервиса, я посчитал что для такого важного дела как маршрутизация траффика было бы опрометчиво полагаться на сторонний сервис из-за рисков для жизнеспособности нашего дела, несмотря на богатый функционал и сколько-то выгодные расценки. Поэтому мы написали свой сервис с минимально нужным функционалом и теперь отправили его в opensource -  graphql-schema-registry .

Вторая причина была в том что-бы следовать модели распределенной архитектруры датацентров Pipedrive. У нас нет центрального датацентра, каждый DC самодостаточен. Это дает нам как более высокую надежность, так и способность открывать новые датацентры в Китае, России, Иране или на Марсе ?

Версионируйте свою схему

Федеративная схема и graphql gateway очень хрупкие. Если у вас появится конфликт имен у типов или неверная ссылка между типами в одном из сервисов, то gateway может этого не пережить.

По умолчанию, gateway напрямую опрашивает все сервисы касательно их схемы, поэтому достаточно одного из сервисов с ошибкой что-бы поломать всем трафик. Apollo studio решает это посредством проверки схемы еще до деплоя и отказе если видит какой-то конфликт.

Проверка совместимости схем правильный подход, но конкретное решение у Apollo зависит от их внутреннего состояния, т.к. они хранят правильную склеенную схему на данный момент. Это делает протокол регистрации сложным и зависящим от времени.

Мы же напротив, связали версии микросервисов (генерируемые на основе хешей докер-образов) со схемой. Микросервисы регистрируют уже в ходе своей работы и делают это один раз без последующих повторений. Gateway делает выборку всех федерированных сервисов из консула и спрашивает schema-registry составить схему /schema/compose , предоставляя их версии.

Если schema-registry видит что предоставленные версии несовместимы, она откатывается на прошлые версии

Регистрация схемы при старте сервиса

Микросервисы могут выдавать как REST так и Graphql API, поэтому мы используем оповещения тревоги при неудачной регистрации схемы, оставляя при этом работу REST.

Определение схемы на основе REST не просто

Поскольку я не знал как лучше перенести схему из нашего REST в graphql, я сначала попробовал openapi-to-graphql, нормально наша документация на тот момент не имела детального описания запросов и ответов.

Спрашивать у каждой команды что-бы они написали бы ее, очевидно заняло бы уйму времени, поэтому мы определили основные сущности сами ? просто глядя на ответ при разных REST запросах.

В дальнейшем это нам аукнулось, так как оказалось что REST API иногда зависит от того кто делает запрос, либо зависит от внутреннего состояния и логики.

Например после настраиваемые поля данных меняют REST API в зависимости от пользователя. Если его добавить к сделке, то он оно будет выдаваться с ключем-хешем на уровне deal.pipeline_id. С федеративным graphql, динамическая схема невозможно, поэтому нам пришлось передвигать эти настраиваемые поля в отдельный список

Другая проблема это стиль наименований в json-ответе. Мы хотели поддерживать верблюжийСтиль, но почти весь REST отвечал нам в змеином_стиле, пришлось пока остаться на смешанном.

федеративный граф данных Pipedrive (слева) с 2 сервисами (из 539) и еще-не-федеративный граф сервиса leads (справа)

CQRS и кеш

Модель данных в Pipedrive не может полагаться на простой TTL-кеш.

Например, когда инженер поддержки создает глобальное оповещение о предстоящем техническом обслуживании приложения из нашей админки, он ожидает что сообщение будет сразу же видно пользователям. Эти глобальные оповещения могут быть адресованы как всем пользователям, так и конкретным компаниям и индивидуальным людям. Чистить такой кеш приходится используя 3 разных ключа.

Что-бы делать параллельные запросы к PHP-монолиту, мы написали сервис на nodejs (назвали его monograph), который кеширует все что отвечает монолит в memcached. Этот кеш надо чистить из монолита в зависимости от бизнес-логики. Это немного антипаттерн, потому что кеш получается общим для разных сервисов и сильно их связывает.

Тут можно заметить CQRS pattern. Впрочем, такой кеш позволяет ускорять 80% запросов и дает итоговую среднюю задержку такую же как и у монолита.

Время средней задержки ответа монолита (слева) и graphql gateway (справа) в регионе US на основе NewRelic

Еще одна сложность - язык пользователя. Как только он меняется, нам надо чистить уйму разных сущностей - от типов активности до того какие настройки должны быть при показе гугл карт. И что еще хуже - язык уже управляется не в монолите, а был вынесен в отдельный сервис identity. Связывать его с общим кешем очень не хотелось.

Identity владеет данными пользователей, поэтому мы сделали так что он кидает событие через kafka, которое ловит monograph и чистит все нужные кеши. Тут есть недостаток, что событие приходит с задержкой (может максимум 1 сек), поэтому чистка кеша происходит не мгновенно. Нормально это не критично, потому что пользователь не так быстро уходит со страницы что-бы сделать повторный запрос к бекенду и заметить что в кешах еще старый язык.

Следите за производительностью

Производительность была главной целью и для этого нам пришлось хорошо прокачаться в знании APM и выслеживании распределенных запросов между микросервисами что-бы определить самые медленные места. Мы на тот момент использовали Datadog и он хорошо себя зарекомендовал.

Для ускорения, мы кешировали все 30 параллельных запросов в memcached. Но по ходу дела возникали проблемы. Например на картинке слева видно что некоторые резолверы делают запрос в memcached и получают ответ за 10мс, а другие тупят, словно жду своей очереди выростая до 220мс. Как оказалось, это было из-за того что я использовал один и тот же хост mcrouter. Как только я стал их ротировать, мемкеш стал отдавать все за 20мс максимум.

Что-бы уменьшить количество этих сетевых запросов к мемкешу, мы склеили его в один используя getMulti функцию. Получается что несмотря на то что все resolver'ы выполняются независимо, они блочатся на 5мс с помощью debounce и склеиваются воедино.

На картинке справа видна другая проблема. Эти желтые линии это налог graphql gateway на приведение данных к строгому типу. Причем чем больше данных вы гоняете через сервис, тем дольше будет эта фаза выполнятся.

Вобщем по ходу миссии постоянно приходилось мониторить какие же запросы нам надо еще закешировать и под конец стало бесить что скажем 28 из 30 запросов прекрасно кешируются, а оставшиеся два мало того что отвечают целых 500мс, так их еще и невозможно легко закешировать.

Нам пришлось вынести их в отдельный graphql запрос, который делается после первого /graphql запроса на инициализацю страницы. Так что сейчас мы на деле делаем уже порядка 3-5 независимых запросов, в зависимости от тайминга фронтенда (там тоже debounce зависящий от параллельной подгрузки FE компонентов)

Не следите за производительностью

Казалось бы полностью противоположный урок, но на деле это значит что в лайв-среде вам лучше отключить мониторинг приложения (APM) и tracing:true, если вы используете их.

Отключение отслеживания у нас ускорило среднюю задержку в два раза с 700мс до 300мс для нашей тестовой компании. Насколько я понимаю, причина крылась в функциях которые замеряют время (типа performance.now()), которые необходимы для замера каждого резолвера.

Мы делали профайлинг graphql gateway с помощью  Chrome DevTools, но так просто вызов этих мелких функций, раскиданных повсюду не заметишь.

Бен тут делал замеры и тоже пришел к этому.

Попробуйте ранний запрос

На фронтенде, время создания graphql запроса для нас оказалось сложновато. Я хотел передвинуть первый /graphql как можно раньше (даже до загрузки всего кода в vendors.js) в графике сетевых запросов. Мы смогли это сделать, но веб-приложение из-за этого стало значительно сложней.

Что-бы делать запрос вам понадобится graphql клиент и парсинг gql литералов для определения самого запроса. Дальше его надо либо выдавать в отдельном bundle, либо отказываться от всех удобств и использовтать сырой fetch. Даже если делать сырой запрос, дальше надо что-то делать с ответом - распределять данные по моделям либо сохранять в глобальную переменную. Вобщем мы решили отказаться от этого и надеяться на server-side-rendering и service workers в будущем.

Перенос /graphql запроса левей

Оценивайте сложность запроса

Чем graphql примечателен, так это то что тут можно оценивать насколько больно запрос клиента будет бить по инфраструктуре не делая при этом еще никакой работы. Для оценки сложности надо указывать цену внутри самой схемы с помощью директив, а дальше делать саму проверку при получении запроса и отклонять его если сложность слишком велика, подобно нашему rate limit на частоту запросов.

Сначала мы попробовали graphql-cost-analysis библиотеку, но в итоге написали свою, поскольку захотели больше фич - множителях при выдаче списков данных, вложенности и рекурсии, а так же определении типов влияния на инфраструктуру (сетевой запрос, БД, CPU, работа с файлами). Тут сложней всего оказалось внедрять поддержку своей директивы в gateway и schema-registry. Надеюсь мы нашу библиотеку тоже опубликуем.

У схемы много лиц

Работать со схемой в js/typescript на низком уровне порой мучительно. Это начинаешь понимать когда пробуешь интегрировать существующий graphql сервис в федерацию.

Например настройки koa-graphql и apollo-server-koa ожидают что вы создадите вложенный объект  GraphQLSchema  который включает и сами резолверы. Но федеративный apollo/server хочет схему и резолверы отдельно:

buildFederatedSchema([{typeDefs, resolvers}])

В другом случае, как я уже писал, вам захочется определить схему с помощью gql литерала, или сохранить схему в schema.graphql файл, а когда вам надо будетделать проход по всей схеме для оценки сложности, понадобится ASTNode (и функции parse / buildASTSchema)

Плавная canary деплой

Во время миссии мы выкатывали изменения сначала всем разработчикам что-бы отследить очевидные ошибки.

Под конец миссии в феврале, мы выкатили graphql только на 100 компаний-"везунчиков". Дальше мы медленно выкатывали их до 1000 компаний, потом 1%, 10%, 30%, 50% и наконец в июне выкатили всем.

Деплоили мы просто используя логику деления company ID без остатка. В дополнение, у нас было несколько настроек - кому еще включать, кому не включить никогда и рубильник выключающий graphql сразу у всех на фронтенде. Было очень полезно во время инцидентов сразу избавиться от подозрений что это все из-за нового graphql и упростить дебаг.

Учитывая насколько большие изменения мы делали, это был хороший опыт быстро получить обратную связь, найти баги (с кешированием), при этом снизив риски для большинства пользователей.

Надежды и мечты

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

Как только наш graphql API станет стабильным и достаточным для использования любыми пользователями, его можно выкатить в качестве второй версии нашего API. Для публикации впрочем понадобятся еще директивы для ограничения доступа к сущностям согласно OAuth видимости (для приложений из рынка) и согласно нашим продуктам.

В самой schema-registry надо добавить отслеживание клиентов, связать ее gateway для получения аналитики трафика что-бы проще было понять какие поля можно удалять, надо добавить фильтрацию и подсветку сущностей в зависимости от директив цены и видимости, валидировать именование, хранить постоянные запросы (persisted query), публиковать историю изменений итоговой схемы для сторонних разработчиков.

Кроме того, поскольку у нас есть сервисы на go, еще непонятно как стоит делать внутренний трафик - стоит ли использовать GRPC из-за скорости и надежности, либо использовать независимые graphql эндпоинты, либо ставить централизованный внутренний gateway. Если GRPC лучше только из-за его бинарности, может мы можем написать библиотечку которая тоже упакует graphql данные с помощью msgpack?

Что касается внешнего мира, надеюсь Apollo со своим проектом Constellation ускорят обработку запроса на Rust что-бы небыло этих 10% налога на производительность и смогут федерировать graphql сервисы без серьезных изменений последних.

Вобщем прекрасное время что-бы наслаждаться разработкой где уйма сложности!

Шпаргалка по golang

· 3 мин. чтения

После php и node начал писать на go, поэтому по-аналогии с unix-шпаргалкой, выпишу для себя основы..

Запуск

go run main.go → компиляция и запуск exe
go build main.go → только компиляция и создание exe, без запуска
go get -u https://github.com/x/y → импортирование зависимостей

Переменные и типы

типдекларациязначение по умолчанию
целыеint8, int 16, int32, int64, uint, uint640
бинарныеbyte
битboolfalse
с плавающей точкойfloat, float32, float640
строкаstring, rune(пустая строка)
var i int = 10;
var myfirstvar, mysecondvar int = 3, 4; // объявление разом
var autoInt = -10 // компилятор сам подберёт тип
var mystring = "test\n" // двойные для строк, всё в уникоде
var mysymbol = '\u2318' // один символ
var rawBinary = '\x27' // одинарные кавычки для символа

myNewDeclared := 42 // быстрая инициализация
myMathVar = 7+3i // математикам

Приведение типов

int(myfloat)
float64(myInt)
string(48) //будет номер символа, а не строчка "48"
bin := []byte(mystr) //перевод строки в массив байтов для итерации по байтам

Константа

const fullName = "Artjom"

//объявление нескольких переменных с инкрементом
const (
one = iota + 1 //iota начало итерации с нуля
_ // пропуск "двойки"
KB uint64 = 1 << (10*iota)
)

Массив

Низкоуровневый тип. Длина массива фиксирована.

var a1 [3]int //длина массива, сразу заполнится [0 0 0]
var a1 [2*size]bool //длина зависит от перменной
a3:=[...]int (1,2,3) //инициализирование значениями
var aa[3][3] int //массив массивов

len(mystring) //длина строк и массивов

Слайс (резиновый массив)

Более сложная структура. Состоит из массива с фиксированной длиной (capacity) в которую записано меньшее количество элементов (length). Как только length превышает capacity, размер capacity удваивается

var s1 []int //без размера
append(s1, 100) // добавление
len(s1)
cap(s1) // узнать capacity
s2:= []int(1,2,3,4); //создание со значениями

s1 = append (s1, s2...) //Соединение слайсов через оператор развёртывания значений
s3 := make([]int, 10) //Создание слайса сразу нужной длины, значения уже будут заполнены
s4 := make([]int, 10, 15) // Создание слайса с нужной capacity
copy(s7, s6) // копирование значений, но размер целевого слайса должен совпадать
fmt.Println( s7[1:5] ) // часть слайса

// слайс из массива
a:=[...]int(5,6)
b:=a[:] //слайс теперь ссылается на массив

Мап (ассоциативный массив)

var myEmptyMap map[string]string // [тип ключа] и тип данных, но пустой, в него нельзя писать

var mm1 map[string]string = map[string]string{} //пустой интерфейс
mm2 := map[string]string{}
mm3 = make(map[string]string)

mm2["firstName"] = "Artjom" //write
fmt.Println(mm2["firstName"]) //read
delete(mm3, "somekey") //delete

fmt.Println(mm3["nonexistingkey"]) //выдаст значение по умолчанию (пустая строка)

_, exists = mm3["missing"] // проверка есть ли такой ключ

Ключевые слова в управляющих конструкциях

  • fallthrough - проваливание в switch case

  • break - выход из  switch

  • range - итерирование по символам строки, например

  • for indexval := range mystr

Функция

init() - вызывается при старте программы в любом из пакетов (порядок неконтролируемый)

//аргументы передаются по значению
func avg(x int) float{ //возвращаемый тип в конце
}

func multipleArgs(severalInts ...int){ //развёртывание аргументов в слайс
}

func avg2(y int) (res float){ //сокращение return res в конце
res := 3
}

func mysql(file File, bool x){
file.open()
defer file.close() //заранее объявляет что надо вызвать в конце исполнения функции

if(x) return 0
//do more lots of stuff
return 1
}

См. также

Dashboard на основе Grafana и InfluxDB

· 2 мин. чтения

В продолжение темы умного дома где мой котёл умел выдавать API для мониторинга температур, захотелось мне вывести эти данные в более приятный вид. Кроме того, хотя сам котёл умеет рисовать графики, он показывал на них только температурную зону одного контура, а мне хотелось видеть два.

Поэтому я решил поизучать бесплатный dashboard и графико-генератор на основе Grafana. Зарегился и упёрся в то что он сам не хранит данные. Он умеет только подключаться к внешним источникам и делать запросы туда. 

Новый MySQL мне не хотелось поднимать, а открывать существующий тем более. Cloudwatch от Амазона - видимо полезен для мониторинга сервисов если вы активно пользуетесь этой инфраструктурой. Prometheus я могу и на работе посмотреть. Выбрал InfluxDB - специальную БД для хранения временных событий.

Ставить Influx было достаточно просто. Сложней было связать графану с influx.

Сервис гоняется на 8086 порту и Grafana хочет SSL и CORS. Поэтому пришлось делать proxy на nginx и добавлять header:

location /influx/ {
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header 'Access-Control-Allow-Credentials' 'true';

proxy_pass http://localhost:8086/;
}

Теперь встаёт вопрос как данные добавить в influx. Писать будет процесс на node, поэтому я по-быстрому нашёл библиотечку и пример подключения: 

const Influx = require('influx');

const influx = new Influx.InfluxDB({
host: 'localhost',
database: 'home',
schema: [
{
measurement: 'response_times',
fields: {
boiler_smoketemp: Influx.FieldType.FLOAT,
boiler_output: Influx.FieldType.FLOAT,
boiler_power: Influx.FieldType.FLOAT,
boiler_light: Influx.FieldType.FLOAT,
boiler_oxygen: Influx.FieldType.FLOAT,
boiler_oxygenlow: Influx.FieldType.FLOAT,
boiler_oxygenmid: Influx.FieldType.FLOAT,
boiler_oxygenhigh: Influx.FieldType.FLOAT,
boiler_connectionindex: Influx.FieldType.FLOAT,
boiler_return: Influx.FieldType.FLOAT,
zone_1: Influx.FieldType.FLOAT,
zone_2: Influx.FieldType.FLOAT
},
tags: [
'host'
]
}
]
});

//заполняем data..

influx.writePoints([
{
measurement: 'heating',
fields: data
}
]).catch(err => {
console.error(`Error saving data to InfluxDB! ${err.stack}`)
});

Теперь что-бы проверить записались ли данные, можно использовать хронограф - скачиваемый UI-сервис который в более ранних версиях был встроен в influx в виде админ-панели.

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

Микросервисы 2

· 3 мин. чтения

В прошлой статье я начал изучать микросервисный подход, но до полноценного решения тогда не дошёл. Тогда я пробовал сам поднимать процессы на php, управлять ими, связывать между собой через очереди потому что монолит не мог справиться с асинхронными задачами. Но чего у меня реально нехватало, так это нормальной изоляции и деплоймента, межсервисного взаимодействия и стабильной работы.

Docker

Я сразу скажу что не знаю как настраивать production с докер-контейнерами, но сервисы в рабочем окружении поднимаются тривиально

Концепции

Images (образы) это изолированная среда, как правило с конкретным языком (node, php) или библиотекой. За это отвечает Dockerfile, который ссылается на существующий, скачиваемый образ на докер хабе.

Что-бы увидеть установленные образы, есть комманда  docker images . 

Контейнеры  это запущенные образы которые изолированы от host-машины. Обычно контейнер поднимается для какого-то сервиса, поэтому у него есть своё название, явно объявлен маппинг сетевых портов на host-машину и способ монтирования файловой системы -  за это отвечает docker-compose.yml

Что-бы увидеть установленные образы, есть комманда  docker images . 

Контейнеры  это запущенные образы которые изолированы от host-машины. Обычно контейнер поднимается для какого-то сервиса, поэтому у него есть своё название, явно объявлен маппинг сетевых портов на host-машину и способ монтирования файловой системы -  за это отвечает docker-compose.yml

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

Что-бы увидеть запущенные контейнеры, есть комманда  docker ps  (а лучше dry)

Что-бы запустить текущую папку есть  docker-compose down && docker-compose up  — это основная комманда которой вы будете пользоваться в разработке если будете делать изменения

Для автоматизации перезагрузки контейнера при изменении файлов на hostе, для node надо копать в сторону pm2-docker

Consul

Консул это сервер, который служит для связывания сервисов воедино (service discovery), поддержания их состояния здоровья (healthcheck) и для их конфигурации без перезапуска (key-value storage)

Каждый поднимающийся сервис сам должен зарегистрировать себя в консуле, указать на каком IP/порте он работает. Сам должен открыть HTTP endpoint который будет выдавать состояние здоровья

Сложности

При создании микросервисов самая сложная часть это граница и управление.  Сервис должен отвечать за конкретную бизнес-функцию и доменную область. Это вертикальный срез приложения.  Но при этом сервис должен выполнять весь стек работы — ui, backend, db storage, queue processor. Впихивать столько ответсвенности в один сервис технологически сложно, поэтому приходится дробить вертикальный стек ещё на несколько горизонтальных слоёв. Образно был article-manager - стал article-frontend, article-server, article-worker, над которыми нависают ещё всякие сервисы мониторинга. Это тяжело связывать вместе и деплоить разом.

Node

Поскольку нода висит постоянно в памяти, в отличие от php, который запускается при запросе, то возникает проблема перезагрузки кода при его изменении. Вот как выглядит Dockerfile с PM2:

FROM node:6-alpine
USER root
ENV PORT=3000
EXPOSE 3000
RUN npm install pm2 -g
WORKDIR /app
CMD ["pm2-docker", "start", "pm2.json", "--watch"]

Теперь в pm2.json описывается при каком случае надо перезапускать докер сервис

{
"apps": [
{
"name": "article-manager",
"script": "index.js",
"env": {
"PORT": 3000,
"CONSUL_IP": "192.168.10.10",
"CONSUL_PORT": 8500,
"SERVICE_NAME": "article-manager"
},
"watch": true,
"ignore_watch": [
"node_modules",
"npm-debug.log",
".idea",
".git",
"test/coverage"
],
"watch_options": {
"followSymlinks": false
}
}
]
}

Redis

Очень быстрая in-memory база для кеширования данных

version: '2'
services:
redis-master:
image: 'redis:2.8'
network_mode: bridge
volumes:
- ./data:/data
ports:
- 6379:6379
command: redis-server --appendonly yes

Alias

Для удобства, советую добавить в ваш ~/.bashrc или ~/.zshrc алиас на быстрое поднятие контейнера:

alias doc='docker-compose down && docker-compose up'

Создаём виртуальный образ ОС для среды разработки

· 2 мин. чтения

В качестве основы, будем использовать CentOS 6.5

  • Устанавливаем последние Virtualbox и Vagrant
  • Создаём новую виртуалку в virtualbox на основе федоры, поставив 2ГБ RAM и 20 GB под жёсткий диск
  • Скачиваем CentOS-6.5-x86_64-LiveCD.iso с http://mirror.nsc.liu.se/centos-store/6.5/isos/x86_64/. Он включает мгновенный бут (загрузчик), recovery и установку
  • Ставим root'у пароль vagrant (потом сменить можно)
  • Добавляем стандартного пользователя vagrant:vagrant

Обновляем систему:

#dont update unless you want CentOS 6.8 upgrade
#yum update -y

#install basic tools
yum install -y htop nano

#reboot for kernel updates to take place
shutdown -r now

Систему надо настроить для вагранта, если мы хотим её автоматически выкатывать под разные проекты..

Сначала - настроить пользователя

sudo visudo -f /etc/sudoers.d/vagrant
#adding to this file:
vagrant ALL(ALL) NOPASSWD:ALL

#setup vagrant public key access
mkdir -p /home/vagrant/.ssh
chmod 0700 /home/vagrant/.ssh
wget --no-check-certificate \
https://raw.github.com/mitchellh/vagrant/master/keys/vagrant.pub \
-O /home/vagrant/.ssh/authorized_keys
chmod 0600 /home/vagrant/.ssh/authorized_keys
chown -R vagrant /home/vagrant/.ssh

nano /etc/ssh/sshd_config
#uncomment AuthorizedKeysFile %h/.ssh/authorized_keys

#enable public key authentication
service sshd restart

#start ssh on reboot
chkconfig sshd on

visudo #comment out requiretty option

Теперь для вагранта нужны Guest Additions. Это iso файл который у virtual box'а уже есть. Надо смонтировать его на диск и запустить установку пакетов

yum install -y gcc build-essential perl kernel-devel

#mount VBoxGuestAdditions.iso on CD in Virtualbox and run these:
sudo mount /dev/cdrom /mnt
cd /mnt
sudo ./VBoxLinuxAdditions.run

echo export KERN_DIR=/usr/src/kernels/`uname -r`/ >> ~/.bashrc

Теперь, у вас вероятно стоит надстройка для безопасности - SELinux. Если вы будете разрабатывать локально, то лучше его отключить

setenforce Permissive
nano /etc/sysconfig/selinux
#set to:
#SELINUX=disabled

Теперь когда всё готово, выходим из виртуалки и упаковываем её для vagrant'а. Для этого надо зайти в папку где лежит виртуалка и запустить комманду которая сгенерит package.box файлик на который в дальнейшем будет ссылаться Vagrantfile в нашем проекте:

#package box and move it to desktop
vagrant package --base CentosEmpty && mv package.box ~/Desktop/

#clean vagrant cache
vagrant global-status --prune

#cd to your project that references new box from desktop
vagrant up

См также:

http://williamwalker.me/blog/creating-a-custom-vagrant-box.html
https://www.vagrantup.com/docs/boxes/base.html
http://aruizca.com/steps-to-create-a-vagrant-base-box-with-ubuntu-14-04-desktop-gui-and-virtualbox/
https://stefanwrobel.com/how-to-make-vagrant-performance-not-suck

http://unix.stackexchange.com/questions/18435/how-to-install-virtualbox-guest-additions-on-centos-via-command-line-only

Управление количеством backend-процессов в реактивных приложениях

· 1 мин. чтения

Одна из проблем с которой сталкивается backend разработчик разрабатывая приложение с отложенными вычислениями это управление параллельными процессами. Например, если вы делаете загрузку картинок которые необходимо в дальнейшем обработать (уменьшить, вырезать, передвинуть на другой сервер), то для масштабирования такого решения под N -> infinity пользователей, каждый этап имеет смысл делать в отдельном процессе, число которых тоже можно масштабировать вместе с ростом N. 

Я по-прежнему использую php демонов, которые рождаются кроном (если на nohup полагаться не хочется). Но поскольку мне хочется одновременно делать ресайз картинки в 5 разных размеров одновременно, я хочу управлять количеством instance'ов php процессов. Это решается простой bash-коммандой:

[ "$(ps -ef| grep -v grep | grep imageResizer|wc -l)" -lt 5 ] && php imageResizer.php > /var/logs/imageResizer.log

Заметьте что ключевая комманда -lt. Если коротко, то мы ищем процессы (ps) по их имени (grep), исключая самих себя и считаем количество найденных строк (wc), количество которых должно быть меньше (lt) количества заданных мною инстансов. Как вариант вместо -lt можно использовать -eq 0, если хочется только одного демона

Микросервисы

· 7 мин. чтения

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

Микросервисы — не панацея и тоже усложняют всё приложение в плане транзактивности, логов, и обработки ошибок, управления конфигураций, версионирования, деплоя, возникает дублирование bootstrap-кода из-за изоляции сервисов. Поэтому стоит осторожно подходить к тому, что вы хотите выделить в микросервис и что он даст по характеристикам приложения (performance, стабильность, масштабирование, разделение нагрузки). Как правило, веб-приложения создаются сначала монолитом, а потом разделяются на сервисы — так эффективней и проще эволюционировать.

Паттерны взаимодействия

Общение между сервисами можно делать по-разному:

  1. Синхронно — сервис вызывают явно как веб-сервис по HTTP/REST, делая работу синхронно по-старинке, подобно веб-серверу, тогда как клиент ждёт ответа (подобно ajax)

  2. Асинхронно — cервис вызывают явно по HTTP/REST, регистрируется задача и клиент тут же получает job ID. Клиент обязан сам проверить состояние задачи, периодически спрашивая у сервиса (polling, aws transcoder)

  3. Асинхронно — сервис как работник (демон) без публичного доступа. Слушает какой-то ресурс и генерирует новые вызовы. Это может быть

  • единая шина сообщений (RabbitMQ,Gearman0MQ, Kafka, AWS SQS, AWS Kinesis)>
  • хранилище с локами (файлы, память, Memcache, Redis)
  • другие ресурсы (процессы, сеть, железо)
  1. Асинхронно, не используя MQ, регистрирует слушателей в себе и сам знает к кому куда стучать

Главное что-бы подход был единый, с предсказуемым форматом данных (JSONprotobufthriftmessagepack).Для удобной конфигурации и дружбы между сервисами, надо ставить ConsulETCD или ZooKeeper — они позволят абстрагироваться от конкретных IP адресов и PID процессов.

Пример

Например пользователь приложения имеет возможность загружать файлы.Обработка файла очень тяжёлая по CPU, генерирует несколько результатов и занимает много времени из-за долгих внутренних сетевых запросов по загрузке готовых обработанных файлов в хранилище. Допустим эта синхронная операция upload+resize+store занимает 20 секунд в монолитном приложении. В микросервисном приложении, вы решаете создать сервис обработки картинки, а для очередей сначала создаёте табличку в БД. Отлично, это даёт возможность сервис положить на другую машину и не нагружать основной сайт. Теперь возникают практические вопросы — как это всё сделать? Практически, если раньше в монолите разные слои приложения проверяли всё за вас, то теперь чуть ли не все значимые классы надо будет выделять отдельные сервисы авторизацию, логирование, менеджер картинок, менеджер транзакций и сам ресайз картинок.

Веб-сервис на PHP

Если ваш сервис должен иметь публичный веб-интерфейс, проще всего запустить php в качестве сервера под конкретную папку и она будет по запросу дёргаться. Тут пригодятся микро-фреймворки типа SilexSlim или Lumen

#run me as:
#php -S 0.0.0.0:80 -t /var/www/silex

#install me with:
#composer require silex/silex:~1.3

$loader = require __DIR__.'/vendor/autoload.php';

$app = new Silex\Application();

#синхронная работа
$app->post('/resizeImage', function(Request $request) use($app) {
#загрузить imagemagick, сделать resize, сохранить

return "{success:true}";
});

#асинхронная работа
$app->post('/resizeImageAsync', function(Request $request) use($app) {
#зарегистрировать новую работу и передать её демону через RabbitMQ, см. ниже
#клиента оповестим автоматом через websocketы

return "{success:true}";
});

Демон-работник на PHP

Хотя php изначально разрабатывался как интерпретатор html файлов, современный php может работать в постоянном режиме как сервис если его запустить в CLI режиме. В реальной жизни, php демоны как правило нестабильны из-за утечек памяти и не так эффективны по скорости как сервисы на nodejs или go. Запустить демон можно из коммандной строки..

php -f /var/www/app/daemon/myImageProcessor.php

#let it live after terminal exit:
nohup php -f myImageProcessor.php &

С демонами есть несколько особенностей

  • Работая в бесконечном цикле надо чистить ресурсы (DB-соединения, file handles)
  • Надо мониторить рост своей памяти что-бы не умереть от fatal error
  • Надо поддерживать graceful stop при получении SIGTERM и прочих сигналов что-бы не повредить данные на случай если кто-то остановит демона из консоли или перезапустит сервис
  • Надо ловить все исключения, делать альтернативную функциональность бэкапов и откатов, формировать красивый log
  • Перезапуск сервера должен запускать демоны заново — через крон или через регистрацию демона в виде системного сервиса

У вас может быть несколько экземпляров (instance) одного и того же демона в виде разных процессов. Тогда очень важно не попасть в проблемы многопоточности — дедлоки транзакций, запись в один log файл параллельно и обработка одного и того же задания. Обо всём этом чуть позже..

А пока, вот простой пример с бесконечным циклом и псевдо-кодом:

//myImageProcessor.php
//set custom process name
cli_set_process_title('php - daemon - myImageProcessor.php');

//enable process interruption handling
declare(ticks = 1);
$interrupted = false;
function handleSignal(){
$interrupted = true;
}

pcntl_signal(SIGTERM, "handleSignal");
pcntl_signal(SIGHUP, "handleSignal");
pcntl_signal(SIGUSR1, "handleSignal");
pcntl_signal(SIGINT, "handleSignal");

while(true){
$db->connect();

$db->query("SELECT * FROM images WHERE processed = 0");

//do some work here with resize & upload

$db->disconnect();

//memory cleanup & check
gc_collect_cycles();
const MAX_MEMORY_EXTRA = 10 * 1024 * 1024;
if (convertToBytes(ini_get('memory_limit')) < (memory_get_usage(true) + MAX_MEMORY_EXTRA)) {
exit();
}

//graceful shutdown
if($interrupted){
exit();
}

//wait for some time to decrease load on DB above
sleep(10);
}

Управление процессами

Проверку на УЖЕ запущенного демона можно сделать как на уровне bash, так и на уровне самого php с использованием экзотических флагов в базе, флагов во временных файлах или же используя такой же доступ к shell. С такой проверкой, комманду можно смело класть в cron и демон будет запускаться автоматом (раз в минуту)

Bash

#check if daemon is not running yet
#add logs
[ "$(ps -ef| grep -v grep | grep myImageProcessor|wc -l)" -eq 0 ] && php -f myImageProcessor.php > /var/www/logs/myImageProcessor.log

#up to 5 instances!
[ "$(ps -ef| grep -v grep | grep imageResizer|wc -l)" -lt 5 ] && php myImageProcessor.php > /var/logs/myImageProcessor.log

Остановить демона можно так же из шелла..

sudo /usr/bin/kill -9 `ps aux | grep 'myImageProcessor' | grep -v grep | awk '{print $2}'`

PM2

Для более могучего nodejs, существует отличный process manager, который может управлять и php-демонами! С его помощью можно сразу видеть только процессы демонов. Кроме того:

  • перезапускать демона сразу после его падения
  • следить за изменениями файла что-бы перезапустить демон (watch)
  • следить за памятью и CPU и перезапускать при их превышениях

Менеджеры попроще — forever и guvnor

Очереди и RabbitMQ

Как только вы начали использовать демонов, вероятней всего вы станете использовать и паттерн Command, по которому микросервисы выполняют роль работников (workers). MQ-сервер выступает в роли брокера и шины, связывающей асинхронные процессы воедино. Он отвечает за надёжность передачи сообщения сервису и за то что несколько сервисов не получат одну и ту же комманду. Реализовывать это на стандартной БД было бы сложней

Для PHP-демонов, подключение rabbitMQ делается с помощью php-amqplib библиотеки и бесконечный цикл в этом случае начинает зависеть от неё..

$callback = function($msg){
//actual work here
$data = json_decode($msg->body, true);

//Ack message that everything is done
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
}


//technical details..
$connection = new AMQPStreamConnection('localhost', 5672, 'root', 'pass');
$channel = $connection->channel();

//new queue if it was missing
$channel->queue_declare($queue, false, true, false, false);

//one entry at a time
$channel->basic_qos(null, 1, null);
$channel->basic_consume($queue, $receiverName, false, false, false, false, $callback);

//almost infinite cycle
while (count($channel->callbacks)) {
$channel->wait(null);
}

$channel->close();
$connection->close();

Понятно что один сервис хорошо, но архитектура ценна именно тем что сервисы взаимодействуют между собой. Поэтому сообщения можно генерировать другому сервису. Например если обработалось изображение, значит надо оповестить пользователя в реальном времени и обновить UI.

$queue = 'datasync';
$message = ['imageID'=>34, 'status'=>'processed'];
$message = json_encode($message);

$connection = new AMQPStreamConnection($server, $port, $login, $pass);
$channel = $connection->channel();
$channel->queue_declare($queue, false, true, false, false);

$channel->basic_publish(new AMQPMessage($message, ['delivery_mode' => 2]), '', $routeKey);
$channel->close();
$connection->close();

Перспективы

Более серьёзные сервисы пишутся на nodejs, а лучше на Go.

Грубо говоря, если прогнать нагрузочный тест, то с PHP вы получите 300 req/s, тогда как с node 7000 req/s, а с go 9000 req/s.

С Go можно копать в сторону go-micro

См. также

Amazon S3 direct upload

· 1 мин. чтения

вторник, 1 марта 2016 г. в 11:07:40

Amazon S3 поддерживает прямой upload. Делается всё просто со сторонними библиотеками, особенно если у вас ангуляр

bower install evaporate angular-evaporate --save

Добавляете в настройках путь к серверной подписке, где с php очень просто делается подпись:

echo base64_encode(hash_hmac('sha1', $_GET['to_sign'], AWS_SECRET, true));

Добавляете CORS-файл на S3, вбиваете в нужные места ключи — вот у вас готов аплоадер, поддерживающий multipart, параллельную загрузку файлов, необходимую для файлов в 100MB и выше

Как параллельно запустить процессы в sh скрипте

· 2 мин. чтения

Порой надо написать какой-то установочный скрипт, который требует одновременный запуск нескольких задач, из которых некоторые достаточно долгие — сервер, билд проекта. Это не то же самое что последовательный запуск с «&&» или «;» разделителями. Shell-скрипты могут и это с помощью комманды trap.

Комманда работает на прерывании процесса (второй параметр). В данном случае это EXIT и TERM. Основная комманда - убиение запущенных этим терминалом в фоне процессов (jobs -p). Сами они перечисляются дальше с группировкой фигурными скобками.

Первый trap таким образом вызывает последующие вызовы. Заметьте что второй вызов с инициализацией базы mongo стоит с задержкой в 3 секунды, что-бы сервер успел подняться (который уже будет бежать бесконечно)

echo 'db.createCollection("myCollection");' > mongoInit.js

trap 'echo Starting MongoDB with preinstalled collections; kill $(jobs -p)' EXIT
{ trap '/usr/local/bin/mongod --dbpath myMongoDBStorage' TERM; sleep 5 & wait; } &
{ trap 'sleep 3 && mongo myProjectDB mongoInit.js' TERM; sleep 5 & wait; } &
sleep 1

 

На практике получается впрочем так, что комманды бегут бесконечно, т.е. mongod запускается, вы нажимаете Ctrl+C, а он всё ещё работает.. jobs ничего не показывает, но если возникает соединение - контекст возвращается. Приходится использовать killall mongod

Вдохновлено stackoverflow. См. теорию - Signals & Traps 

upd. как оказалось, более простой и эффективный способ - использовать одинарный &, который пушит комманды в фоновый режим и запускает дальше следующие

/usr/local/bin/mongod --dbpath myMongoDBStorage & sleep 3 && mongo myProjectDB mongoInit.js

 

В таком случае jobs показывает работающий mongod и вобщем то его тоже приходится убивать вручную.

Популярные 404 ошибки

· 1 мин. чтения

У моего веб-сервера постоянно спрашивают какую-то фигню. Поэтому я решил выписать какие файлы и запросы наиболее частые или могут быть необходимы для разных случаев

Атаки

Сайт можно опрашивать ради какой-то полезной информации, а можно сканировать. Наиболее популярные атаки это проверки на движки и попытки их взлома

  • Wordpress - wp-login.php
  • PHPmyadmin - phpmyadmin/scripts/setup.php

Иконки для браузера

Роботам

  • robots.txt  - запреты на индексацию для поисковиков

  • crossdomain.xml - запрашивается флешем/sliverlight при кроссдоменных запросах

  • sitemap.xml - для поисковиков

  • humans.txt - авторы сайта

Дебаг JS

  • *.map файлы для дебага минимизированных js-библиотек

Ретина - *@2x.png

Для экранов с высокой плотностью пикселей некоторые сайты добавляют дублирование картинок под ios. Обычно это решается @2x суффиксом и retina.js, либо с помощью css/background-image как то описано чуть ниже. Проблема в том что файлы указывать, а иногда они проскальзывают

.icon{
width: 32px;
height: 32px;
background-image: url(icon.png);
}

@media only screen and (-moz-min-device-pixel-ratio: 1.5),
only screen and (-o-min-device-pixel-ratio: 3/2),
only screen and (-webkit-min-device-pixel-ratio: 1.5),
only screen and (min-devicepixel-ratio: 1.5),
only screen and (min-resolution: 1.5dppx) {
.icon{
background-image: url(icon@2x.png);
background-size: 32px 32px;
}
}

Мониторинг 404 ошибок с google analytics

Дебаг и установка работающего Magento

· 1 мин. чтения

Небольшая заметка для себя о том как управиться с копированием и установкой на локалку существующего production-magento сайта.

  • Что-бы видеть ошибки
    cp /errors/local.xml.sample /errors/local.xml

  • Что-бы поменять настройки БД
    nano /app/etc/local.xml

  • Что-бы сменить пути идём в БД табличку core_config_data и находим по path LIKE '%base_url' ссылки. Меняем на новые.

  • Не помогло? Пути по прежнему кидают на старый сайт? Это скорей всего из-за того что Magento кеширует настройки в xml файлах. Если вы удалите кеш-файлы в var-папке..
    rm -rf var/*

    Этого может быть мало. В моём случае хардкорный дебаг Varien_Simplexml_Configкласса указал что файлы подтягиваются из временных папок.. в моём случае на маке в /var/tmp. Тоесть надо ещё
    rm -rf /var/tmp/magento/*

  • Ещё одна фишка - не ставьте localhost в качестве base_url

Особенности мобильной версии сайта

· 3 мин. чтения

Сделал себе мобильную версию блога, просто потому что надо — даже на андроиде с умным форматированием текста (который изобрела Опера) читать сайт не оптимизированный под мобильники неудобно. Для разработчиков это значит два варианта — либо сделать css файл который будет переделывать некоторые вещи и прятать ненужные блоки, либо же использовать отдельные шаблоны и в лучшем случае использовать параметр в том же контроллере что и основное приложение.

Устройство определяется через параметр user-agent в заголовке запроса - это можно использовать дальше в бизнес логике, например через php функцию. Знание устройства может влиять на то какую функциональность стоит подгружать, но это вовсе не обязательно потому что ..


Игра в прятки со стилями

Мобильная версия в лучшем случае заключается лишь в одном css-файле, таком же как и версия для печати где вначале прячутся ненужные блоки, реклама которая слишком мелкая что-бы её даже заметили, всякие виджеты "самого популярного", статистика... Код подгрузки стилей в шапке выглядит следовательно так..

<link rel="stylesheet" type="text/css" href="screen.css"media="screen" /> <link rel="stylesheet" type="text/css" href="print.css"media="print" /> <link rel="stylesheet" type="text/css" href="mobile.css"media="handheld" />

.. и просматриваются в Firefox благодаря Webdeveloper-плагину:

Однако если в печати элементы форм прячутся, то в мобильной версии практически всё, и кнопки в особенности увеличивается. В этот момент вы вероятно задумываетесь над порядком HTML схемы — можно ли просто так спрятать пару блоков и при этом ничего не сломать. В простом блоге как у меня в общем то да, потому что общая структура осталось той же - шапка с поиском, меню и поток статей, а вот в более сложных сайтах хочется какую-то табуляцию взамен меню.

Теперь что-бы браузер в смартфоне не начинал сразу показывать страницу с птичьего полёта вставим немножко кода

    <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />     <meta name="HandheldFriendly" content="True" />

Первая строчка говорит фактически что ширина такая как задана у устройства, а zoom-out делать нельзя. Впрочем Андроидный webkit ограничения проигнорировал и бегунки по прежнему видны. А всё из-за фиксированных в пикселях размерах. Злоо.. Переопределям все блоки в ширину 100%, а все размеры шрифтов в em единицах. Таблицы, картинки и флеш-объекты всё ещё лезут за экран? Overflow:hidden на них. На картинки можно даже повесить уменьшение браузером с соблюдением пропорций

-webkit-transform: scale(0.5);-moz-transform:scale(0.5);

Но чудится мне что более качественные решения требуют более сложных решений с ресайзом.. я пока что повесил 100% ширину. С объектами (youtube) полегче - тоже вешается 100% ширина и минимальная высота в 200px.

Всякие важные мелочи

Но самое главное естественно повесить самое нужное. В моём случае это личные контакты — телефон и email. Телефон можно сделать ссылкой которую поддерживают Android и iOS

<a href="tel:112">Телефон спасения</a>

В случае магазинов это наверняка будут времена работы и местоположение на карте (которые тоже нужно ссылками делать).

  • В iOS у полей ввода <input> на html5-сайтах надо указывать параметр type (email, url, tel) что-бы виртуальная клавиатура менялась на соответсвующий вид

  • Mobile Best Practices рекомендует короткие ссылки, как например поддомен "m" m.flickr.com и m.facebook.com

  • Единые стили есть разве что для iPhone в виде плагина jQTouch

Создание полноценной мобильной версии (а не просто css слоя) - сложная задача поскольку может ориентирована и на разные устройства (ipad, iphone, android, tv, старые мобильники) и соответсвенно может нуждаться в изменении кода (если скажем ajax не поддерживается). У Gmail к примеру 4 версии для мобильников.

См по теме

Infinite scolling

· 2 мин. чтения

Я вот уже больше месяца как делаю социальную сеть pling.ee, которая акцентируется на связи посредством мобильных телефонов (SMS/MMS) и позиционировании людей с их помощью. Достаточно перспективный проект (как твиттер на дрожжах) и популярный среди местной молодёжи тем что можно почти нахаляву общаться.

Но технически возникла небольшая получасовая техническая задачка с навигацией, и раз уж я давно не писал, то может вам тоже будет полезно. Дело в том что в поток сообщений показывается ajax-ом, подгружаясь по X-сообщений  за раз чистым html (для json просто пришлось бы больше писать). Задача - прятать кнопку "ещё" если сообщений больше нет. Очень просто, но как оказывается не всё так очевидно.

Вот возможные решения (от худшего к лучшему)

  1. При первой загрузке страницы делать второй запрос и узнавать какой ID у последнего сообщения и потом детектить его показ с помощью js. Проблема в том что как правило SQL для запроса и так сложный, а тут надо его продублировать с изменением сортировки. Уже пахнет говнокодом.

  2. Если число подгружаемых результатов меньше ожидаемых X элементов на странице то сразу прятать кнопку. Конечно с вероятностью 1/X она всё-таки будет показываться, зато

  3. Сделать что-бы нажатие кнопки сразу показывало спрятанные закешированные результаты (и если их нет - то не показывать кнопку) + делать ajax-запрос и результаты прятать (а если их нет то тоже прятать кнопку). Тут много игры с js и к тому же подгружаются лишние данные (не факт что пользователь всегда нажимает на продолжение)

  4. Использовать SQL_CALC_FOUND_ROWS что-бы расчитать число всех элементов и если их меньше чем offset + число на странице, то просто отметить последний элемент css-классом и через javascript проверить и спрятать кнопку если класс присутсвует

Цифровая подпись документов в Эстонии с помощью DigiDocService

· 7 мин. чтения

В Эстонии с 2000 года вступил в силу закон о цифровых подписях, которые стали юридически равноценны обычным рукописным. Вскоре была создана и техническая основа - компания SertifitseerimisKeskus (буквально - «центр сертификации») принадлежащая банкам и телекоммуникацонным операторам (а не государству, представляете себе!) и схема обмена данными по X.509 стандарту. Эта статья расчитана в большей мере на программистов.

Цифровая подпись?

Подпись как оказывается очень важна, а признаваемая государством - тем более. Снижаются затраты на распечатку и/или доставку счетов по оплате, договоров между работником и работодателем. Я уже не говорю про обычное подтверждение что документ прислан точно нужным человеком, а не хакером. Спасает положение то что у каждого гражданина Эстоини есть сертификат подписи, но его недостаточно. Проблема в том что одной подписи-закарючки в IT-мире недостаточно. Подпись в расширенном виде на самом деле включает в себя набор данные, в том числе не статичные.

  1. Стороны подписывающие документ

  2. Собственно документ или его отпечаток (говорящий о неизменном состоянии со времени подписания)

  3. Свидетели (нотариус) и роль сторон

  4. Время, место

Контейнер всей этой информации решили сделать на XML и назвать .ddoc расширением и связать с онлайн-сервисом создания и подтверждения подписей — Digidoc. За основу берутся основные свойства эстонской ID-карточки - авторизацияподпись и шифрование и в результате имеем:

  • цифровая подпись файлов (DigiDoc клиент, портал или третья сторона через DigiDocService)
  • шифрование и дешифрование файлов (DigiDoc клиент)
  • подтверждение действительности (digidoccheck)
  • подпись электронной почты
  • подпись или авторизация с помощью мобильного телефона (Mobiil-ID)

Контейнер со времени создания претерпел некоторые изменения, сейчас есть версия 1.3 основана на стандарте XAdES-X-L расширенных электронных подписей.

Процесс создания подписи с DigiDocService

Теперь собственно о главном что может понадобится на любом сайте. Допустим вы продаёте рога и копыта и хотите всё юридически правильно оформить. По-старинке это было бы типичный checkbox мол «согласен с условиями». Теперь же можно получить юридически действительную подпись клиента под любым договором, распиской купли-продажи или договора предоставления услуги.


Что-бы это у себя сделать Sertifitseerimiskeskus предоставляет услугу DigiDocService по SOAP, и для этого опубликованы следующие списки WSDL-методов: http://www.sk.ee/DigiDocService/DigiDocService_2_3.wsdl //почти live https://digidocservice.sk.ee/?wsdl //live - работает с CURL только вместе с Juur-SK.crt https://www.openxades.org:8443/?wsdl //test
В обмене данными участвуют следующие стороны

  • Клиент с нормальной ид-карточкой и софтом

  • Наше серверное приложение

  • Digidoc-узел (см. wsdl выше)

  • OCSP сервер, публикующий устаревшие или отозванные сертификаты id-карт

Если опустить очевидное, то процесс в общем выглядит так

  1. Создание сессии между приложением и digidoc (StartSession) с передачей инфы о контейнере (который может включать несколько подписываемых файлов) — либо целиком файлы, либо их SHA1-хэш. Запоминаем вернувшися SessionCode у себя

  2. Можно запросить с помощью GetSignatureModules сразу готовый html (со всякими апплетами, activex компонентами..) для того что-бы получить сертификаты клиента

  3. Клиент авторизуется передавая данные серверу, который вызывает PrepareSignature (signCertHex, signCertId), получает обратно бинарный хэш контейнера документов SignedInfoDigest который клиент должен подписать

  4. Клиент подписывает SignedInfoDigest введя PIN2 — генерируется подпись signValueHex и передаётся в FinalizeSignature(). На этом моменте digidoc проверяет действительность сертификата пользователя у OSCP

  5. В успешном случае можно уже скачать .ddoc файл. Если оригинальные файлы не отсылались, то их в base64-форме внедряют в вернувшийся xml. Сессия закрывается CloseSession.

Авторизацию и подпись можно поставить и с помощью мобильного телефона, где добавляются ещё и сторона оператора (MSSP), но я этот случай здесь не рассматриваю.

О качестве ddservice

Поставляемый php-пакет в качестве примера полон багов и говнокода — написанный под php4 надо постараться что-бы прикрутить его к PEAR под php5, надо увеличить таймаут curl во всех запросах с 4 секунд на что-либо существенное.

У меня в процессе вылетали ошибкииспользовал test-среду вместо live 200: Failed to get signature confirmation/notary // используйте live среду вместо test Validation constraint violation: data type mismatch xsd:string in element 'Sesscode' //приведите сессию в int-тип

Чтение сертификата с ID-карты

Возвращаемся к двум основным пунктам подписи. До вызова PrepareSignature метода, на странице есть компонент (Applet, ActiveX либо зависимый от браузера plugin) который считывает два параметра

  • signCertHex — сертификат подписи переведённый из цифрового DER формата в HEX

  • signCertId — идентификатор приватного ключа

В качестве компонента DigiDocService возвращает ActiveX для IE (EIDCard.cab файлик) и аплет для остальных ( SignApplet_sig.jar,iaikPkcs11Wrapper_sig.jar ). ActiveX в плане интеграции довольно прост - можно javascriptом вызвать оба метода, следовательно прикрутить их к любому дизайну, другое дело что у меня этот компонент не работает (уж и драйверы переустанавливал - не помогло). А вот аплет мало того что подгружает яву, так и в дизайн не вписывается со своими кнопками.

Firefox - EstEID XPCOM v0.4

В идеале каждый браузер мог бы завести свой plugin который бы поддерживал обработку созданного в RIA

<object type="application/x-esteid" />

Но глядя как это тянется c 2006 года в Opera, очевидно что это будет долго. Впрочем Firefox уже умеет использовать onepin-opensc-pkcs11.dll как с помощью SK'шного хака, так и с помощью плагина EstEID XPCOM v0.4 написанного в RIA и Smartlink. В результате имеем красивую картинку в Firefox и нет нужды читать страшные мануалы по копированию dll-файлов и ошибок «Teie arvutisse on vaja installeerida PKCS#11 ohjurprogramm

Этот компонент успешно может яваскриптом выкачивать данные об id-карте к примеру— document.getElementById('esteid').signCert.cert. Но этот сертификат подписи для DigiDoc надо первести в шестнадцатиричный формат из PEM. Примерно так:
        $sTempKey=str_replace(array(         "-----BEGIN CERTIFICATE-----",         "-----END CERTIFICATE-----",         "\n"         ),'',$sTempKey);         $sTempKey=base64_decode($sTempKey);         $sTempKey=bin2hex($sTempKey);
EstID-плагин теперь переехал и выпускается как open-source

Internet explorer - EIDCard / dsiglite2 + idutil

EIDCard.cab компонент судя по существующему коду прост - создаёшь объект, ссылаешься на этот файл, потом через vbscript спрашиваешь сертификат или запрашиваешь подпись digest'а - всё как в Firefox. Что-бы он заработал нормально надо соблюдать следующие заповеди

  1. Выключить автоматическое удаление сертификатов в ID-card tool если вы под Windows Vista с правами администратора
  2. Держать объект EIDCard за пределами формы, иначе он не будет доступен вовсе
  3. При создании подписи VBscript по умолчанию пытается записать результат в поле signValueHex, вот только видимо кто-то использует такую же переменную в компоненте из-за чего подпись не передаётся серверу.

dsiglite2.cab это альтернатива для создания подписи, правда его найти удалось только у swedbank'а на странице и он слишком низкоуровневый, пришлось отказаться. Третий компонент очень полезен - idutil.cab понимает события вытаскивания карточки, хоть и с использованием JScript'а (в итоге получается каша из трёх ECMAscript диалектов)

<!--Использование idutil для чтения личных данных в IE--> <div id="mTag"></div> <OBJECT  ID="myCard" CLASSID="CLSID:7F9F89F2-F12B-4B25-9C69-7358F38B898B" CodeBase="idutil.cab"></OBJECT> <SCRIPT LANGUAGE="JScript" FOR="myCard" EVENT="CardInserted() ">     var mTag = document.getElementById("mTag");     mTag.innerHTML = "Reading data...";     timerId = window.setInterval("reading()",500); </SCRIPT> <SCRIPT LANGUAGE="JScript" FOR="myCard" EVENT="CardRemoved() ">     var mTag = document.getElementById("mTag");     window.clearInterval(timerId);     mTag.innerHTML = "Card removed"; </SCRIPT> <SCRIPT LANGUAGE="JScript"> var timerId; function reading(){     var mTag = document.getElementById("mTag");     window.clearInterval(timerId);     mTag.innerHTML = "";     try {         myCard.ReadCard();         mTag.innerHTML='Hello,'+myCard.familyName+' '+myCard.personalCode;         }     catch(e) {         mTag.innerHTML="Reading failed";         }     } </SCRIPT>

Если вы «переписываете» ddservice, то имеет смысл сохранить корень (wsdl класс) и переписать обёртку. Обратите внимание что после чтения сертификата и PREPARE, нельзя перенаправлять на другую страницу - надо делать POST на ту же самую страницу, иначе аплет будет ругаться. Вторая проблема - правильно заменять маркеры вида 1 на реальные данные. Ну и третья проблема - расставить пути к jar-файлам если у вас используется ЧПУ, иначе плагины будут искаться в локальной несуществующей папке.

Validity confirmation

Как я выше писал - существует digidoccheck с помощью которого можно проверить подпись. Листочек этот юридической силы не имеет в качестве распечатки цифровой подписи, но иметь на всякий случай не помешает. Разберёмся же какие тут данные есть и откуда они приходят

  • Данные о файле и пользователе - приходят в ответе FinalizeSignature. Там очень просто - ходи по массиву и выдёргивай что тебе надо.

  • Серийный номер сертификата - десятичная версия  находится в ['SignedDocInfo']->SignatureInfo->Signer->Certificate->IssuerSerial и легко переводится в hex

  • Сертифицирующая сторона (собственно SK) в явном виде отсутсвует, надо выдирать из ['SignedDocInfo']->SignatureInfo->Confirmation->ResponderCertificate->Issuer

  • Хеш публичного ключа сертифицирующей стороны. На данный момент это ESTEID-SK 2007 и он есть на сайте, но правильней конечно запрашивать его динамически через WSDL метод GetSignersCertificate(). В php к результату надо приделать "-----BEGIN CERTIFICATE-----" и концовку (что-бы получить PEM формат) и взять от него openssl_x509_parse(). Внутри и будет находится заветная 4806DEBE ... Вариант захардкодить и обновлять каждые X лет я не рассматриваю

  • Хеш OCSP-сертификата о действительности карточки (HASH VALUE OF VALIDITY CONFIRMATION (OCSP RESPONSE)) берётся из WSDL метода getNotary() из которого OcspData попадает в sha1(base64_decode(...))

Ваш покорный слуга на время написания статьи нашёл мега-баг в том листочке что генерирует Digidoc Client - там OCSP responce hash всегда одинаковый. Кроме того я бы добавил хеши самих документов для печати, иначе слишком много зависимости от памяти SK. Ну и в подарок - метод hex2bin

        function hex2bin($data) {             $len = strlen($data);             for($i=0;$i<$len;$i+=2) {             $newdata .= pack("C",hexdec(substr($data,$i,2)));             }             return $newdata;         }

Основные отличия HTML5

· 3 мин. чтения

Недавно вышел HTML5, и хотя я надеялся что де-факто стандартом станет XHTML2, видимо этому не судьба. Следование веб-стандартам впринципе дело самодисциплины — кто-то пишет как умеет, а для кого-то это средство подчеркнуть свой проффесионализм.

Тэги

Основным нововведением стало внесение давно ожидаемых новых элементов, благодаря которым содержание выглядит чуть более семантичным, хотя только разве что в блогах. Хитрые дизайнерские сайты с плавающими блоками видимо лучше делать на более общих div'ах

Структурные тэгиТэги содержания
- header — понятное дело, шапка с логотипом, логином, навигацией..- nav — навигация.. как меню, так и «хлебные крошки»- footer — подвал с копирайтами, контактами- article — очевидно влияние блогосферы и rss- section — подраздел документа- aside — боковая панель (видимо с коментариями, самыми популярными статьями, тэгами и тп)- video и audio — как альтернатива flash-плеерам. Врядли заменит флеш из-за проблем с кодеками и отсутствии поддержки уже созданных .flv видео. Мало поддерживается пока браузерами.- progress — полоса завершённости процесса. Полезно при заполнении форм- time — полезно для указания точного времени элемента- details — просто дополнительная информация- datalist — нечто типа автозаполнения, но с прописанными вариантами


Формы

Что особенно радует что работа форм улучшена новыми типами input-элементов.

  • date, datetime, time, month, week
  • email, url
  • range, number
  • color

Их давно пора было вводить в стандарт, потому что число компонентов на js+dom которые делают эту работу растут постоянно (календарики для выбора даты, бегунки), более того - пишутся даже специальные надстройки типа jquery ui. Кроме новых типов, введены интересные параметры, в частности параметр multiple для массовой загрузки файлов. И опять прийдётся распрощаться с uploadify и swfupload. Впрочем не обязательно.

Перетащи в браузер

С давних пор я не понимал почему нельзя было сделать удобное перетаскивание чего-то с рабочего стола внутрь браузера. Точней перетащить то можно, но браузер как правило не поймёт что это за файлы или текст вы в него суёте. HTML5 предполагает распознавание таких случаев с помощью javascript drop-событий, при которых к содержанию получается доступ с помощью e.dataTransfer.getData() метода. Опять же - досвидания java аплеты и google gears.

Данные на стороне клиента

Ещё одной отличительной особенностью стала возможность кэшировать чистые данные на стороне клиента без необходимости постоянно пересылать их с каждым обновлением страницы (т.е. явно не конкурент json-объектам), а объём достигает 5-10 мб (уже не конкурент cookies).

Web storage очень полезная особенно для мобильных устройств. Без HTML5 на это способна лишь Google gears.

А сейчас как быть?

HTML5 всё ещё в режиме черновика, многие браузерные движки лишь пробуют внедрять новые возможности в бета-версиях (особенно заметен Presto). Надо дважды подумать перед тем как перепрыгивать со скачущей лошади на ещё теоретический мотоцикл, особенно если вас заботит совместимость с IE. Для него даже надо прописывать существование тэгов

document.createElement('header'); document.createElement('footer'); document.createElement('section'); document.createElement('aside'); document.createElement('nav'); document.createElement('article');

См. также

Пишем Facebook приложение

· 5 мин. чтения

Facebook - популярная социальная сеть где можно написать своё приложение. Не люблю толочь воду в ступе, поэтому сразу к делу. Встраивать можно двумя направлениями: внешнее приложение в Facebook или Facebook-данные во внешнее приложение (aka Facebook Connect). Тут я буду говорить о первом, что в принципе более трудоёмко и интересно. Как правило смысл facebook-приложение несёт две функциональности - взаимодействие с друзьями и информативное интегрирование в профиль пользователя.

Основы

Встраивать приложение можно в следующие места..

  • Canvas - собственно страница с приложением. Доступна по ссылке http://apps.facebook.com/НАЗВАНИЕ_ПРОГРАММЫ/
  • Profile box - маленький бокс внутри самого профиля пользователя
  • Profile tab - новый таб в профиле
  • Boxes tab - небольшой блок в табе boxes
  • News feed - доступ к потоку обновлений
  • Requests box - интерактивные сообщения другим пользователям

Интеграция производится смешанными возможностями..

  • REST API (http://api.new.facebook.com/restserver.php) который даёт "тяжёлый" доступ для backend-а с возможностями загрузки фото, видео, получении списков друзей, событий, комментариев и тп.
  • FQL - способ запрашивать данные по REST не просто через параметры метода, а уже через SQL-подобный синтаксис
  • FBML - урезанный HTML + свои тэги которые Facebook интерпретирует в окне в своём стиле и дизайне и кэширует при инлайновом показе. Куча заморочек с встроенным валидатором тэгов
  • xFBML - FBML-тэги используемые в своём приложении
  • FBJS - урезанный JS

Два пути

Теперь когда основные термины понятны перейдём к самому приложению которое размещается в Canvas. После создания нового приложения через developer app, скачивания REST-библиотеки для php, выкладывании приложения на свой сайт и установки в настройках URL для Canvas становится видно что доступно два способа запуска - через iframe (+XFBML) либо чистый FBML который будет храниться на facebook. Понятное дело первый вариант самый простой. После создания программы и добавления/подтверждения в своём профиле, показ Canvas'а будет сопровождаться обычным iframe + GET-параметрами с префиксом fb_sig_, из которых самый важный это fb_sig_canvas_user. Второй вариант более муторный, но более тесно связан с FB.

Права

Теперь надо подумать о том что приложение делает в принципе. В моём случае это quiz-тест - пользователь отвечает на вопросы и в конце статус ставится на стенку в профиль (profile wall).

Первым делом оказывается что очень полезно иметь подтверждение пользователя для получения данных (Authorization) которое вызывается методом Facebook::require_login и для пользователя выглядит просто как окошко передачи прав. Покопавшись в документации для публикации данных в Wall (News feed), есть метод Feed.publishTemplatizedAction, но оказалось что он устаревший (deprecated). Альтернатива - Stream.publish, и теперь переходим ко второй важной теме - расширенные права (Extended permissions).

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

$facebook->api_client->stream_publish("My new status");
Uncaught exception 'FacebookRestClientException' with message 'The user hasn't authorized the application to perform this action' i

Теперь немного тонкостей - документацию в wiki и на форумах надо читать с большим подозрением, потому что часто встречается устаревший код (к примеру названия привилегий/методов stream_publish вместо publish_stream). Методы на проверку привилегий выдают либо фатальную ошибку либо невразумительную отписку на параметры, в том числе и тестовая API console

if($facebook->api_client->users_hasAppPermission("publish_stream")){
//обновить статус тут
}

FBML-соблазн

К этому времени становятся понятными плюсы FBML-режима (принуждение к похожему интерфейсу и поддерживаемые FBML-тэги). У меня таки сработали

  • FBML mode + onclick + тэг<fb:prompt-permission perms="stream_publish">Heelp</fb:prompt-permission>
  • FBML mode +
<form promptpermission="status_update"> + onclick

Iframe + редирект на http://www.facebook.com/authorize.php?api_key=МОЙ_API_KEY&v=1.0&ext_perm=publish_stream&next=http://google.com

Хотя и очень глючно выглядит:

  • FBML mode + redirect выдали "Error while loading page" сообщение

xFBML

Понятно что права получать через iframe таким образом глючно, хочется одновременно и вместе и порознь с фейсбуком жить. Для этого есть xFBML-тэги которые интерпретируются фейсбуковским яваскриптом. Итого надо в своём приложении надо:

  1. Подключить яваскрипт http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php
  2. Сделать xd_receiver.htm
  3. Инициализировать своим апи-ключём
  4. Указать путь к корню в настройках Connect-приложения в фейсбуке

Теперь уже права можно спрашивать

FB.Connect.showPermissionDialog('publish_stream');

Затерянное печенько ослика

Нельзя обойти не упомянув об IE 6/7 и тут. Дело в том что по умолчанию iframe в этих браузерах теряет переменные сессии. Проще говоря - печеньки (cookies) не доходят до сервера, потому что iframe считается "неблагонадёжным" содержанием и он даже показывает глазик в своём статус-баре. Если подробно в этом разбираться то для этого есть обоснование в виде W3C platform for privacy preferences. Для этого проще добавить заголовок

header('P3P: CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"');  //for IE6/7

В конце концов приложение успешно обновило профиль (куда удалось впихнуть и картинку через аттачмент)

Редирект хаоса

Любой тестер свихнулся бы увидя странный мандельбаг с отсутсвием прав на постинг в wall. Вторая меньшая проблема проявлялась в хаотичном выпрыгивании приложения из iframe в большое окно. Как оказалось, фейсбук в разных браузерах странным образом интерпретирует переход по ссылкам и внутренним редиректам. Для этого специально нашёлся метод facebook->require_frame до логина, который привязал в обязательном порядке страницу с фреймом фейсбука. Впрочем внутри между переходами страниц всё-равно засел login.php

По теме:

Из Regio.ee в Google maps

· 2 мин. чтения

Regio.ee - ведущая компания картографических и геодезических работ в Эстонии. У них очень неплохой сервис карт типа google maps отличающийся большей точностью. Хоть и работает на flash. Для моего проекта terrideal.com возникла задача переноса координат которые используются в regio на координаты google maps.

Как оказалось Regio использует коническую проекцию Lambert-EST введённую в советское время для хорошей точности на местности. Google же использует мировую геодезическую систему WGS84. Существует конвертатор на самом сайте regio, но понятное дело хотелось собственный вариант. Сначала я методом тыка пришёл к приблизительной формуле:

wgs_x= 57.0014 + (est_x-6317802.5) / 111368; wgs_y= 24.0314 + (est_y-500000) / 59097.825;

Но точность оставала желать лучшего. И вот наткнулся на идеальный вариант написанный Саней Смирновым из Regio. Пришлось порыться в коде и найти нужную функцию с переносом в php. Там же можно найти и обратное направление и конвертирование в систему Меркатора.

/**
* Converts Estonian L-EST coordinates to World Geodetic System
*
* @param float $X
* @param float $Y
* @return array
*/
function Est2Wgs($X, $Y) {

$_PI= 3.14159265358979323846;
$_n = 0.85417585805;
$_x0= 6375000;
$_y0= 500000;
$_a = 6378137;
$_F = 1.7988478514;
$_e = 0.0818191910428158;
$_p0= 4020205.479;

function sqr($a) {
return $a*$a;
}

//$theta, $p, $t, $u;
$Lo = 24 * pi() / 180;

$ux = $X - $_y0;
$uy = $Y - $_x0;

$sx = $ux;
$ux = $uy;
$uy = $sx;

$theta = atan($uy / ($_p0 - $ux));
$tmpL = $theta / $_n + $Lo;

$p = sqr($_p0 - $ux);
$p += sqr($uy);
$p = sqrt($p);

$t = pow(($p / ($_a * $_F)), 1 / $_n);
$u = pi() / 2 - 2 * atan($t);

$tmpB = $u +
(sqr($_e)/2 + 5 * sqr($_e) * sqr($_e)/24 + pow($_e,6)/12 + 13 * pow($_e, 8)/360) * sin(2 * $u) +
(7 * pow($_e,4) /48 + 29 * pow($_e, 6)/240 + 811 * pow($_e, 8)/11520) * sin(4 * $u) +
(7 * pow($_e, 6)/120 + 81 * pow($_e, 8)/1120) * sin( 6 * $u);

$X=rad2deg($tmpL);
$Y=rad2deg($tmpB);

return array($X, $Y);
}

Экспорт данных используя Create View и Aggregate-функции

· 2 мин. чтения

Встала задача экспортирования данных из таблицы Firma в качестве строки. Если бы дело было в одноразовом копировании то проблемы особой и не-было бы - всегда есть клиентские программы, которые экспортируют хоть в Excel, хоть в текстовый файл какие угодно колонки. Но мне необходимо было это сделать при помощи SQL, потому что прямого доступа к live-базе данных у меня нет, а размер таблицы мне неизвестен.

Вход - таблица Firma

IDname
1Google
2Microsoft
Выход - строка
1=>Google;2=>Microsoft

Агрегирующие функции занимаются именно такой задачей - на входе у них массив данных, а на выходе - одно значение. Типичным примером являются функции COUNT, MAX, SUM. Postgre пошла по пути универсальности и позволяет пользователю самому создавать такие функции. Создаём CONCAT функцию:

CREATE AGGREGATE concat (  
BASETYPE = text,
SFUNC = textcat,
STYPE = text,
INITCOND = ''
);

Теперь можно сразу получить строку с данными, но мне нужна сортировка по названию фирмы, но никакие вложенные запросы и ORDER BY этому не помогают:

SELECT CONCAT(name||';') FROM Firma;

Поэтому приходится изучить ещё одну новую для себя функциональность и создать Представление (View). View это виртуальная таблица, данные которой формируются на лету. Она может использоваться и в качестве ограничения прав на изменение родительской таблицы. Представления можно кэшировать - тн. материализация. Но сейчас оно поможет реализовать обычную сортировку:

CREATE VIEW FirmaSorted AS SELECT * FROM Firma ORDER BY name;

Остаётся сделать выборку, в которую я добавлю ещё одно поле, уберу последний символ-разделитель и сделаю UPDATE полученного значения в другую таблицу:

UPDATE tblLists SET strValue=(  
SELECT TRIM(';' FROM (SELECT CONCAT(id||'=>'||name||';') FROM FirmaSorted))
) WHERE strName='companies';

Введение в Postgre

· 2 мин. чтения

PostgreSQL - крутая СУБД, потому что..

  1. Она не принадлежит какой-либо компании, как например Oracle или mysql и распространяется по BSD-лицензии

  2. Имеет поддержку продвинутых фишек - PL/pgSQL, триггеров. Теоретически это очень полезно при создании очень масштабной системы.

  3. Транзакции по принципам ACID придают скорости - несколько запросов в одной транзакции выполняются как одна атомарная операция. А мультиверсионность помогает избежать блокировки таблиц.

  4. Объектность. Таблица выступает фактически в виде класса, а ряды - в виде объектов, так же как классы могут наследоваться, так и таблицы могут наследоваться.

  5. Система привилегий операций с таблицами, схожа с системой по принципу с Oracle.

  6. Репликации, т.е. использование нескольких серверов даёт масштабируемость.

Переход на PostgreSQL как правило связан с обработкой больших объемов данных - когда число рядов переваливает миллион, то mysql начинает испытывать трудности, а Natural Join нескольких таких таблиц может привести вообще к падению сервера. Да и тесты на параллельность поддерживают Postgre. Энтузиастам может быть интересна же не столь масштабируемость, сколько объектность, позволяющая большую свободу, или если хотите - сложность, по сравнению с mysql.

При переходе из mysql, следует отметить различия, которые естественно следует предусмотреть:

  • Различные типы данных. Нет datetime, зато есть возможность создать тучу своих типов.
  • Отсутсвие autoincrement. Такая функция заменена наличием sequence, и в упрощённом виде может использоваться так:
    CREATE TABLE mynewtable ( id SERIAL }
  • pgAdmin III очень удобная консольная программа, а в качестве замены phpMyAdmin есть phpPgAdmin
  • Аналогом DATE_FORMAT является to_char
  • Заглавные буквы в названиях таблиц и полей по умолчанию переводятся в нижний регистр, это можно обойти используя двойные кавычки
  • Форматом для LIMIT конструкции стал также поддерживаемый с mysql 4LIMIT # OFFSET #
  • Просмотр запущенных процессов реализуется аналогом
    --show processlisе``SELECT * from pg_stat_activity ;

Читайте также: