Bruce Lindsay disse uma vez"Há três coisas importantes no mundo dos bancos de dados: Desempenho, desempenho e desempenho". A maioria dos arquitetos corporativos sabe que, à medida que avançamos nos recursos e nas arquiteturas de banco de dados, é importante medir o desempenho de forma aberta para que eles possam comparar o custo total de propriedade de forma confiável.
YCSB fez um excelente trabalho de benchmarking de armazenamentos de dados que atendem aos aplicativos "Cloud OLTP". Esses armazenamentos de dados eram simples, com operações simples de obter, colocar e excluir. A versão original Referência YCSB consiste em operações simples de inserção, atualização, exclusão e varredura em um documento simples de 10 valores-chave; as cargas de trabalho são definidas com uma combinação dessas operações em várias porcentagens.
JSON bancos de dados como Couchbase e MongoDB têm um modelo de dados mais avançado com escalares, objetos aninhados, matrizes, matrizes de objetos, matrizes e matrizes de objetos. Os bancos de dados JSON também têm consulta linguagem, índices e recursos. Além das operações de CRUD, os aplicativos usam rotineiramente as linguagens de consulta declarativas nesses bancos de dados para pesquisar, paginar e executar relatórios. Portanto, para ajudar os arquitetos a avaliar as plataformas de forma eficaz, precisamos de um benchmark adicional para medir esses recursos, além das operações básicas de CRUD. Este tutorial do YCSB explica seus recursos para preencher essa lacuna.
Papel YCSB estados: Também esperamos promover o desenvolvimento de outros conjuntos de benchmark de nuvem que representem outras classes de aplicativos, disponibilizando nossa ferramenta de benchmark por meio de código aberto. Nesse sentido, um dos principais recursos da estrutura/ferramenta YCSB é o fato de ser extensível - ela suporta a definição fácil de novas cargas de trabalho, além de facilitar a avaliação comparativa de novos sistemas.
Esse benchmark estende o YCSB para bancos de dados JSON, ampliando as operações existentes para JSON e, em seguida, definindo novas operações e novas cargas de trabalho.
Aqui está o esboço.
- Introdução
- Modelo de dados
- Operações de referência
- Cargas de trabalho de referência
- Implementação do YCSB-JSON
- Como executar o YCSB-JSON?
- Referências
1. Introdução
O YCSB foi desenvolvido para medir o desempenho de armazenamentos de dados de valor-chave NoSQL dimensionáveis. A infraestrutura do YCSB faz bem esse trabalho. O YCSB usa uma chave-valor simples e plana. O Couchbase usa um modelo JSON, que os clientes usam para aplicativos interativos em massa. Criamos e estamos criando recursos no produto para permitir que os clientes criem esses aplicativos de forma eficaz. Precisamos de medições de desempenho para esses casos de uso.
Há outros bancos de dados compatíveis com o modelo JSON: MongoDB, DocumentDB, DynamoDB, RethinkDB, Oracle NoSQL. Ao executar o YCSB em bancos de dados JSON (Couchbase, MongoDB etc.), o driver simplesmente armazena e recupera cadeias de caracteres na estrutura de valor-chave JSON. Todos esses bancos de dados exigem um novo benchmark para medir o processamento da estrutura avançada do JSON (objetos aninhados, matrizes) e operações como paginação, agrupamento e agregações.
O objetivo do YCSB-JSON é estender o benchmark YCSB para medir a capacidade do banco de dados JSON para cobrir esses dois aspectos:
- Representante de operações de aplicativos interativos em massa.
- Operações no modelo de dados JSON, incluindo objetos aninhados e matrizes.
- Crie cargas de trabalho que representem as operações desses aplicativos.
Veja estes casos de uso de clientes:
- Marriott criou seu sistema de reservas no IBM Mainframe e no DB2. Eles enfrentaram desafios de custo e desempenho à medida que mais e mais clientes tentavam navegar pelo inventário disponível. Os sistemas em DB2 foram originalmente criados para receber reservas de um sistema telefônico ou de agentes. A taxa de procura por reserva é baixa. Atualmente, essa proporção é alta, pois o número de solicitações de pesquisa aumentou exponencialmente. Isso também aumentou drasticamente o custo do banco de dados. A Marriott transferiu todos os seus dados de inventário para o Couchbase com sincronização contínua de seus sistemas de mainframe; os aplicativos da Web usam o Couchbase para as operações de consulta/pesquisa.
- Carros.com é um portal para listar e vender carros. Eles têm os dados de listagem no Oracle. Quando os disponibilizam na Web, eles precisam não apenas apresentar as informações básicas sobre o carro, mas também fornecer insights adicionais, como quantos usuários estão procurando um carro ou o salvaram em sua lista de desejos. Essa é uma forma de aumentar o envolvimento e o senso de urgência. Todos os dados necessários para essas operações interativas são armazenados no Couchbase.
De modo mais geral, os aplicativos interativos em massa incluem o seguinte:
- Consulte a disponibilidade de quartos, detalhes de preços, comodidades (pesquisas por clientes finais)
- Procurar informações sobre marca/modelo de carro ou oficinas de reparo (habilitar consumidores e parceiros em escala da Web)
- Fornecer informações ao cliente no contexto (serviços baseados em localização)
- Atende tanto aos dados mestre quanto aos dados transacionais (em escala)
Para dar suporte a esses requisitos, os aplicativos e bancos de dados fazem o seguinte:
- Descarga de consultas de bancos de dados de sistemas de registro de alto custo (mainframe, Oracle)
- (aplicativos de reservas e receitas)
- Abertura das funções de back-office para acesso à Web/móvel
- (permitir que os usuários da Web verifiquem os detalhes do quarto)
- Dimensione o banco de dados/consultas com melhor TCO
- (dimensionar mainframes com servidores de commodities)
- Modernize os sistemas legados com os recursos exigidos pelos novos aplicativos de colaboração/engajamento
- (inventário de navegação, voos, disponibilidade de quartos, análise departamental)
O novo benchmark precisa medir o desempenho das consultas que implementam essas operações.
2. Modelo de dados
Consideramos o cliente e os pedidos como duas coleções distintas de documentos JSON. Cada pedido tem uma referência ao seu cliente.
Abaixo estão os exemplos de cliente e documento de pedido. Eles foram gerados por meio do gerador de dados fakeit. Essa ferramenta está disponível em: https://github.com/bentonam/fakeit
Consulte o apêndice para ver o arquivo YAML usado para definir o modelo de dados e o domínio.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Amostra cliente documento Documento Chave: 100_advjson { "_id": "100_advjson", "doc_id": 100, "gid": "48a8e177-15e5-5116-95d0-41478601bbdd", "first_name": "Stella", "middle_name": "Jackson", "last_name": "Brinquedo", "ballance_current": "$1084.94", "dob": "2016-05-11", "email": "Alysson83@yahoo.com", "isActive": verdadeiro, "linear_score": 31, "Pontuação ponderada": 40, "phone_country" (país do telefone): "fr", "phone_by_country" (telefone por país): "01 80 03 25 39", "age_group" (grupo de idade): "criança", |
3. Operações de referência:
As quatro primeiras operações são iguais às do YCSB padrão, exceto pelo fato de que se trata de documentos JSON. O restante das operações é novo.
- Inserir: Insere um novo documento JSON.
- Atualização: Atualize um documento JSON substituindo o valor de um campo escalar.
- Ler: Lê um documento JSON, seja um campo escolhido aleatoriamente ou todos os campos.
- Excluir: Exclui um documento JSON com uma determinada chave.
- Escaneamento: Digitaliza documentos JSON em ordem, começando por uma chave de registro escolhida aleatoriamente. O número de registros a serem digitalizados é escolhido aleatoriamente (LIMIT).
- Pesquisa: Pesquise documentos JSON com base em predicados de intervalo em 3 campos (personalizável para n campos).
- Página: Paginar o conjunto de resultados de uma consulta com predicado em um campo do documento.
- Todos os clientes em zip com OFFSET e LIMIT escolhidos aleatoriamente em SQL, N1QL.
- NestScan: Consulta a documentos JSON com base em um predicado em um campo aninhado de 1 nível.
- ArrayScan: Consulta de documentos JSON com base em um predicado dentro do campo de matriz de nível único.
- ArrayDeepScan: Consulta de documentos JSON com base em um predicado em um campo de matriz de dois níveis (matriz de matrizes).
- Relatório: Consultar detalhes de pedidos de clientes em um código postal específico.
- Cada cliente tem vários pedidos.
- O documento do pedido contém detalhes do pedido.
- Relatório2: Gerar resumo de pedidos de vendas para um determinado dia, agrupado por CEP.
- Carga: Carregamento de dados.
- Sincronização: Transmissão e sincronização de dados de outro sistema.
- Agregado: Faça alguns agrupamentos e agregações.
Para o Couchbase: Exemplos de implementação de operações de benchmark
As quatro primeiras operações são iguais às do YCSB padrão, exceto pelo fato de que se trata de documentos JSON. O restante das operações é novo.
O Couchbase implementa o YCSB em dois modos.
KV=verdadeiro. KV significa key-value (valor-chave). As operações simples INSERT, UPDATE e DELETE do YCSB podem ser implementadas por meio de APIs KV em vez de consultas. Definir KV=true significa usar a API KV e KV=false significa usar a API N1QL (SQL para JSON). Veja o tutorial do N1QL em https://query-tutorial.couchbase.com
- Inserir: Insere um novo documento JSON.
1 2 |
KV=verdadeiro: KV chamada para inserir KV=falso: INSERIR PARA cliente VALORES(...) |
2. Atualização: Atualize um documento JSON substituindo o valor de um campo escalar.
1 2 3 4 |
KV=verdadeiro: KV chamada para ATUALIZAÇÃO a único documento. KV=falso: ATUALIZAÇÃO cliente CONJUNTO campo1 = valor USO CHAVES [documentkey]<extensão estilo="font-weight: 400"><forte>Ler</forte>: Ler a JSON documento, ou um aleatoriamente escolhido campo em o documento ou todos o campos.</span> |
1 2 3 4 |
KV=verdadeiro: KV chamada para buscar a único documento. KV=falso: SELECIONAR * DE cliente USO CHAVES [documentkey] |
3. Leia: Obtém um documento JSON com uma determinada chave.
1 2 3 4 |
KV=verdadeiro: KV chamada para buscar a único documento. KV=falso: SELECIONAR * DE cliente USO CHAVES [documentkey] |
4. Excluir: Exclui um documento JSON com uma determinada chave.
1 2 3 4 |
KV=verdadeiro: KV chamada para buscar a único documento. KV=falso: DELETE DE cliente USO CHAVES [documentkey] |
5. Digitalização: Digitaliza documentos JSON em ordem, começando por uma chave de registro escolhida aleatoriamente. O número de registros a serem digitalizados é escolhido aleatoriamente (LIMIT).
1 2 3 4 5 6 7 |
KV=VERDADEIRO: SELECIONAR META().id DE cliente ONDE META().id > "val" ORDER BY META().id LIMITE <num> Buscar o real documentos diretamente usando KV chamadas de o referência motorista. KV=falso: SELECIONAR * DE cliente ONDE META().id > "val" ORDER BY META().id LIMITE <num> |
6. Página: Paginar o conjunto de resultados de uma consulta com predicado em um campo do documento.
1 2 3 4 5 6 7 8 9 10 |
Todos clientes em endereço.zip com aleatoriamente escolhido DESLOCAMENTO e LIMITE em SQL, N1QL KV=VERDADEIRO: SELECIONAR META().id DE cliente ONDE endereço.zip = "valor" DESLOCAMENTO <num> LIMITE <num> Buscar o real documentos diretamente usando KV chamadas de o referência motorista. KV=falso: SELECIONAR * DE cliente ONDE endereço.zip = "valor" DESLOCAMENTO <num> LIMITE <num> |
7. Busca: Pesquisar documentos JSON com base em predicados de intervalo em 3 campos (personalizável para n campos).
1 2 3 4 5 6 7 8 9 10 11 12 |
Todos clientes ONDE (país = "valor1" E grupo_idade = "valor2" e ANO(dob) = "valor" ) Todos clientes recuperado com aleatoriamente escolhido DESLOCAMENTO e LIMITE em SQL, N1QL KV=VERDADEIRO: SELECIONAR META().id DE cliente ONDE país = "valor1" E grupo_idade = "valor2" e ANO(dob) = "valor" ORDEM BY país, grupo_idade, ANO(dob) DESLOCAMENTO <num> LIMITE <num> Buscar o real documentos diretamente usando KV chamadas de o referência motorista. KV=falso: SELECT * DE cliente ONDE ONDE país = "valor1" E grupo_idade = "valor2" e ANO(dob) = "valor" ORDEM BY país, grupo_idade, ANO(dob) DESLOCAMENTO <num> LIMITE <num> |
8. NestScan: Consulta a documentos JSON com base em um predicado em um campo aninhado de 1 nível.
1 2 3 4 5 6 7 8 9 |
KV=VERDADEIRO: SELECIONAR META().id DE cliente ONDE address.prev_address.zip = "valor" LIMITE <num> Buscar o real documentos diretamente usando KV chamadas de o referência motorista. KV=falso: SELECIONAR * DE cliente ONDE address.prev_address.zip = "valor" LIMITE <num> |
9. ArrayScan: Consulta de documentos JSON com base em um predicado dentro do campo de matriz de nível único.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Localizar todos clientes que ter dispositivos com a valor. E.g. FF-012 Amostra dispositivos campo "dispositivos": [ "EE-245", "FF-012", "GG-789", "HH-246" ], KV=VERDADEIRO: SELECIONAR META().id DE cliente ONDE QUALQUER v IN dispositivos SATISFAÇÕES v = "FF-012" FIM ORDEM BY META().id LIMITE <num> Buscar o real documentos diretamente usando KV chamadas de o referência motorista. KV=falso: SELECT * DE cliente ONDE QUALQUER v IN dispositivos SATISFAÇÕES v = "FF-012" ORDEM BY META().id FIM LIMITE <num> |
10. ArrayDeepscan: Consulta de documentos JSON com base em um predicado em um campo de matriz de dois níveis (matriz de matrizes).
- Obtenha uma lista de todos os clientes que visitaram Paris, França.
KV=verdadeiro:
1 2 3 4 5 6 7 8 |
SELECIONAR META().id DE cliente ONDE QUALQUER v em locais_visitados SATISFAÇÕES v.país = "França" E QUALQUER c em v.cidades SATISFAÇÕES c = "Paris" FIM ORDEM BY META().id LIMITE <num> |
Obtenha os documentos reais diretamente usando chamadas KV do driver de benchmark.
KV=false:
1 2 3 4 5 6 7 8 |
SELECIONAR * DE cliente ONDE QUALQUER v em locais_visitados SATISFAÇÕES v.país = "França" E QUALQUER c em v.cidades SATISFAÇÕES c = "Paris" FIM FIM ORDEM BY META().id LIMITE <num> |
11. Relatório: Consultar detalhes de pedidos de clientes em um código postal específico.
-
123456789101112131415161718Cada cliente tem múltiplos pedidos.Pedido documento tem ordem detalhes.KV=VERDADEIRO:Não possível (facilmente sem significativo perf impacto.KV=falso:SELECT *DE cliente c INNER JUNTAR pedidos oON (META(id) IN c.lista_de_ordens)ONDE endereço.zíper = "val"ANSI JUNTAR com HASH unir-se:SELECT *DE cliente c INNER JUNTAR pedidos o USO HASH (sonda)ON (META(id) IN c.lista_de_ordens)ONDE endereço.zíper = "val"
12. Relatório2: Gerar resumo de pedidos de vendas para um determinado dia, agrupado por CEP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
KV=VERDADEIRO: Necessidade para escrever a programa KV=falso: SELECIONAR o.dia, c.zip, SUM(o.salesamt) DE cliente c INNER JOIN pedidos o ON (META(id) IN c.order_list) ONDE c.zip = "valor" E o.dia = "valor" GRUPO BY c.dia, c.zip ORDER BY SUM(o.sales_amt) ----ANSI unir-se SELECIONAR o.dia, c.zip, SUM(o.salesamt) DE cliente c INNER JOIN pedidos o ON (META(id) IN c.order_list) ONDE c.zip = "valor" E o.dia = "valor" GRUPO BY c.dia, c.zip ORDER BY SUM(o.sales_amt) ------ANSI unir-se com HASH unir-se SELECIONAR o.dia, c.zip, SUM(o.salesamt) DE cliente c INNER JOIN pedidos o USO HASH (sonda) ON (META(id) IN c.order_list) ONDE c.zip = "valor" E o.dia = "valor" GRUPO BY c.dia, c.zip ORDER BY SUM(o.sales_amt) |
13. Carga: Carregamento de dados.
- CARREGAR 1 milhão de documentos.
- CARREGAR 10 milhões de documentos.
14. Sincronização: Transmissão e sincronização de dados de outro sistema
- Necessidade de medir o desempenho da sincronização de dados.
- Sincronização de 1 milhão de documentos. Atualização do 50%, inserção do 50%.
- Sincronização de 10 milhões de documentos. Atualização 80%, inserção 20%.
- O ideal é que essa sincronização seja feita a partir do Kafka ou de algum outro conector que extraia dados de uma fonte diferente.
15. Agregado: Faça alguns agrupamentos e agregações.
1 2 3 4 5 6 |
---Grupo Consulta 1 SELECIONAR c.zíper, CONTAGEM(1) DE cliente c ONDE c.zíper entre "value1" e "value2" GRUPO BY c.zíper |
1 2 3 4 5 6 7 8 9 10 |
---GRUPO BY consulta 2 SELECIONAR o.dia, SUM(o.salesamt) DE pedidos o ONDE o.dia entre "value1" e "value2" GRUPO BY o.dia; |
4. Cargas de trabalho de referência
As cargas de trabalho são uma combinação dessas operações.
Para começar, a definição da carga de trabalho pode reutilizar as definições da definição do YCSB: carga de trabalho A até carga de trabalho E. Os detalhes estão disponíveis em https://github.com/brianfrankcooper/YCSB/wiki/Core-Workloads. Precisaremos definir cargas de trabalho adicionais com uma combinação das operações definidas acima.
A carga de trabalho SA é a mesma que a carga de trabalho A no novo modelo. O mesmo acontece com a carga de trabalho de B a F. Vamos chamá-las de SB a SF para diferenciar da carga de trabalho de B a F.
Carga de trabalho | Operações | Seleção de registros | Exemplo de aplicativo |
SA - Atualização pesada | Leia: 50%
Atualização 50% |
Zipfiano | Armazenamento de sessão que registra ações recentes em uma sessão de usuário |
SB - Ler pesado | Ler: 95%
Atualização: 5% |
Zipfiano | Marcação de fotos; adicionar uma tag é uma atualização, mas a maioria das operações
Atualização: 5% são para ler etiquetas |
SC - Somente leitura | Ler: 100% | Zipfiano | Cache de perfil de usuário, em que os perfis são construídos em outro lugar (por exemplo, Hadoop) |
SD - Leia mais recente | Ler: 95%
Inserto 5% |
Mais recentes | Atualizações de status do usuário; as pessoas querem ler os status mais recentes |
SE - Alcances curtos | Escanear: 95%
Inserção: 5% |
Zipfian/Uniforme | Conversas encadeadas, em que cada varredura é para as postagens em um determinado encadeamento (supostamente agrupadas por ID de encadeamento) |
SF - Ler, modificar, gravar | Leia: 50%
Escreva: 50% |
Zipfiano | banco de dados de usuários, em que os registros de usuários são lidos e modificados pelo usuário ou para registrar a atividade do usuário. |
SG - Página pesada | Página: 90%
Inserção: 5% Atualização:5% |
Zipfiano | Banco de dados de usuários, onde novos usuários são adicionados, registros existentes são atualizados, consultas de paginação no sistema. |
SH - Pesquisa pesada | Busca: 90%
Inserção: 5% Atualização: 5% |
Zipfiano | Banco de dados de usuários, onde novos usuários são adicionados, registros existentes são atualizados e consultas de pesquisa no sistema. |
SI - NestScan pesado | Nestscan: 90%
Inserção: 5% Atualização: 5% |
Zipfiano | Banco de dados de usuários, onde novos usuários são adicionados, registros existentes são atualizados, consultas de nestscan no sistema. |
SJ - Arrayscan pesado | Arrayscan: 90%
Inserção: 5% Atualização: 5% |
Zipfiano | |
SK - ArrayDeepscan pesado | ArrayDeepScan: 90%
Inserção: 5% Atualização: 5% |
Zipfiano | |
SL - Relatório | Relatório: 100% | ||
SL - Relatório2 | Relatório2: 100% | ||
SLoad - Carga | Carga: 100% | Tudo | Carga de dados para configurar o SoE |
SN - Agregado
(SN1, SN2) |
Agregação: 90%
Inserção: 5% Atualização: 5% |
||
SMIX - Carga de trabalho mista | Página:20%
Busca:20% Arrayscan:15% MatrizDeepscan:10% Agregado: 10% Relatório: 10% |
Veja abaixo. | |
SSync - Sincronização | Sincronização: 100%
Mesclar/Atualizar: 70% Novo/Inserto: 30% |
Sincronização contínua de dados de outros sistemas com os sistemas de engajamento. Veja abaixo. |
Exemplo de configuração para carga de trabalho YCSB/JSON
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
contagem de registros=1000 operationcount=1000 carga de trabalho=com.yahoo.ycsb.cargas de trabalho.Carga de trabalho principal Filternumlow = 2 Filternumhigh = 14 Sortnumlow = 3 Sortnumhigh = 6 page1propotion=0.95 insertproportion=0.05 requestdistribution=zipfian maxscanlength=100 distribuição de comprimento de varredura=uniforme |
Agradecimentos
Agradecimentos a Raju Suravarjjala, Ao diretor sênior de QE e desempenho do Couchbase, por nos incentivar a fazer isso, e a toda a equipe de desempenho por apoiar esse esforço. O benchmark YCSB-JSON foi desenvolvido em colaboração com Alex Gyryk, Engenheiro de desempenho principal do Couchbase. Ele desenvolveu os modelos de dados para clientes e pedidos usados neste artigo e implementou as operações e cargas de trabalho no YCSB-JSON para Couchbase e MongoDB. A implementação do YCSB-JSON está disponível em: https://github.com/couchbaselabs/YCSB
Agradecimentos a Aron Benton, Arquiteto de soluções do Couchase, por desenvolver um gerador de dados JSON fácil de usar e eficiente, o fakeit. Ele o desenvolveu antes de ingressar no Couchbase. Ele está disponível em: https://github.com/bentonam/fakeit
Próxima parte
No próximo artigo sobre o YCSB-JSON, Alex explicará as implementações desse benchmark para o Couchbase e o MongoDB. O código-fonte da implementação está disponível em: https://github.com/couchbaselabs/YCSB
Referências
- Benchmarking Cloud Serving Systems com YCSB: https://www.cs.duke.edu/courses/fall13/cps296.4/838-CloudPapers/ycsb.pdf
- JSON: http://json.org
- Gerador de JSON: http://www.json-generator.com/
- Implementação do YCSB-JSON: https://github.com/couchbaselabs/YCSB
Apêndice
YAML para gerar o conjunto de dados do cliente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
nome: AdvJSON tipo: objeto chave: _id dados: fixo: 10000 propriedades: _id: tipo: string dados: post_build: "return '' + this.doc_id + '_advjson';" doc_id: tipo: inteiro descrição: O ID do documento dados: construir: "return document_index + 1" gid: tipo: descrição: "guid" dados: construir: "return chance.guid();" primeiro_nome: tipo: string descrição: "Primeiro nome - string, vinculado para url como o pessoal página" dados: |
Existe um YAML para pedidos para gerar o conjunto de dados de pedidos?
Também estou procurando por isso. O YAML no apêndice não contém a chave "order_list".
Olá, ótimo trabalho! Você poderia fornecer mais instruções sobre como chegar à implementação mencionada aqui? Acabei de verificar a ramificação principal de https://github.com/couchbaselabs/YCSB e não consigo encontrar nem as cargas de trabalho mencionadas aqui nem a implementação das novas operações.
Veja os detalhes no artigo de acompanhamento: https://www.couchbase.com/ycsb-json-implementation-for-couchbase-and-mongodb/
Incrível, obrigado!
Muito obrigado,
Por favor, tenho uma pergunta: como podemos gerar uma nova carga de trabalho com base em novos requisitos?