Pull to refresh

Опыт внедрения Shiny в качестве корпоративной отчетности

System Analysis and DesignR

Всем привет! Меня зовут Сергей, я аналитик в ГК «Везёт». Исторически так сложилось, что в нашей компании было множество систем отчетности: от платных в виде Looker и Qlick – до самописных веб-сервисов. Однажды решив, что так дальше жить нельзя, мы стали выбирать единую систему, на которой будет все, и в итоге остановились на Shiny. В этой статье я расскажу про наш опыт внедрения Shiny в качестве корпоративного BI. Эта статья будет полезна всем, кто только выбирает инструмент для корпоративной отчетности.


Содержание



Причины выбора Shiny


Краткий рассказ о том, что такое Shiny

Shiny – это пакет для R, который позволяет создавать интерактивные веб-приложения и отчеты.
Официальный сайт проекта.
Здесь можно посмотреть примеры веб-приложений и дашбордов.


  • Динамические UI – в ряде отчетов, в зависимости от условий выбора нужно было показывать дополнительные фильтры, селекторы.
  • Гибкость настройки отчетов – в разных BI использовался разный функционал работы с отчетами, конечный бизнес-пользователь хотел сохранить функционал изначального отчета.
  • Контроль изменений в отчетах с помощью гита – это для удобства, нужно было понимать кто, когда и что менял в отчете.
  • Быстрое прототипирование сложных отчетов – иногда конечный бизнес-пользователь может не до конца понимать, какой результат он хочет в итоге, поэтому желательно быстрее пройти процесс согласования конечного функционала отчета.
  • Малое потребление серверных ресурсов.
  • Удобная отладка отчетов – т.к. периодически пользователи запрашивают сложные отчеты, нужна его удобная отладка, чтобы можно было понять, в каком месте отчет не работает.
  • Бесплатность – как дополнительный плюс

Выбираем способ развертывания приложений


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


Shiny Server (Бесплатный)


Запуск всех отчетов на одном процессе R


Плюсы:


  • Неограниченный хостинг приложений.
  • Простота в настройке.

Минусы:


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


Источник картинки


Shiny Server Pro или RStudio Connect (Платный)


Запуск разных отчетов в разных процессах R.


Плюсы:


  • Неограниченный хостинг приложений.
  • Неограниченное количество одновременных пользователей.
  • Множество способов аутентификации пользователя.
  • Можно установить пароль на отдельное приложение.
  • Работа приложений в нескольких процессах.
  • Простота в настройке.

Минусы:


  • Не нашел.


Источник картинки


ShinyProxy (Бесплатный)


В данном продукте используется концепция запуска каждого приложения пользователя в отдельном докере.


Плюсы:


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

Минусы:


  • Так как для каждого пользователя ShinyProxy инициализирует контейнер, то он может потреблять много оперативной памяти сервера.
  • Пользователь должен дождаться инициализации контейнера.


Источник картинки


Load Balancing (Платный)


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


Плюсы:


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

Минусы:


  • Load Balancing инициализирует контейнер, поэтому он может потреблять много оперативной памяти сервера.


Источник картинки


После рассмотрения различных вариантов решили остановиться на ShinyProxy.


Настройка ShinyProxy


В целом настройка по инструкции прошла стандартно, были настроены два сервера и HAProxy в качестве балансировщика между ними.
Для аутентификации был выбран протокол LDAP через корпоративный Active Directory.


Итоговые настройки ShinyProxy на двух серверах выглядят так.


proxy:
  title: ShinyProxy
  bind-address:
    - 0.0.0.0
  heartbeat-timeout: 60000
  container-wait-time: 60000
  authentication: ldap
  admin-groups: Domain Users
# LDAP configuration
  ldap:
    url: ldap://host:port/dc=ad,dc=corp
    user-search-base:
    user-search-filter: (sAMAccountName={0})
    group-search-base: OU=Groups
    group-search-filter: (member={0})
    manager-dn: CN=shinyproxy,CN=Users,DC=ad,DC=corp
    manager-password: password
# Docker configuration
  docker:
    cert-path: /home/none
    url: http://localhost:2375
    port-range-start: 20010
    port-range-max: 20900

В процессе эксплуатации выяснилось следующее:


  • ShinyProxy теряет некоторые контейнеры докера и не может их найти для повторного подключения, если прошло больше получаса. Решено было повесить на крон килл контейнеров, которые работают больше 45 мин.
    */5 * * * * docker ps --format='{{.Names}}' | grep -v cadvisor | xargs -n 1 -r docker inspect -f '{{.ID}} {{.State.Running}} {{.State.StartedAt}}' | awk '$2 == "true" && $3 <= "'$(date -d '45 minutes ago' -Ins --utc | sed 's/+0000/Z/')'" { print $1 }' | xargs -r docker kill
  • Блокировщик рекламы запрещал доступ на отчеты, у которых был префикс webstat. Поэтому мы попросили всех пользователей отключить блокировщики рекламы на сайте с отчетностью.

Деплой отчетов


Исходники всех отчетов хранятся в GitLab. Его и решили использовать в качестве инструмента загрузки отчетов на сервер с помощью пайплайнов. Для этого было сделано следующее:


  • Был создан "главный" докер, в котором установлен ShinyServer, и основной набор пакетов для R. Таким образом мы уменьшим суммарный объем занимаемого дискового пространства и ускорим сборку отчетов.

Исходник главного докера
FROM rocker/r-ver:latest

RUN apt-get update -y && \
  apt-get install --no-install-recommends -y sudo gdebi-core pandoc pandoc-citeproc libcurl4-gnutls-dev libcairo2-dev libxt-dev wget libpq-dev \
    libjpeg-dev libssl-dev libprotobuf-dev libjq-dev protobuf-compiler libudunits2-dev gdal-bin proj-bin libgdal-dev libproj-dev #gnupg dirmngr 

RUN apt-get update && \
  apt-get install --no-install-recommends -y java-common

RUN echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
RUN TEMP_DEB="$(mktemp)" && \
  wget -O "$TEMP_DEB" 'https://launchpad.net/~webupd8team/+archive/ubuntu/java/+build/12469417/+files/oracle-java8-installer_8u131-1~webupd8~2_all.deb' && \
  dpkg -i "$TEMP_DEB" && \
  rm -f "$TEMP_DEB"

RUN wget --no-verbose https://download3.rstudio.org/ubuntu-14.04/x86_64/VERSION  -O "version.txt" && \
    VERSION=$(cat version.txt)  && \
    wget --no-verbose "https://download3.rstudio.org/ubuntu-14.04/x86_64/shiny-server-$VERSION-amd64.deb" -O ss-latest.deb && \
    gdebi -n ss-latest.deb && \
    rm -f version.txt ss-latest.deb && \
    . /etc/environment && \
    rm -rf /var/lib/apt/lists/*

RUN R -e "install.packages(c('highcharter','clickhouse','data.table','DataCombine','DBI','devtools','dplyr','dqshiny','DT','esquisse','forcats','ggplot2','ggthemes','gridExtra','hexbin','htmlwidgets','lattice','lazyeval','leaflet','leaflet.extras','lubridate','openxlsx','pivottabler','plotly','raster','RClickhouse','reactable','readxl','reshape','reshape2','rgdal','rhandsontable','RPostgres','RPostgreSQL','RSQLite','scales','sf','shiny','shinycssloaders','shinydashboard','shinyjqui','shinyjs','shinyMatrix','shinyTime','shinyWidgets','sjmisc','sp','sqldf','stringr','tibble','tidyr','writexl','yaml','geosphere'), repos='https://cran.rstudio.com/')"

RUN apt-get update && \
  apt-get install -y r-cran-rjava

RUN R CMD javareconf

RUN R -e "install.packages(c('RJDBC'), repos='https://cran.rstudio.com/')"

RUN java -version

  • "Главный" докер импортируется в каждом отчете

Пример докер файла отчета
FROM host:port/db/for_shiny_reports/shiny_docker:latest # Главный докер
EXPOSE 3838
COPY app /srv/shiny-server/app
RUN mkdir -p /var/lib/shiny-server/bookmarks/shiny 
CMD ["R", "-e", "shiny::runApp('/srv/shiny-server/app', port = 3838, host = '0.0.0.0')"]

  • В каждом проекте прописан пайпалйн сборки, таким образом каждый раз, когда мы оставляем комит в отчете, то начинается пересборка отчета.

Пример пайплайна сборки отчета(.gitlab-ci.yml)
stages:
- build

variables:
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
  DOCKER_DRIVER: overlay2

build-master:
  image: docker:18.09.7
  services:
  - name: docker:18.09.7-dind
  stage: build
  before_script:
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
  script:
    - apk add --update curl
    - docker pull $CI_REGISTRY_IMAGE || true
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest --pull -t $CI_REGISTRY_IMAGE .
    - docker run -d -e SHINYPROXY_USERNAME=$SHINYPROXY_USERNAME1 -e SHINYPROXY_USERGROUPS=$SHINYPROXY_USERGROUPS1  -p 0.0.0.0:3838:3838/tcp $CI_REGISTRY_IMAGE R -e "shiny::runApp('/srv/shiny-server/app', port = 3838, host = '0.0.0.0')"
    - sleep 30 && curl -Is http://docker:3838/ | grep "200 OK"
    - docker push $CI_REGISTRY_IMAGE
  only:
    - master

  • Т.к. за день может быть несколько правок по различным отчетам и добавление какого-нибудь нового отчета, то заливку докер-образов на сервера было решено вынести в отдельный проект с отдельным CI/CD и использованием плейбуков Ansible. При добавлении нового отчета будет заново пересоздаваться конфиг для ShinyProxy. Ниже шаблон генерации конфига для Ansible

Шаблон application.yml
proxy:
  title: ShinyProxy
  bind-address:
    - 0.0.0.0
  heartbeat-timeout: 60000
  container-wait-time: {{ shinyproxy_container_wait_time }}
  authentication: {{ shinyproxy_authentication }}
  admin-groups: {{ shinyproxy_admin_group }}
  usage-stats-url: {{shinyproxy_usage_stats_url}}
  usage-stats-username: {{shinyproxy_usage_stats_username}}
  usage-stats-password: {{shinyproxy_usage_stats_password}}
# LDAP configuration
  ldap:
    url: {{ shinyproxy_ldap_server }}
    user-search-base: {{ shinyproxy_ldap_user_search_base }}
    user-search-filter: {{ shinyproxy_ldap_user_search_filter }}
    group-search-base: {{ shinyproxy_ldap_group_search_base }}
    group-search-filter: {{ shinyproxy_ldap_group_search_filter }}
    manager-dn: {{ shinyproxy_ldap_admin }}
    manager-password: {{ shinyproxy_ldap_admin_pwd }}
# Docker configuration
  docker:
    cert-path: /home/none
    url: {{ shinyproxy_docker_url }}
    port-range-start: {{ shinyproxy_docker_port_range_start }}
    port-range-max: {{ shinyproxy_docker_port_range_max }}
  specs:
{% if shinyproxy_apps is defined %}
{% for app in shinyproxy_apps %}
  - id: {{ app.id }}
    display-name: {{ app.display_name }}
{% if app.description is defined %}
    description: {{ app.description }}
{% endif %}
    container-cmd: ["R", "-e", "shiny::runApp('/srv/shiny-server/app', port = 3838, host = '0.0.0.0')"]
    container-image: {{ CI_REGISTRY }}/db/shinyproxy/{{ app.container_image }}
#    docker-memory: {{ app.docker_memory | default('2g') }}
{% if app.access_groups is defined %}
    access-groups: [{{ app.access_groups }}]
{% endif %}
    groups: [{{ app.groups }}]
{% if app.container_volumes is defined %}
    container-volumes: ["{{ app.container_volumes }}"]
{% endif %}

{% endfor %}
{% endif %}

server:
  servlet.session.timeout: 900

logging:
  file:
    shinyproxy.log

Этот шаблон подается в задачник ansible


Задачи по заливке отчетов и сборке конфига
---
- name: Log into registry and force re-authorization
  docker_login:
    registry: "{{ CI_REGISTRY }}"
    username: gitlab-ci-token
    password: "{{ CI_JOB_TOKEN }}"
    reauthorize: true

- name: Pull the docker images
  command: docker pull host:port/db/shinyproxy/{{ item.container_image }}:latest
  with_items: "{{ shinyproxy_apps }}"

- name: Install the shinyproxy configuration file
  template: src=shinyproxy-conf.yml.j2 dest=/etc/shinyproxy/application.yml
  become: true
  when: not ansible_check_mode
  notify: Restart shinyproxy

- name: Ensure that the shinyproxy service is enabled and running
  service: name=shinyproxy state=started enabled=yes
  become: true

При этом информация о приложениях (shinyproxy_apps) хранится в json, в таком виде.


Шаблон application.yml
{
  "shinyproxy_apps": [
      {
      "id": "report_one",
      "display_name": "Отчет 1",
      "container_image": "report_one",
      "access_groups": "Группа доступа 1, Группа доступа 2",
      "groups": "Domain Users"
    },
    {
      "id": "report_two",
      "display_name": "Отчет 2",
      "container_image": "report_two",
      "access_groups": "Группа доступа 1, Группа доступа 2",
      "groups": "Domain Users"
    }
  ]
}

Все это безобразие запускается таким CI


.gitlab-ci.yml
stages:
- deploy
- test

variables:
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG

deploy:
  stage: deploy
  image: $CI_REGISTRY_IMAGE
  script:
    - ansible-playbook --check playbook.yml -i hosts.inil --extra-vars=@apps.json -e CI_PROJECT_NAME=$CI_PROJECT_NAME -e CI_REGISTRY_IMAGE=$IMAGE_TAG -e CI_PROJECT_NAMESPACE=$CI_PROJECT_NAMESPACE -e CI_REGISTRY=$CI_REGISTRY -eCI_JOB_TOKEN=$CI_JOB_TOKEN -vv
    - ansible-playbook playbook.yml -i hosts.ini --extra-vars=@apps.json -e CI_PROJECT_NAME=$CI_PROJECT_NAME -e CI_REGISTRY_IMAGE=$IMAGE_TAG -e CI_PROJECT_NAMESPACE=$CI_PROJECT_NAMESPACE -e CI_REGISTRY=$CI_REGISTRY -e CI_JOB_TOKEN=$CI_JOB_TOKEN -vv
  except:
    - triggers

Сбор статистики


В ShinyProxy есть средство сбора статистики, на данный момент оно поддерживает только 2 СУБД; InfluxDB и MonetDB. Собирается статистика следующего вида: время события; пользователь; тип события (логин/открытие/закрытие отчета); название отчета.


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


Пример сбора статистики
con_stat <- dbConnect(RPostgres::Postgres(),host = 'host', port = 5432, dbname = "dbname", user = "user", password = "password")
vizit_df <- data.frame(user = Sys.getenv("SHINYPROXY_USERNAME"), groups = Sys.getenv("SHINYPROXY_USERGROUPS"), app = 'app_name', timie_visit = Sys.time())
dbWriteTable(con_stat, "visit_apps", vizit_df, append=TRUE)
dbDisconnect(con_stat)
rm(vizit_df)

Общая схема всей системы отчетности выглядит как-то так



Общая структура отчетов и контроль доступов


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


В самом ShinyProxy есть возможность показывать разным группам пользователей разный набор отчетов, он осуществляется с помощью указания групп Active Directory в конфиге отчета, в свойстве access-groups. Для этих целей были заведены отдельные группы LDAP для разных профессий, вида: Маркетинг, Аналитика, Топ-менеджмент и пр.


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


  1. У всех региональных менеджеров в группах Active Directory прописан город, а ShinyProxy при авторизации через LDAP возвращает строку с описанными группами в переменной среды SHINYPROXY_USERGROUPS. Прочитав переменную, мы сможем извлечь список доступных городов для пользователя.
  2. Также все группы пользователей (Маркетинг, Аналитика, Топ-менеджмент и пр.) были разделены на тех, кому по умолчанию можно видеть все города, и тех, кто может видеть только города привязанные в LDAP. Так, при открытии отчета, мы сможем понять, нужно ли отображать пользователю один город или все (например, если это аналитик или топ-менеджер).
  3. Ещё была добавлена таблица «Исключений». У тех же маркетологов или менеджеров города есть руководители, эти люди должны видеть все города при том же самом наборе отчетов. Так нам не нужно будет для подобных пользователей прописывать 100+ городов в Active Directory. Соответственно, в эту таблицу заносятся логины руководителей отделов, и при открытии отчета происходит проверка того, что пользователь может видеть все города.

Общая структура отчета Shiny


username <- Sys.getenv("SHINYPROXY_USERNAME") # Получаем имя пользователя
user_groups <- Sys.getenv("SHINYPROXY_USERGROUPS") # Получаем список групп пользователя в LDAP. Так мы узнаем, можно ли смотреть пользователю все города или только один и какой именно

# блок сбора статистики, пишем кто открыл отчет с какими правами
con_stat <- dbConnect(RPostgres::Postgres(),host = 'host', port = 5432, dbname = "dbname", user = "user", password = "password")
vizit_df <- data.frame(user = username, groups = user_groups, app = 'report_name', timie_visit = Sys.time())
dbWriteTable(con_stat, "visit_apps", vizit_df, append=TRUE)
dbDisconnect(con_stat)
rm(vizit_df)

# Определяем, можно ли пользователю смотреть все города
# Возвращаем список городов

# Определяем, можно ли пользователю видеть только один город
#   Проверяем, не является ли пользователь руководителем. Если руководитель, то показываем все города
# Возвращаем список городов

ui <- dashboardPage(
      # Описание UI
)

server <- function(input, output) {
  # Обработка событий и генератор SQL запросов 
}

shinyApp(ui, server)

Процесс внедрения


Процесс перехода в данном случае можно разбить на 4 этапа:


  1. По началу стояла задача закрыть операционные потребности региональных директоров и менеджеров, чтобы они могли получить расширенные сведения и статистику по водителям, пассажирам (обезличенные данные) и поездкам. Нужно было реализовать раздельный доступ к городам-платформам, чтобы каждый региональщик видел только свой город. И стояла задача перенести отчетность Looker'а.
  2. Затем мы стали закрывать информационные потребности маркетологов. Так же на данном этапе был осуществлен перенос отчетов из Qlick Sense.
  3. Следующим шагом мы стали переносить отчеты из самописных веб-сервисов, там уже преимущественно была задача закрыть потребности финансистов.
  4. Создание общей отчетности в рамках всей группы компаний. Здесь уже стояла задача унификации показателей между различными бэкофисами, с целью оперативного понимания о том, что происходит по всей группе компаний. Создание максимально универсального отчета чем-то похожего на сводную таблицу со множеством срезов и показателей. На данном этапе заказчиками отчетности являлись продакт-менеджеры и топ-менеджмент.

Недостатки и неудобства


Теперь стоит поговорить о недостатках данной системы корпоративной отчётности:


  • Спустя время слои от докеров могут занять все дисковое пространство, даже если использовать один главный докер. Поэтому приходится постоянно удалять лишние слои, которые по какой-либо причине больше не используются.
  • При открытии тяжелых отчетов система может долго думать (секунд 5-15) пока запускается докер и выполняются запросы к БД. В этом плане хотелось бы большей бесшовности, но это, видимо, сугубо мои хотелки, т.к. конечных пользователей данный факт не смущает.
  • У некоторых пользователей при первом открытии отчета, иногда по неведомой причине, система может выдавать ошибку о том, что адрес не отвечает, т.к. докер контейнер не успел загрузиться, что конечного пользователя вводит в заблуждение. При повторном открытии отчета данная проблема пропадает.
  • Создание отчётов не получиться доверить "абы кому" т.к. нужны знания R. И если придет новый аналитик, то он должен быть либо изначально со знаниями R, либо должен будет потратить время на его изучение уже на рабочем месте.
  • После внесения правок в отчет или добавление нового отчета приходится перезагружать службу ShinyProxy на сервере.

Вывод


В целом данная система отчетности показала себя хорошо. За год ее использования не было замечено каких-то критически важных нерешаемых проблем. Конечного бизнес-пользователя все устраивает, т.к. можно реализовать любой функционал отчета. В большинстве случаев можно оптимизировать отчет и запросы в нем так, чтобы он быстро выполнялся. Кроме того, мы сэкономили средства за счет отказа от Looker'a, Qlick’a и серверов, т.к перевели разную отчётность на одну систему.


На данный момент отчетностью пользуются 30 человек ежедневно. В среднем один пользователь открывает 3 отчета. Общее количество пользователей – 200 человек, общее количество отчетов – 83 штуки.


Большинство отчетов было создано 1-3 аналитиками в течение полугода. На данный момент отчетность поддерживается преимущественно одним аналитиком.

Tags:shinyshinyproxyRdockergitansibleкорпоративная отчетность
Hubs: System Analysis and Design R
Total votes 10: ↑10 and ↓0 +10
Views1.7K

Comments 4

Only those users with full accounts are able to leave comments. Log in, please.

Popular right now