Базы данных: введение, часть четвертая

Илья Тетерин
2012-10-15

(use arrow keys or PgUp/PgDown to move slides)

Oct 10, 2011 - Сбой в работе mail.ru

ИТАР ТАСС: В работе российского почтового интернет-сервиса "Мейл.ру" произошел сбой

МОСКВА, 10 октября. /ИТАР-ТАСС/. Пользователи крупнейшего российского почтового сервиса "Мейл.ру" ... испытывают трудности с доступом к своим почтовым ящикам. При попытке проверить почту или открыть письмо на экране появляются сообщения то об ошибке сервера, то о перегрузке базы данных.

Проблемы с доступом к почтовому сервису начались примерно в полдень, при этом остальные сервисы портала "Мейл.ру" работают нормально.

roem.ru 11.10.2011: В результате внедрения новой более оптимальной системы хранения данных произошел программно-аппаратный сбой. Наши специалисты устранили проблему в течении 2-х часов. Сбой состоял в недоступности почты для примерно 8-10% пользователей. Никаких потерь или утечей данных не было.

wikipedia:mail.ru ... почта (22,7 млн человек ежемесячно) ...

pulser: 22.7м в месяц ... 756k в день ... 30 000 человек в час ... 10% - это 3 000 человек в течение часа

это не много, не мало - это проблемы availability в кластере ...

... но они заметны на уровне ИТАР-ТАСС ...

Хочу свой маленький сервис - курс валют.

Маленький сервис.

Что-то типа: get USD 15/10/2015

Дабы можно было в моих интернет магазинах пересчитывать цены в рубли, доллары и евро.


Яндекс: Динамика курса USD ЦБ РФ, руб.

Историческая справка: РБК (rbc.ru) начинался с рассылки факсом курсов валют.

А где брать информацию о курсах?

Наверное надо "парсить" сайт ЦБ РФ...

Ой, а у них есть технические ресурсы: http://www.cbr.ru/scripts/root.asp.

И там есть Получение данных, использую XML

$ curl http://www.cbr.ru/scripts/XML_dynamic.asp?\
date_req1=12/10/2012\&date_req2=15/10/2012\&VAL_NM_RQ=R01235

<?xml version="1.0" encoding="windows-1251" ?>
<ValCurs ID="R01235" 
  DateRange1="12/10/2012" DateRange2="15/10/2012"
  name="Foreign Currency Market Dynamic">

<Record Date="12.10.2012" Id="R01235">
  <Nominal>1</Nominal><Value>31,1667</Value></Record>

<Record Date="13.10.2012" Id="R01235">
  <Nominal>1</Nominal><Value>30,9738</Value></Record>

</ValCurs>

Проверим это из Java

public static void main(final String[] args) throws Exception {
    // http://www.cbr.ru
    // /scripts/XML_dynamic.asp?date_req1=12/10/2012
    // &date_req2=15/10/2012&VAL_NM_RQ=R01235
    final URL url = new URL("http", "www.cbr.ru", 80, "" +
            "/scripts/XML_dynamic.asp" +
            "?date_req1=12/10/2012" +
            "&date_req2=15/10/2012" +
            "&VAL_NM_RQ=R01235");

    final URLConnection uc = url.openConnection();
    uc.connect();
    final BufferedReader ir = new BufferedReader(
        new InputStreamReader(uc.getInputStream()));
    String line;
    while ((line = ir.readLine()) != null) {
        System.out.println(line);
    }
    ir.close();
}

Ура! Всё работает!

Можно делать сервис!

Превратим в программу ... запрос

    "/scripts/XML_dynamic.asp?" +
    "date_req1=12/10/2012" +
    "&date_req2=15/10/2012" +
    "&VAL_NM_RQ=R01235");

Хотим на произвольную дату, а тут строки надо вводить.

Плюс торги только в рабочий день, а что на выходных?

final String date1 = getDateAsParameter(new Date(
    date.getTime() - 7 * 3600 * 24 * 1000));
final String date2 = getDateAsParameter(date);
final URL url = new URL("http", "www.cbr.ru", 80, "" +
    "/scripts/XML_dynamic.asp?" +
    "date_req1=" + date1 +
    "&date_req2=" + date2 +
    "&VAL_NM_RQ=R01235");

private static String getDateAsParameter(final Date dt) {
    return new SimpleDateFormat("dd/MM/yyyy").format(dt); }

public static void testDate() {
    System.out.println("mark = " + getDateAsParameter(new Date())); }

Превратим в программу ... распарсим ответ

public static Pair<Date, Double> parseLine(final String line) throws Exception {
    final Pattern pattern = Pattern.compile("" +
            "<Record Date=\"(.+?)\" Id=\"R01235\"><Nominal>1</Nominal>" + 
            "<Value>(.+?)</Value></Record>");
    final Matcher m = pattern.matcher(line);
    if (!m.matches()) {
        return Pair.of(new Date(0), 0d);
    }
    return Pair.of(
            new SimpleDateFormat("dd.MM.yyyy").parse(m.group(1)), 
            Double.parseDouble(m.group(2).replaceAll(",", ".")));
}

public static void testPattern() {
    final Pattern pattern = Pattern.compile("" +
            "<Record Date=\"(.+?)\" Id=\"R01235\"><Nominal>1</Nominal>" + 
            "<Value>(.+?)</Value></Record>");
    final String sample =
            "<Record Date=\"13.10.2012\" Id=\"R01235\"><Nominal>1</Nominal>" + 
            "<Value>30,9738</Value></Record>";
    final Matcher m = pattern.matcher(sample);
    if (!m.matches()) return;
    System.out.println(m.groupCount());
    System.out.println("m1 = " + m.group(1));
    System.out.println("m2 = " + m.group(2));
}

Не хватает в Java? Добавь ...

В Python есть понятие Tuple (x,y) ... в Java нет ... добавим :)

public static class Pair {
    public final K first;
    public final V second;
    private Pair(final K k, final V v) {
        this.first = k;
        this.second = v;
    }
    public static  Pair of(final K k, final V v) {
        return new Pair(k, v);
    }
    public String toString() {
        return "[" + first + "," + second + "]";
    }
}
public static void testPair() { // [7,gugu]
    System.out.println(Pair.of(7, "gugu"));
}

Q: What makes Java so manly?

A: It forces every programmer to grow a Pair.

http://james-iry.blogspot.com/2010/05/anatomy-of-annoyance.html

Основной метод

public static Double getCurrency(final Date date) throws Exception {
    final String date1 = getDateAsParameter(new Date(
            date.getTime() - 7 * 3600 * 24 * 1000));
    final String date2 = getDateAsParameter(date);
    final URL url = new URL("http", "www.cbr.ru", 80, "" +
            "/scripts/XML_dynamic.asp?" +
            "date_req1=" + date1 +
            "&date_req2=" + date2 +
            "&VAL_NM_RQ=R01235");

    final URLConnection uc = url.openConnection();
    uc.connect();
    final BufferedReader ir = new BufferedReader(
        new InputStreamReader(uc.getInputStream()));
    Pair max = Pair.of(new Date(0), 0d);
    String line;
    while ((line = ir.readLine()) != null) {
        final Pair parsed = parseLine(line);
        if (parsed.first.after(max.first)) {
            max = parsed;
        }
    }
    ir.close();
    return max.second;
}

Маленькие кусочки ...

1. Делаем внутренний интерфейс:

/**
 * Сервис получения курса валюты
 */
interface MyCurrency {
    /**
     * Возвращает курс доллара на заданную дату.
     * Данные выдаются по самой последней дате торгов.
     * @param date Date на которую нужна информация
     * @return значение курса на дату или 0 в случае ошибки 
     */
    Double getCurrency(Date date) throws Exception;
}

2. Заворачиваем в http обертку, через которую наши клиенты получают информацию.

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class HttpServerEx implements HttpHandler {

Всё получилось ...

Тормозит, бывает ... и не только на нашей стороне

Бывает банят - отказывают в обслуживании, ибо слишком много запросов и слишком часто посылаем.

Ответ: обернем getCurrency() в мемкеш:

// psevdo code !!!
public Double getCurrency(Date date) throws Exception {
   Double out = memcached.get(asKey(date));
   if ( out == null ) {
      out = main.getCurrency(date);
      memcached.put(asKey(date),out);
   }
   return out;
}

Почему сервис "тормозит"?

Наш "сервис" - это клиент к удаленной базе. Почему он "тормозит"?

final String date1 = getDateAsParameter(new Date(
        date.getTime() - 7 * 3600 * 24 * 1000));
final String date2 = getDateAsParameter(date);
final URL url = new URL("http", "www.cbr.ru", 80, "" +
        "/scripts/XML_dynamic.asp?" +
        "date_req1=" + date1 +
        "&date_req2=" + date2 +
        "&VAL_NM_RQ=R01235");

final URLConnection uc = url.openConnection();
uc.connect();
final BufferedReader ir = new BufferedReader(new InputStreamReader(uc.getInputStream()));
Pair max = Pair.of(new Date(0), 0d);
String line;
while ((line = ir.readLine()) != null) {
    final Pair parsed = parseLine(line);
    if (parsed.first.after(max.first)) {
        max = parsed;
    }}
ir.close();
return max.second;

Составные части одного запроса:

1. Собрать исходный запрос

- в нашем случае это две даты

2. Установить соединение по сети

- DNS resolve, буфера всякие, ожидания и таймауты

3. Дождаться обработки на удаленной стороне

- кто его знает, что ЦБ внутри делает для ответа

4. Выкачать к себе результаты

- сеть, несколько пакетов TCP etc

5. Распарсить результат из строк (сериализованная форма)

- в нашем случае это дата и дробное число

6. Закрыть сетевое соединение

- может включать сообщение дальней стороне "вешай трубку"

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

private static String getDateAsParameter(final Date dt) {
    return new SimpleDateFormat("dd/MM/yyyy").format(dt); }

Ой, внутри же большой жирный регекс матчинг из-за "гибкого" формата dd/MM/yyyy - вещь универсальная и не быстрая.

Парсинг результата

public static Pair<Date, Double> parseLine(final String line) throws Exception {
    final Pattern pattern = Pattern.compile("" +
            "<Record Date=\"(.+?)\" Id=\"R01235\"><Nominal>1</Nominal>" + 
            "<Value>(.+?)</Value></Record>");
    final Matcher m = pattern.matcher(line);
    if (!m.matches()) {
        return Pair.of(new Date(0), 0d);
    }
    return Pair.of(
            new SimpleDateFormat("dd.MM.yyyy").parse(m.group(1)),
            Double.parseDouble(m.group(2).replaceAll(",", ".")));
}

Здесь и capturing регекс и формат и парсинг числа ...

Почему долго работает мой запрос?

На примере: дайте седьмую запись

Мы точно знаем что мы хотим получить и хотим получить одну единственную запись.

Запрос

SQL Select:

-- ключевое слово - команда
select 
-- какие поля ( * )
  fio, phone 
-- ключевое слово - разделитель
from 
-- имя коллекции
  phones 
-- ключевое слово - ограничения
where 
-- ограничения через AND / OR 
  id = 7

Сценарий исполнения запроса

формирует запрос

соединяется с базой

Class.forName("com.mysql.jdbc.Driver");
String myDatabaseURL = "jdbc:mysql://mydomain.com/database?user=" 
    + myUsername + "&password=" + myPassword;
java.sql.Connection con = DriverManager.getConnection(myDatabaseURL);

передаем запрос

сервер парсит запрос

Пример кода обработки команд

public interface Command {
  void process(Processor p, String cmd) throws IOException;
  boolean isApplicable(String cmd); }

public abstract class AbstractDump implements Command {
  public boolean isApplicable(String cmd) {
    return "/dump".equals(cmd) || "Dump".equals(cmd); }}

public class MasterDump extends AbstractDump {
  public void process(Processor p, String cmd) throws IOException {
    boolean result = PhoneBook.getPhoneBook().dump();
    p.writeResponse(result ? "OK" : "ERROR"); }}

COMMANDS = new ArrayList();
COMMANDS.add(new MasterCommit());
COMMANDS.add(new MasterDump());

for (MasterCommand cmd : COMMANDS) {
  if (cmd.isApplicable(string)) {
    cmd.process(this, string);
    break; }} 
by Ксения Мамич
Generated in IntelliJ IDEA by yFiles

находит хранилище / коллекцию

получает данные из коллекции

преобразует в данные для ответа

отправляет по сети

восстановление на клиенте

закрытие соединения с базой

Оптимизации

Что можно сделать, дабы было быстрее

Клиент и соединения

Используем "горячий" connection - всегда держим соединение, пока жив наш клиент.

Connection pool (Apache DBCP) - держим N соединений и отдаем их round-robin клиенту.

Чем плохо?

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

Пример?

Сервер - Оракл, у нас кластер на 32 клиента, в каждом по 16 коннектов в пуле = 2 Mb * 2^5 * 2^4 = 2^10Mb памяти = 1Gb памяти сервера всегда занято.

Prepared Statement

PreparedStatement ps = null;
try {
    ps = connection.prepareStatement("select name from user where user_id = ?");
    ps.setLong(1, 928303);
    final ResultSet rs = ps.executeQuery();
    return rs.next() ? rs.getString(1) : "UNKNOWN";
} finally {
    if (ps != null) {
        ps.close();
    }
}

Отдельно идет запрос, отдельно параметры.

Запрос "обезличен" и может кешироваться как клиентской библиотекой, так и сервером.

Экономия на парсинге и на проверке валидности запроса.

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

Сервер и соединения

Словарь базы

Коллекция, содержащая описание коллекций базы - таблиц, полей, индексов, констрейнов етс.

Подвержено фрагментированию и "торможению".

Важно при парсинге запросов.

Получение данных

Получение данных - индексы

Преобразование формата data file - response

Форматы сериализации

JVM-serializers - бенчмарки

Protocol Buffers, Etch, Hadoop and Thrift Comparison

wiki: IDL (interface definition language)

Protocol buffers

http://code.google.com/apis/protocolbuffers/docs/overview.html

Why not just use XML?

Protocol buffers:

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
} 

Thrift sample

Зародился в Facebook для внутреннего RPC.

С какого-то момента стал Apache Thrift - http://thrift.apache.org/ .

namespace java pulser 
struct UserProfile {
    1: i32 uid,
    2: string name,
    3: string phone
}
service UserStorage {
    void store(1: UserProfile user),
    UserProfile retrieve(1: i32 uid)
}

thrift --gen java sample.thrift

pulser$ wc -l gen-java/pulser/*.java
     581 gen-java/pulser/UserProfile.java
    1552 gen-java/pulser/UserStorage.java

/**
 * Autogenerated by Thrift Compiler (0.8.0)
 *
 * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
 *  @generated
 */

Thrift server

package pulser;

import org.apache.thrift.TException;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TServer.Args;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;

public class MyThriftServer {
    public static void startServer(final UserStorage.Processor processor) 
           throws Exception {
        final TServerTransport serverTransport = new TServerSocket(9090);
        final TServer server = new TSimpleServer(
                new Args(serverTransport).processor(processor));

        // Use this for a multithreaded server
        // TServer server = new TThreadPoolServer(new
        // TThreadPoolServer.Args(serverTransport).processor(processor));

        System.out.println("Starting the simple server...");
        server.serve();
    }

    public static void main(final String[] args) throws Exception {
        startServer(new UserStorage.Processor(new MyHandler()));
    }
}

Thrift client

public class MyThriftClient {
    public static void main(final String[] args) {
        try {
            final TTransport transport = new TSocket("localhost", 9090);
            transport.open();
            final UserStorage.Client client = new UserStorage.Client(
                    new TBinaryProtocol(transport));
            System.out.println(client.retrieve(7));
            transport.close();
        } catch (TTransportException e) {
            e.printStackTrace();
        } catch (TException x) {
            x.printStackTrace();
        }
    } 
}
class MyHandler implements UserStorage.Iface {
    public void store(final UserProfile user) throws TException {
        System.out.println("user = " + user);
    }

    public UserProfile retrieve(final int uid) throws TException {
        return new UserProfile(7, "Ivanov", "8-800-MEGA-623");
    } 
}

Итого

Вопросы?