De Niel Markwick, arquiteto de soluções, e Robert Saxby, especialista em Big Data
O Cloud Spanner é um serviço de banco de dados relacional e escalonável horizontalmente desenvolvido a partir de uma perspectiva de projeto distribuído/em nuvem. Ele oferece eficiência e alta disponibilidade a desenvolvedores e administradores de banco de dados (DBAs), mas é estruturalmente diferente dos bancos de dados comuns que você conhece. Nesta série do blog, vamos falar sobre as principais diferenças que os DBAs e desenvolvedores encontrarão ao migrarem dos sistemas de gerenciamento de banco de dados relacional (RBDMS) de escalonamento vertical (scale-up) para o Cloud Spanner. Vamos discutir ações recomendadas e não recomendadas, apresentar melhores práticas e entender por que o Cloud Spanner é diferente.
Nesta série, vamos abordar diversos assuntos, entre eles:
Seleção de chaves e uso de índices
Como abordar a lógica empresarial
Importação e exportação de dados
Migração do seu RDBMS atual
Otimização do desempenho
Controle de acesso e geração de registros
Você vai passar a compreender como usar o Cloud Spanner da melhor forma e aproveitar seu potencial para atingir um desempenho linearmente escalonável em bancos de dados gigantes. Na primeira parte, vamos começar entendendo mais a fundo como os conceitos de chaves e índices funcionam no Cloud Spanner.
Escolhendo chaves no Cloud Spanner
Assim como em outros bancos de dados, a escolha de chaves é vital para otimizar o desempenho do banco de dados. Isso é ainda mais importante no Cloud Spanner por conta da forma que os mecanismos dele distribuem a carga dos bancos de dados. Ao contrário do RDBMS tradicional, você precisa ter cuidado na hora de escolher as chaves primárias para as tabelas e quais colunas devem ser indexadas.
Usar chaves bem distribuídas gera uma tabela com tamanho e desempenho linearmente escalonável pelo número de nós do Cloud Spanner, enquanto que usar chaves mal distribuídas pode produzir pontos sobrecarregados, onde um único nó é responsável pela maioria das leituras e gravações da tabela.
Em um RDBMS tradicional, que é verticalmente escalonado, há um único nó para gerenciar todas as tabelas (dependendo da instalação, talvez haja réplicas que você pode usar para leitura ou failover). Portanto, esse único nó tem controle total sobre os bloqueios de linha da tabela e a emissão de chaves exclusivas a partir de uma sequência numérica.
O Cloud Spanner é um sistema distribuído, com muitos nós lendo e gravando dados no banco de dados em algum momento. No entanto, para ter escalonabilidade, além de transações ACID globais e consistência forte, apenas um nó em determinado momento pode ter a responsabilidade pela gravação de uma linha específica.
O Cloud Spanner distribui o gerenciamento de linhas em vários nós ao quebrar cada tabela em várias seções , usando intervalos da chave primária classificada por lexicografia.
Dessa forma, o Cloud Spanner consegue oferecer alta disponibilidade e escalonabilidade, mas isso também significa que usar uma sequência continuamente crescente ou decrescente como chave prejudica o desempenho. Para explicar por que, vamos explorar como o Cloud Spanner cria e gerencia suas seções de tabela.
Seções de tabela e escolha de chave
O Cloud Spanner gerencia seções usando o Paxos (se quiser conhecer como, acesse a documentação detalhada: Vida das leituras e gravações do Cloud Spanner e Spanner: banco de dados distribuído globalmente do Google ). Em uma instância regional do Cloud Spanner, a responsabilidade por ler/gravar cada seção é distribuída em um grupo de três nós, cada um em uma das três zonas de disponibilidade da instância do Cloud Spanner.
Um nó deste grupo de três é escolhido como o líder da seção, gerencia as gravações e fica responsável por todas as linhas na seção. Todos os três nós do grupo podem fazer leituras.
Para criar um exemplo visual, imagine uma tabela com 600 linhas que usa uma chave de número inteiro monotonicamente crescente, simples e contínua (como é bastante comum nos RDBMSs tradicionais), divididas em seis seções, operando em uma instância de dois nós (por zona) do Cloud Spanner. Num cenário ideal, a tabela teria seis seções, e os líderes delas seriam os seis nós disponíveis na instância.
Essa distribuição geraria o desempenho ideal na hora de ler/gravar as linhas, desde que as leituras e atualizações sejam distribuídas uniformemente no intervalo da chave.
Problemas com pontos sobrecarregados
O problema surge quando novas linhas são acrescentadas ao banco de dados. Cada nova linha terá um ID crescente e será adicionada à última seção, o que significa que dos seis nós disponíveis, apenas um vai ser responsável por todas as gravações. No exemplo acima, o nó "2c" gerenciaria todas as gravações. A partir daí, esse nó se torna um ponto sobrecarregado, limitando o desempenho geral de gravação do banco de dados. Além disso, a distribuição de linhas fica desequilibrada, com a última seção bem maior e, portanto, gerenciando mais leituras de linha do que as outras.
O Cloud Spanner tenta compensar a carga desequilibrada adicionando e removendo seções em segundo plano , de acordo com a carga de leitura e gravação, e criando uma nova seção quando o tamanho de uma seção ultrapassa um limite determinado. Porém, em uma tabela que cresce constantemente, isso não acontece rápido o suficiente para evitar a criação de um ponto sobrecarregado.
Junto com as chaves monotonicamente crescentes ou decrescentes, esse problema também afeta tabelas indexadas por uma chave determinística, como um carimbo de data e hora crescente em uma tabela de log de eventos. Tabelas chaveadas por um carimbo de data e hora têm mais probabilidade de ter um ponto de acesso de leitura porque, na maioria dos casos, as linhas recentemente marcadas com data e hora são acessadas com mais frequência que as demais (confira Cloud Spanner – escolhendo as chaves primárias certas para ver informações detalhadas sobre como detectar e evitar pontos sobrecarregados).
Problemas com geradores de sequência
O conceito dos geradores de sequência, ou a falta deles, é uma área importante de se explorar mais. RDBMSs verticais tradicionais têm geradores de sequência integrados, que criam novas chaves de número inteiro em uma determinada sequência durante uma transação. O Cloud Spanner não pode ter esse recurso por causa da sua arquitetura distribuída, pois haveria condições de corrida entre os nós do líder da seção ao inserir chaves novas ou a tabela teria que ser globalmente bloqueada na hora de gerar uma nova chave. Qualquer uma dessas opções prejudicaria o desempenho.
Uma solução seria a chave ser gerada pelo aplicativo, por exemplo, armazenando o valor da próxima chave em outra tabela no banco de dados ou extraindo o valor máximo atual da chave da tabela. No entanto, você teria os mesmos problemas de desempenho. Pense que, como o aplicativo é provável que o aplicativo também seja distribuído, haverá vários clientes do banco de dados tentando acrescentar uma linha ao mesmo tempo, produzindo dois possíveis resultados dependendo de como a nova chave é gerada:
Se a SELEÇÃO da chave atual é realizada na transação, uma instância do aplicativo que tentar acrescentar bloquearia todas as outras instâncias do aplicativo que estivessem tentando acrescentar por causa do bloqueio de linha.
Se a SELEÇÃO da chave atual é feita fora da transação, haveria uma corrida entre as instâncias do aplicativo que estivessem tentando acrescentar a nova linha. Uma teria sucesso, enquanto que as outras teriam que tentar de novo (e inclusive gerar uma nova chave) quando a adição falhasse, já que a chave já existiria.
O que torna uma chave boa?
Bom, se chaves sequenciais limitam o desempenho do banco de dados no Cloud Spanner, qual seria uma boa chave para se usar? O ideal é que bits de ordem superior estejam uniforme e semialeatoriamente distribuídos quando as chaves forem geradas.
Um jeito fácil de gerar uma chave dessas é usar números aleatórios, como o identificador aleatório universalmente exclusivo (UUID). Veja que há diversas classes de UUID . As versões 1 e 2 usam prefixos determinísticos, como carimbos de data e hora ou endereços MAC. Veja se o método de geração de UUIDs que você usa é aleatoriamente distribuído de verdade, ou seja, v4, pelo menos para os bytes de ordem superior. Assim, você garante que as chaves sejam uniformemente distribuídas no keyspace, o que, por sua vez, garante que a carga seja uniformemente distribuída aos nós do Spanner.
Embora outra abordagem possa usar alguns atributos reais dos dados, que são imutáveis e uniformemente distribuídos no intervalo da chave, isso é bem complicado, pois a maioria dos atributos uniformemente distribuídos é discreta, não contínua. Por exemplo, o resultado aleatório de um dado jogado é uniformemente distribuído e tem seis valores finitos. Uma distribuição contínua pode trabalhar com um número irracional, como "n".
E se eu realmente precisar que uma sequência de números inteiros seja a chave?
Apesar de não ser recomendado, em algumas circunstâncias, uma chave de sequência de números inteiros é necessária, seja por questões de legado ou por motivos externos, como o ID de um funcionário.
Para usar uma chave de sequência de números inteiros, primeiro você precisa de um gerador de sequência que seja sólido em um sistema distribuído. Uma forma de fazer isso é criar uma tabela no Cloud Spanner com uma linha para cada sequência necessária que contenha o valor seguinte da sequência, o que ficaria mais ou menos assim:
CREATE TABLE Sequences (
Sequence_ID STRING(MAX) NOT NULL, -- The name of the sequence
Next_Value INT64 NOT NULL
) PRIMARY KEY (Sequence_ID)
Quando um novo valor de ID é necessário, o valor seguinte da sequência é lido, incrementado e atualizado na mesma transação que a inserção da nova linha.
Isso limitaria o desempenho quando várias linhas fossem inseridas, pois cada inserção bloquearia todas as outras por conta da atualização da tabela "Sequences" que criamos acima.
Esse problema de desempenho pode ser minimizado, mas provavelmente a custo de possíveis "buracos" na sequência. Hipoteticamente, cada instância do aplicativo pode reservar um bloco de, por exemplo, 100 valores da sequência de uma vez incrementando Next_Value em 100 e, a partir daí, gerenciar internamente a emissão de IDs individuais daquele bloco.
Na tabela que usa a sequência, a chave não pode ser meramente o valor da sequência numérica em si, já que isso transformaria a última seção em um ponto sobrecarregado (como explicamos acima). Então, o aplicativo precisa gerar uma chave complexa que distribua as linhas uniformemente nas seções.
Isso é conhecido como fragmentação no aplicativo e pode ser feito prefixando o ID sequencial com mais uma coluna contendo um valor uniformemente distribuído no espaço da chave (por exemplo, um hash do ID original) ou revertendo os bits do ID. E isso fica assim:
CREATE TABLE Table1 (
Hashed_Id INT64 NOT NULL,
ID INT64 NOT NULL,
-- other columns with data values follow....
) PRIMARY KEY (Hashed_Id, Id)
Até uma soma de verificação de 32 simples para uma verificação de redundância cíclica (CRC) é suficiente para gerar o Hashed_ID pseudoaleatório necessário. Não precisa ser muito seguro, só o suficiente para aleatorizar a ordem das linhas das chaves sequencialmente numeradas, como na tabela abaixo:
Veja que sempre que uma linha é lida diretamente, o ID e o Hashed_Id têm que estar especificados para evitar uma verificação da tabela, como no exemplo abaixo:
SELECT * FROM Table1
WHERE t1.Hashed_Id = 0xDEADBEEF
AND t1.Id = 1234
Seguindo essa mesma lógica, sempre que essa tabela for unida a outras tabelas na consulta por ID, a união também deve usar o ID e o Hashed_Id. Caso contrário, você perderá desempenho, já que seria necessário fazer uma verificação da tabela para encontrar a linha. Isso significa que a tabela que referencia o ID também deve conter o Hashed_Id, assim:
CREATE TABLE Table2 (
Id String(MAX), -- UUID
Table1_Hashed_Id INT64 NOT NULL,
Table1_Id INT64 NOT NULL,
-- other columns with data values follow....
) PRIMARY KEY (Id)
SELECT * from Table2 t2 INNER JOIN Table1 t1
ON t1.Hashed_Id = t2.Table1_Hashed_Id
AND t1.Id = t2.Table1_Id
WHERE ... -- some criteria
E se eu realmente precisar que um carimbo de data e hora seja a chave?
Em muitos casos, a linha que usa o carimbo de data e hora como chave também referencia outros dados da tabela. Por exemplo, as transações de uma conta bancária vão referenciar a conta de origem. Neste caso, presumindo que o número da conta de origem já esteja razoavelmente distribuído de maneira uniforme, você pode usar uma chave complexa contendo o número da conta primeiro e depois a data e a hora:
CREATE TABLE Transactions (
account_number INT64 NOT NULL,
timestamp TIMESTAMP NOT NULL,
transaction_info ...,
) PRIMARY KEY (account_number, timestamp DESC)
As seções seriam criadas primeiramente usando o número da conta, e não a data e a hora, e isso faria com que as linhas recém-adicionadas fossem distribuídas entre diversas seções.
Vale destacar que, nessa tabela, o carimbo de data e hora foi organizado em ordem decrescente. Isso porque, na maioria dos casos, o ideal é ler as transações mais recentes, que serão as primeiras da tabela, para evitar a necessidade de verificar toda a tabela para encontrar as linhas mais recentes.
Se você não tem ou não pode ter uma referência externa ou qualquer outro dado que possa usar na chave para distribuir a ordem, será preciso realizar a fragmentação no aplicativo, mostrada no exemplo de sequência de números inteiros acima.
No entanto, se usar uma hash simples, deixará as consultas pelo intervalo de datas e horas extremamente lentas, pois buscar um intervalo de datas e horas exige uma verificação completa da tabela para não deixar nenhum hash de fora. Em vez disso, recomendamos gerar um ShardId com o carimbo de data e hora. Então, por exemplo,
TimestampShardId = CRC32(Timestamp) % 100
vai retornar um valor pseudoaleatório entre 0 e 99 a partir da marcação de data e hora. A partir disso, você pode usar esse ShardId na chave da tabela de forma que os carimbos de data e hora sequenciais sejam distribuídos em várias seções, deste modo:
CREATE TABLE Events (
TimestampShardId INT64 NOT NULL
Timestamp TIMESTAMP NOT NULL,
event_info...
) PRIMARY KEY (TimestampShardId, Timestamp DESC)
Por exemplo, uma tabela com a data dos 10 primeiros dias de 2018 (que, sem o ShardId, seria armazenada na tabela por ordem de datas) teria a seguinte ordem:
Ao fazer uma consulta, você deve usar uma cláusula BETWEEN para poder selecionar qualquer fragmento sem fazer uma verificação da tabela:
Select * from Events
WHERE
TimestampShardId BETWEEN 0 AND 99
AND Timestamp > @lower_bound
AND Timestamp < @upper_bound;
Veja que o ShardId é só uma forma de melhorar a distribuição de chaves, de forma que o Cloud Spanner possa usar diversas seções para armazenar carimbos de data e hora sequenciais. Ele não identifica efetivamente uma seção do banco de dados, e linhas de tabelas diferentes com o mesmo ShardId podem muito bem estar em seções diferentes.
Efeitos da migração
Ao migrar de um RDBMS que usa chaves não recomendadas para o Cloud Spanner, pense no que falamos acima. Se necessário, adicione hashes de chave a tabelas ou mude a ordenação das chaves.
Escolhendo índices no Cloud Spanner
Em um RDBMS tradicional, os índices são uma ferramenta muito eficiente para procurar linhas de uma tabela por um valor que não seja a chave principal. Na maioria das circunstâncias, buscar uma linha pelo índice leva mais ou menos o mesmo tempo que buscar uma linha pela chave. Isso acontece porque a tabela e o índice são gerenciados por um único nó, então o índice pode apontar diretamente para a linha em disco da tabela.
No Cloud Spanner, os índices são implementados usando tabelas, o que permite que eles sejam distribuídos e oferece o mesmo nível de escalonabilidade e desempenho que as tabelas normais.
Mas, por causa desse tipo de implementação, usar índices para ler os dados de uma linha da tabela é menos eficiente do que em um RDBMS tradicional. Na prática, é uma união interna com a tabela original, então ler uma tabela usando uma chave indexada se transforma no seguinte processo:
Buscar chave do índice na seção
Ler linha do índice a partir da seção para obter a chave da tabela
Buscar chave da tabela na seção
Ler linha da tabela na seção para obter valores da linha
Retornar valores da linha
Uma coisa que é importante destacar é que não há garantia de que a seção da chave do índice esteja no mesmo nó que a seção da chave da tabela, ou seja, uma simples consulta do índice pode exigir comunicação cruzada entre nós só para ler uma linha.
Seguindo esse raciocínio, atualizar uma tabela indexada provavelmente vai exigir uma gravação multinós para atualizar a linha da tabela e a linha do índice. Portanto, usar um índice no Cloud Spanner sempre é uma troca entre melhorar o desempenho de leitura e reduzir o desempenho de gravação.
Chaves de índice e pontos sobrecarregados
Como os índices são implementados como tabela no Cloud Spanner, você vai encontrar os mesmos problemas que encontrou nas chaves da tabela nas colunas indexadas: Um índice de uma coluna com valores mal distribuídos (como um carimbo de data e hora) levará à criação de um ponto sobrecarregado, mesmo que a tabela em questão esteja usando chaves bem distribuídas. Isso ocorre porque, quando se adicionam linhas à tabela, o índice recebe também novas linhas, e as gravações dessas novas linhas sempre serão enviadas à mesma seção.
Portanto, é preciso ter cuidado na hora de criar índices. Nossa recomendação é criar índices usando apenas colunas que tenham um conjunto bem distribuído de valores, como quando se escolhe uma chave de tabela.
Em alguns casos, você vai precisar de fragmentação no aplicativo para as colunas indexadas para criar uma coluna ShardId sintética, que poderá ser usada no índice para distribuir os valores às seções.
Por exemplo, esta configuração abaixo cria um ponto de acesso quando se acrescentam eventos por causa do índice, mesmo que UserId esteja aleatoriamente distribuído.
CREATE TABLE Events (
UserId String(MAX),
Timestamp TIMESTAMP,
EventData)
PRIMARY KEY (UserId, Timestamp DESC);
CREATE INDEX EventsByTimestamp ON Events (Timestamp DESC);
Assim como com uma tabela chaveada somente por data e hora, uma coluna ShardId sintética precisaria ser adicionada à tabela e usada como a primeira coluna indexada para ajudar na distribuição do índice dentre as seções.
Um gerador de ShardId simples pode ser:
TimestampShardId = CRC32(Timestamp) % 100
que dará um valor de hash entre 0 e 99 a partir do carimbo de data e hora. Você vai precisar adicioná-lo à tabela original como uma nova coluna e usá-lo como a primeira chave do índice. Você pode fazer isso assim:
CREATE TABLE Events (
UserId String(MAX),
Timestamp TIMESTAMP,
TimestampShardId INT64,
EventData)
PRIMARY KEY (UserId, Timestamp DESC);
CREATE INDEX EventsByTimestamp ON Events (TimestampShardId,Timestamp);
Com isso, você vai remover o ponto sobrecarregado na atualização do índice, mas vai desacelerar as consultas de intervalo de data e hora, já que vai ser preciso executar a consulta para cada valor de ShardId (0 a 99) para obter ter o intervalo de data e hora de todos os fragmentos:
Select * from Events@{FORCE_INDEX=EventsByTimestamp}
WHERE
TimestampShardId BETWEEN 0 AND 99
AND Timestamp > @lower_bound
AND Timestamp < @upper_bound;
Esse tipo de estratégia de índice e fragmentação deve criar um equilíbrio entre a maior complexidade para a leitura e o aumento de desempenho de uma consulta indexada.
Outros índices que valem a pena conhecer
Quando migrar para o Cloud Spanner, é uma boa ideia entender como estes outros tipos de índice funcionam e em que situações você pode precisar usá-los:
Índices NULL_FILTERED
Por padrão, o Cloud Spanner indexa linhas usando valores NULL da coluna indexada. Um NULL é considerado o menor valor possível, por isso eles aparecerão no início do índice.
Além disso, é possível suspender esse comportamento usando a sintaxe CREATE NULL_FILTERED INDEX , que vai criar um índice que ignora linhas com valores NULL na coluna indexada.
Esse índice será menor que o índice completo, pois na verdade será uma visualização filtrada materializada na tabela, e será mais rápido de consultar do que a tabela completa quando for necessário fazer uma verificação de tabela.
Índices UNIQUE
Você pode usar um índice UNIQUE para determinar que uma coluna da tabela tenha valores únicos. Essa condição é aplicada no momento da realização da transação.
Índices de cobertura e a cláusula STORING
Para otimizar o desempenho de leitura de índices, o Cloud Spanner pode armazenar os valores da coluna da linha da tabela no próprio índice, removendo a necessidade de ler a tabela. Isso é conhecido como índice de cobertura. Para fazer um índice de cobertura, é preciso usar a cláusula STORING na hora de defini-lo. Dessa forma, os valores da coluna podem ser lidos diretamente no índice e, com isso, ler no índice passa a apresentar o mesmo desempenho que ler na tabela. Por exemplo, a tabela abaixo contém dados de funcionários:
CREATE TABLE Employees (
CompanyUUID INT64,
EmployeeUUID INT64,
FullName STRING(MAX)
...
) PRIMARY KEY (CompanyUUID,EmployeeUUID)
Se você precisar ver o nome completo de um funcionário com frequência, por exemplo, é só criar um índice em employeeUUID, armazenando o nome completo para buscas rápidas:
CREATE INDEX EmployeesById
ON Employees (EmployeeUUID)
STORING (FullName);
Forçando o uso do índice
O mecanismo de consulta do Cloud Spanner só usa índices automaticamente em raras circunstâncias (quando é uma consulta totalmente coberta pelo índice), por isso é importante usar uma diretriz FORCE_INDEX na declaração SQL SELECT para garantir que o Cloud Spanner busque valores a partir do índice (leia mais sobre o assunto na documentação ).
Select *
from Employees@{FORCE_INDEX=EmployeesById}
Where EmployeeUUID=xxx;
Note que, ao usar as Read APIs do Cloud Spanner, você só pode realizar consultas totalmente cobertas, ou seja, consultas em que o índice armazena todas as colunas solicitadas. Para ler as colunas da tabela original usando um índice, você precisa de uma consulta SQL. Leia a seção "Usar um índice secundário" do documento "Primeiros passos" para ver exemplos.
Continuando com a educação do Cloud Spanner
Existem grandes diferenças conceituais quando se um banco de dados baseado em nuvem e horizontalmente escalonável, como o Cloud Spanner, em vez do RDBMS que você usa há anos. Quando se acostumar com o funcionamento das chaves e dos índices, você vai poder começar a aproveitar os benefícios do Cloud Spanner para escalonar mais rápido.
No próximo episódio da série, vamos ver como lidar com lógica empresarial, que antes seria implementada por acionadores e procedimentos armazenados, mas nenhum desses dois recursos existe no Cloud Spanner.
Quer saber mais sobre o Cloud Spanner em um evento presencial? Vamos falar sobre migração de dados e outros assuntos nesta seção , na Next 2018, em julho. Para saber mais e se inscrever, acesse o site da Next 18 .
Conteúdo relacionado:
Como usamos o Cloud Spanner para criar nosso sistema de personalização de e-mails – de A a Z
2 comentários :
Situs Judi Terpercaya dewapoker online pasti memberikan fasilitas sistem permainan poker online yang canggih, proses transaksi yang sangat cepat, dan pelayanan terbaik demi kenyamanan semua member. Bagi anda yang ingin bermain judi online seperti permainan poker online,domino qiu qiu, ceme,blackjack, ataupun capsa susun bisa mendaftarkan diri anda di dewa poker . dewa poker online menjadi Trendsetter Game Judi Poker Online Paling Terpercaya dan Recommended dari berbagai Situs, Blog, Media Sosial dan Beberapa Grup Pecinta Game Online Uang A
Tidak ada orang yang selalu beruntung dalam bermain poker online. Raih kemenangan judi online anda bersama kami sekarang juga, dan rasakan sensasi permainan Player Vs Player 100% dan dapatkan jackpot yang sangat mudah hanya di poker online 2019. Pokerbo888 BANDAR POKER ONLINE DAN AGEN DOMINO QIU QIU TERPERCAYA DI INDONESIA. Dan tidak lupa lagi POKER 88 akan memberikan tambahan chips untuk member baru yang mendaftar dan melakukan deposit dengan ketentuan sebagai berikut : 1. BONUS DEPOSIT MEMBER BARU 100.000. TERPERCAYA DAN PERTAMA YANG MENERIMA DEPOSIT VIA GOPAY ,GABUNG DAN DAPATKAN BONUS DEPOSIT PERDANA DIWEBSITE KAMI POKERBO888.
Anda pengen game yang asyik dan seru? Mungkin game kartupoker yang mempunyai konsep atau memiliki jenis game kartu bisa menjadi salah satu solusi terbaik untuk Anda. remipoker 2019 Selain game kartu remi ataupun game domino, Anda bisa mengandalkan game poker sebagai salah satu game kartu yang tergolong paling seru dan menarik untuk dimainkan di HP Android. sekarang bukan jamannya repot main game poker cukup dengan gadget atau komputer dirumah anda semua sudah bisa memainkan rajaqq dirumah sambil tiduran atau bersantai. anda bisa bermain mengajak teman-teman anda, cukup dengan login dengan facebook atau media sosial lainnya anda sudah bisa memainkan game indoqq ini. segera daftar di link alternatif dominoqq 2019dan raih hoki anda sebanyak-banyaknya.
Postar um comentário